- Turn the Today card into a day navigator (‹/› arrows + "back to today")
so users can step back and check off a day they forgot. Editable range is
the active image's start through today; reveal recomputes from the whole
log, so backfilling a missed day re-bridges that habit's streak and paints
the pixels it earns.
- Add a Changelog tab to Settings (after About): dated release notes distilled
from the project's beans, one concise sentence each, newest first.
- Add a "Feedback? Email pop@elijah.run" line to the About tab.
Updated DESIGN.md and ARCHITECTURE.md to match. Validated with Playwright
(backfill streak-bridging 4→10px, nav bounds, changelog render) — no console
errors, no non-file:// requests.
Replace the global day-level streak + triangular(checks) multiplier
with per-habit streaks: each habit tracks its own consecutive-day
streak and reveals pixels equal to that streak length when checked.
Removes the global streak header/stat chip in favor of an inline
streak badge on each habit row.
Beans records for the triangular per-day reveal work (Nth habit check
of the day reveals N units): vibed-fdph (completed, with the shipped
implementation summary) and a duplicate vibed-i315 opened in the same
session. Retained for the historical task record.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Milestone vibed-f05l and its seven subtasks laying out a new Rust CLI
binary that queries howlongtobeat.com and prints completion times.
Covers project init, request/response types, CLI arg parsing, the
HTTP search call, output formatting, and a validation/smoke-test pass.
No implementation yet — planning only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds .claude/settings.json opting bit-by-bit into the frontend-design
plugin for future UI/design work on the pixel-art tracker.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop the pixelsPerCheck calibration constant that scaled units to a
fixed "perfect month" pace regardless of image size. 1 unit now equals
1 pixel directly (Nth check of the day reveals N pixels, multiplied by
that day's streak length), so bigger images genuinely take longer and
the numbers in the UI match what's actually happening. Also removes
the now-obsolete "~N days to finish" upload estimate and corrects the
About copy, which previously implied the streak multiplier applied
directly to pixel counts.
- Reward is now streak-multiplied: units per day = streakOnDay(d) × triangular(checks);
recalibrate pixelsPerCheck (÷ triangular(30)) and the upload finish-day estimate.
- Rename the "Backup" settings tab to "Data" and add a double-confirm "Wipe all data" action.
- Add a page footer disclosing authorship (Claude Code — Sonnet 5, Opus 4.8 & Fable 5).
- About page now explains exactly how pixels are earned (per-check escalation + streak multiplier).
- Allow uploading an image during first-run onboarding, not just presets.
- Update DESIGN.md, ARCHITECTURE.md, README.md to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add Paint by Habits, a local-only single-file HTML habit tracker that
reveals a pixel-art image as daily habits are checked off.
The browser-tab icon is a 64x64 miniature of the current artwork —
revealed pixels solid, unrevealed ones a two-tone checker — drawn into
an offscreen canvas and set as a data-URL PNG, so the tab fills in with
the art and the page still makes zero network requests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Updating a page meant deleting it and re-creating it under the same name.
Add PUT /api/pages/:slug, which replaces an existing page's content while
keeping its URL, owner, trust flag, and created_at.
Authorized to owner-or-admin (core::origin::can_replace, same rule as
delete) *and* can_upload, so a revoked uploader can't keep pushing content
to pages they already own.
The replace can't roll back the way create does — R2 has no multi-object
transaction and the previous bytes aren't retained — so the ordering bounds
the damage instead: new objects are written before any old key is deleted (a
viewer mid-update sees old-or-new per file, never a blank page), stale keys
are pruned last and best-effort (a failure only orphans an object), and the
D1 stats are refreshed only once R2 succeeds. Design §4.6b covers the
reasoning; §5's "no page overwrite" scope cut is retired.
In the UI, the always-visible upload card becomes a modal driven by two
entry points: "+ New Page" (create) and a per-row "Update" (replace, with
the slug pre-filled and read-only).
Verified end to end against wrangler dev with local D1/R2 — create, serve,
replace, stale assets pruned, stats/trust/ownership preserved, plus the
401/403/404/400/413 paths — and the UI driven in jsdom (37 assertions).
152 unit + 19 doctests, both-target clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Micro static-site host running entirely on Cloudflare: a Rust->wasm
Worker (workers-rs 0.7 + axum 0.8) serving user-uploaded .html/.zip
content from R2 at pages.elijah.run/<slug>/, with D1 for users/
sessions/page metadata and GitHub OAuth for login. Admins approve
uploaders, delete pages, and can mark pages/users trusted.
Security design (docs/plans/2026-07-12-pages-design.md): hosted
content is served with a CSP sandbox (opaque origin — no cookies or
storage, cannot make credentialed /api calls) unless admin-trusted;
strict slug grammar + reserved names; streaming zip validation
(traversal, bombs, size/entry/depth limits); SHA-256-hashed session
tokens in __Host- cookies; Origin checks on all mutations; upload
quotas and size caps.
Includes OpenTofu infra (R2, D1, DNS; wrangler owns the Worker
deploy), an embedded single-file management UI, deployment guide,
and 148 native unit tests + 18 doctests (clippy -D warnings clean on
native and wasm32). Deployed and verified in production.
Squash of 15 commits from pages-v0: each ticket implemented by a
Claude Sonnet agent, reviewed by a Claude Opus agent, tracked with
beans (pages-lxjz..pages-shqc).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Archive self-play beans, update shader/LLM parent beans to completed,
and add self-play chapter content to ml-self-play.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Markov epic (edu-svom) incorrectly used blocked-by relationships to
reference its 10 section tasks. Replaced with proper parent-child
hierarchy: removed blocked-by entries from the epic and set parent on
each section task.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds llm-from-scratch.md stub with 14 sections (GPT-1 style: character
tokenisation, self-attention, transformer block, candle model, training
loop, sampling). Creates beans edu-32xl through edu-9sb7 for each section.
Add edu/src/ml-self-play.md with 14 stubbed sections across 5 parts:
foundations (RL, MCTS, self-play), game engine (Tic-Tac-Toe), MCTS
implementation, neural network integration, and the full self-play loop.
Create one beans ticket per section (edu-wobk through edu-brtk).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add shaders.md with 18 stub sections across 6 parts (GPU model,
wgpu setup, vertex/fragment shaders, textures, compute shaders,
going further). Create beans tickets edu-5g0l through edu-7m8d
for each section. Add # Graphics section to SUMMARY.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>