Files
jpmschweitzerandClaude 2baf8b4ba3 docs: qualify workspace decision ids cited from this repo
Decision ids are per-vault sequences, so they collide by construction
once there is more than one vault -- and every repo now has one. A bare
D-15 here will mean this repo's D-15 the moment this repo records one.
Cross-vault references are therefore qualified: workspace D-15.

Not hypothetical: pql holds D-1 through D-31 while the workspace holds
D-1 through D-21, so every workspace id currently collides with an
unrelated pql one. A bare id is not wrong the day it is written -- it
decays into wrong as the other vault grows, and nothing flags it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-09 04:17:07 +02:00

164 lines
7.9 KiB
Markdown

# CLAUDE.md — tatlock-ui
Flutter/Dart frontend for the homelab — the dashboard at `home.schweitz.net`. Riverpod state,
Material 3, Go-Router. Built as a **Flutter web** app, served as compiled static assets by nginx
in the `tatlock-ui` container. `pubspec.yaml` version **1.7.1+1**, package name `tatlock_ui`,
Dart SDK `^3.10.4`.
It is a **client**, not a service. It talks to core-api (:8083) and tatlock (:8000); it exposes
no API of its own and has no `/openapi.json`.
## Read first
- **[PHILOSOPHY.md](PHILOSOPHY.md)** — the system vision and the architectural patterns all work
should move toward. The previous AGENTS.md made this a mandatory pre-work read and that
requirement is carried forward deliberately.
- **[docs/UI_LAYOUT.md](docs/UI_LAYOUT.md)** — **read before touching the widget tree.** Check
`lib/shared/components/` for an existing component before building a new one, and look at how a
comparable feature already does it. Deviating from the established patterns is the main source
of drift here.
- `docs/` also holds `ARCHITECTURE.md`, `API_INTEGRATION.md`, `TESTING.md`, `THEMING.md`,
`DATAGRID.md`, `DEPLOYMENT.md`.
## Ports and where it runs
| | |
|---|---|
| Container | `tatlock-ui`, `127.0.0.1:9999 -> 80` (nginx serving the web build) |
| External | `home.schweitz.net` |
| Backends | core-api `:8083`, tatlock `:8000` |
The old AGENTS.md said the app is "available at `http://tower:8092`". **That is stale** — the
published port is 9999, verified against `docker ps` on 2026-08-09.
It also pointed at `portainer-core` for full-stack documentation. **`portainer-core` is
deprecated** and must not be used as a source of infra facts; it was merged into
`system-admin-toj/containers/`. The live inventory is `CONTAINERS.md` there.
The Gitea SSO-bypass trick is real and still works: `http://localhost:3002` reaches Gitea
directly, verified returning `{"version":"1.27.1"}`. Useful for reading a sibling repo's raw
files without going through Authentik.
## Layout
`lib/main.dart``lib/app.dart`; `lib/core/` (api, auth, config, error, providers, semantics,
theme), `lib/features/<room>/` (control_room, front_hall, media_room, parlor, security,
settings), `lib/routing/`, `lib/shared/` (components, layouts, theme, widgets). 132 Dart files
under `lib/`, 24 test files.
## Establishing what is live — and the trap in it
There is no `sys.modules` here and nothing to `docker exec` into: the container holds compiled
assets, not source. The Dart analogue is a transitive walk of `import`/`export`/`part`
directives from `lib/main.dart`, resolving `package:tatlock_ui/…` to `lib/…`. Run 2026-08-09:
124 of 132 files reachable, 8 not.
**Do not read that as a delete list. Five of the eight are the code that actually runs in
production.** They are conditional-import targets:
```dart
import 'api_client_native.dart' if (dart.library.html) 'api_client_web.dart';
```
A naive walk captures the *first* string and misses the branch. Since this app ships as Flutter
**web**, the `_web.dart` half is the live one and the `_stub`/`_native` half is the dormant one —
the exact inverse of what the reachability count suggests. The five: `api_client_web.dart`,
`web_utils_web.dart`, `url_strategy_web.dart`, `url_state_web.dart`,
`iframe_view_web.dart`. Find them all with `grep -rn "if (dart.library" lib/`.
That leaves three genuinely unreferenced files, and they are **not** all the same thing:
| File | Status |
|---|---|
| `lib/core/auth/permission_gate.dart` | no reference anywhere in `lib/` or `test/` |
| `lib/core/semantics/semantic_widget.dart` | no reference anywhere in `lib/` or `test/` |
| `lib/features/control_room/stacks/data/models/stack_model.dart` | **referenced only by its own test** |
The third is the interesting one: `stack_model_test.dart` imports and exercises it, so the suite
is green and gives confidence about a model the app never uses. A passing test is not evidence a
thing is wired in.
Before deleting any of the three, check whether it is intended groundwork rather than debris —
`TODO_AUTH_REFACTOR.md` describes an unimplemented auth redesign, and `permission_gate.dart` sits
squarely in that area. Neither that file nor `PLAN.md` mentions it by name, so its status is
**undetermined**, not dead. Ask before removing.
## Tooling
`flutter` and `dart` resolve from `/snap/bin`, which **is** on the non-interactive `PATH` — so
bare commands work here (unlike `pql`, which needs its absolute path).
```bash
flutter pub get
flutter test # 24 test files
flutter analyze # static analysis; analysis_options.yaml at the repo root
flutter build web --release
```
**Always add tests for new code before committing** — happy path plus key edge cases, widget
tests for widgets, unit tests for logic. Coverage should not decrease. Carried over from the
previous AGENTS.md, which stated it as "no exceptions".
Note there is **no CI test gate**: `.gitea/workflows/build.yml` triggers only on `v*` tag push
and goes straight to build and release. `flutter test` runs locally or not at all.
## Work tracking
Work lives in **pql**, not a markdown TODO. **This repo's vault is standalone** — its tickets and
its internal decisions live here in `.pql/` and `governance/`, and travel with a clone, because
`.pql/changelog/` is committed and replayed by the git hooks (workspace D-15). The databases are gitignored
and rebuildable with `pql plan rebuild`.
`pql` is **not** on the non-interactive `PATH` — invoke it as `/home/jpmschweitzer/.local/bin/pql`.
From inside this repo no `--vault` is needed: pql anchors at the nearest `.git/` ancestor, which
is this repo.
```bash
/home/jpmschweitzer/.local/bin/pql ticket list # this repo's open work
/home/jpmschweitzer/.local/bin/pql plan whatsnext # next unblocked item, with context
/home/jpmschweitzer/.local/bin/pql decisions list # this repo's own decisions
```
Stack-level decisions that constrain this app live in the **workspace** vault and need the flag:
```bash
/home/jpmschweitzer/.local/bin/pql --vault /mnt/media/Projects decisions list --domain tatlock-ui
```
Note `ticket new --decision D-N` resolves ids within **one** vault, so a ticket here cannot link
to a workspace decision. Cite the id in the ticket body instead.
`PLAN.md` and `TODO_AUTH_REFACTOR.md` predate this convention. Treat them as research notes;
new work goes in pql.
## Git
- **History is linear — no merge commits.** Work on `main`, or a short-lived branch that is
fast-forwarded and deleted.
- **Conventional Commits**: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:`.
- **Stage explicitly. Never `git add -A`** — denied by policy, and it sweeps in whatever else is
dirty.
- **Every version increment gets a tag** — `vX.Y.Z`, on the `major.minor.patch` part, not the
build number.
- Update `CHANGELOG.md` for every user-facing change.
**`.claude/settings.local.json` is currently tracked in git here.** Contents are benign — a
four-entry permission allow list, no `env` block, no secrets — but it is machine-local state that
should not be shared. There is now a `.gitignore` rule for it, and **that rule is inert**: git
applies ignore rules only to untracked paths, so edits still show in `git status` and still get
committed. It starts working only after `git rm --cached .claude/settings.local.json`, which is a
history decision and was deliberately left out of normalization.
Consequence for checking: plain `git check-ignore` prints nothing for this path — it consults the
index — which looks identical to "no rule exists". Use `--no-index` to test the rule itself.
## Releasing
1. Bump `version` in `pubspec.yaml` (the `major.minor.patch` part).
2. Move `[Unreleased]` entries into a dated section in `CHANGELOG.md`.
3. Stage by name, commit, tag `vX.Y.Z`, `git push origin main --tags`.
4. Gitea CI builds the web app in Docker, pushes `:latest` and `:vX.Y.Z`; Watchtower deploys.
5. Verify at `home.schweitz.net`, or `curl -I http://localhost:9999`.
**Rollback:** in Portainer, point the image tag at the previous version.