Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cb5eab5
Merge pull request #1 from lorem-dev/main
lorem-dev Aug 2, 2026
7d04a9c
Merge pull request #2 from lorem-dev/main
lorem-dev Aug 2, 2026
7b2336f
Merge pull request #4 from lorem-dev/main
lorem-dev Aug 2, 2026
231decf
Merge pull request #5 from lorem-dev/main
lorem-dev Aug 2, 2026
890b8bc
Merge pull request #6 from lorem-dev/main
lorem-dev Aug 3, 2026
b8146e7
Merge pull request #7 from lorem-dev/main
lorem-dev Aug 11, 2026
d41f379
Merge pull request #8 from lorem-dev/main
lorem-dev Aug 11, 2026
9547b7a
build: share the icons between the docs build and the frontend
lorem-dev Aug 12, 2026
7f2d227
refactor: store the configuration as JSON, not a column per field
lorem-dev Aug 11, 2026
e9e23b5
feat: add admin.dashboard and admin.title
lorem-dev Aug 11, 2026
382d61a
feat: report the caller's own rights at /api/v1/access
lorem-dev Aug 11, 2026
32042fd
feat: a browser dashboard on the admin listener
lorem-dev Aug 12, 2026
5d274d0
build: let the image compile its own binary when none is staged
lorem-dev Aug 11, 2026
94e83bd
fix: colour the page, expire its notices, and keep its rows
lorem-dev Aug 12, 2026
295427e
refactor: put every admin endpoint under /api/
lorem-dev Aug 12, 2026
eb0f5fc
fix: make the page readable, and refetch it when the token changes
lorem-dev Aug 12, 2026
6376496
feat: draw the wordmark, settle the footer, and size the editors
lorem-dev Aug 12, 2026
e622bac
feat: serve the configuration schema, and refuse a bad field by it
lorem-dev Aug 12, 2026
b5b6a10
feat: colour what the page edits, and point every field at its docume…
lorem-dev Aug 12, 2026
334976f
fix: mount the configuration's directory, and say so when a save cannot
lorem-dev Aug 12, 2026
fec97cc
feat: what the first round of use asked for
lorem-dev Aug 12, 2026
b4aa8bd
build: let the image build the dashboard when nobody has
lorem-dev Aug 12, 2026
d41cabe
docs: say which checks belong to iterating and which to finishing
lorem-dev Aug 12, 2026
70f7a04
feat: leave access overrides out when the admin API is public
lorem-dev Aug 12, 2026
e432f10
docs: show the dashboard instead of only describing it
lorem-dev Aug 12, 2026
bb9e52c
fix: stop ignoring every directory called config
lorem-dev Aug 12, 2026
4043c58
fix: answer the admin listener's paths as the tools that call them ex…
lorem-dev Aug 12, 2026
6ca2220
feat: name Doppel's own host when rewriting an upstream's redirect
lorem-dev Aug 12, 2026
72b9b57
test: draw a free port on the wildcard, not on loopback
lorem-dev Aug 12, 2026
8b026ee
fix: make a write through the admin API take effect
lorem-dev Aug 12, 2026
deaa0d0
fix: unbreak the Swagger UI, move it out of the API, and compress res…
lorem-dev Aug 12, 2026
aa446d4
build: minify index.html on the way into the binary
lorem-dev Aug 12, 2026
2686f87
feat: say more with the metrics, and publish the ones that must alway…
lorem-dev Aug 12, 2026
19d1cf1
fix: give the token field room, and cover what a token can do
lorem-dev Aug 12, 2026
ae614e8
feat: rewrite the upstream's own address out of the bodies it sends
lorem-dev Aug 12, 2026
1f82f4c
feat: draw a named deployment's own title like the wordmark, and cred…
lorem-dev Aug 12, 2026
c35a6da
test: wait for what a slower machine has not rendered yet
lorem-dev Aug 12, 2026
0656dba
Merge pull request #9 from lorem-dev/feat/frontend
lorem-dev Aug 12, 2026
08c472d
docs: make the release's documentation say what shipped
lorem-dev Aug 12, 2026
5cdb57c
chore: release 1.0.0
lorem-dev Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/bump-version/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ part of it -- see the end of this file.
uv run scripts/bump_version.py <version>
```

There is exactly one place a version lives: `[workspace.package]` in
`Cargo.toml`. `frontend/package.json` deliberately has no `version` field -- the
dashboard reports the binary's, read from the configuration the listener injects --
so nothing there needs bumping, and adding a version to it would create a second
thing to forget.

It sets the version under `[workspace.package]` in `Cargo.toml`, renames
`## Development` in CHANGES.md to `## <version> -- <date>`, and opens a fresh
empty Development section above it. It prints what it changed and what to do
Expand Down
68 changes: 68 additions & 0 deletions .agents/skills/check-docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,43 @@ grep -rn 'env::var\|env = "DOPPEL' crates/
statuses, must match the table in the docs. A code added without a doc row is
invisible to whoever has to handle it.

**The parameter reference.** `docs/usage/parameters.md` is generated from
`doppel-config.schema.json` by `scripts/parameters_doc.py`. Never edit it by hand
-- regenerate it and read the diff:

```bash
uv run scripts/config_schema.py && uv run scripts/parameters_doc.py
```

What the generator cannot judge is whether a field's *description* still says
something true, or whether its example is still the example worth showing. Those
come from doc comments and `examples(...)` in `crates/doppel-core/src/config/`, so
read them there when the behaviour changed.

**The links the dashboard builds.** The (i) beside a field links into that page,
and the link is assembled at runtime from the field's path and the running version
-- so mkdocs cannot see it and `--strict` cannot check it. A wrong fragment is
silent: the browser opens the page at the top and nobody notices.

```bash
uv run scripts/check_docs_links.py
```

It holds three things, and each has a failure it exists for:

- every `info="..."` path in `frontend/src` has a section under the anchor
`frontend/src/services/docs.ts` derives from it -- a renamed field is the way
this breaks;
- every documentation URL in the frontend is built through `docsUrl()`, so it
carries the version. A hand-written link to the site root shows whoever follows
it the rules of whatever has been released since;
- every relative link and fragment inside the generated page resolves.

Both checks are in `make gate` and in CI, so what is left for a human is the
half a script cannot see: follow two or three of the (i) links in a running
dashboard and read the sections they land on. An anchor that exists but explains
a different field passes every check here.

**`main.example.yaml`.** It is the schema made concrete and is asserted against
by tests, so it cannot be stale -- but it can be *misleading*, which tests do
not catch. Read it as a newcomer copying it: does the ordering of its mocks
Expand Down Expand Up @@ -120,3 +157,34 @@ Say what you compared, not just that you checked. "Every config field has a
reference entry; the rule-map tests pass; two `usage/` pages named crates and
were reworded; `mkdocs build --strict` is clean" is a useful report. "Docs look
fine" is not.

## The dashboard and the frontend

`docs/usage/dashboard.md` is the operator-facing page: the two configuration
fields, who may use it, what each screen does, the theme, and what a 503 at the
root means. `docs/development/architecture.md` carries the developer-facing half.

Check these together, because they go stale as a set:

- `admin.dashboard` and `admin.title` in `docs/usage/configuration.md` and in
`main.example.yaml`.
- `GET /api/v1/access` and the routes in `docs/usage/admin-api.md`, plus
`DASHBOARD_NOT_BUILT` in its error table. The endpoint table is the one thing
there that is checked by a test -- `openapi.rs` refuses a document describing a
route that is not served -- so a path that changed shows up in `cargo test`
before it shows up here. What does not is prose quoting a path, and every
healthcheck: the compose file, the Docker Hub overview, `observability.md`.
- The build order in `README.md`: assets first, then the binary. A reader who
follows a stale version of that gets a 503 and no idea why.
- `make help`. Every target carries its own description, so the list cannot go
stale on its own -- but a documented command that no longer matches a target is
worth catching here.
- The four pages a reader reaches for before the dashboard's own: `docs/index.md`
(the feature list and the reading order), `usage/getting-started.md` (a first run
ends by opening it), `usage/troubleshooting.md` (the 503, disabled controls, the
refresh timer) and `DOCKERHUB.md` (the image serves it too). A feature documented
only on its own page is a feature nobody finds.

The icons live in `assets/` at the repository root and are staged into
`docs/assets/` by `scripts/mkdocs_assets.py` on every build. `docs/assets/` is
git-ignored: a link that points there is fine, a file committed there is not.
55 changes: 51 additions & 4 deletions .agents/skills/check-licenses/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: check-licenses
description: Use after editing any Cargo.toml, adding or removing a dependency, or before cutting a release. Verifies every direct dependency's licence against the project policy and updates LICENSE.
description: Use after editing any Cargo.toml or frontend/package.json, adding or removing a dependency, or before cutting a release. Verifies every direct dependency's licence against the project policy and updates LICENSE and THIRD-PARTY.md.
---

# Check licences
Expand All @@ -21,6 +21,20 @@ so the dynamic-linking exception does not apply.

## How to check

Both halves are generated, so start by running the generator and reading what it
says:

```bash
uv run scripts/third_party.py # rewrites THIRD-PARTY.md
uv run scripts/third_party.py --check # what CI runs
```

It exits non-zero and prints `SOME LICENCES ARE NOT COVERED BY THE POLICY` with
the offending packages named. That is the check; the rest of this skill is for
deciding what to do about a new dependency.

### The cargo half

Read the licence from the registry rather than from memory:

```bash
Expand All @@ -33,6 +47,30 @@ Transitive crates come in under their parents' justification; if one of them
carries a forbidden licence that is a real problem, but it is found by auditing
the tree, not by this skill.

### The npm half

The direct set is `dependencies` and `devDependencies` in
`frontend/package.json`. The two are not equivalent, and the difference decides
whether a licence matters at all:

- `dependencies` is compiled into `frontend/dist` and embedded in the binary, so
it is **redistributed**. Its licences bind, and it belongs in `LICENSE` and
`THIRD-PARTY.md`.
- `devDependencies` -- vite, jest, eslint, tailwind's compiler, the tooling
underneath them -- runs on a developer's machine and ships nowhere. It is
deliberately absent from both files.

A build tool in `dependencies` is therefore a real mistake and not a matter of
taste: it drags the policy over licences the project never distributes.
Tailwind sat there once, and the tooling closure it pulled in carried Blue Oak
and CC-BY -- which would have meant widening the permitted set for code nobody
ships.

```bash
npm view <package> license # one package
npm ls --omit=dev --all --prefix frontend # exactly what ships
```

## Ask the question the policy is actually for

For each dependency that is new since the last check, answer in one line: what
Expand All @@ -51,8 +89,17 @@ sweep late in phase 1, each declared and referenced by no source file.
grep -rn "<crate_name>" crates --include='*.rs' | head
```

## Update LICENSE
## Update LICENSE and THIRD-PARTY.md

`LICENSE` carries the Apache 2.0 text plus a table of the *direct* set, from both
ecosystems. `THIRD-PARTY.md` carries the whole closure and is generated -- never
edit it by hand.

```bash
uv run scripts/third_party.py
```

`LICENSE` carries the Apache 2.0 text plus the third-party notices. When the
direct set changes, update the notices to match. Report what you changed and
Both modes need `frontend/node_modules` present and refuse to run without it,
rather than writing a cargo-only file that looks complete or reporting a stale one
that is not. Report what you changed and
what you found -- including "nothing changed", which is a useful result.
54 changes: 54 additions & 0 deletions .agents/skills/pre-release-check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,60 @@ changes things. `regenerate-config-schema` is the one exception, and only
because the thing it writes is generated: if it produces a diff, that diff is a
commit of its own before the release, not part of the release commit.

## Then check the dashboard

`run-tests-and-linters` covers the frontend gate, but a release has two questions
of its own, because the dashboard is embedded at compile time and a binary without
it still builds:

```bash
make e2e # the browser suite, which the frontend gate leaves out
```

Then confirm the release will not ship a page-less binary. The workflow greps each
archive's binary for the configuration element before publishing -- read that step
and check it is still there, since it is the only thing standing between a missing
artifact and a release whose root answers 503:

```bash
grep -n "carries the dashboard" .github/workflows/release.yml
```

The dashboard's own version is not bumped and must not be: `frontend/package.json`
has no `version` field, and the page reports the binary's.

## Then check the Docker Hub overview

`DOCKERHUB.md` is the only page here that is published somewhere this repository
cannot see, by the `describe` job, and nothing fails when it goes stale: the
release ships and the overview quietly describes an older Doppel.

It is not a copy of `docs/usage/docker.md`. It is written for a reader who has
already chosen the image, so it opens with `docker run` and never mentions
building from source. What to check, in this order:

- **Against the code.** Every command, port, path, volume and environment
variable in it exists and still means what it says. The paths are the ones that
moved most recently, so read the route table rather than trusting the text.
- **Against `docs/usage/docker.md`.** The two cover the same ground at different
lengths, and the failure mode is a fact that changed in one and not the other:
a mount, a variable, a healthcheck path. Diff them by eye, fact by fact.
- **Against the dashboard.** It describes what the page can do, which is the
claim that ages fastest -- it has already been wrong once about templates.
- **The version stays the placeholder `1.2.3`.** The `describe` job rewrites
every `loremdev/doppel:<version>-alpine` to the tag being released; a real
version checked in is a second number to bump and no reader benefits.
- **Size.** Docker Hub truncates the overview past 25000 bytes and the short
description past 100, silently:

```bash
wc -c DOCKERHUB.md
grep -n 'loremdev/doppel:' DOCKERHUB.md
```

Anything found here is a commit of its own before the release, like everything
else this skill turns up.

## Then check the version

```bash
Expand Down
42 changes: 41 additions & 1 deletion .agents/skills/run-tests-and-linters/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,54 @@ description: Use before marking any task done, before opening a pull request, or

# Run tests and linters

The gate is three commands, in this order, all of which must pass:
The whole gate, in one command:

```bash
make gate
```

That is `fmt-check`, `lint`, `test`, `test-frontend`, `size`, `docs`, `schema`
and `licences`. It deliberately leaves out the browser suite -- `make e2e`
downloads Chromium on first run -- so run that too whenever the change touches
`frontend/` or the dashboard routes.

The Rust half by hand, in this order, all of which must pass:

```bash
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
```

## The frontend half

The dashboard has its own gate, and one ordering that matters: the assets are
embedded into the binary at compile time, so they are built before anything that
reads them.

```bash
npm --prefix frontend ci
npm --prefix frontend run lint # eslint
npm --prefix frontend run typecheck # tsc, the application
(cd frontend && npx tsc --noEmit -p tsconfig.e2e.json) # tsc, the specs
npm --prefix frontend run build # writes frontend/dist
npm --prefix frontend test # jest, including the bundle-size budgets
cargo build -p doppel-cli # re-embeds what the build just wrote
npm --prefix frontend run e2e # Playwright, against that binary
```

Two traps this order exists to avoid:

- `npm run build` alone changes nothing about a running binary. Rebuilding the
frontend without rebuilding the binary is how half an hour goes into chasing a
fixed bug that is still in the embedded bundle.
- The bundle-size suite measures `frontend/dist`. Running jest before the build
measures the previous one, or fails because there is nothing to measure.

`DOPPEL_REQUIRE_DASHBOARD_ASSETS=1` on `cargo test` turns a skipped
asset-delivery suite into a failure, exactly as `DOPPEL_REQUIRE_DATABASE` does
for the store. `make test` sets it.

## Capture the output, do not recall it

Run the gate so its output lands in a file, and quote the file:
Expand Down
48 changes: 42 additions & 6 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,45 @@
# The image copies one already-built binary in; nothing else in the repository
# belongs in the build context. Keeping it small matters more than usual here:
# `target/` alone is tens of gigabytes on a working checkout, and every byte of
# the context is sent to the daemon before the first instruction runs.
# The image prefers an already-built binary and only compiles one when none was
# staged, so the context is what those two paths need and nothing else. Keeping it
# small matters more than usual here: `target/` alone is tens of gigabytes on a
# working checkout, and every byte of the context is sent to the daemon before the
# first instruction runs.
*

# The staged binary, which the `docker` release job puts here and the local
# build instructions in the Dockerfile copy here too.
# This file itself, because the Dockerfile copies it. That is not decoration:
# `COPY .dockerignore dist/<platform>/doppel* /staged/` is how "copy the binary if
# it is there" is expressed, and it needs one source that always matches or the
# COPY fails when no binary was staged.
!.dockerignore

# The staged binary, which the `docker` release job puts here and the local build
# instructions in the Dockerfile copy here too. When it is present nothing below
# is compiled.
!dist/

# What the fallback build needs: the workspace, and the dashboard it embeds.
!Cargo.toml
!Cargo.lock
!rust-toolchain.toml
!crates/

# The dashboard, either already built or buildable. `frontend/dist` is generated, so
# it is listed explicitly; the sources beside it are what the image builds from when
# it is not there, which is why this list is exactly what `vite build` reads and not
# the whole directory -- `node_modules` is bigger than everything else here together.
#
# `assets/favicon.ico` is in the list because the build's `prebuild` step copies it
# into `public/`: one icon, one source of truth, and the copy is git-ignored.
!frontend/dist/
!frontend/package.json
!frontend/package-lock.json
!frontend/index.html
!frontend/vite.config.ts
!frontend/tsconfig.json
!frontend/src/
!frontend/scripts/
!assets/favicon.ico

# Nothing inside the workspace that the build does not read. `target/` is the one
# that matters; the rest keeps the context honest.
crates/**/target/
**/*.rs.bk
34 changes: 34 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Line endings are normalised on commit, so a checkout on Windows does not turn
# every file into a diff, and the ASCII-only rule in AGENTS.md is not quietly
# broken by a stray CR.
* text=auto eol=lf

# The icons are binary. git-lfs keeps them out of the packfile, and the `-text`
# marker stops git from ever trying to reconcile two versions line by line.
#
# Any workflow that reads these files must check out with `lfs: true`. Without
# it the working tree holds a pointer file, and the documentation build then
# ships a few lines of text where an image was expected -- which fails as a
# broken image rather than as a missing file, so it is worth stating here.
*.png filter=lfs diff=lfs merge=lfs -text
*.ico filter=lfs diff=lfs merge=lfs -text

# One exemption, and it is not a matter of taste. DOCKERHUB.md points Docker
# Hub's overview image at
# raw.githubusercontent.com/lorem-dev/doppel/main/assets/icon-128.png, and that
# endpoint serves the LFS *pointer* rather than the bytes -- verified on
# 2026-08-11, 130 bytes of ASCII beginning "version
# https://git-lfs.github.com/spec/v1". Tracked through LFS, the overview would
# render a broken image, and nothing in this repository would fail to say so.
# This file is small enough that a plain blob costs nothing.
assets/icon-128.png -filter -diff -merge text=auto

# The documentation's screenshots, for the same reason as the icons: a new
# screenshot is a new whole file rather than a diff, and every UI change brings
# one. `docs/screenshots/` is read only by the mkdocs build, whose workflow
# checks out with `lfs: true` -- without that it would ship the pointer file as
# a broken image.
*.webp filter=lfs diff=lfs merge=lfs -text

# An SVG is XML, and a readable diff is worth more than treating it as opaque.
*.svg text
Loading