general appview bugfixes

This commit is contained in:
Evan Jarrett
2026-04-09 10:31:19 -05:00
parent 9033d74a19
commit 564019d1c3
12 changed files with 151 additions and 64 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jetstream:
relay_endpoints:
- https://relay1.us-east.bsky.network
- https://relay1.us-west.bsky.network
- https://zlay.waow.tech
- https://relay.waow.tech
# JWT authentication settings.
auth:
# RSA private key for signing registry JWTs issued to Docker clients.
+3 -2
View File
@@ -333,9 +333,10 @@ func GetAvailableHolds(db DBTX, userDID string) ([]AvailableHold, error) {
c.permissions
FROM hold_captain_records h
LEFT JOIN hold_crew_members c ON h.hold_did = c.hold_did AND c.member_did = ?1
WHERE h.allow_all_crew = 1
WHERE (h.successor IS NULL OR h.successor = '')
AND (h.allow_all_crew = 1
OR h.owner_did = ?1
OR c.member_did IS NOT NULL
OR c.member_did IS NOT NULL)
ORDER BY
CASE
WHEN h.owner_did = ?1 THEN 0
+26
View File
@@ -2022,6 +2022,32 @@ func (h *HoldDIDDB) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (i
return UpdateManifestHoldDID(h.db, did, oldHoldDID, newHoldDID)
}
// GetDistinctManifestHoldDIDs returns all distinct hold DIDs referenced by a user's manifests.
func GetDistinctManifestHoldDIDs(db DBTX, did string) ([]string, error) {
rows, err := db.Query(`
SELECT DISTINCT hold_endpoint FROM manifests
WHERE did = ? AND hold_endpoint != ''
`, did)
if err != nil {
return nil, err
}
defer rows.Close()
var holds []string
for rows.Next() {
var h string
if err := rows.Scan(&h); err != nil {
return nil, err
}
holds = append(holds, h)
}
return holds, rows.Err()
}
// GetDistinctManifestHoldDIDs wraps the package-level function.
func (h *HoldDIDDB) GetDistinctManifestHoldDIDs(did string) ([]string, error) {
return GetDistinctManifestHoldDIDs(h.db, did)
}
// IsManifestReferenced checks if a digest is a child of any manifest list for the user.
// Implements storage.ManifestReferenceChecker.
func (h *HoldDIDDB) IsManifestReferenced(did, digest string) (bool, error) {
+28 -20
View File
@@ -594,28 +594,36 @@ func (h *RepositoryTagsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request
ociClient = user.OciClient
}
// Resolve viewer's default hold for per-entry badges
var viewerDefaultHold string
if viewerDID != "" {
viewerDefaultHold = db.GetUserHoldDID(h.ReadOnlyDB, viewerDID)
}
data := struct {
Owner *db.User
Repository *db.Repository
Entries []db.ManifestEntry
IsOwner bool
ScanBatchParams []template.HTML
RegistryURL string
OciClient string
HasMore bool
NextOffset int
IsFirstPage bool
Owner *db.User
Repository *db.Repository
Entries []db.ManifestEntry
IsOwner bool
ScanBatchParams []template.HTML
RegistryURL string
OciClient string
HasMore bool
NextOffset int
IsFirstPage bool
ViewerDefaultHold string
}{
Owner: owner,
Repository: &db.Repository{Name: repository},
Entries: entries,
IsOwner: isOwner,
ScanBatchParams: scanBatchParams,
RegistryURL: h.RegistryURL,
OciClient: ociClient,
HasMore: hasMore,
NextOffset: offset + pageSize,
IsFirstPage: isFirstPage,
Owner: owner,
Repository: &db.Repository{Name: repository},
Entries: entries,
IsOwner: isOwner,
ScanBatchParams: scanBatchParams,
RegistryURL: h.RegistryURL,
OciClient: ociClient,
HasMore: hasMore,
NextOffset: offset + pageSize,
IsFirstPage: isFirstPage,
ViewerDefaultHold: viewerDefaultHold,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
+4 -9
View File
@@ -66,20 +66,15 @@ func NewProcessor(database db.DBTX, useCache bool, statsCache *StatsCache) *Proc
// EnsureUser resolves and upserts a user by DID
// Uses cache if enabled (Worker), queries DB if cache disabled (Backfill)
func (p *Processor) EnsureUser(ctx context.Context, did string) error {
// Check cache first (if enabled)
// Check cache first (if enabled) — within a single backfill run,
// a user's identity won't change, so the cache hit is safe.
if p.useCache && p.userCache != nil {
if _, ok := p.userCache.cache[did]; ok {
// User in cache - just update last seen timestamp
return db.UpdateUserLastSeen(p.db, did)
}
} else if !p.useCache {
// No cache - check if user already exists in DB
existingUser, err := db.GetUserByDID(p.db, did)
if err == nil && existingUser != nil {
// User exists - just update last seen timestamp
return db.UpdateUserLastSeen(p.db, did)
}
}
// No cache early-return: always re-resolve identity so stale handles
// (e.g., user changed handle between backfill runs) get corrected.
// Resolve DID to get handle and PDS endpoint
resolvedDID, handle, pdsEndpoint, err := atproto.ResolveIdentity(ctx, did)
+1
View File
@@ -40,6 +40,7 @@ type PushWebhookEvent struct {
type HoldDIDLookup interface {
GetLatestHoldDIDForRepo(did, repository string) (string, error)
UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string) (int64, error)
GetDistinctManifestHoldDIDs(did string) ([]string, error)
}
// RegistryContext bundles all the context needed for registry operations
+7
View File
@@ -21,6 +21,13 @@ func (m *mockHoldDIDLookup) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID st
return 0, nil
}
func (m *mockHoldDIDLookup) GetDistinctManifestHoldDIDs(did string) ([]string, error) {
if m.holdDID != "" {
return []string{m.holdDID}, nil
}
return nil, nil
}
func TestRegistryContext_Fields(t *testing.T) {
// Create a sample RegistryContext
ctx := &RegistryContext{
+57 -28
View File
@@ -17,13 +17,13 @@ import (
var drainLocks sync.Map
// MigrateManifestsForSuccessor rewrites manifest records and profile
// when a user's defaultHold has a successor. Best-effort, runs in background.
// when any of the user's holds have a successor. Best-effort, runs in background.
//
// Steps:
// 1. Get user's sailor profile — check if defaultHold has a successor
// 2. Update profile.DefaultHold from oldHold → newHold
// 3. Walk all io.atcr.manifest records, rewrite holdDid from oldHold → newHold
// 4. Update appview's local manifests table to match
// 1. Get user's sailor profile
// 2. Collect candidate holds: profile.DefaultHold + distinct holds from manifests DB
// 3. For each hold with a successor: rewrite PDS manifest records and local DB
// 4. If profile.DefaultHold itself had a successor, update the profile too
func MigrateManifestsForSuccessor(
ctx context.Context,
client *atproto.Client,
@@ -43,35 +43,64 @@ func MigrateManifestsForSuccessor(
slog.Debug("Drain: failed to get profile", "component", "storage/drain", "did", did, "error", err)
return
}
if profile == nil || profile.DefaultHold == "" {
// 2. Collect candidate holds to check for successors.
// Start with profile.DefaultHold, then add any distinct holds from the DB.
candidates := make(map[string]bool)
if profile != nil && profile.DefaultHold != "" {
candidates[profile.DefaultHold] = true
}
if db != nil {
manifestHolds, err := db.GetDistinctManifestHoldDIDs(did)
if err != nil {
slog.Warn("Drain: failed to get distinct manifest holds", "component", "storage/drain", "did", did, "error", err)
}
for _, h := range manifestHolds {
candidates[h] = true
}
}
if len(candidates) == 0 {
return
}
// 2. Check if their defaultHold has a successor
oldHold := profile.DefaultHold
captain, err := authorizer.GetCaptainRecord(ctx, oldHold)
if err != nil {
slog.Debug("Drain: failed to get captain record", "component", "storage/drain", "did", did, "hold", oldHold, "error", err)
return
}
if captain == nil || captain.Successor == "" {
return // No successor — nothing to drain
}
newHold := captain.Successor
// 3. Check each candidate for a successor and drain if found
for oldHold := range candidates {
captain, err := authorizer.GetCaptainRecord(ctx, oldHold)
if err != nil {
slog.Debug("Drain: failed to get captain record", "component", "storage/drain", "did", did, "hold", oldHold, "error", err)
continue
}
if captain == nil || captain.Successor == "" {
continue
}
newHold := captain.Successor
slog.Info("Starting hold drain", "component", "storage/drain", "did", did, "from", oldHold, "to", newHold)
slog.Info("Starting hold drain", "component", "storage/drain", "did", did, "from", oldHold, "to", newHold)
// 3. Update profile.DefaultHold
profile.DefaultHold = newHold
profile.UpdatedAt = time.Now()
if err := UpdateProfile(ctx, client, profile); err != nil {
slog.Warn("Drain: failed to update profile", "component", "storage/drain", "did", did, "error", err)
// Continue — manifest rewrite is still valuable even if profile update fails
} else {
slog.Info("Drain: updated profile defaultHold", "component", "storage/drain", "did", did, "newHold", newHold)
drainHold(ctx, client, db, did, oldHold, newHold)
// 4. If profile.DefaultHold pointed to this old hold, update it
if profile != nil && profile.DefaultHold == oldHold {
profile.DefaultHold = newHold
profile.UpdatedAt = time.Now()
if err := UpdateProfile(ctx, client, profile); err != nil {
slog.Warn("Drain: failed to update profile", "component", "storage/drain", "did", did, "error", err)
} else {
slog.Info("Drain: updated profile defaultHold", "component", "storage/drain", "did", did, "newHold", newHold)
}
}
}
}
// 4. Walk manifest records, rewrite holdDid
// drainHold rewrites all PDS manifest records and local DB rows from oldHold to newHold.
func drainHold(
ctx context.Context,
client *atproto.Client,
db HoldDIDLookup,
did, oldHold, newHold string,
) {
// Walk PDS manifest records, rewrite holdDid
cursor := ""
rewritten := 0
for {
@@ -126,7 +155,7 @@ func MigrateManifestsForSuccessor(
cursor = nextCursor
}
// 5. Update appview's local manifests table
// Update appview's local manifests table
if db != nil {
dbUpdated, err := db.UpdateManifestHoldDID(did, oldHold, newHold)
if err != nil {
@@ -37,6 +37,13 @@ func (m *mockDatabase) UpdateManifestHoldDID(did, oldHoldDID, newHoldDID string)
return 0, nil
}
func (m *mockDatabase) GetDistinctManifestHoldDIDs(did string) ([]string, error) {
if m.holdDID != "" {
return []string{m.holdDID}, nil
}
return nil, nil
}
func TestNewRoutingRepository(t *testing.T) {
ctx := &RegistryContext{
DID: "did:plc:test123",
@@ -18,6 +18,9 @@
{{ icon "shield-check" "size-3" }} Attested
</button>
{{ end }}
{{ if and .ViewerDefaultHold .Entry.HoldEndpoint (ne .Entry.HoldEndpoint .ViewerDefaultHold) }}
<span class="badge badge-xs badge-soft badge-warning" title="{{ .Entry.HoldEndpoint }}">{{ icon "hard-drive" "size-3" }} {{ displayHoldDID .Entry.HoldEndpoint }}</span>
{{ end }}
</div>
<div class="flex items-center gap-2 shrink-0">
<span class="text-base-content text-sm flex items-center gap-1" title="{{ .Entry.CreatedAt.Format "2006-01-02T15:04:05Z07:00" }}">{{ icon "history" "size-4" }}{{ timeAgoShort .Entry.CreatedAt }}</span>
@@ -160,7 +163,7 @@
<div class="card bg-base-100 shadow-sm border border-base-300">
<div class="divide-y divide-base-200" id="tags-list">
{{ range .Entries }}
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner) }}
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner "ViewerDefaultHold" $.ViewerDefaultHold) }}
{{ end }}
</div>
{{ template "load-more-button" . }}
@@ -175,7 +178,7 @@
{{ define "repo-tags-page" }}
{{ range .Entries }}
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner) }}
{{ template "artifact-entry-markup" (dict "Entry" . "OwnerDID" $.Owner.DID "OwnerHandle" $.Owner.Handle "RepoName" $.Repository.Name "RegistryURL" $.RegistryURL "OciClient" $.OciClient "IsOwner" $.IsOwner "ViewerDefaultHold" $.ViewerDefaultHold) }}
{{ end }}
{{ template "load-more-button" . }}
{{ template "scan-batch-triggers" . }}
-1
View File
@@ -34,7 +34,6 @@ var KnownRelays = []KnownRelay{
{Name: "Xero", URL: "https://relay.xero.systems"},
{Name: "Feeds Blue", URL: "https://relay.feeds.blue"},
{Name: "Waow", URL: "https://relay.waow.tech"},
{Name: "Zlay", URL: "https://zlay.waow.tech"},
{Name: "Bassh", URL: "https://relay.bas.sh"},
}
+12 -1
View File
@@ -103,6 +103,7 @@ func (a *RemoteHoldAuthorizer) cleanupRecentDenials() {
// 3. Update cache
func (a *RemoteHoldAuthorizer) GetCaptainRecord(ctx context.Context, holdDID string) (*atproto.CaptainRecord, error) {
// Try cache first
var staleCached *captainRecordWithMeta
if a.db != nil {
cached, err := a.getCachedCaptainRecord(holdDID)
if err == nil && cached != nil {
@@ -110,13 +111,23 @@ func (a *RemoteHoldAuthorizer) GetCaptainRecord(ctx context.Context, holdDID str
if time.Since(cached.UpdatedAt) < a.cacheTTL {
return cached.CaptainRecord, nil
}
// Cache expired - continue to fetch fresh data
// Cache expired - keep as fallback in case XRPC fetch fails
staleCached = cached
}
}
// Cache miss or expired - query XRPC endpoint
record, err := a.fetchCaptainRecordFromXRPC(ctx, holdDID)
if err != nil {
// If the hold is unreachable but we have stale cache, use it.
// Successor fields don't change once set, so stale data is safe.
if staleCached != nil {
slog.Warn("Captain record fetch failed, using stale cache",
"holdDID", holdDID,
"cache_age", time.Since(staleCached.UpdatedAt),
"error", err)
return staleCached.CaptainRecord, nil
}
slog.Error("Captain record fetch failed",
"holdDID", holdDID,
"denial_reason", "captain_record_fetch_failed",