mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-31 13:17:09 +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.
148 lines
6.8 KiB
HTML
148 lines
6.8 KiB
HTML
{{define "partials/tab_crew.html"}}
|
|
<div class="flex justify-between items-center min-h-12 mb-6">
|
|
<h1 class="text-2xl font-bold">Crew Management</h1>
|
|
<div class="flex gap-2">
|
|
{{if .Crew}}
|
|
<a href="/admin/crew/export" class="btn btn-ghost btn-sm gap-2" title="Export crew to JSON">
|
|
{{ icon "download" "size-4" }}
|
|
Export
|
|
</a>
|
|
{{end}}
|
|
<a href="/admin/crew/import" class="btn btn-ghost btn-sm gap-2" title="Import crew from JSON">
|
|
{{ icon "upload" "size-4" }}
|
|
Import
|
|
</a>
|
|
<a href="/admin/crew/add" class="btn btn-primary gap-2">
|
|
{{ icon "user-plus" "size-4" }}
|
|
Add Crew Member
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{if .StaleTiers}}
|
|
<div class="card bg-base-100 border border-warning/40 shadow-sm mb-6">
|
|
<div class="card-body p-6">
|
|
<div class="flex items-start gap-3 mb-4">
|
|
<span class="text-warning mt-0.5" aria-hidden="true">{{ icon "triangle-alert" "size-5" }}</span>
|
|
<div>
|
|
<h2 class="font-semibold text-base">Tier reconciliation needed</h2>
|
|
<p class="text-sm text-base-content/70 mt-1">
|
|
{{len .StaleTiers}} tier name{{if ne (len .StaleTiers) 1}}s{{end}} on crew records no longer exist in this hold's quota config. Affected crew fall back to the default tier at lookup time, but their stored tier name is stale. Pick a current tier to remap them.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-sm">
|
|
<caption class="sr-only">Stale tier names</caption>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Stored tier name</th>
|
|
<th scope="col" class="text-right">Crew</th>
|
|
<th scope="col">Remap to</th>
|
|
<th scope="col"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .StaleTiers}}
|
|
<tr>
|
|
<td><code class="text-sm">{{.Name}}</code></td>
|
|
<td class="text-right tabular-nums">{{.Count}}</td>
|
|
<td colspan="2">
|
|
<form hx-post="/admin/crew/remap-tier"
|
|
hx-target="#remap-status-{{.Name}}"
|
|
hx-swap="innerHTML"
|
|
class="flex gap-2 items-center">
|
|
{{ csrfInput $.CSRFToken }}
|
|
<input type="hidden" name="from" value="{{.Name}}">
|
|
<select name="to" required class="select select-bordered select-sm w-full max-w-xs">
|
|
<option value="" disabled selected>Select tier…</option>
|
|
{{range $.Tiers}}
|
|
<option value="{{.Key}}">{{.Name}} ({{.Limit}})</option>
|
|
{{end}}
|
|
</select>
|
|
<button type="submit" class="btn btn-warning btn-sm">Apply</button>
|
|
</form>
|
|
<div id="remap-status-{{.Name}}" class="mt-2"></div>
|
|
</td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<p class="text-xs text-base-content/50 mt-3">
|
|
Tip: individual crew can also be remapped via the Edit action on their row below.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
{{if .Crew}}
|
|
<div class="card bg-base-100 shadow-sm">
|
|
<div class="overflow-x-auto">
|
|
<table class="table table-zebra table-fixed">
|
|
<caption class="sr-only">Crew members</caption>
|
|
<colgroup>
|
|
<col style="width: 22%">
|
|
<col style="width: 10%">
|
|
<col style="width: 18%">
|
|
<col style="width: 14%">
|
|
<col style="width: 16%">
|
|
<col style="width: 12%">
|
|
<col style="width: 8%">
|
|
</colgroup>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">Member</th>
|
|
<th scope="col">Role</th>
|
|
<th scope="col">Permissions</th>
|
|
<th scope="col">Tier</th>
|
|
<th scope="col">Usage</th>
|
|
<th scope="col">Added</th>
|
|
<th scope="col" class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="crew-list">
|
|
{{range .Crew}}
|
|
<tr id="crew-{{.RKey}}"
|
|
hx-get="/admin/api/crew/member?rkey={{.RKey}}"
|
|
hx-trigger="load"
|
|
hx-swap="outerHTML"
|
|
aria-busy="true">
|
|
<td>
|
|
<div class="flex flex-col gap-0.5">
|
|
<span class="loading loading-spinner loading-xs" aria-hidden="true"></span>
|
|
<code class="text-xs text-base-content/50 font-mono truncate max-w-full" title="{{.DID}}">{{truncate .DID 32}}</code>
|
|
</div>
|
|
</td>
|
|
<td>{{.Role}}</td>
|
|
<td>
|
|
{{range .Permissions}}
|
|
<span class="badge badge-ghost badge-sm mr-1 mb-1">{{.}}</span>
|
|
{{end}}
|
|
</td>
|
|
<td>
|
|
<span class="badge badge-primary badge-sm">{{.Tier}}</span>
|
|
<br><small class="text-base-content/50">{{.TierLimit}}</small>
|
|
</td>
|
|
<td>
|
|
<div class="flex flex-col gap-1 min-w-24">
|
|
<span class="text-sm">{{.UsageHuman}}</span>
|
|
<progress class="progress {{if gt .UsagePercent 90}}progress-error{{else if gt .UsagePercent 75}}progress-warning{{else}}progress-primary{{end}} w-full" value="{{.UsagePercent}}" max="100" aria-label="Storage usage for {{if .Handle}}{{.Handle}}{{else}}{{.DID}}{{end}}: {{.UsagePercent}}%"></progress>
|
|
<small class="text-base-content/50">{{.UsagePercent}}%</small>
|
|
</div>
|
|
</td>
|
|
<td class="text-sm text-base-content/70">{{if .AddedAt.IsZero}}<span class="text-base-content/30">—</span>{{else}}{{formatTime .AddedAt}}{{end}}</td>
|
|
<td></td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<div role="status" aria-live="polite" class="text-center py-12 text-base-content/60">
|
|
<p>No crew members yet. <a href="/admin/crew/add" class="link link-primary">Add your first crew member</a>.</p>
|
|
</div>
|
|
{{end}}
|
|
{{end}}
|