49 Commits (2dd808cdd39351418708f2ec69f66f1d5738812c)

Author SHA1 Message Date
Elijah Voigt 7fda9905c8 feat(quotesdb): add admin verify endpoint, remove reset-auth-code UI
- 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>
3 months ago
Elijah Voigt df58b02540 feat(quotesdb): support ADMIN_AUTH_CODE Cloudflare secret for admin auth
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>
3 months ago
Elijah Voigt 5bd44e3d73 fix(quotesdb): make site footer stick to bottom of viewport
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>
3 months ago
Elijah Voigt 2ede2869e9 refactor(quotesdb): move db/handlers to lib modules, upgrade worker to 0.7, update infra
- 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>
3 months ago
Elijah Voigt c64b6bf56e fix(quotesdb): fix D1 exec() newline truncation in migrations, add justfile and migrate command
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>
3 months ago
Elijah Voigt 2be0fc4fd3 fix(quotesdb): fix report submit button always disabled, add auth code hint
- 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>
3 months ago
Elijah Voigt d50abc5495 fix(quotesdb): add PartialEq to report types, remove unused import
- 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>
3 months ago
Elijah Voigt 9cf1cf8228 feat(quotesdb): add admin moderation tab with report detail modal
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>
3 months ago
Elijah Voigt 08eb9d398b feat(quotesdb): add hidden toggle on quote detail page
- 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>
3 months ago
Elijah Voigt 201dfe55a0 feat(quotesdb): add report button with modal on quote page
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>
3 months ago
Elijah Voigt 2adbc95645 feat(quotesdb): collapsible filter panel on browse page
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>
3 months ago
Elijah Voigt 88f14b00f8 feat(quotesdb): add admin moderation endpoints
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>
3 months ago
Elijah Voigt 5d2cdcae8e feat(quotesdb): add footer with contact email to all pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt f4757a8923 fix(quotesdb): remove spurious X-Auth-Code header, drop unused current param, fix error messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 390f9b6868 feat(quotesdb): admin page auth-first flow, remove admin from nav
- 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>
3 months ago
Elijah Voigt 6c2ee37feb fix(quotesdb): use char count for reason validation, remove duplicate CountRow in d1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt f8c6dcb832 feat(quotesdb): add reports table and POST /api/quotes/:id/report endpoint
- 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>
3 months ago
Elijah Voigt dab88e66ac test(quotesdb): add hidden flag filter tests to native repository
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>
3 months ago
Elijah Voigt 1c0d1eb37f feat(quotesdb): add hidden flag to quotes
- 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>
3 months ago
Elijah Voigt 7d0df10d75 feat(quotesdb): show locked banner on /submit when submissions are closed
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>
3 months ago
Elijah Voigt 0be1193759 fix(quotesdb): fix admin_reset_auth_code call, handle status fetch error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 2d1634c815 fix(quotesdb): fix admin_reset_auth_code call, handle status fetch error
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>
3 months ago
Elijah Voigt d9659a1c00 feat(quotesdb): /admin page component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 37cc5ce443 fix(quotesdb): atomic update_admin_auth_code, fix handler docstring
Replace the two-step read-check-write in update_admin_auth_code with a
single atomic UPDATE … WHERE key = 'admin_auth_code' AND value = ?current
in both NativeRepository and D1Repository. Rows-affected count is checked:
zero means the code was absent or mismatched → DbError::Forbidden; one
means success.

Also remove the now-unnecessary replacement2 clone binding in native.rs.

Fix the reset_auth_code handler doc comment to accurately describe that a
missing X-Admin-Code header is caught by the handler itself (before any DB
call), while a wrong-but-present code reaches the DB layer which returns
DbError::Forbidden.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 8b301d23bb feat(quotesdb): POST /api/admin/reset-auth-code endpoint
Adds handler, route registration, request/response types, and five unit
tests for the admin auth-code rotation endpoint. Updates openapi.yaml
with the new path and a ResetAuthCodeResponse component schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 7619391d43 feat(quotesdb): enforce submission lock on PUT /api/quotes
Add a pre-flight check at the top of create_handler that calls
get_submissions_locked() before processing the request. Returns
423 Locked with {"error": "submissions are closed"} when locked.

Update openapi.yaml to document the 423 response on PUT /api/quotes.

Add three unit tests: locked → 423, unlocked → 201, unlock-then-create → 201.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 64cd35ce1e fix(quotesdb): fix verify_admin_code docstring, add 500 to OpenAPI, make handlers private
- Clarify verify_admin_code docstring to say "standard string equality"
  instead of leaving comparison method implicit
- Add missing "500" response entries to /api/admin/lock and
  /api/admin/unlock in openapi.yaml
- Remove pub from lock_submissions and unlock_submissions to match all
  other handlers in the file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt d693ce18cb feat(quotesdb): POST /api/admin/lock and /api/admin/unlock endpoints
Add two admin-protected endpoints that toggle the global submissions lock:
- POST /api/admin/lock  — sets submissions_locked = true
- POST /api/admin/unlock — sets submissions_locked = false

Both require the X-Admin-Code header and return { "submissions_locked": bool }
on success, or 403 on missing/wrong code. Operation is idempotent.

Shared helper verify_admin_code() fetches and compares the stored admin code.
Routes registered in the router() function. Five unit tests added covering
correct code, wrong code, missing header, and idempotent lock behaviour.

OpenAPI spec updated with AdminCode security scheme, LockResponse schema,
/api/admin/lock and /api/admin/unlock path entries, and an admin tag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt feac146403 feat(quotesdb): GET /api/status public endpoint
Adds the GET /api/status handler that returns {"submissions_locked": bool}.
Registers the route in the router before the quotes routes.
Adds three unit tests covering unlocked state, locked state, and default false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 177a892d94 feat(quotesdb): admin API client functions in UI
Add ApiError::Forbidden variant, StatusResponse / ResetAuthCodeResponse /
LockResponse / ResetAuthCodeBody types, and four new async functions to
src/bin/ui/api.rs:
- get_status()           → GET /api/status
- admin_reset_auth_code() → POST /api/admin/reset-auth-code (X-Admin-Code + X-Auth-Code)
- admin_lock()           → POST /api/admin/lock (X-Admin-Code)
- admin_unlock()         → POST /api/admin/unlock (X-Admin-Code)

HTTP 403 responses map to ApiError::Forbidden in all three admin functions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt a7bd635b59 test(quotesdb): add handler-level tests for DB admin methods
Make MockRepo stateful for admin_auth_code and submissions_locked so
the new QuoteRepository methods can be exercised without a real DB.

Add four tests to src/bin/api/handlers/mod.rs:
- get_submissions_locked returns false by default
- set_submissions_locked(true) then get_submissions_locked returns true
- update_admin_auth_code with correct current succeeds and returns new code
- update_admin_auth_code with wrong current returns Forbidden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 67d247afd1 feat(quotesdb): DB layer — add submissions_locked + update_admin_auth_code
Add three new QuoteRepository trait methods and a seed helper:
- update_admin_auth_code(current, new_code): replaces the admin code if
  `current` matches; generates a fresh passphrase when new_code is None;
  returns DbError::Forbidden on mismatch.
- get_submissions_locked(): reads the submissions_locked key from
  admin_config; returns false when the key is absent.
- set_submissions_locked(locked): upserts "1"/"0" into admin_config.
- seed_submissions_locked(): INSERT OR IGNORE "0" — safe to call on every
  startup without clobbering an active lock.

Implemented in both NativeRepository (rusqlite) and D1Repository (wasm32).
Updated startup seeding in main.rs (native and wasm32 paths) to call
seed_submissions_locked after the existing admin auth code seeding.

Added 7 unit tests in db/native.rs covering all four specified scenarios:
default false, set-then-get, seed does not overwrite, correct code succeeds,
None new_code generates passphrase, wrong code returns Forbidden, stored
code unchanged after Forbidden.

MockRepo in handlers/mod.rs updated with stub implementations of all four
new trait methods to satisfy the trait bound.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 2ccad33921 feat(quotesdb): Cloudflare Turnstile CAPTCHA on submit
- infra/turnstile.tf: provision Turnstile widget (managed mode, quotes.elijah.run domain) with site_key and secret_key outputs
- infra/variables.tf: add var.domain (default: quotes.elijah.run)
- src/lib.rs: add cf_turnstile_token: Option<String> (#[serde(default)]) to CreateQuoteInput; update doctest
- Cargo.toml: add reqwest (0.12, rustls-tls) under native-only dependencies
- src/bin/api/handlers/mod.rs: add verify_turnstile() and CAPTCHA gate in create_handler, both gated on #[cfg(not(target_arch = "wasm32"))]
- src/bin/api/db/native.rs: add cf_turnstile_token: None to all CreateQuoteInput struct literals in tests
- api/openapi.yaml: document cf_turnstile_token field in QuoteCreateRequest schema
- index.html: add Turnstile JS script tag
- src/bin/ui/pages/submit.rs: add turnstile_token state, use_effect_with callback registration, widget div, token included in CreateQuoteInput
- docs/LOCAL_DEV.md: add Cloudflare Turnstile CAPTCHA section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt fb93483f5c feat(quotesdb): date range filter for quotes list
Add 6 optional query parameters to GET /api/quotes:
  date_after_year/month/day and date_before_year/month/day

Changes:
- QuoteRepository::list_quotes gains date_after and date_before params
- NativeRepository and D1Repository build ISO date prefix WHERE clauses;
  quotes with NULL date are excluded when any bound is set
- list_handler validates component ordering (month requires year, etc.)
  and returns 400 on invalid combinations
- build_date_bound helper converts y/m/d components to ISO prefix strings
- UI api::list_quotes and browse page gain From/To year filter inputs
- author page call updated to pass None for the new date params
- openapi.yaml extended with 6 new query parameter entries
- 6 new integration tests covering after, before, range, and 400 cases
- 1 new native DB unit test covering all filter combinations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt caf2246bff feat(quotesdb): admin super auth code for quote moderation
Add an admin_config table storing a single admin auth code that
bypasses per-quote auth checks for update and delete operations.
The code is auto-generated on first startup and printed to stderr.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt c9e4d10934 feat(quotesdb): add workers-rs WASM entry point to api binary
- Gate native Tokio/Axum main() with #[cfg(not(target_arch = "wasm32"))]
- Add #![cfg_attr(target_arch = "wasm32", no_main)] to suppress missing-main error
- Add #[worker::event(fetch)] entry point using worker::HttpRequest / http::Response<axum::body::Body>
- Enable `http` feature on worker dep so fetch handler uses standard http types
- Add axum (json+query features), tower-service, and http to wasm32 deps
- Move async-trait to shared [dependencies] so both targets have it
- Make db::d1 module pub so main.rs can access D1Repository on wasm32
- Fix worker::d1::Database → D1Database and PreparedStatement → D1PreparedStatement
- Add #[cfg_attr(target_arch = "wasm32", worker::send)] to all 7 handler fns
  so their futures satisfy Axum's Handler<Send> bound on single-threaded wasm32
3 months ago
Elijah Voigt 84088ef45b fix(quotesdb): resolve compiler warnings in api and ui
- Remove redundant #![cfg(target_arch="wasm32")] from d1.rs (module
  declaration in mod.rs already gates it)
- Remove unused D1Repository re-export from db/mod.rs
- Drop unused page/total_count fields from UI ListResponse struct
  (only total_pages is consumed by the browse page)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt c52eb820d2 feat(quotesdb): implement D1Repository for Cloudflare Workers
Replace all 7 stub methods in src/bin/api/db/d1.rs with full working
implementations using the Cloudflare D1 API from workers-rs 0.5.

Implements:
- run_migrations: executes four DDL statements via db.exec()
- list_quotes: dynamic WHERE clause with positional params, COUNT query,
  paginated SELECT, per-quote tag fetch
- get_quote: prepared statement with first::<QuoteRow>()
- get_random_quote: ORDER BY RANDOM() LIMIT 1
- create_quote: INSERT + batch tag insert + read-back for timestamps
- update_quote: auth check, dynamic SET clause, optional tag replacement,
  read-back of updated row
- delete_quote: auth check, DELETE, returns DeleteResult enum

Also adds helper structs (QuoteRow, AuthRow, TagRow, CountRow),
fetch_tags() helper method, and unsafe Send/Sync impls required for
Arc<dyn QuoteRepository + Send + Sync> on single-threaded wasm32.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt cd3afa1920 fix(quotesdb): home page friendly empty state when no quotes
Detect 404 from random quote endpoint and render a friendly
'Nothing here yet — Submit a quote!' message with a link to
/submit instead of the generic error display.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 8086ca06a1 fix(quotesdb): submit form UX fixes (6a4c61, dfd185, 5b3475)
- Replace 'Submit another' link with 'Browse all quotes' -> Route::Browse
- Change date input from type=text to type=date, remove placeholder
- Make author optional (defaults to Anonymous), update label
- Clarify auth code label to 'Edit/delete passphrase'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 739e6b8476 test(quotesdb): add integration test suite for all API endpoints
Adds 26 integration tests in handlers::integration_tests using a real
NativeRepository backed by a NamedTempFile SQLite database. Each test
gets an isolated database via test_router(). tempfile added to
[dev-dependencies]. Closes tickets: 5f5ba0, 9b581f, 789d0f, aa0eab,
f9f448, 4a4c26, 93f1b6, fae330, 8c87db, 893eba, e8f5cf, ce1e4f.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt d4be4653d8 fix(quotesdb): gate UI binary on wasm32, update UI ticket statuses
- main.rs: add #[cfg(target_arch = "wasm32")] guards on all wasm32-only
  code; add stub fn main() for native targets so cargo check passes
- Ticket statuses updated for all completed UI page/component tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 92e0f691cf feat(quotesdb): add comprehensive BEM stylesheet for all UI components
Covers all component and page classes:
- Navigation bar (nav, nav__brand, nav__links, nav__link)
- QuoteCard (quote-card, quote-card__text, footer, author, source, tags, tag, link)
- ErrorDisplay (error-display, error-display__message)
- Pagination (pagination, pagination__btn, pagination__info)
- AuthModal (auth-modal__overlay, auth-modal, auth-modal__title/input/actions)
- All page containers (page-home, page-browse, page-quote, page-author, page-submit)
- Edit form, submit form, success state with auth code box
- Base styles: reset, typography, buttons, form inputs
- Responsive breakpoint at 640px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 5aa31b59f0 feat(quotesdb): implement all UI page components (Home, Browse, QuoteDetail, Author, Submit)
- HomePage: fetches random quote on mount, displays with QuoteCard and browse/submit links
- BrowsePage: paginated list with author and tag filter inputs, Pagination component
- QuotePage: view/edit/delete with AuthModal gating, 403/404 handling, sessionStorage auth
- AuthorPage: lists quotes by author with tag filter and pagination
- SubmitPage: full form with all fields, success state showing auth code prominently
- Tag filter (d3d502) integrated into Browse and Author pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt a5b7c8d856 feat(quotesdb): implement API DB layer and all HTTP handlers
DB layer (src/bin/api/db/):
- native.rs: NativeRepository (tokio-rusqlite) implementing all CRUD ops,
  dynamic WHERE for filters, two-phase auth check for update, 13 unit tests
- d1.rs: D1Repository wasm32 stub (all methods return Internal error)
- connection.rs: open() helper — WAL + foreign_keys pragmas
- mod.rs: cfg-gate async_trait (Send on native, ?Send on wasm32)

Handlers (src/bin/api/handlers/mod.rs):
- All 7 routes: GET /api/, random, {id}, list, PUT create, POST update, DELETE
- Router order: random BEFORE {id} (prevents "random" matching as id)
- Auth: X-Auth-Code header validation → 403 on mismatch
- 13 handler unit tests with MockRepo

main.rs: opens DB, runs migrations, wraps in Arc<dyn Repo + Send + Sync>,
  binds on $PORT (default 3000)

Cargo.toml: tower dev-dep for ServiceExt::oneshot in tests

All 32 tests pass (26 api + 6 lib)

Tickets closed: 00aff0 a5049d 6e829e 28e7d9 886bfd 2ce22e 5dbb7d 05f8ae
                d792e2 5d9f5a b20b5a 175382 03bb91

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 682d15b40d feat(quotesdb): implement UI app shell, API client, storage, and base components
- Add BrowserRouter with all 5 routes (Home, Browse, QuoteDetail, Author, Submit)
- Implement typed API client (list, get, random, create, update, delete)
- Implement sessionStorage auth code helpers (set/get/clear)
- Add ErrorDisplay, QuoteCard, AuthModal, Pagination components
- Add stub page components for initial compilation
- Fix Cargo.toml: uuid js feature for wasm32, getrandom 0.3 wasm_js for rand dep,
  js-sys and Storage web-sys feature for API client and storage module

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt ea6fa981fc feat(quotesdb): add QuoteRepository trait, migrations, and thiserror dependency
- db/mod.rs: QuoteRepository async trait + ListResult/DeleteResult/DbError types
- db/migrations.rs: SQL DDL strings for quotes, quote_tags, and indexes
- lib.rs: fix rand 0.9 trait import (SliceRandom → IndexedRandom)
- Cargo.toml: add thiserror = "2" for DbError derive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt 66e5302d9b feat(quotesdb): add generate_id() using UUID v4 — WASM-compatible ID generation
- Add uuid = { version = "1", features = ["v4", "serde"] } to [dependencies]
- Add getrandom = { version = "0.4", features = ["wasm_js"] } under wasm32 cfg
- Implement generate_id() in src/lib.rs with rustdoc and doctest
- Add unit tests for length, hyphen count, and uniqueness

Closes ticket 7a0d9f

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago
Elijah Voigt bd0c2af085 refactor(quotesdb): collapse to single crate with api and ui binaries
Replaces the three separate sub-crates (api/, ui/, tests/) with a single
Cargo crate at the quotesdb/ root. Shared code lives in src/lib.rs; the
api and ui are multi-binary targets; integration tests use the standard
Cargo tests/ layout. Trunk files moved to project root with data-bin="ui".

Closes ticket b38032.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3 months ago