Skip to content

docs: Update bug report template for platform options - #1348

Merged
perber merged 1 commit into
mainfrom
feature/update-template
Jul 27, 2026
Merged

docs: Update bug report template for platform options#1348
perber merged 1 commit into
mainfrom
feature/update-template

Conversation

@perber

@perber perber commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Added options for running on different platforms in the bug report template.

Added options for running on different platforms in the bug report template.
@perber
perber merged commit f8a1bef into main Jul 27, 2026
9 checks passed
@perber
perber deleted the feature/update-template branch July 27, 2026 19:41
mrchypark added a commit to mrchypark/leafwiki that referenced this pull request Jul 29, 2026
* feat(editor): make preview split resizable (perber#1279)

* feat(tree): drag & drop page/section reordering in the page tree (perber#1307)

Reorder and move pages/sections directly in the sidebar tree by dragging
rows: drop between rows to reorder (insertion indicator), drop onto a
section to move into it (auto-expands collapsed sections on hover), with
optimistic updates and a silent tree re-sync. Same-parent reorders use the
existing sort endpoint; cross-parent drops use the move endpoint, which now
accepts an optional position for atomic move+placement (MoveNodeToPosition;
omitted position appends, so existing callers are unchanged).

Also folds in two fixes surfaced while testing the feature:
- Firefox performed a full page navigation on drop because the post-drag
  click bypasses React's synthetic handlers; click suppression is now a
  native window-level capture listener armed during the drag.
- MarkdownPreview re-parsed and re-rendered the whole article on every
  tree store update (byId identity churn); the rendered markdown element
  is now memoized on the resolved content string.

Fixes perber#1110

* feat(favorites): add per-user favorite pages (perber#1310)

Adds a private, per-user favorites list, separate from the global
editorial "Pinned Pages" flag: any authenticated user (not just
editor/admin) can favorite/unfavorite any page they can read.

Backend: new internal/favorites package (own favorites.db, outside
resync's reach per ADR-0001), AddFavorite/RemoveFavorite/ListFavorites
use cases, PUT/DELETE /api/pages/:id/favorite and GET /api/favorites
routes, cascade cleanup on page delete and user delete.

Frontend: stores/favorites.ts (optimistic add/remove), star toggle on
tree rows and the page viewer header, new "Favorites" sidebar
accordion section between Pinned and Pages, loaded/cleared reactively
off the session user id in App.tsx.

* refactor(sqliteutil): share open/corruption-recovery logic across stores (perber#1311)

Extract the "open → ensure schema → on recoverable SQLite corruption,
wipe and retry once" sequence, previously copy-pasted near-verbatim in
tags, links, properties, and search's store constructors, into a single
sqliteutil.RetryOnCorruption helper. Each store keeps its own opening
mechanics (eager sql.Open, links' lazy Connect(), search's fully lazy
withDB); only the recovery policy is now shared.

* feat(preview): add AutoHotkey syntax highlighting (perber#1312)

Co-authored-by: Chen Zhang <c436zhan@gmail.com>

* feat: .leafwikiignore (perber#1263)

IgnoreFile struct wrapping github.com/sabhiram/go-gitignore with
LoadFromDir(), Matches(path, isDir), and PatternCount() methods.

* docs: Document external edits, resync triggers, and ID write-back in README (perber#1316)

Covers the two resync trigger paths (admin UI, SIGUSR1/SIGHUP) and
what happens when a manually added .md file has no leafwiki_id yet.

* feat(auth): add TOTP core service and storage layer

Adds the storage and cryptography foundation for optional per-user TOTP
two-factor authentication: additive users.db schema migration, a TOTP
service (secret generation/verification via pquerna/otp, AES-256-GCM
encryption at rest, bcrypt-hashed recovery codes), and the accompanying
UserStore/UserService/SessionStore methods. Not yet wired into login or
any HTTP endpoint.

* feat(auth): add two-step TOTP login handshake and self-service methods

AuthService.Login now issues a short-lived, single-use login challenge
instead of tokens when a user has TOTP enabled; CompleteTOTPLogin
finishes the handshake with a TOTP or recovery code. Also adds
StartTOTPSetup/ConfirmTOTPSetup/DisableTOTP/GetTOTPStatus for the
self-service flow, with "revoke every other session, keep the current
one" wired through RevokeAllUserSessionsExceptCurrent.

Wires the --totp-encryption-key/LEAFWIKI_TOTP_ENCRYPTION_KEY config
through main.go and the wiki.go composition root: if unset, TOTP
self-service is simply unavailable rather than failing startup, since
V1 has no separate feature toggle and every existing install would
otherwise need a new secret on upgrade. Startup still fails if a key
is provided but shorter than the required 32 bytes.

No HTTP routes changed yet.

* feat(auth): expose TOTP login and self-service HTTP endpoints

New endpoints on the auth domain:
- POST /api/auth/login/totp completes the handshake started by
  /api/auth/login when it returns {requiresTotp, loginChallengeToken};
  cookies are only set on success. Shares its rate-limit budget with
  /api/auth/login.
- POST /api/users/me/totp/setup/start|confirm, disable, and
  GET .../status implement self-service enable/disable, behind their
  own rate limiter separate from the pre-auth login endpoints.

New LocalizedError codes (auth_totp_invalid_code, _challenge_invalid,
_not_configured, _already_enabled, _setup_not_started, _not_enabled)
mapped to HTTP statuses in errors.go.

* feat(users): add two-factor authentication UI

LoginForm now handles the two-step handshake: a password step, then
(only if the account has TOTP enabled) a single code step accepting
either a TOTP or recovery code.

New TOTPSetupDialog (password -> QR code/manual key/code -> one-time
recovery codes) and TOTPDisableDialog (password + code), opened from
two new conditional items in the user avatar dropdown. QR rendering
uses qrcode.react. UserManagement gets a "2FA" status column reusing
the existing settings__pill idiom.

Adds the "users" i18n namespace (didn't exist before), new login.totp.*
keys in auth.json, and 6 new errors.json entries for the backend's TOTP
error templates. Also fixes /api/auth/me (handleMe) to return
totpEnabled, which had been missed when PublicUser gained the field.

* test(e2e): add TOTP end-to-end coverage

Covers the full flow through a real browser: enable TOTP (password ->
QR/manual-key/code -> recovery codes) -> log out -> log in with a TOTP
code; wrong code rejected; login with a recovery code and its single-use
enforcement; disable and confirm plain password login resumes.

generateTotpCode() reimplements RFC 6238 directly (HMAC-SHA1, no new
dependency) rather than adding a library just for this; verified against
the server's own pquerna/otp-based codes before use.

e2e/run.sh now passes --totp-encryption-key to both the docker and local
run modes, without which every self-service TOTP endpoint would 503.

Verified: the 4 new TOTP tests pass, and a full run of the existing
e2e suite (151 tests, 3 pre-existing skips) shows no regressions.

* fix(auth): close recovery-code double-redemption race

Recovery-code consumption was a read-then-write: verifyTOTPOrRecoveryCode
matched the code against an in-memory snapshot of the stored hashes and
wrote back the reduced list unconditionally, with no transaction or
concurrency guard. Two concurrent requests presenting the same valid
recovery code (e.g. two parallel POST /api/auth/login/totp calls) could
both read the code as present before either wrote back, letting a single
recovery code authenticate more than one session instead of exactly one.

Fixes it with optimistic concurrency: UserStore.ConsumeRecoveryCodeHash
does a compare-and-swap UPDATE guarded by the row's current
totp_recovery_codes_json, so only the first writer can succeed; the
loser gets swapped=false and verifyTOTPOrRecoveryCode re-reads the
current hashes and retries (bounded, denying on exhaustion rather than
risking ambiguity). Also sets a busy_timeout on the users.db connection
so legitimate concurrent writers wait and retry instead of failing
immediately with SQLITE_BUSY, which the compare-and-swap path does not
itself retry on.

Adds regression tests at both the store level (concurrent compare-and-
swap attempts, exactly one wins) and the AuthService level (concurrent
logins with the same recovery code, exactly one succeeds).

* fix(auth): close TOTP lockout bypass and DisableTOTP panic from code review

Two correctness/security bugs found by an 8-angle diff review, both
confirmed by independent verification:

- Login() unconditionally reset the shared per-user lockout counter on a
  correct password, before checking whether TOTP was required. Since
  CompleteTOTPLogin uses the same counter to rate-limit TOTP/recovery-code
  guesses, an attacker who already knows the password could resubmit it to
  wipe out failed TOTP attempts, defeating the 5-attempt lockout entirely
  and turning the 6-digit code into an unlimited-guess target. Login no
  longer resets the counter while TOTP is still required; it's only reset
  once the full handshake (password + TOTP/recovery code) succeeds.

- DisableTOTP was missing the `a.totp == nil` guard present in every other
  TOTP entry point. If an operator runs without --totp-encryption-key while
  a user still has TOTP enabled (e.g. the key was removed after being used),
  calling disable panicked instead of returning a clean error. Login() had
  the same gap the other way: it issued a login challenge for such a user
  that CompleteTOTPLogin's own nil-check could never redeem, permanently
  locking them out instead of failing at the password step with a clear
  error.

Also addresses smaller findings from the same review:
- StartTOTPSetup/DisableTOTP each fetched the user row twice (once inside
  DoesIDAndPasswordMatch, once via a separate GetUserByID). It now returns
  the user it already fetched.
- TOTP secret decryption failures reaching CompleteTOTPLogin/ConfirmTOTPSetup/
  DisableTOTP were bare fmt.Errorf, violating this repo's convention that
  domain errors reaching HTTP handlers must be *sharederrors.LocalizedError;
  now wrapped as auth_totp_verification_failed (503).
- Removed the unconditional UpdateRecoveryCodeHashes, dead in production
  since recovery-code consumption moved to the atomic ConsumeRecoveryCodeHash
  compare-and-swap; left in place it was a foot-gun for reintroducing the
  race that method was added to close.
- handleConfirmTOTPSetup/handleDisableTOTP now log when ReadRefresh fails,
  since that silently falls back to revoking every session for the user
  (including the one performing the action) instead of just the others.
- Migrated "Change Own Password"/"Loading users..."/"No users found." to
  react-i18next, since these components were already being touched to add
  new translated TOTP strings.

New regression tests: repeated correct-password logins interleaved with
wrong TOTP guesses must still hit the account lock; DisableTOTP without a
configured TOTP service returns auth_totp_not_configured instead of
panicking. Full backend suite (incl. -race), frontend suite, and the full
Playwright e2e suite (148 passed, 3 pre-existing skips) all pass.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* feat(snapshot): complete full-backup snapshot scheduler, retention, and admin API

Finishes the previously backend-only snapshot package (PR perber#1255): adds an
automatic Scheduler mirroring the git-backup one, retention pruning after
each run, collision-safe snapshot IDs, panic-safe status handling, and the
internal/wiki/snapshot admin HTTP layer (status/list/trigger/download/delete)
wired into main.go behind --snapshot/--snapshot-interval/--snapshot-retention.

* feat(snapshot): add Full Backup settings UI

New /settings/snapshots admin page to trigger, list, download, and delete
full backups, gated behind the snapshotEnabled config flag and kept
separate from the existing Git Content Backup page since this one
includes the full database.

* docs: document Full Backup feature and manual restore procedure

Adds a README section alongside the existing Git Backup docs covering
CLI flags/env vars, what's included (root/assets/branding/schema/users.db),
retention behavior, and the manual stop-extract-restart restore steps.

* fix(snapshot): address Copilot review findings

- Guard handleList/handleDownload/handleDelete against a nil manager
  (only reachable if Routes were ever registered while disabled),
  matching the existing handleStatus/handleTrigger pattern instead of
  panicking.
- Add Cache-Control: no-store / Pragma: no-cache to the download
  response, since the zip contains users.db (password hashes, TOTP
  secrets).
- triggerNow() now reloads status/list in a finally block so a failed
  trigger (e.g. 409 already-running) doesn't leave the UI stale.

* feat: API key handling - behind feature flag (perber#1281)

* docs: remove API Keys section from README (perber#1322)

Removed the experimental API Keys section from the README.

* feat: restore from full backup snapshot (perber#1323)

Adds a live, zero-downtime restore path for Full Backup snapshots (validate,
gate writes, swap files, hot-swap AuthService's user store, invalidate
sessions, reload branding, resync), an offline `restore-snapshot` CLI
subcommand for disaster recovery, and a self-restart fallback for the rare
case a failed restore can't be cleanly rolled back. Also fixes a pre-existing
gap where branding.json (site name/logo/favicon selection) was never
included in snapshot ZIPs.

* fix: fixes tree auth sidebar (perber#1325)

* fix: totp button styling (perber#1326)

* feat: add --log-format flag to switch between text and json logging (perber#1328)

Defaults to text (more readable for operators tailing logs directly);
json remains available via --log-format json / LEAFWIKI_LOG_FORMAT=json.

* refactor(auth): extract SessionManager from AuthService

Splits JWT/session issuance, refresh, revocation, and validation out of
AuthService into a standalone SessionManager, leaving AuthService with
just local password + TOTP verification. AuthService's public API is
unchanged (RefreshToken/RevokeRefreshToken/RevokeAllUserSessions/
ValidateToken now delegate to sessions), so no caller outside
internal/core/auth needed to change.

This sets up the seam a future native OIDC provider would reuse
(SessionManager.IssueSession) without duplicating session handling,
per ADR-0009. Behavior-preserving: full existing test suite passes
unmodified; only construction call sites were updated for the smaller
NewAuthService/new NewSessionManager constructor signatures.

* fix(auth): SessionManager robustness + token-type/timing findings from review

- ValidateToken now requires typ=="access" before resolving a user. It
  previously accepted any validly-signed token regardless of typ, so a
  refresh token (long-lived, and never checked against the session store
  here) could be used as an access token. Pre-existing on main since the
  original JWT auth commit; not introduced by the SessionManager
  extraction, but caught by review while the code was fresh.
- RefreshToken/ValidateToken now return ErrSessionManagerNotWired instead
  of panicking if resolveUser is unset (only possible if a SessionManager
  is used directly instead of via NewAuthService, which always wires it).
- IssueSession/RefreshToken now store the new refresh session's expiry
  using the exp already embedded in the freshly generated refresh token,
  instead of a second, independent now().Add(lifetime) call a moment
  later — the two could drift by up to a second, leaving a token
  cryptographically valid while its session row was already treated as
  expired (or vice versa). Also pre-existing on main.

All three are pinned down with regression tests (session_manager_test.go),
using a small injectable clock seam (SessionManager.now) to make the
timing issue deterministically reproducible rather than a real-time race.

* feat(auth): structured logging + Prometheus metrics for auth events

Adds observability across the auth domain (ADR-0008):
- Prometheus counters (auth_login_attempts_total, auth_totp_verifications_total,
  auth_sessions_total, auth_totp_enrollment_total) wired through the wiki/auth
  use cases.
- Structured slog logging in AuthService, SessionManager, APIKeyService,
  SessionStore, UserService (user create/update/delete/password-change/
  admin-reset events), replacing ad-hoc log/slog.Default() calls with a
  per-component logger.
- UserResolver's preload failure is no longer logged at the point it occurs
  (only returned), avoiding a double-log with main.go's top-level fatal
  logging, per ADR-0008's "log once, at the swallow point" rule.

* test(auth): TOTP recovery-code security regression tests

- Recovery codes must only ever be stored as hashes, never plaintext, so
  no later read path (GetUserByID, admin user list, etc.) can re-display
  them once ConfirmTOTPSetup has shown them.
- GetTOTPStatus's RecoveryCodesRemaining must correctly reach 0 after all
  codes have been consumed one by one, while TOTP itself stays enabled.

* test(http): viewer role cannot move or sort pages

Regression coverage for authorization on the move/sort endpoints —
mirrors the existing viewer-cannot-delete test pattern.

* test(restore): write-gate concurrency, self-restart exec, and permission-denied swap coverage

- TestManager_Restore_BlocksConcurrentWritesDuringSwap: real goroutines
  racing WriteGate.TryEnter against an actual in-flight Manager restore,
  closing the gap between the gate primitive's own tests and the HTTP
  middleware's gate tests, neither of which exercised a real restore.
- SelfRestart gains an execFn seam over syscall.Exec so it's testable
  without terminating the test binary; covers argv/env propagation and
  error passthrough.
- Swapper.SwapAll: new regression for a permission-denied move-aside
  (live content must stay untouched, RollbackAll must stay a safe no-op).

* fix(cli): validate --user-management-url like --login-url/--logout-url

It's rendered as a plain <a href> in the frontend, but an unsafe scheme
(e.g. javascript:) there is still attacker-controlled markup — so it
gets the same http(s)-only, no-relative-path validation as the other
two redirect flags, refusing to start otherwise.

Also extracts the restore-snapshot subcommand into a testable
runRestoreSnapshotCommand function (usage-error vs. restore-error are
now unit tested directly, rather than only reachable via the CLI path).

* fix(ui): TOC side panel entries show full text as a hover tooltip

Long entry titles get truncated (ellipsis) in the collapsed side panel;
a title attribute now surfaces the full text on hover. Also adds right
padding so the ellipsis doesn't visually crowd the panel edge.

* test(ui): add coverage for LoginForm and TOTP setup/disable dialogs

Previously untested: the two-step credentials→TOTP login handshake in
LoginForm, and the TOTPSetupDialog/TOTPDisableDialog self-service flows.

* fix(auth): avoid logging login identifiers

* fix(ui): patch brace-expansion transitive dependency

* fix(markdown): recover unquoted colon in leafwiki_* frontmatter fields (perber#1332)

An externally-authored page whose leafwiki_title (or other leafwiki_*
string field) contains an unquoted colon, e.g. "ADR-0001: Filesystem as
Source of Truth", failed YAML frontmatter parsing entirely and was
silently dropped from the tree during filesystem reconstruction.

parseFrontmatterYAML now falls back to a real-parse-validated sanitizer
that quotes leafwiki_* values which don't parse on their own (unquoted
colons, but also stray backticks/@ signs), while leaving genuine YAML
constructs (flow-collections, anchors, block scalars) alone so malformed
ones keep failing loudly. The sanitizer is anchored to column 0 so it
can never reach into an unrelated field's indented block-scalar content.

Frontmatter now exposes WasRepaired(), and node_store logs a warning
when a file's frontmatter needed this recovery, so auto-repair isn't
silent.

* fix(tree): skip duplicate leafwiki_id during reconstruct instead of a… (perber#1333)

A duplicate leafwiki_id across two files used to hard-abort the whole
filesystem resync (including startup). Now the later-occurring
duplicate (and, for a section, its whole subtree) is skipped without
touching disk, logged at Warn, and the rest of the tree reconstructs
normally.

* chore: bump postcss from 8.5.16 to 8.5.23 in /ui/leafwiki-ui (perber#1336)

Bumps [postcss](https://github.com/postcss/postcss) from 8.5.16 to 8.5.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss@8.5.16...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(ci): add /rebase ChatOps slash command for PRs (perber#1338)

Lets collaborators with write access rebase a PR onto its base branch
by commenting /rebase, using peter-evans/rebase under the default
GITHUB_TOKEN. Permission is checked via the collaborator permission
API before anything runs.

* chore: bump dompurify from 3.4.11 to 3.4.12 in /ui/leafwiki-ui (perber#1337)

Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.11 to 3.4.12.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](cure53/DOMPurify@3.4.11...3.4.12)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(e2e): patch brace-expansion transitive dependency (perber#1339)

Same override pattern already applied to ui/leafwiki-ui (989aaf3);
resolves brace-expansion to 5.0.8 without downgrading
@trivago/prettier-plugin-sort-imports.

* feat(metrics): expose build version via leafwiki_build_info gauge (perber#1335)

Adds a Prometheus leafwiki_build_info{version} gauge so the running
build version is scrapable alongside the existing HTTP/workflow metrics.

* fix(toc): cap sticky side panel height so it stops overflowing the viewport (perber#1340)

The ToC side panel was position: sticky with no max-height, so pages with
many headings grew the panel past the bottom of the window with no way to
scroll or reach the lower entries. It now caps to the available height and
scrolls the entry list internally once it's too long, while staying sticky
and visible during scroll.

* feat(restore): add restore-from-uploaded-ZIP and fix Windows users.db… (perber#1341)

Adds POST /restore/upload with a configurable max size
(--restore-upload-max-size / LEAFWIKI_RESTORE_UPLOAD_MAX_SIZE) so a backup
ZIP can be restored without CLI access. Also replaces the previous
best-effort Windows warning with an actual fix: AuthService now suspends
the UserStore's DB connection before a live restore renames users.db,
avoiding a sharing-violation race on Windows.

* fix(auth): hide TOTP enable option when no encryption key is configured (perber#1343)

Expose totpAvailable via /api/config so the frontend can gate the
"Enable two-factor authentication" menu item on server-side TOTP
support, instead of only failing after the user submits their
password.

* docs(readme): document missing CLI flags and env vars (perber#1346)

Adds --revision-coalesce-window, --log-format, --totp-encryption-key,
--enable-metrics/--metrics-host/--metrics-port, --snapshot and its
related flags, and --restore-upload-max-size (plus their env var
equivalents) to the Configuration tables, matching cmd/leafwiki/main.go.
Also adds the env-only LEAFWIKI_LOG_LEVEL. API-key flags are left out
for now.

* feat(i18n): close hardcoded-string gaps in frontend features (perber#1347)

Migrates page, importer, assets, history, page-switcher, toolbar,
tags, designtoggle, imagepreview, and links features to react-i18next
(new page/importer/assets/history locale namespaces), and closes
follow-up gaps discovered in editor, preview, tree, users, and viewer
features that had picked up hardcoded strings since their migration.

* feat(i18n): close remaining hardcoded-string gaps (perber#1349)

Migrate leftover hardcoded English strings to react-i18next across tree,
editor, viewer, and settings components, and fix API/store fallback
errors that bypassed translated toast messages via mapApiError's
err.message passthrough. Adds a common.json namespace for cross-cutting
UI (error boundary, 404 page, dialog primitives, sidebar tabs).

* docs: Update bug report template for platform options (perber#1348)

Added options for running on different platforms in the bug report template.

* fix: preserve fork behavior after upstream sync

* test: retain upstream auth coverage

* test(e2e): follow favorite action menu

* fix(deps): update x/text for CVE-2026-56852

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Abu Hossain Foysal <ahfoysal30@gmail.com>
Co-authored-by: Aurelio Arcabascio <140868181+arcabaa@users.noreply.github.com>
Co-authored-by: perber <patrick.erber@gmail.com>
Co-authored-by: c436zhan <zc19970919@gmail.com>
Co-authored-by: Chen Zhang <c436zhan@gmail.com>
Co-authored-by: John Porter <john@designermonkey.co.uk>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stefan <zorak1103@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant