133 Commits
Author SHA1 Message Date
Evan JarrettandClaude Opus 5 1253ca15ec appview: report a never-scanned image as unscanned, not as a failure
digest_content.go branched on Error == "never-scanned" to pick the
"not scanned yet" copy, but nothing anywhere produced that string: both
FetchVulnDetails and FetchSbomDetails returned the human sentence
"No scan record found" for a missing record. So vulnReason and sbomReason
could never be "not-scanned", the friendly branches in vulns-section.html
and sbom-section.html were dead code, and every unscanned image fell
through to fetch-failed.

Free-tier accounts have scan_on_push off, so this was every image they
push, told "Scan data couldn't be loaded... try refreshing in a minute"
about something that had never been scanned and never would be by
refreshing. The digest page showed the raw internal string instead.

Replace the prose sentinel with a NotScanned bool the 404 path actually
sets, and give other non-200 statuses a distinct message so a 500 from the
hold stops being indistinguishable from an absent record. The detail
templates branch on it before Error, so nothing leaks the internal value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UAqi2hS2dhZoatqcWoYZQk
2026-09-02 12:44:48 -05:00
Evan JarrettandClaude Opus 5 606338b33e appview: fix mockup-code contrast in both themes
daisyUI dims the gutter prefix with `opacity: .5`, which multiplies
against whatever opacity the line's own text color carries. On
bg-base-300 a plain `$` measured 3.20:1 in light and 4.19:1 in dark,
and the `#` on a line already dimmed to /70 compounded to 2.15:1. The
text needs 4.5:1, so this failed in dark mode too, just less visibly
than the light-mode report that surfaced it.

Give the prefix an absolute muted color instead of a multiplying
opacity. The override has to sit unlayered: daisyUI ships this selector
in `@layer daisyui`, declared after `@layer components`, so a rule in
components loses on layer order however specific it is. A first attempt
inside components left daisyUI's `opacity: .5` live on top of the new
70% color, which made the `$` worse (0.5 -> 0.35 effective) rather than
better.

The hero tagline drops its /70 and now reads at the same weight as the
docker commands above it. install.html's two comment lines were at /50,
which failed on the text itself (3.20:1 light), and move to /70 where
they still read as comments.

Measured in Chromium against the built stylesheet, compositing each
pseudo-element color over its real background on a canvas: every prefix
and comment is now 5.84:1 light / 6.71:1 dark, against 14.03:1 / 12.02:1
for the command text, so the gutter stays visibly de-emphasized.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpqkSRqpcnAjsSZgFTUN5z
2026-08-31 17:05:15 -05:00
Evan JarrettandClaude Opus 5 ee9ccc1fb8 appview: drop crossorigin from the imgs.blue preconnect
Connections are keyed by credentials mode. The crossorigin attribute
opened the imgs.blue socket in anonymous-CORS mode, but every request
to that origin is a plain <img src> avatar fetch (BlobCDNURL /
resizeImage) in no-cors mode, so nothing could reuse it. The browser
opened a second connection anyway and Lighthouse flagged the hint as
unused while still listing imgs.blue as a preconnect candidate worth
~300ms of LCP.

The attribute is correct for the font preloads below, which is where it
was likely copied from; comment the distinction so it stays put.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpqkSRqpcnAjsSZgFTUN5z
2026-08-31 16:57:49 -05:00
Evan JarrettandClaude Opus 5 5112425673 appview: make the footer Bluesky link configurable
The link was hardcoded before fa34da0 too — it pointed at
bsky.app/profile/atcr.io. That commit was right to switch to a DID, since
handles change and a stale handle link breaks silently, but the DID went into
components/footer.html, a shared template. Every self-hoster's footer therefore
links to the project's Bluesky account.

Now ui.bluesky_profile, following source_url in the same footer exactly:
config field with a default, plumbed through UIDependencies and PageData, and
guarded with {{ with }} so an unset value omits the link rather than rendering
something wrong. It takes a handle or a DID; the comment says to prefer a DID
for the reason fa34da0 changed it.

Defaulting to the project account matches source_url's logic — both name the
upstream project rather than the operator — and self-hosters who want their own
or none set one line.

The aria-label switched to $.ClientShortName: `with` rebinds the dot, so the
label would otherwise have silently rendered empty.

Two things worth recording:

  * `{{ with }}` hides the link on an empty value, but you cannot get an empty
    value from the environment. Viper runs with AllowEmptyEnv(false), so an
    empty env var reads as unset and the default wins. Only "" in YAML works.
    That applies to every string field in this config, not just this one, and
    the comment now says so.
  * config-appview.example.yaml must NOT be regenerated with `config init`,
    despite what the checklist in CLAUDE.md says. The file is hand-curated well
    past the defaults, and regenerating replaces real Stripe price IDs with
    price_xxx placeholders and blanks registry_domains, managed_holds, theme
    and the tier names. Added by hand instead.

Verified live both ways: the configured value renders, and `bluesky_profile: ""`
in YAML drops the link while leaving the Source link intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
2026-08-25 16:34:26 -05:00
Evan JarrettandClaude Opus 5 2b71be59f7 billing: require a managed hold for paid features
Entitlements were keyed on the Stripe subscription alone, so a subscriber
who switched to a self-hosted hold kept paying for features the appview
cannot deliver, and could still reach checkout.

  - billing.ActiveHoldChecker and Manager.onManagedHold gate every
    entitlement. An empty default hold counts as managed: the user has no
    explicit preference and falls back to the operator's primary managed
    hold.
  - The checker reads the primary DB, not the read replica. A hold switch
    writes default_hold_did to the primary, and replica lag would keep
    paid features alive after a switch away.
  - db.GetUserDefaultHoldDID is the clean default-hold signal, unlike
    GetUserHoldDID which falls back to a manifest hold_endpoint (a URL,
    not a DID).
  - Jetstream fails closed: an unresolvable hold reference is cached raw
    rather than left empty, since an empty value reads as managed.
  - UI: the billing tab is hidden on self-hosted, a cancel/manage banner
    appears when a self-hosted user still has an active plan, the image
    advisor returns managed_hold_required instead of upgrade_required,
    and the checkout route returns 403. The portal stays open so existing
    subscribers can still cancel.

Two consistency fixes fall out of wiring this up:

The settings UI reads the resolved default_hold_did rather than the raw
profile.DefaultHold. The profile field is the record value as written and
may be a URL-form reference; jetstream resolves it to a DID on the way
into the DB, and the server-side gate reads that resolved value. Comparing
the raw form against managed DIDs would show the "you are self-hosted"
banner and hide billing from a user whose entitlements say otherwise.

HasAIAdvisor falls back to the free tier's AIAdvisor setting when
off-managed instead of a hard false, matching GetWebhookLimits. Losing a
managed hold should drop a user to free-tier entitlements, not below them.

BEHAVIOR CHANGE for existing paying users on self-hosted holds: they lose
the AI advisor, supporter badge and paid webhook limits as soon as this
deploys, while Stripe keeps charging them. The only notice is the banner
on /settings/storage, which they have to visit to see. Decide on a
migration (notification, or a one-time reconciliation over active
subscriptions) before shipping this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 21:14:58 -05:00
Evan JarrettandClaude Opus 5 fa34da0f26 appview: point the footer Bluesky link at the DID
Handles can change; the DID cannot. Linking the DID keeps the footer
correct if the account's handle is ever reassigned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-09 20:49:24 -05:00
Evan JarrettandClaude Opus 5 3298797603 appview: resolve managed hold names off the privacy render path
Follow-up to 6e77311. Listing the operated holds on /privacy put
resolveHoldDisplayName in the render path, where it makes up to two sequential
network calls per DID: the DID document fetch, then handle verification. /privacy
is public and unauthenticated, and the server sets no HTTP read or write timeout,
so an unreachable hold or a plc.directory outage stalled the page for every
visitor. The identity directory's negative-cache TTL is short, so the stall
recurred rather than settling after the first hit.

The DIDs come from config and never change while the process runs, so resolution
happens once in a background goroutine and lands in an atomic.Pointer. The
handler is constructed once at route registration, so the cache is process-wide.
Until resolution completes the page renders offline names, which are already
correct for did:web holds since those decode straight from the DID.

Dropped the did:plc truncation. resolveHoldDisplayName's last fallback cut a DID
to 24 characters plus an ellipsis, which is shorter than a did:plc, so the result
could not be resolved back to a hold. That is tolerable in a settings dropdown
and actively misleading in a privacy policy naming the services we operate.
Shortening for display belongs in the template. The non-network fallbacks are now
in holdDisplayNameOffline so the background resolver and the render path share
them.

Also fixed the surrounding copy, which scoped coverage to *.<site> domains while
the list immediately above it could contain holds on other domains — the two
sentences contradicted each other. It now refers to the listed holds, and only
makes that claim when there is a real list; with no managed holds configured the
template still shows an illustrative placeholder, which must not be presented as
fact in a legal document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 19:20:11 -05:00
Evan Jarrett 6e77311c22 minor ui fixes, update privacy page with manged holds list 2026-08-02 13:38:45 -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 63909aaca0 add webhook for storage quota percentage 2026-05-17 19:57:09 -05:00
Evan Jarrett 04e10b6818 fix quota message 2026-05-16 17:20:12 -05:00
Evan Jarrett 98a2cfea59 improve UI around credential helper authorization. have the hold requestCrawl on restart. Update comments that relay_endpoints must suport listreposbycollection 2026-05-08 20:44:04 -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 13a793ca90 improve admin tooling 2026-04-29 10:37:36 -05:00
Evan Jarrett 9af6eccc9d improvements to how scanning works, and helmchart ui 2026-04-29 10:12:25 -05:00
Evan Jarrett 1bff09ecbe fix settings page 2026-04-22 21:49:51 -05:00
Evan Jarrett a602bf08d1 fix missing icons, update light mode theme colors 2026-04-22 21:41:48 -05:00
Evan Jarrett c7783bf87c more hardening, inline tangled svg into the sprite 2026-04-22 20:13:40 -05:00
Evan Jarrett 6b6ce093d3 new signup flow 2026-04-21 22:29:23 -05:00
Evan Jarrett f057f169f0 large list of ui fixes for accessibility/hardening etc. 2026-04-21 21:18:13 -05:00
Evan Jarrett 7a6775d4fa missed new files for jetstream improvements 2026-04-19 18:04:57 -05:00
Evan Jarrett 38c693acc9 impeccable pass 2026-04-19 17:35:41 -05:00
Evan Jarrett 9809c26281 update fonts 2026-04-14 20:56:53 -05:00
Evan Jarrett e843b7233c more ui fixes and scanner fixes 2026-04-12 20:48:24 -05:00
Evan Jarrett 2f30c22f0a impeccable fixes, scanner fixes 2026-04-12 20:36:57 -05:00
Evan Jarrett 25628dad2c update the login page 2026-04-11 21:01:31 -05:00
Evan Jarrett 564019d1c3 general appview bugfixes 2026-04-09 10:31:19 -05:00
Evan Jarrett 20202aa0df update credential helper to pull latest update from tangled directly 2026-04-04 20:59:50 -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 183d89530d add diff support for layers and vulns 2026-03-29 21:42:55 -07:00
Evan Jarrett f20170f595 digest page improvements 2026-03-29 13:01:40 -07: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 23db9be665 add repo page editor. fix deleting all untagged actually deleting all untagged 2026-03-23 21:16:13 -05:00
Evan Jarrett d6816fd00e add new files for getting image configs from hold etc 2026-03-22 21:17:28 -05:00
Evan Jarrett 385f8987fe overhaul repo pages, add tab for 'artifacts' (tags, manifests, helm charts). implement digest page with layer commands and vuln reports 2026-03-22 21:10:47 -05:00
Evan Jarrett 29ef8138aa fix svg formatting 2026-03-18 09:04:38 -05:00
Evan Jarrett 7d8e195189 more brand changes 2026-03-17 21:43:02 -05:00
Evan Jarrett e886192aeb update seamark theme, add 'delete all untagged' option on record page. add garbage collection flag for untagged 2026-03-16 20:26:56 -05:00
Evan Jarrett 7d74e76772 more billing/settings/webhook tweaks 2026-02-28 14:42:35 -06:00
Evan Jarrett 136c0a0ecc billing refactor, move billing to appview, move webhooks to appview 2026-02-26 22:28:09 -06:00
Evan Jarrett dc31ca2f35 more work on webhook, implement getMetadata endpoint for appview and link holds to a preferred appview 2026-02-22 22:49:33 -06:00
Evan Jarrett 2b9ea997ac fix tier and supporter badge assignments. normalize did:web adresses with ports. various minor fixes 2026-02-22 11:16:55 -06:00
Evan Jarrett f90a46e0a4 begin implement supporter badges, clean up lexicons, various other changes 2026-02-20 22:12:18 -06:00
Evan Jarrett 76383ec764 fix vuln scanner db not refreshing 2026-02-19 22:08:02 -06:00
Evan Jarrett dba201998e move the vuln report to tags instead of manifests 2026-02-16 22:32:18 -06:00
Evan Jarrett 2df5377541 more did:plc fixes, more vulnerability scanner fixes 2026-02-15 22:28:36 -06:00
Evan Jarrett ec2063ef52 fix star not being filled in. add ability to deploy scanner on the same server as the hold 2026-02-13 20:41:36 -06:00
Evan Jarrett 8048921f5e show attestation details 2026-02-13 19:40:05 -06:00