# Meads (`md`) Task Tracking Context ## Overview `md` is a git-native task tracker that stores all tasks in a single file — `TASKS.md` (Markdown) or `TASKS.csv` (CSV). No database, no config files — just your task file and git. ## Essential Commands ### Finding Work - `md ready` - Show open tasks not blocked by dependencies (sorted by priority) - `md list` - List all tasks - `md list --json` - List all tasks as JSON - `md get ` - Get a specific task - `md get --json ` - Get a specific task as JSON ### Creating Tasks - `md add "Fix the login bug"` - Add a simple task - `md add "bug: Fix login P1. Session cookie expires"` - Rich input parsing - Type prefix: `bug:`, `task:`, `feature:` (optional) - Priority: `P0`-`P9` (0=critical, 4=backlog, default=P2) - Title: everything before first `.` - Description: everything after the `.` - `md add --title="Fix login" --type=bug --priority=P1 --description="Details here"` - Flag-based - `md add --title="Fix login" --description-file=/path/to/notes.md` - Description from file ### Updating Tasks - `md update --status=draft|open|inprogress|closed` - Update status - `md update --priority=P1` - Update priority - `md update --title="New title"` - Update title - `md update --description-file=/path/to/notes.md` - Update description from file - `md set-status ` - Shorthand for status changes - `md del ` - Delete a task ### Dependencies - `md add-dep ` - Make child depend on parent - `md add --depends-on= "Task title"` - Add task with dependency - Tasks blocked by unclosed dependencies are excluded from `md ready` ## Common Workflows **Starting a session:** ```bash md ready # Find available work md get # Review task details md set-status inprogress # Claim it ``` **Creating dependent tasks:** ```bash md add "feature: Build API endpoint" # Returns ID, e.g. 5 md add "Write tests for API. Cover edge cases" --depends-on=5 ``` **Completing work:** ```bash md set-status closed # Mark task done ``` ## Rules - **Do NOT read or edit the task file (TASKS.md or TASKS.csv) directly** - always use `md` commands to read and modify tasks - **ALWAYS include changes to the task file in the next git commit after task is closed** - Task IDs are auto-assigned integers - Git handles versioning and history - The file uses optimistic locking for concurrent access # Front-end Testing Run `./dev.ts e2e` for the automated UI → realtime sync → verified-content contract. It uses isolated Playwright Chromium and a pinned local fixture; `--public-fixture` adds the public Big Buck Bunny poster check. For exploratory front-end / UI testing, use the **Chrome DevTools MCP** when available. Drive the running app in a real browser to verify UI behaviour and end-to-end flows — e.g. add a torrent in the Omni bar → track progress in the Torrent engine → download a file — rather than asserting from code alone. Reach for the Chrome DevTools MCP when manually validating the Vue front-end or confirming a UI change works. When a screenshot successfully validates a task, send it to the user via the announce endpoint (`https://announce.jpillora.com`, with an `attachment` — see the global instructions for image attachments). # Project Architecture (the `airewrite` rewrite) Ground-up rewrite of cloud-torrent preserving the original UX. **Go** backend (`anacrolix/torrent` engine, `jpillora/velox` realtime sync) + **Vue 3 + Vite + TypeScript** frontend, compiled to a **single binary** (the built SPA is `go:embed`-ed). Go structs are the single source of truth — TS types are **generated** from them with `tygo`. See `SPEC.md` for the full spec. The **`master` branch is the parity oracle** — `git show master:` to compare; it's the original AngularJS + Semantic-UI build. Layout: `main.go` (CLI via jpillora/opts) → `server/` (state/server/api/files/ stats/search) → `engine/` (anacrolix wrapper) → `static/` (embeds `dist/`) → `frontend/src/` (Vue). `.dev/tygo.yaml` drives type-gen; `dev.ts` has `gen-types`/`check-types`. ## Build / run / test (and the CWD trap) ``` npm --prefix frontend install # first time npm --prefix frontend run build # vue-tsc + vite -> static/dist (NOT frontend/dist) npm --prefix frontend run dev # vite :5173, proxies /sync /api /download /js/velox.js -> :3000 go build -o /tmp/ct . # from REPO ROOT; embeds static/dist go test ./... # unit (fast, hermetic) go test -tags e2e -run E2E ./server/... # live core-loop E2E (downloads a real fixture, ~6s) ./dev.ts check-types # regen types + git diff --exit-code (CI guard) ``` - **CWD trap:** the Bash tool's working dir persists. `cd frontend && npm build` leaves you in `frontend/`, so a following `go build .` runs there and fails ("no Go files"). Always `npm --prefix frontend …` from the repo root. - **Rebuild order:** the binary embeds `static/dist` **at compile time** — after any frontend change you MUST rebuild the frontend *then* `go build` again, or you'll serve stale assets. - For browser testing, run the binary from a scratch dir (e.g. `/tmp/ct-test`) so `downloads/` + `cloud-torrent.json` don't litter the repo. # Hard-won learnings (read before you trip on them) ## Build & embed - Frontend builds to `../static/dist` (vite `outDir`), not `frontend/dist` — because `go:embed` **can't traverse `..`**, so the embed must live under `static/`. The committed `static/dist/.gitkeep` (+ `//go:embed all:dist`) keeps `go build` compiling before any frontend build. `static/dist/*` is gitignored. ## Type generation (tygo) - tygo is a **go tool**: `go tool tygo generate --config .dev/tygo.yaml` (via `./dev.ts gen-types`). - It **overwrites a shared output**, so each Go package emits its own `*.gen.ts` and `frontend/src/types/generated.ts` is a **hand-written barrel** re-exporting them. Don't edit `*.gen.ts`. - **Cross-package field types become `any`** unless mapped (e.g. `engine.Config` in `State` → fixed via frontmatter import + `type_mappings`). `scraper.Config` is intentionally left `any`. `time.Time → string`. - Non-data structs (`server.Server`, `engine.Engine`) are excluded via `exclude_files`. After changing any **synced** struct, run `./dev.ts check-types` and commit the regenerated `*.gen.ts` (CI fails if stale). ## velox realtime sync (`/sync`) - Velox v0.12.1 negotiates protocol v3 (`p=3`): per-element array operations keep file-progress deltas small and preserve unchanged frontend item identities. The embedded Go handler and served JS client must be upgraded together. - Bind the **embedded** `velox.State` with `velox.SyncHandler(&s.state)` once at startup (NOT `velox.Sync`, which wraps a separate State so `state.Push()` won't reach clients). `SyncHandler` must run **before any `Push()`**. - velox is **SSE by default**; its built-in auto-retry is **opaque** — no waiting/attempting signal and it **swallows SSE connection errors**. We disable it (`{retry:false}`) and drive the reconnect loop ourselves in `frontend/src/ velox.ts` (backoff + an attempt-timeout backstop, since velox never reports a failed SSE attempt; surfaced via `ConnStatus`). Retry-on-`online`/refocus too. - **`/sync` bypasses outer gzip.** Velox owns SSE compression/flush and needs the raw writer for send deadlines. requestlog/v2 exposes Unwrap for those deadlines too. Other responses use minSize 0 and a compressible MIME allowlist; media/archive downloads bypass gzip. - **HTTP/2 must stay disabled** (`http.Server{TLSNextProto: map[...]{}}`) — velox streaming is incompatible with it. Matters under `--cert/--key` TLS. ## Torrent engine (anacrolix) — see `engine/` - Engine owns its mutable cache/config/client behind one mutex. `*Locked` helpers require that lock. `GetTorrents` returns detached torrent/file DTO snapshots; API actions explicitly publish a fresh snapshot before pushing realtime state. - AutoStart-only configuration changes keep the live client. Client-level changes validate the directory/port, preserve metainfo and file start flags, and rebind torrents to the replacement client. Failure restores the previous configuration. anacrolix closes listener sockets asynchronously, so only bind conflicts get a bounded retry; there is no unconditional restart sleep. - `DisableEncryption=true` requires plaintext peer transport (no obfuscation); false retains upstream encryption preference with plaintext fallback. - Whole-torrent Stop disables data upload/download and clears file priorities while retaining metadata and its live handle. Start restores priorities and resumes; only Delete drops the handle. - `DeleteTorrent` drops the torrent but does **NOT** delete files on disk (master parity). `StopFile` returns `"unsupported"` (no per-file pause). The UI shows Selected instead of an unsupported Stop action and directs users to the whole-torrent Stop control. - `str2ih` guards `len==40` before hex-decoding (a longer string panics the fixed-size array). `percent()`/`DownloadRate` formulas are byte-identical to master — don't "simplify" them. - Add → `<-GotInfo()` → auto-start according to the initial `AutoStart` policy. An explicit Start before metadata sets pending whole-torrent intent too; the metadata callback schedules files after they exist. Stop cancels that intent, and reconfiguration preserves it. Partial file starts clear whole-torrent intent. - **`anacrolix/torrent` does NOT compile on OpenBSD** (`unix.SEEK_DATA` undefined) — OpenBSD is dropped from the goreleaser matrix. - E2E timing: poll for `Percent > 0`, not a fully-complete file — piece order can leave any single file < 100% for ages. Tests that spin a real client touch the network (DHT) and are `-short`/`e2e`-gated. ## Configuration persistence - Saves serialize the engine transition and file replacement. Stage and sync JSON beside the destination before applying settings; atomically rename afterward. A replacement failure restores the previous engine config and returns an error. Startup accepts a missing config, but reports read/parse failures. ## HTTP server chain — `server/server.go` - `handler()` composes exactly as master: route mux → **gzip** (whole chain, minSize 0) → **cookieauth** (`--auth user:pass`) → **requestlog** (`--log`), log outermost. TLS via `--cert-path`/`--key-path` (both required). - `/download/` uses `os.Root` in `files.go` to confine reads, archives and deletion during filesystem resolution. Relative symlinks within the root work; external/absolute symlinks and root requests are rejected. Archives omit symlinks; deleting an internal symlink removes the link, retaining its target. - Active incomplete files (including upstream `.part` names) and their ancestors cannot be deleted. `Engine.RemoveDownload` holds the engine lock across the policy check and confined removal, protecting against concurrent starts and config changes; HTTP returns 409 until the torrent is stopped. The frontend active-path index includes ancestors and `.part` paths. - The **download directory is config-file/UI only — there is no CLI flag.** Engine port defaults to **50007**; HTTP port to **3000**. ## Search backend (`server/search.go`) — it scrapes live sites - Providers come from a **remote config** (a gist) fetched on startup + every 30 min, replacing the embedded default. So the live provider set differs from the embedded one. - **Providers are flaky/dead** — they scrape real torrent sites that churn domains (e.g. limetorrents → NXDOMAIN, tpb removed). A failing search is almost always the provider, not a bug. `nyaa` is usually up for testing. - `jpillora/scraper` returns `{"error": "..."}` for failures but **does NOT escape quotes** in the message (Go quotes URLs), so the body is often **invalid JSON**. The search client (`api.ts`) parses leniently (regex fallback) — don't assume `res.json()` succeeds on an error response. ## Frontend / Vue conventions — `frontend/src/` - **Icons: `@mdi/js` paths via `components/Icon.vue`** (inline ``). Tree-shaken, and they inherit `currentColor` so `.green`/`.red` state colours actually work (emoji ignore CSS colour — don't reintroduce emoji icons). - **Async action buttons use `components/AsyncButton.vue`** (`:action` runs, button disables + spins until it settles; works inside `v-for` because each instance owns its state; uncaught errors → global banner). Use it for anything that hits the API. - Cross-component view state (in-flight request count, config/omni toggles, global error banner) lives in `ui.ts`; synced server data in `state.ts`. `api.ts` bumps `ui.requests` around every request (header spinner). - `u\NNNN` escapes only work in `