Files
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

91 lines
3.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package admin
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"atcr.io/pkg/hold/pds"
)
// handleScanBackfill kicks off a scan-status backfill as a background job and
// returns a progress fragment that polls /admin/api/jobs/scan-backfill/status.
// Idempotent — clicking again while a run is in flight just shows the current
// progress (startJob returns false).
//
// Why background: reverse proxies typically cap upstream HTTP timeouts at
// 1060s, which would cancel a synchronous request mid-loop. The job runs under
// its own detached context (see jobs.go), so it survives the request ending.
//
// JSON callers (Accept: application/json) get a synchronous run instead —
// useful for curl + scripting.
func (ui *AdminUI) handleScanBackfill(w http.ResponseWriter, r *http.Request) {
session := getSessionFromContext(r.Context())
wantJSON := strings.Contains(r.Header.Get("Accept"), "application/json")
if wantJSON {
// Synchronous JSON path — caller decides their own timeout.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
res, err := ui.pds.BackfillScanStatus(ctx, scanBackfillLogger, nil)
if err != nil {
slog.Error("scan-backfill failed", "by", session.DID, "error", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
slog.Info("scan-status backfill complete (sync)",
"by", session.DID,
"scanned", res.Scanned,
"already_tagged", res.AlreadyTagged,
"marked_skipped", res.MarkedSkipped,
"marked_failed", res.MarkedFailed,
"rewritten", res.Rewritten,
)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(res)
return
}
started := ui.startJob("scan-backfill", "Backfilling scan records",
"partials/scan_backfill_result.html", 10*time.Minute,
func(ctx context.Context, progress func(jobProgress)) (any, error) {
res, err := ui.pds.BackfillScanStatus(ctx, scanBackfillLogger, func(snap *pds.ScanBackfillResult) {
progress(jobProgress{
Done: snap.Scanned,
Message: fmt.Sprintf("Scanned %d · rewrites %d (%d skipped, %d failed)",
snap.Scanned, snap.Rewritten, snap.MarkedSkipped, snap.MarkedFailed),
})
})
if err != nil {
return nil, err
}
slog.Info("scan-status backfill complete",
"scanned", res.Scanned,
"already_tagged", res.AlreadyTagged,
"marked_skipped", res.MarkedSkipped,
"marked_failed", res.MarkedFailed,
"rewritten", res.Rewritten,
)
return res, nil
})
if started {
slog.Info("scan-status backfill started via admin panel", "by", session.DID)
} else {
slog.Debug("scan-status backfill already in progress; returning current state")
}
ui.renderTemplate(w, "partials/job_progress.html", ui.jobSnapshot("scan-backfill"))
}
// scanBackfillLogger formats the printf-style messages from BackfillScanStatus
// into a single slog message — slog's variadic args are key/value pairs, not
// printf operands.
func scanBackfillLogger(format string, args ...any) {
slog.Warn("scan-backfill: " + fmt.Sprintf(format, args...))
}