Commit Graph
25 Commits
Author SHA1 Message Date
Evan JarrettandClaude Opus 5 521cf143e5 appview: cover the half of 13edb71 that had no tests
That commit throttles two writes and states a statement order. Only one of the
three claims was defended.

touchLastSeen is the hotter of the two writes — it ran once per indexed record,
so a busy firehose meant a database round trip per event for a timestamp read
in hours or days. Deleting its throttle outright left every existing test green,
as did keying it globally instead of per DID, which would let one busy account
suppress every other account's first write. Both now fail.

The statement order is the third claim: UpdateLastUsed stamps the throttle
before the write rather than after, so a slow or failing write cannot let every
concurrent caller through to queue another attempt behind it. That matters
because this runs on the authentication path, once per layer during a push, and
the pile-up is worst exactly when the database is least able to absorb it.

A failing write makes the ordering observable without timing anything: with the
stamp after the write every call retries, with it before only the first does.
Dropping the table leaves no row to inspect, so the attempts are counted through
the warning the function already logs. Under the reordering it reports 10
attempts across 10 calls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rmjvU2gSRL9wFnmqVsWaF
2026-08-25 16:34:26 -05:00
Evan JarrettandClaude Opus 5 4c04983e23 appview: stop the backfill claiming every user was just active
last_seen means "this user did something recently". The backfill walks every
historical record in the network, so stamping it there recorded when the
backfill ran, not when the user was active — for every user at once, on every
run. That destroys the only signal the column carries, and it is the one column
in users that nothing upstream can rebuild.

It is now written on the two paths that represent real activity: an interactive
login, and a live commit event on the firehose, which does mean the user just
wrote a record. The backfill still corrects handle, PDS endpoint and avatar,
which is why it re-resolves rather than trusting a cache; it just no longer
claims the user was present.

UpsertUser grows an options form rather than a fourth named variant, since the
avatar and last_seen decisions are independent and all four combinations occur.

Anyone computing MAU from this column should know it was unreliable for every
backfill run before this change.

Also corrects docs/HORIZONTAL_SCALING.md, which claimed oci_client and
registry_domain were local-only preferences. They are fields on
io.atcr.sailor.profile: settings writes them to the user's PDS and
ProcessSailorProfile refreshes the local cache. users is fully derived apart
from last_seen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 09:24:26 -05:00
Evan JarrettandClaude Opus 5 454a6bad3d db: key manifests by manifest_key and drop the rowid
Completes the swap 0033 set up. layers and manifest_references move onto
manifest_key and manifests.id is gone, which removes the last node-allocated
identifier in the AppView schema.

Statement order in 0034 is load-bearing. With foreign keys on, DROP TABLE
performs an implicit DELETE FROM, so dropping manifests while layers still holds
an ON DELETE CASCADE reference deletes every layer row. Migration 0009 did
exactly that; it went unnoticed because the Jetstream backfill rebuilds layers
from PDS records, so the damage healed itself. PRAGMA foreign_keys is no help:
it is a no-op inside a transaction and migrations run in one. So the new
children are built pointing at manifests_new, the old children are dropped
first, and only then is the old manifests table dropped, by which point nothing
references it. Verified both behaviors before relying on them.

manifest_key is declared NOT NULL as well as PRIMARY KEY, because in SQLite a
PRIMARY KEY column still accepts NULL unless it is INTEGER PRIMARY KEY. That
constraint immediately caught four test helpers inserting manifests without one.

Five queries used MAX(id) as "the newest manifest in this repo", which I had
previously reported as absent after grepping only for ORDER BY. A derived key
has no ordering, so recency now comes from created_at with manifest_key as a
deterministic tiebreak. This is a real behavior change, and a fix: the two
disagree whenever a manifest is indexed out of order, which the backfill does
routinely, and created_at is the push time these queries always wanted. Both
directions are tested, including that ties resolve the same way every run.

InsertManifest and BatchInsertManifests no longer read anything back. The key is
derived from (did, repository, digest), so the writer knows it before the
statement runs: the select-back, its per-DID IN list, and the "manifest missing
id after batch insert" branch all go away, along with the UNIQUE-conflict
fallback that existed only to recover a rowid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:44:41 -05:00
Evan JarrettandClaude Opus 5 802cc4ba96 db: add manifest_key and let the existing backfill populate it
First half of replacing manifests.id with a node-independent identity. Nothing
depends on the column yet: id is still the primary key, and layers and
manifest_references still reference it. Getting the column in place and filled
first means the eventual swap operates on data that is already complete and
already proven unique, instead of doing the fill and three table rebuilds in one
step.

The value cannot be computed by the migration. It is a truncated sha256, SQLite
has no hash builtin, and go-libsql exposes no way to register one. So the fill
uses machinery that already exists: the Jetstream backfill re-upserts every
manifest across the protocol on startup, and both upsert paths now write
manifest_key.

That only works because of one extra clause. Both upserts guard their DO UPDATE
with a WHERE that skips rows where nothing changed, which on a backfill re-run is
nearly every row, so they would have skipped the very manifests that need
filling. Adding "OR manifests.manifest_key IS NULL" is what makes an otherwise
no-op pass populate the column. Verified by removing it: the backfill then fills
zero of three manifests instead of three of three.

The index is UNIQUE even though the column is nullable. SQLite treats NULLs as
distinct, so unfilled rows coexist while every filled row is checked. That makes
production data verify the 16-byte truncation rather than us assuming it: if two
manifests ever derived the same key, it fails loudly at insert instead of
silently attaching one manifest's layers to another after the swap.

AppView logs the unfilled count at startup, since there is no single moment at
which this becomes complete and the follow-up migration is only safe at zero.

ManifestKey replaces the old fat "did|repo|digest" map key rather than sitting
beside it; they were always the same question, answered without asking the
database.

The jetstream tests hand-maintained their own CREATE TABLE statements, which is
the drift problem moved into a test: the copy had already fallen behind (it still
had tags.id) and only failed once a query touched the difference. They use
db.InitDB now, with foreign keys switched off to preserve the behavior the
hand-rolled schema had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 22:17:07 -05:00
Evan Jarrett 6758996300 add SBOM package diffing, verify hold-service captain records
- diff view gains a Packages tab with added/removed/changed/unchanged
  package tables and purl-derived type/license/upstream links
- captain records verified against the DID's atcr_hold service before
  caching (processor + batch backfill), preventing forged holds
- fix empty-handle updates clobbering cached handles and colliding on
  the UNIQUE constraint
- move fillPrevCIDs into repo.go; DirectRepoOperator is now canonical,
  repomgr kept as a test oracle
- surface read-only crew status in hold selector
- reconcile docs
2026-06-13 12:49:03 -05:00
Evan Jarrett ab4a4ebf9d admin panel long running imrovements, billing fixes, ui cleanup
1. Multiple registry domains + per-user domain preference

The biggest feature. The appview can serve several registry domains (e.g. buoy.cr, atcr.io),
and users can now pick which one shows up in their pull/push commands.

- Lexicon/record: adds registryDomain (and documents ociClient)
to the sailor profile (lexicons/.../profile.json, pkg/atproto/lexicon.go).
- DB: new registry_domain column on users (schema.sql + migration 0027),
with GetUserByDID/Handle reads, UpdateUserRegistryDomain writer,
and Jetstream caching it on profile updates (writes unconditionally so clearing propagates).
- UI/handlers: new UpdateRegistryDomainHandler + /api/profile/registry-domain route,
a <select> in the user settings panel (only shown when >1 domain configured), and resolveRegistryURL()
which falls back to the primary domain if the user's pref is stale/removed. Tests added for all of it.

2. default_hold_did removed → first managed_holds entry is the default

Consolidates two overlapping config fields into one. ServerConfig.DefaultHoldDID is gone;
PrimaryHoldDID() now returns managed_holds[0]. managed_holds is now REQUIRED.
Updated in config, validation, server wiring, test harness, example YAML, and the deploy template.

3. Admin long-running operations → generic background-job framework

New pkg/hold/admin/jobs.go introduces a reusable startJob/jobRegistry pattern
 (a detached context.Background() job + a /admin/api/jobs/{key}/status polling endpoint).
This replaces the bespoke scan-backfill goroutine state machine, and now also wraps crew tier remap and crew import
all three previously looped synchronously on the request context and got 504'd/cancelled mid-run by the reverse proxy.
 Forms switched from POST-redirect to htmx fragments (job_progress.html, job_result.html, crew_import_results.html)
 the old crew_import_results.html page and scan_backfill_progress.html partial were deleted.
This is also captured as a new rule in CLAUDE.md.

4. Cascade-delete manifest on last-tag deletion

DeleteTagHandler now, after removing the last tag pointing to a digest, cascade-deletes the manifest itself
 (PDS + DB + hold blob purge) — but only if it's not a child of a manifest list (multi-arch parent).
 New GetTagDigest and ShouldCascadeDeleteManifest queries back it, plus cascade_delete_test.go.
 Also switches tag rkey computation to the atproto.RepositoryTagToRKey helper.

5. Billing simplification

Drops the OwnerBadge config option (hold-owner supporter badge).
The user-profile template no longer special-cases an "owner" badge value (only "Captain").
Example tiers renamed to the nautical scheme (deckhand/bosun/quartermaster).

6. Build/deploy: go generate always runs via Make

make generate is now a phony target that always runs go generate ./... (regenerating cbor_gen, icon sprites, etc.),
 and build-trixie depends on it. The deploy tooling (provision.go/update.go)
drops its own runGenerate calls since the Makefile handles it.

7. New cmd/firehose-tap tool (untracked)

A standalone CLI that subscribes to a com.atproto.sync.subscribeRepos endpoint and pretty-prints events,
with emphasis on Sync 1.1 compliance fields (per-op prev CIDs, commit prevData) and a --validate CI mode.
Fits with the recent "more sync1.1 compliant" commit.
2026-06-05 20:57:25 -05:00
Evan Jarrett 902fba4553 convert alert to modal. go fix the codebase 2026-05-16 11:59:47 -05:00
Evan Jarrett 3533f07ecb minor bug fixes, add ability see starred repos 2026-05-06 21:55:47 -05:00
Evan Jarrett b2d6842bb7 clean up old migration code. minor bug fixes with appview ui 2026-05-04 21:52:28 -05:00
Evan Jarrett fd5bfc3c50 ui fixes for repo page, fix scanner priority, cleanup goreleaser scripts 2026-04-03 16:48:21 -05:00
Evan Jarrett 22cf108a5f fix quirks on repo and digest pages. fix ips not showing in server logs. add basic spam blocking to LB. add setting to configure your oci (docker) client. 2026-03-24 22:24:51 -05:00
Evan Jarrett f90a46e0a4 begin implement supporter badges, clean up lexicons, various other changes 2026-02-20 22:12:18 -06:00
Evan Jarrett e3843db9d8 Implement did:plc support for holds with the ability to import/export CARs.
did:plc Identity Support (pkg/hold/pds/did.go, pkg/hold/config.go, pkg/hold/server.go)

  The big feature — holds can now use did:plc identities instead of only did:web. This adds:
  - LoadOrCreateDID() — resolves hold DID by priority: config DID > did.txt on disk > create new
  - CreatePLCIdentity() — builds a genesis operation, signs with rotation key, submits to PLC directory
  - EnsurePLCCurrent() — on boot, compares local signing key + URL against PLC directory and auto-updates if they've drifted (requires rotation key)
  - New config fields: did_method (web/plc), did, plc_directory_url, rotation_key_path
  - GenerateDIDDocument() now uses the stored DID instead of always deriving did:web from URL
  - NewHoldServer wired up to call LoadOrCreateDID instead of GenerateDIDFromURL

  CAR Export/Import (pkg/hold/pds/export.go, pkg/hold/pds/import.go, cmd/hold/repo.go)

  New CLI subcommands for repo backup/restore:
  - atcr-hold repo export — streams the hold's repo as a CAR file to stdout
  - atcr-hold repo import <file>... — reads CAR files, upserts all records in a single atomic commit. Uses a bulkImportRecords method that opens a delta session, checks each record for
  create vs update, commits once, and fires repo events.
  - openHoldPDS() helper to spin up a HoldPDS from config for offline CLI operations

  Admin UI Fixes (pkg/hold/admin/)

  - Logout changed from GET to POST — nav template now uses a <form method=POST> instead of an <a> link (prevents CSRF on logout)
  - Removed return_to parameter from login flow — simplified redirect logic, auth middleware now redirects to /admin/auth/login without query params

  Config/Deploy

  - config-hold.example.yaml and deploy/upcloud/configs/hold.yaml.tmpl updated with the four new did:plc config fields
  - go.mod / go.sum — added github.com/did-method-plc/go-didplc dependency
2026-02-14 15:17:53 -06:00
Evan Jarrett 7ba42080c5 more admin panel fixes, allow for fallback relays and jetstreams, improve star lexicon to allow for repo_page backlinks 2026-02-09 21:53:02 -06:00
Evan Jarrett 4d9452bb75 update configs, fix foreign key issues 2026-02-07 23:28:42 -06:00
Evan Jarrett 834bb8d36c libsql instead of sqlite for turso/bunnydb replicated sqlite 2026-02-05 20:43:04 -06:00
Evan Jarrett b1767cfb6b publish xrpc endpoint lexicons. fix backfill and jetstream to actually validate records match schema 2026-01-12 21:11:55 -06:00
Evan Jarrett efef46b15a various linting fixes 2026-01-04 22:02:01 -06:00
Evan Jarrett 506d8b002b fix unit tests 2026-01-02 15:25:09 -06:00
Evan Jarrett 1df1bb57a4 clean up logs, delete cached data when atproto account is deleted 2025-12-31 12:21:17 -06:00
Evan Jarrett f19dfa2716 move download stats to the hold account so it can persist across different appviews 2025-12-31 11:04:15 -06:00
Evan Jarrett 044d408cf8 deployment fixes. add open graph 2025-12-18 11:19:49 -06:00
Evan Jarrett 93b1d0d4ba add identity invalidation to logstream. refresh avatar urls on login 2025-10-28 10:12:34 -05:00
Evan Jarrett 26c1b4e28e fix backfilling manifests in the correct order, not just digest order 2025-10-22 12:03:44 -05:00
Evan Jarrett a118904cb8 refactor jetstream code to unify shared functionality between that and backfill. add tests 2025-10-22 00:08:21 -05:00