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>
164 lines
7.9 KiB
Markdown
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.
|