August 12, 2026 at 12:00:00 AM UTC
MetalBear's frontend becomes a self-service console, and CI starts testing the containers it ships
MetalBear
The server-side work in this range -- OAuth/session hardening, admin and repo parity -- is covered elsewhere. This is the frontend, the containers, and the tooling underneath them: frontend/ went from a handful of auth pages to a real self-service console, and a run of Docker and CI fixes closed a gap where the shipped image had quietly never worked.
from auth pages to a self-service console
The frontend started this range with just enough to function: an auth store backed by localStorage, a login form, an account page, app-password CRUD, and an OAuth consent screen. Everything built on top of it followed the same pattern -- the backend already had the endpoint, nobody had put a page in front of it. Password reset, email change, handle change, deactivate/reactivate, and account deletion all existed server-side (requestPasswordReset/resetPassword, requestEmailUpdate/updateEmail, com.atproto.identity.updateHandle, deactivateAccount, requestAccountDelete/deleteAccount) with no UI reaching any of them until /account/security and /account/danger-zone shipped, built against the reference PDS's oauth-provider-ui dialogs (update-password-dialog.tsx, deactivate/reactivate/delete-account-dialog.tsx, and so on) to match its flow shape rather than invent one.
The same no-server-changes pattern repeats through passkeys (webauthn.ts wraps the browser's native WebAuthn API directly, no client library), a "download your data" button against the already-public com.atproto.sync.getRepo, a guided /account/migrate page that walks through exporting a DID's data and requesting a PLC signing token but deliberately doesn't try to automate the cross-server handoff -- that's pointed at the goat CLI instead, since getting a PLC operation wrong is identity-critical -- and a personal-recovery-key page that guides registering a self-held PLC rotation key ahead of any migration need, inspired by PDS MOOver's approach to the same problem.
The admin side got its own console: /admin is a password-only login (Basic auth against com.atproto.admin.*, credential kept in sessionStorage only) surveyed against other implementations first -- the reference PDS's oauth-provider-ui has no admin UI at all, neither do bluepds or millipds, but Tranquil PDS does and informed the shape here, though its custom list-accounts endpoint isn't something MetalBear's backend exposes, so that stayed out. A day later it gained a paginated accounts browser on top of com.atproto.sync.listRepos's existing cursor pagination, since the admin panel could look up an account you already had a DID or handle for but had no way to discover what existed.
Smaller frontend fixes rode alongside all of this:
+error.sveltereadstatus/errorfrom$props(), which SvelteKit never actually passes there -- they live in the$pagestore;+layout.sveltealso carried a dead branch comparing against a/errorroute that doesn't exist.- Four call sites used
toLocaleDateString(), which renders differently per visitor locale; replaced with a sharedformatDate()that always emits ISO 8601. downloadReposurfaced a bare"getRepo: 400"for theRepoNotFoundcase -- exactly what a brand-new, record-less account hits -- instead of a real message.signInDevicewas only called on the OAuth consent redirect, so a plain top-level login left no device session and passkey registration failed with "No device session for this account".- The nav's
Statuslink pointed at the root, i.e. itself; removed as redundant with the brand link. - Login and consent pages were rebuilt against the reference provider's layout -- a shared
AuthShell, structured error views, the global nav hidden on OAuth pages, and the raw scope string shown above the humanized permission list.
the container that never started
METALBEAR_BUILD_COMMIT/_TIME and a METALBEAR_RELEASE_STAGE variable were added to surface build provenance on /operator.json, the landing page, and admin-gated /_debug/health -- and then immediately broke under Docker, because .dockerignore excludes .git to keep the build context small, so the commit auto-detection could never find anything from inside a build. The landing page showed "unknown" on the one deployment that actually mattered until it was made a CACHE STRING that a -D/--build-arg override wins over, with docker-compose.yaml passing the host's real git HEAD at build time -- verified against bear1.croft.click before the fix was written.
A more serious container bug followed. metalbear_core links C++ translation units (account.cpp and others), so the metalbear binary depends on libstdc++.so.6/libgcc_s.so.1 even though main.c is plain C -- and neither runtime image installs those packages, since the image is built to copy in only the final binary. The dynamic linker failed before main() ever ran; the shipped container had never been able to start. Fixed with -static-libgcc -static-libstdc++, scoped to non-Apple Unix since this project doesn't ship a macOS image anyway. Two smaller Docker fixes landed around it: the Dockerfiles built every CMake target, including the wolfram CLI, so a CLI build failure could block the server image it had nothing to do with -- scoped to --target metalbear -- and Debian bookworm's packaged libc-ares (1.18.1) is older than the DNS record API Wolfram requires (>= 1.28), so the Dockerfile builds c-ares from source, first pinned to a release tag and then switched to tracking the main branch directly.
CI starts testing what it ships
The libstdc++ regression above went undetected for three releases because nothing in CI ever actually ran the shipped container: the multi-platform image build only pushes (buildx can't --load a multi-arch manifest), and the "binaries" job smoke-tests the bare binary built directly on the runner, never the containerized one. The fix adds a single-native-platform --load build of the runtime target, runs it, and polls /_debug/health before the real multi-platform build-and-push proceeds -- a direct response to the bug two commits earlier.
The rest of the release pipeline picked up smaller correctness fixes in the same window: the release notes' docker pull command used github.ref_name verbatim (v0.13.6), but docker/metadata-action's semver pattern strips the leading v when it pushes the tag, so the printed command would never resolve -- caught while verifying the image actually ran; actions/download-artifact@v4 failed to fetch the *.dockerbuild attestation artifacts on every tag release so far, killing the publish step after five retries, fixed by filtering to metalbear-* artifacts only; the build directory wasn't cleaned before configure, letting stale CMake cache leak into a fresh build; and python3 came out of the CI workflow entirely once the dev tools were rewritten in C++. Pinned actions were bumped to their Node 24 runtimes (checkout v4→v7, setup-uv v5→v9, upload/download-artifact v4→v7/v8, action-gh-release v2→v3), with a same-day follow-up fixing the setup-uv pin -- no floating v9 tag actually exists, only v9.0.0.
clang-format arrives, and disagrees with itself
.clang-format and a changed-lines-only format-check job landed together, deliberately scoped so the pre-existing tree didn't need a mass reformat to pass -- only new and modified lines had to conform. The same commit added a non-blocking cmake-format check and an informational desloppify scan, kept advisory because its score is "75% subjective design review that needs an LLM in the loop this workflow doesn't run." The very next commit did the mass reformat anyway: 87 files, +7445/-5740 across the tree.
That reformat then spent a week fighting version skew between the clang-format used locally (22) and the one ubuntu-latest's apt install actually shipped in CI (18) -- the two disagree on braced-init spacing (struct X var{} vs struct X var {}) and on the space before a parenthesized declarator like char (*paths)[512], producing two separate fix commits for the same underlying mismatch. git-clang-format itself then turned out to unconditionally call clang-format -list-ignored, a flag from a newer LLVM than CI's apt clang-format has, crashing immediately and -- running under bash -e -- failing the job with zero diagnostic output. Replaced with a plain per-file clang-format --dry-run --Werror over the changed files. CI's unpinned clang-format then drifted again and re-flagged pre-existing lines in a two-line test guard edit, so the check was finally pinned to clang-format-18 outright. A handful of individual style() commits rode along fixing whatever the pinned checker caught next: a feature-macro guard, an importRepo change, new email/emailConfirmed tests, identity_plc_operation, and firehose_probe.cpp twice.
tools rewritten in C++, and platform-specific test bugs
tools/firehose_probe.py (185 lines) and tools/verify_repo_car.py (128 lines) were replaced with C++ equivalents (634 and 1060 lines), along with a small RAII header wrapper set under cpp/MetalBear/MetalBear/ -- the change that let Python come out of CI entirely a few commits later. firehose_probe's poll() calls had no timeout in the rewrite, fixed shortly after.
A handful of test bugs surfaced real platform and memory issues rather than just flakiness. metalbear_repo_store_create_record takes char **out_uri/out_cid, but a parallel-write test passed fixed-size stack arrays where a char* was expected -- the function wrote a heap pointer into the array's first eight bytes instead of a string, corrupting the stack. A mock PLC server's stop_mock_plc was called twice in one test, the second pthread_join on an already-joined thread being undefined behavior that segfaulted only on Linux CI. The same mock server's shutdown assumed close() unblocks a thread parked in accept() -- true on macOS/BSD, not on Linux, so the test hung forever in the join; fixed by polling the listen fd with a timeout instead. A rate-limit test sent exactly 99 requests and asserted the 101st got a 429, correct only if the loop finished inside the token bucket's ~3-second refill window -- a slow runner let the 101st through and flaked the suite, fixed by looping until the limiter actually rejects. And a has_bytes check compared an int return value against NULL, a comparison -Wpointer-integer-compare rejects and which had only ever been correct by accident of NULL's representation.
all entries