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
# Ketchup

A personal tool for tracking recurring tasks and catching up on what's overdue.

## Overview

Ketchup tracks recurring obligations — "Call Mom every 2 weeks," "Renew passport every 10 years" — and surfaces what's overdue so nothing slips through the cracks. It's a personal tool for me and my family, not a SaaS product.

There's no login screen. Users are identified by a reverse proxy header (`AUTH_HEADER`, defaults to `Remote-User`), so authentication is handled at the network layer — originally Tailscale, but any authenticating proxy works.

The stack is deliberately simple:

- **Roda** — routing
- **Sequel** + **SQLite** — persistence
- **Phlex** — views
- **Alpine.js** — client-side reactivity
- **OverType** — inline markdown editing
- **Puma** — app server
- **Sentry** / **OpenTelemetry** — optional observability

## Domain model

A **Series** defines a recurring obligation — "Call Mom every 2 weeks." It holds a markdown note, an interval unit (day/week/month/quarter/year), and an interval count. The note's first line serves as the display name.

Each Series has one active **Task** at a time. Completing a task creates the next one, with a due date advanced by the interval from today. Tasks hold optional per-completion notes.

A **User** owns Series and, transitively, Tasks. The `many_through_many` association on User provides direct task access for ownership scoping.

There's a [screenshot gallery](https://kejadlen.github.io/ketchup/) — it's auto-generated for development, not a showcase, but it gives a sense of the UI.

## Primary use case

Catching up with friends and family — some weekly, some quarterly, some yearly.

## Development

Largely vibe-coded with Claude Code. I steer direction and make design calls; Claude writes most of the code.

### Setup

```
bundle install
rake seed       # populate sample data (requires visiting the app first to create a user)
rake dev        # start dev server with auto-restart, served via Tailscale
```

### Testing

```
rake test       # Minitest suite (the default rake task)
```

### Visual snapshots

Ferrum captures headless Chrome screenshots of the app in key states. The snapshot tasks compare current screenshots against the baseline from the latest GitHub release.

```
rake snapshots:capture   # take screenshots
rake snapshots:diff      # capture and generate a side-by-side diff viewer
rake snapshots:review    # capture, diff, and open in browser
rake snapshots:gallery   # generate an HTML gallery
```

### CI/CD

GitHub Actions runs tests, builds a Docker image tagged `YYYYMMDD-<sha>`, and pushes it to `ghcr.io`. Each push to main also creates a GitHub release with snapshot artifacts. A separate workflow deploys a snapshot gallery to GitHub Pages.

### Docker

```
docker build -t ketchup .
docker run -p 9292:9292 -e TZ=America/Los_Angeles ketchup
```

> [!IMPORTANT]
> Set `TZ` to match the users' timezone. The app uses `Date.today` to decide what's overdue — if the container's timezone is wrong, tasks will flip between overdue and upcoming at the wrong time of day.