1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# Agents

Read the README for project context, domain concepts, and design intent.

The project backlog is managed with `ranger` (default backlog: `ketchup`). Use the `ranger` skill for commands and workflow.

## Layout

```
lib/
  ketchup/
    config.rb        # Config data object + CONFIG constant, reads env vars
    db.rb            # Sequel connection + auto-migration
    models.rb        # User, Series, Task models and associations
    seed.rb          # Seed.call(user:, series:) — creates series, tasks, and history
    snapshots.rb     # Ferrum-driven headless screenshot capture
    web.rb           # Roda app (routes, current_user from auth header)
    views/
      layout.rb      # Phlex base layout (head, nav, body wrapper)
      dashboard.rb   # Main view: overdue, upcoming, series detail/new sidebar
      series/
        new.rb       # Standalone new-series form page
  sequel/
    plugins/
      sole.rb        # Custom Sequel plugin: Dataset#sole
db/
  migrate/           # Sequel migrations (numbered)
test/
  test_db.rb         # Schema constraint tests
  test_web.rb        # Minitest + Rack::Test integration tests
  test_sole.rb       # Sole plugin tests
  test_seed.rb       # Seed module tests
templates/           # ERB templates for snapshot diff and gallery viewers
public/
  js/app.js          # Alpine components, OverType editor setup
  css/               # Static stylesheets
config.ru            # Rack entrypoint (OTel, Sentry, Web.app)
```

## Running

```sh
rake                    # runs tests, type checking, and binstubs (default)
rake test               # tests only (use this to run tests, not ruby directly)
rake check              # rbs-inline + steep check
rake dev                # starts dev server with Tailscale serve + auto-restart via entr
rake seed               # seeds database with sample series and tasks
rake snapshots:capture  # headless Chrome screenshots of key app states
rake snapshots:diff     # compare current screenshots against latest release baseline
rake snapshots:review   # capture, diff, and open in browser
rake snapshots:gallery  # generate an HTML gallery of screenshots
```

Binstubs are installed to `.direnv/`, which direnv adds to `$PATH`. **Never use `bundle exec`** — run commands directly (`rake`, `rbs-inline`, `steep`, etc.).

Tests set `DATABASE_URL=:memory:` so they never touch the real database.

## Snapshots

Headless Chrome screenshots for visual review after UI changes. `Capture#run_capture` in `lib/ketchup/snapshots.rb` scripts a browser session against an in-memory database — no real data is touched.

Run `rake snapshots:review` to capture screenshots and open a side-by-side diff against the baseline from the latest GitHub release. CI uploads new baselines on each push to main.

To add a snapshot, add a `snap("name")` call in `run_capture`. Pass a block for navigation before the screenshot, or `selector:` to capture a single element instead of the full page:

```ruby
snap("my-state") do
  goto @base
  wait_for(".some-element")
end

snap("just-sidebar", selector: ".column-aside")
```

Output goes to `~/.cache/ketchup/snapshots/` (or `$XDG_CACHE_HOME`). Templates for the diff and gallery viewers live in `templates/`.

## Conventions

- **Views:** Phlex component classes under `lib/ketchup/views/`, not ERB templates.
- **Migrations:** Sequel migrations in `db/migrate/`, numbered sequentially (`001_`, `002_`, …). Migrations auto-run on boot.
- **User identification:** Current user from a single auth header (`AUTH_HEADER` env var, defaults to `Remote-User`). Set `AUTH_HEADER=Tailscale-User-Login` for Tailscale deployments.
- **Testing:** Minitest with `Rack::Test`. Fake auth headers via helper.
- **Client-side:** Alpine.js for reactivity, Alpine Persist for state persistence, OverType for markdown editing. No build step — all loaded via CDN with pinned versions and SRI hashes in `views/layout.rb`. See [Updating CDN dependencies](#updating-cdn-dependencies) below.
- **Ownership scoping:** User has `many_through_many :tasks` through `:series`. Routes use `@user.tasks_dataset` and `@user.series_dataset` to scope lookups.
- **Changelog:** This project does not maintain a changelog. Do not create or update one.
- **Observability:** OpenTelemetry with Rack instrumentation, gated on `OTEL_EXPORTER_OTLP_ENDPOINT`. The SDK reads standard `OTEL_EXPORTER_OTLP_*` env vars directly — no app-level proxying. No-op when unset.

## Updating CDN dependencies

CDN scripts in `lib/ketchup/views/layout.rb` use [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes. Renovate bumps the version in the `src` URL but cannot update the SRI hash. To complete a Renovate CDN PR:

1. Check out the Renovate branch: `jj new <bookmark>`
2. Run `rake cdn:rehash` to regenerate integrity hashes
3. Commit, push to the branch, and merge the PR