nbd has been superseded by beans for issue tracking. All tickets were
migrated to .beans/ in the previous commits. Remove the nbd/ project
directory, its flake input, and all references.
- Remove nbd/ (source, tests, docs, beans, skills, flake)
- Remove nbd flake input and nbd package from devShell in flake.nix
- Update flake.lock to drop nbd and its transitive inputs
- Remove nbd entry from PROJECTS.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Simplify all issue-tracking documentation to defer to `beans prime`
rather than maintaining a parallel description of beans commands.
- CLAUDE.md: collapse task-tracking section to just `beans prime` callout
- edu/.claude/skills/work: nbd next → beans list --ready
- nbd/.claude/skills/work: nbd next → beans list --ready
- nbd/.claude/skills/triage: nbd tickets/.nbd → beans beans/.beans
- nbd/src/claude_md_snippet.md: replace nbd snippet with beans prime stub
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds src/introduction.md explaining the project (LLM-generated learning
resources, Claude Code + Opus 4.6/Sonnet 4.6, CC0 public domain, contact
email) and links it as the first entry in SUMMARY.md.
- OpenTofu config in infra/ for Pages project, DNS CNAME, and custom
domain at vibebooks.elijah.run
- justfile with build, serve, deploy, infra-init, infra-plan, infra-apply
targets
Closes 59c122
- Add GET /api/admin/verify — side-effect-free code check used by the
admin unlock flow; registered before reset-auth-code in the router
- Remove "Reset auth code" section from admin panel (UI + dead API code);
rotation is now CLI-only via `wrangler secret put ADMIN_AUTH_CODE`
- Add rotate-admin-code justfile recipe using pwgen for local key rotation
- Add pwgen to Nix dev shell
- Update OpenAPI spec with /api/admin/verify definition
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add AppState to handlers, read ADMIN_AUTH_CODE from Worker env, gate
reset-auth-code with 409 when secret is active, update tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Yew's Renderer::new() renders into <body>, not #app, so the flex
column layout on #app had no effect. Move display:flex and
flex-direction:column to body so .main-content{flex:1} correctly
pushes .site-footer to the bottom of the viewport.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move src/bin/api/db/ and src/bin/api/handlers/ to src/db/ and
src/handlers/ so they compile as library modules accessible to both
the native binary and the Cloudflare Workers entry point
- Upgrade worker crate 0.5 → 0.7; add workers-api feature flag and
cdylib/rlib crate-type to Cargo.toml
- Update flake.nix: add worker-build and just to the dev shell; bump
flake.lock (nixpkgs + rust-overlay)
- Consolidate rate limit rules to one (Free plan allows only 1 rule
per zone in the http_ratelimit phase)
- Update infra/worker.tf to deploy via wrangler rather than Terraform
(Cloudflare provider v4 can't upload ES module + wasm bundles)
- Extend .gitignore to exclude *.wasm build artifacts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
D1's exec() treats newlines as statement separators, causing multiline
CREATE TABLE statements to be truncated after the first line and return
"incomplete input: SQLITE_ERROR" on every request.
Fix: switch run_migrations() in D1Repository to use prepare(sql).run()
instead of exec(sql), which treats the full string as a single statement.
Also moves db and handlers modules from src/bin/api/ to src/ (library
modules), adds justfile with build/deploy/migrate recipes, adds
migrations/schema.sql for direct wrangler d1 execute usage, and adds
wrangler.toml for worker deployment configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove captcha-gated disabled state on report submit button — Turnstile
does not fire a DOM input event so captcha_solved never became true; rate
limiting is handled at the WAF layer so the gate was incorrect
- Token is still read from the hidden Turnstile input at submit time if present
- Add explanatory hint below "Enter Auth Code" heading in AuthModal explaining
the code was provided or generated when the quote was created
- Add .auth-modal__hint CSS class for the hint text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Derive PartialEq on ReportSummary, ReportListResponse, ReportRow, and
QuoteReports so they can be held in Yew state enums that require PartialEq
- Remove unused ApiError import from moderation_tab.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Moderation tab to the admin page (visible after unlock) showing a
paginated list of reported quotes. Clicking a row opens a detail modal with
the full quote, all individual reports, and action buttons to delete the
quote, hide it, or dismiss the reports.
- api.rs: add ReportSummary, ReportListResponse, ReportRow, QuoteReports types
and five admin_* async functions for the reports endpoints
- components/moderation_tab.rs: new ModerationTab function_component with
paginated list, row-click loading state, and an inline detail modal
- components/mod.rs: expose moderation_tab module
- pages/admin.rs: introduce AdminTab enum and tab bar; wrap existing settings
content in the Settings tab; add Moderation tab rendering ModerationTab
- style.css: add styles for admin tabs, moderation list table, and detail modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add HideAuth action variant to the Action enum
- Show yellow badge when quote.hidden is true
- Add Hide/Make Public toggle button in the actions bar
- Reuse AuthModal for the hide/unhide auth prompt
- Call POST /api/quotes/:id with { hidden: !current } and X-Auth-Code
- Update quote state on success; re-prompt on 403
- Add .page-quote__hidden-badge CSS styles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Report button to /quotes/:id that opens a modal containing:
- Optional reason textarea (max 256 chars) with live character counter
- Cloudflare Turnstile CAPTCHA widget (always-passes test sitekey as const)
- Submit button disabled until CAPTCHA token is non-empty
- Cancel button
On submit POSTs { reason?, captcha_token } to POST /api/quotes/:id/report.
Shows a success banner on 201 or an error via ErrorDisplay on failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a toggle button ("Filters ▼ / ▲") above the quote list on the
browse page. Filter controls are hidden by default and expand when
clicked. Each filter (Author, Tag, Date range) is on its own labelled
row with consistent styling. Existing API query logic is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark admin moderation endpoints ticket as done following successful
implementation and test pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements ticket 6c5904 — five admin-authenticated endpoints for
moderation workflows:
GET /api/admin/reports paginated list of reported quotes
GET /api/admin/reports/:id full quote + all report rows
DELETE /api/admin/reports/:id/quote unconditionally delete a quote
POST /api/admin/reports/:id/hide set hidden=1 on a quote
DELETE /api/admin/reports/:id/reports clear all reports for a quote
All endpoints require X-Admin-Code header; 403 on missing/wrong code.
DB layer additions:
- QuoteRepository trait gains list_reports, get_reports_for_quote,
admin_delete_quote, hide_quote, and clear_reports methods
- New ReportRow, ReportSummary, ReportListResult, and QuoteReports
types added to db/mod.rs
- Implementations in native.rs (rusqlite) and d1.rs (Cloudflare D1)
Tests added:
- 14 unit handler tests using MockRepo (3 per endpoint covering
success, 404, and 403 cases)
- 5 integration tests using real SQLite via NativeRepository
- 10 DB-layer unit tests in native.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds infra/rate-limits.tf with a cloudflare_ruleset (phase: http_ratelimit)
implementing per-IP rate limits on all mutating API endpoints:
- PUT /api/quotes: 5 requests per 10 minutes (quote creation)
- POST /api/quotes/:id/report: 3 requests per hour (abuse reports)
- POST /api/quotes/🆔 10 requests per minute (quote updates)
- DELETE /api/quotes/🆔 10 requests per minute (quote deletes)
The report rule is ordered before the general update rule to ensure the
more-specific /report path matches before the broader /api/quotes/:id
pattern. Documents the approach, plan requirements, and layered protection
rationale in docs/ARCHITECTURE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove the Admin link from the top navigation bar; /admin remains
reachable by direct URL but is no longer discoverable from normal
browsing.
- Rework /admin to an auth-first flow: on load the page shows only a
password input and an Unlock button. On success, the admin controls
(submission lock/unlock, auth code reset) are revealed; on failure a
clear error message is shown and the page stays locked. Refreshing
always resets to locked state (code is in component state only).
- Add api::verify_admin_code() — calls POST /api/admin/reset-auth-code
with new_code equal to the entered code, making the call idempotent
on success (code unchanged) while still returning 403 on mismatch.
- Fix pre-existing wasm build breakage in quote.rs: UpdateQuoteInput
gained a hidden field in an earlier ticket but quote.rs was never
updated. Added hidden: None to the struct literal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add CREATE_REPORTS migration constant (was unused — now wired in)
- Wire CREATE_REPORTS into run_migrations for both NativeRepository and D1Repository
- Add create_report to QuoteRepository trait with NotFound semantics
- Implement create_report in NativeRepository (two-step: existence check then insert)
- Implement create_report in D1Repository (two-step: COUNT check then insert)
- Add report_handler: POST /api/quotes/{id}/report, 201/400/404/500
- Register route before /{id} in router so static /report suffix wins
- Add create_report to MockRepo in handler tests
- Add handler tests: test_report_success, test_report_quote_not_found, test_report_reason_too_long
- Add DB tests: test_create_report_success, test_create_report_not_found
- Add ReportInput schema and /api/quotes/{id}/report path to openapi.yaml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add three tests verifying hidden-quote filtering behaviour in
NativeRepository:
- list_quotes_excludes_hidden: hidden quotes do not appear in paginated
listing results.
- get_random_quote_excludes_hidden: get_random_quote returns None when
the only quote is hidden.
- get_quote_returns_hidden_quote: get_quote (direct ID lookup) still
returns the quote when it is hidden.
Also refactor the inline row-mapping closure in list_quotes to use the
existing row_to_quote helper, eliminating duplicated column mapping
logic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `hidden` (boolean, required) to the Quote response schema so all
GET responses reflect the field. Add `hidden` (boolean, optional) to
QuoteUpdateRequest so callers can toggle visibility via POST /api/quotes/:id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `hidden: bool` to the `Quote` struct and `hidden: Option<bool>` to
`UpdateQuoteInput` in `src/lib.rs`
- Add `ALTER_QUOTES_ADD_HIDDEN` migration constant in `db/migrations.rs`
- Apply the ALTER TABLE migration in `NativeRepository::run_migrations` and
`D1Repository::run_migrations` with try/ignore for idempotency
- Exclude hidden quotes from `list_quotes` (WHERE hidden = 0) and
`get_random_quote` in both native and D1 implementations
- Update all SELECT queries to include the `hidden` column
- Handle `hidden` field in `update_quote` SET clause for both implementations
- Update `MockRepo` and `sample_quote` in handler tests to include `hidden`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Check GET /api/status on mount; if submissions_locked is true, hide the
form and show a .submissions-closed-banner instead. Fail-open: on error,
treat as unlocked and display the form normally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass "" for the unused `current` param in admin_reset_auth_code (the
server only checks X-Admin-Code). Handle get_status() failure in the
on-mount effect with fail-open behaviour: set submissions_locked=Some(false)
and display "Could not fetch status." so the UI never hangs on
"Loading status...".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>