mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 12:17:00 +00:00
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.
67 lines
3.0 KiB
HTML
67 lines
3.0 KiB
HTML
{{ define "user" }}
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
{{ template "head" . }}
|
|
{{ template "meta" .Meta }}
|
|
</head>
|
|
<body>
|
|
{{ template "nav" . }}
|
|
|
|
<main id="main-content" class="container mx-auto px-4 py-8">
|
|
<div class="flex flex-col items-center gap-8">
|
|
<!-- User Profile Header -->
|
|
<div class="flex flex-col items-center gap-4">
|
|
{{ if .ViewedUser.Avatar }}
|
|
<div class="avatar">
|
|
<div class="w-20 rounded-full shadow">
|
|
<img src="{{ resizeImage .ViewedUser.Avatar 160 }}" alt="" aria-hidden="true" width="80" height="80" fetchpriority="high" />
|
|
</div>
|
|
</div>
|
|
{{ else if .HasProfile }}
|
|
<div class="avatar avatar-placeholder" role="img" aria-label="Avatar for {{ .ViewedUser.Handle }}">
|
|
<div class="bg-neutral text-neutral-content w-20 rounded-full shadow">
|
|
<span aria-hidden="true" class="text-3xl">{{ firstChar .ViewedUser.Handle }}</span>
|
|
</div>
|
|
</div>
|
|
{{ else }}
|
|
<div class="avatar avatar-placeholder" role="img" aria-label="Unknown user avatar">
|
|
<div class="bg-neutral text-neutral-content/60 w-20 rounded-full shadow">
|
|
<span aria-hidden="true" class="text-3xl">?</span>
|
|
</div>
|
|
</div>
|
|
{{ end }}
|
|
<div class="flex flex-wrap items-center justify-center gap-2 min-w-0">
|
|
<h1 class="text-2xl md:text-3xl font-display font-bold tracking-tight break-all min-w-0">{{ .ViewedUser.Handle }}</h1>
|
|
{{ if eq .SupporterBadge "Captain" }}
|
|
<span class="badge badge-sm supporter-badge-owner">{{ .SupporterBadge }}</span>
|
|
{{ else if .SupporterBadge }}
|
|
<span class="badge badge-sm supporter-badge">{{ .SupporterBadge }}</span>
|
|
{{ end }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
{{ if not .HasProfile }}
|
|
<div class="text-center text-base-content/60 py-12">
|
|
<p>This user hasn't set up their {{ .ClientShortName }} profile yet.</p>
|
|
</div>
|
|
{{ else if .HasError }}
|
|
{{ template "state-error" (dict
|
|
"Title" "We couldn't load their images"
|
|
"Subtext" "The database had trouble fetching this profile. Try refreshing in a moment."
|
|
"RetryURL" (printf "/u/%s" .ViewedUser.Handle)
|
|
) }}
|
|
{{ else }}
|
|
<div class="w-full">
|
|
{{ template "card-grid" (dict "Repositories" .Repositories "Columns" 4 "EmptyMessage" "No images yet.") }}
|
|
</div>
|
|
{{ end }}
|
|
</div>
|
|
</main>
|
|
|
|
{{ template "footer" . }}
|
|
</body>
|
|
</html>
|
|
{{ end }}
|