Skip to content

feat: Wave 1 quick wins — 7 follow-ups from PR #523 review#527

Open
tayebmokni wants to merge 7 commits into
feat/post-session-cleanupfrom
feat/wave-1-quick-wins
Open

feat: Wave 1 quick wins — 7 follow-ups from PR #523 review#527
tayebmokni wants to merge 7 commits into
feat/post-session-cleanupfrom
feat/wave-1-quick-wins

Conversation

@tayebmokni
Copy link
Copy Markdown
Contributor

Stacked on top of PR #523. Seven atomic fixes for the small/medium issues filed during the #522 review tracker.

Commits (each closes a separate issue)

Verification

  • go build ./... clean in apps/api, apps/worker, cli/gonext, packages/go
  • pnpm exec tsc --noEmit 0 errors in apps/admin
  • All package tests pass — incl. 6 new test files / ~40 new tests across these 7 commits
  • Live: rebuilt + restarted api; admin/jobs/dlq returns 200; admin/plugin-pages returns 200; settings reading/writing groups return seeded defaults

Depends on

Next

After this merges, Wave 2: 4 medium features (#507 dead admin routes, #508 wire Settings→public, #509 wire Menus→public nav, #510 show_on_front dispatch).

🤖 Generated with Claude Code

tib0o0o and others added 7 commits May 28, 2026 11:54
…obs/dlq (#502)

On a freshly booted system the \`default\` queue's Redis keys don't
exist, so \`Inspector.ListArchivedTasks\` returns an error wrapping
\`asynq.ErrQueueNotFound\`. The DLQ list handler bubbled that as a
500 with body \"failed to list archived tasks\" — the admin Jobs
page rendered its FailureState UI on every clean install.

Treat \`ErrQueueNotFound\` as the empty case: HTTP 200 with
\`{data: [], pagination: {next_cursor: \"\"}}\`. Operationally
identical to a queue that exists but has no archived tasks.

Test: TestList_QueueNotFoundIsEmpty pins the contract with a fake
inspector that returns a wrapped ErrQueueNotFound.

Closes #502.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…orage (#503)

\`PostgresStorage.List\` propagated \`SQLSTATE 42P01\` (relation
\`plugins\` does not exist) up through \`lifecycle.Manager.List\` to
the /api/v1/admin/plugin-pages handler, which surfaced it as 500
\"failed to list plugins\". The plugins table has no migration today
— clean installs hit this on every admin sidebar render.

Semantically \"no table\" and \"no rows\" are the same outcome for
a read-only lister: there are zero plugins. Treat 42P01 as empty
result rather than a runtime error.

Tests:
- TestPostgresStorage_List_UndefinedTableIsEmpty pins the storage
  contract.
- TestList_EmptyOnCleanInstall pins the handler shape: 200 with a
  non-nil empty \`pages\` slice so JSON encodes \`[]\`, not \`null\`.

Note: the /api/v1/admin/plugin-pages route is also currently
returning 401 because the route mount in main.go is missing the
authmw.RequireSession wrap. That's a separate wiring bug — fix
follows in a sibling commit.

Closes #503.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…ssing (#515)

The PR #523 adapter set \`author.displayName: ''\` (empty) — every
row rendered a blank Author cell. Now fall back to the last 8
chars of \`author_id\` (mirrors the \`shortId\` helper in
posts/[id]/revisions/page.tsx). When/if the list API joins users
and starts emitting \`author.display_name\`, the adapter prefers
that.

Refactored the inline map into an exported \`adaptApiPost\` plus
\`ApiPost\` type so the conversion is unit-testable without
spinning up the Server Component. Three new tests:
- fallback to short id when no display name supplied
- API-supplied display_name wins
- commentsCount stays 0 (documented deferred behavior)

\`commentsCount\` still hard-coded to 0 with a comment — the list
endpoint doesn't emit the aggregate, and adding a per-row count
needs a separate join the API doesn't ship today.

Closes #515.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…rch (#516)

Three handlers all parsed a \`status\` query param + the \`any\` →
empty alias, two by hand and the third (admin/search) silently
treating \`any\` as a literal status value (returning zero matches).

Extract \`packages/go/util/queryparse.ParseStatus(raw, valid)\`
with shared behavior:
- "" and "any" both mean \"no filter\" → returns ""
- otherwise must be in \`valid\`; returns ErrInvalidStatus
- nil-map-safe

Migrate the three call sites:
- apps/api/internal/rest/posts/handlers.go (in parseListQuery)
- apps/api/internal/admin/comments/list.go (uses AllStatuses)
- apps/api/internal/admin/search/handler.go (was unchecked; now
  returns 400 invalid_status on bad input, like the other two)

Behavior change observed: \`/api/v1/admin/search?status=any\` now
maps to "no filter" (previously embedded verbatim in the SQL
predicate, silently returning empty). Unknown statuses on search
now return 400 instead of silently filtering to nothing.

queryparse_test.go: 7 cases including \"any\" alias, valid pass-
through, unknown → ErrInvalidStatus, nil-map handling, case
sensitivity.

Closes #516.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
PR #523 mounted /api/v1/settings but the registry only knew
\`core.site.*\` + \`core.permalinks.format\`. Admin Reading/Writing
sub-pages were calling \`?group=core.reading\` and \`?group=core.writing\`
which returned \`{}\` — the forms had no schema to render.

Register 11 new Setting{} entries (6 reading, 5 writing) using
core.site.name as the template (JSON-Schema string + Default +
Autoload: true + RequiresCapability: CapManageOptions).

Reading keys match the admin ReadingForm.tsx camelCase mapping:
- homepage_type (enum latest_posts | static_page, default latest_posts)
- homepage_page_id (string)
- posts_per_page (integer 1..100, default 10)
- show_summary (bool, default true)
- rss_items (integer 1..100, default 10)
- rss_full_text (bool, default false)

Writing keys match WritingForm.tsx:
- default_category (enum, default uncategorized)
- default_format (enum, default standard)
- default_editor (enum block | classic, default block)
- post_by_email_enabled (bool, default false)
- post_by_email_address (string)

Verified live: \`GET /api/v1/settings?group=core.reading\` and
\`?group=core.writing\` both return the seeded defaults instead
of empty objects.

core_test.go: 6 tests covering presence, group-locking,
defaults-readability, integer round-trip, enum rejection.

Closes #525.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
…526)

The themes/static ActiveResolver called \`themeActiveStore.Get(ctx)\`
on every request to /themes/active/style.css. Postgres options is
single-row + autoloaded so the query is microsecond-cheap, but it
scales linearly with public traffic.

Wrap with a 60s TTL cache:
- RLock-only on the cache-hit path (the common case).
- Refill takes the write lock and re-checks the deadline — single-
  flight across the inner resolver per expiry window.
- Empty values are cached too. The production resolver returns \"\"
  on a DB error, and not caching that would degenerate into a
  hammering loop on a transient outage.
- TTL <= 0 disables caching (test seam).
- Injectable \`now func() time.Time\` for deterministic TTL tests.
- \`Invalidate()\` forces the next call to hit the inner resolver
  (will be wired from the admin theme-activate handler in a
  follow-up; switches lag up to TTL until that lands).

8 tests, all pass under -race: cache hit memoization, TTL expiry,
explicit Invalidate, fresh-resolver Invalidate, zero TTL,
empty-value caching, concurrent access (64 goroutines × 500
iters), value-change pickup after Invalidate.

Closes #526.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.com>
Themes used to require a manual \`docker run --rm -v api-themes:/themes
alpine cp ...\` every time the volume got wiped (e.g. \`compose down
-v\`). Without that, \`/themes/active/style.css\` 404s and the
customizer 500s on \"failed to load theme gn-hello\".

Migrate now seeds the volume idempotently on every run:

- New \`cli/gonext/cmd/migrate/themes.go::seedThemes\` copies
  /themes/* from the bundled CLI image into the mounted volume
  when the destination is empty. Only directories with a
  theme.json are treated as themes (skips README.md and cruft).
- Soft-warning + nil error if the source isn't present (operator
  runs CLI outside the image).
- New --seed-themes flag (default true) on the migrate command.
- Runs after pkgmigrate.Run succeeds, BEFORE the existing DB-side
  runThemeSeed step, so files are on disk before the
  core.active_theme options row is written.

docker-compose.dev.yml: migrate service now mounts
\`api-themes:/var/lib/gonext-themes\` and exports the path env vars.
Both api and migrate services reference the same named volume, so
what migrate writes at /var/lib/gonext-themes shows up to the api
at /themes.

themes_test.go: 8 cases — empty-destination seed, idempotence
(sentinel preservation), non-theme dir filtering, cruft tolerance,
missing-source soft-warning, file-as-source hard error, env
override resolution, --seed-themes flag parsing.

Closes #513.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Tayeb Mokni <tayeb.mokni@gmail.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.

2 participants