Files
at-container-registry/docs/APPVIEW-UI-V1.md
2025-10-06 16:28:58 -05:00

24 KiB

ATCR AppView UI - Version 1 Specification

Overview

The ATCR AppView UI provides a web interface for discovering, managing, and configuring container images in the ATCR registry. Version 1 focuses on three core pages that leverage existing functionality:

  1. Front Page - Federated image discovery via firehose
  2. Settings Page - Profile and hold configuration
  3. Personal Page - Manage your images and tags

Architecture

Tech Stack

  • Backend: Go (existing AppView codebase)
  • Frontend: TBD (Go templates/Templ or separate SPA)
  • Database: SQLite (firehose data cache)
  • Styling: TBD (plain CSS, Tailwind, etc.)
  • Authentication: OAuth with DPoP (reuse existing implementation)

Components

┌─────────────────────────────────────────────────────────────┐
│                        Web UI (Browser)                      │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                    AppView HTTP Server                       │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ UI Endpoints │  │ OCI API      │  │ OAuth Server │      │
│  │ /ui/*        │  │ /v2/*        │  │ /auth/*      │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
                              │
                    ┌─────────┴─────────┐
                    ▼                   ▼
          ┌──────────────────┐  ┌──────────────────┐
          │ SQLite Database  │  │ ATProto Client   │
          │ (Firehose cache) │  │ (PDS operations) │
          └──────────────────┘  └──────────────────┘
                                         ▲
          ┌──────────────────┐           │
          │ Firehose Worker  │───────────┘
          │ (Background)     │
          └──────────────────┘
                    ▲
                    │
          ┌──────────────────┐
          │ ATProto Firehose │
          │ (Jetstream/Relay)│
          └──────────────────┘

Database Schema

SQLite database for caching firehose data and enabling fast queries.

Tables

users

CREATE TABLE users (
    did TEXT PRIMARY KEY,
    handle TEXT NOT NULL,
    pds_endpoint TEXT NOT NULL,
    last_seen TIMESTAMP NOT NULL,
    UNIQUE(handle)
);
CREATE INDEX idx_users_handle ON users(handle);

manifests

CREATE TABLE manifests (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    did TEXT NOT NULL,
    repository TEXT NOT NULL,
    digest TEXT NOT NULL,
    hold_endpoint TEXT NOT NULL,
    schema_version INTEGER NOT NULL,
    media_type TEXT NOT NULL,
    config_digest TEXT,
    config_size INTEGER,
    raw_manifest TEXT NOT NULL, -- JSON blob
    created_at TIMESTAMP NOT NULL,
    UNIQUE(did, repository, digest),
    FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
);
CREATE INDEX idx_manifests_did_repo ON manifests(did, repository);
CREATE INDEX idx_manifests_created_at ON manifests(created_at DESC);
CREATE INDEX idx_manifests_digest ON manifests(digest);

layers

CREATE TABLE layers (
    manifest_id INTEGER NOT NULL,
    digest TEXT NOT NULL,
    size INTEGER NOT NULL,
    media_type TEXT NOT NULL,
    layer_index INTEGER NOT NULL,
    PRIMARY KEY(manifest_id, layer_index),
    FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
);
CREATE INDEX idx_layers_digest ON layers(digest);

tags

CREATE TABLE tags (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    did TEXT NOT NULL,
    repository TEXT NOT NULL,
    tag TEXT NOT NULL,
    digest TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL,
    UNIQUE(did, repository, tag),
    FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
);
CREATE INDEX idx_tags_did_repo ON tags(did, repository);

firehose_cursor

CREATE TABLE firehose_cursor (
    id INTEGER PRIMARY KEY CHECK (id = 1),
    cursor INTEGER NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

Firehose Worker

Background goroutine that subscribes to ATProto firehose and populates the database.

Implementation

// pkg/ui/firehose/worker.go

type Worker struct {
    db          *sql.DB
    jetstream   *JetstreamClient
    resolver    *atproto.Resolver
    stopCh      chan struct{}
}

func (w *Worker) Start() error {
    // Load cursor from database
    cursor := w.loadCursor()

    // Subscribe to firehose
    events := w.jetstream.Subscribe(cursor, []string{
        "io.atcr.manifest",
        "io.atcr.tag",
    })

    for {
        select {
        case event := <-events:
            w.handleEvent(event)
        case <-w.stopCh:
            return nil
        }
    }
}

func (w *Worker) handleEvent(event FirehoseEvent) error {
    switch event.Collection {
    case "io.atcr.manifest":
        return w.handleManifest(event)
    case "io.atcr.tag":
        return w.handleTag(event)
    }
    return nil
}

Event Handling

Manifest create:

  • Resolve DID → handle, PDS endpoint
  • Insert/update user record
  • Parse manifest JSON
  • Insert manifest record
  • Insert layer records

Tag create/update:

  • Insert/update tag record
  • Link to existing manifest

Record deletion:

  • Delete from database (cascade handles related records)

Firehose Connection

Use Jetstream (bluesky-social/jetstream) or connect directly to relay:

  • Jetstream: Websocket to wss://jetstream.atproto.tools/subscribe
  • Relay: Websocket to relay (e.g., wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos)

Jetstream is simpler and filters events server-side.

Page Specifications

1. Front Page - Federated Discovery

URL: /ui/ or /ui/explore

Purpose: Discover recently pushed images across all ATCR users.

Layout:

┌─────────────────────────────────────────────────────────────┐
│  ATCR                           [Search] [@handle] [Login]  │
├─────────────────────────────────────────────────────────────┤
│  Recent Pushes                                    [Filter ▼]│
│                                                               │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ alice.bsky.social/nginx:latest                        │  │
│  │ sha256:abc123... • hold1.alice.com • 2 hours ago      │  │
│  │ [docker pull atcr.io/alice.bsky.social/nginx:latest] │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                               │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ bob.dev/myapp:v1.2.3                                  │  │
│  │ sha256:def456... • atcr-storage.fly.dev • 5 hours ago │  │
│  │ [docker pull atcr.io/bob.dev/myapp:v1.2.3]            │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                               │
│  [Load more...]                                               │
└─────────────────────────────────────────────────────────────┘

Features:

  • List of recent pushes (manifests + tags)
  • Show: handle, repository, tag, digest (truncated), timestamp, hold endpoint
  • Copy-paste pull command with click-to-copy
  • Filter by user (click handle to filter)
  • Search by repository name or tag
  • Click manifest to view details (modal or dedicated page)
  • Pagination (50 items per page)

API Endpoint:

GET /ui/api/recent-pushes
Query params:
  - limit (default: 50)
  - offset (default: 0)
  - user (optional: filter by DID or handle)
  - repository (optional: filter by repo name)

Response:
{
  "pushes": [
    {
      "did": "did:plc:alice123",
      "handle": "alice.bsky.social",
      "repository": "nginx",
      "tag": "latest",
      "digest": "sha256:abc123...",
      "hold_endpoint": "https://hold1.alice.com",
      "created_at": "2025-10-05T12:34:56Z",
      "pull_command": "docker pull atcr.io/alice.bsky.social/nginx:latest"
    }
  ],
  "total": 1234,
  "offset": 0,
  "limit": 50
}

Manifest Details Modal:

  • Full manifest JSON (syntax highlighted)
  • Layer list with digests and sizes
  • Link to ATProto record (at://did/io.atcr.manifest/rkey)
  • Architecture, OS, labels
  • Creation timestamp

2. Settings Page

URL: /ui/settings

Auth: Requires login (OAuth)

Purpose: Configure profile and hold preferences.

Layout:

┌─────────────────────────────────────────────────────────────┐
│  ATCR                                         [@alice] [⚙️]  │
├─────────────────────────────────────────────────────────────┤
│  Settings                                                     │
│                                                               │
│  ┌─ Identity ───────────────────────────────────────────┐   │
│  │ Handle:         alice.bsky.social                     │   │
│  │ DID:            did:plc:alice123abc (read-only)       │   │
│  │ PDS:            https://bsky.social (read-only)       │   │
│  └───────────────────────────────────────────────────────┘   │
│                                                               │
│  ┌─ Default Hold ──────────────────────────────────────┐    │
│  │ Current: https://hold1.alice.com                      │   │
│  │                                                        │   │
│  │ [Dropdown: Select from your holds ▼]                  │   │
│  │   • https://hold1.alice.com (Your BYOS)               │   │
│  │   • https://storage.atcr.io (AppView default)         │   │
│  │   • [Custom URL...]                                   │   │
│  │                                                        │   │
│  │ Custom hold URL: [_____________________]              │   │
│  │                                                        │   │
│  │                                      [Save]            │   │
│  └───────────────────────────────────────────────────────┘   │
│                                                               │
│  ┌─ OAuth Session ─────────────────────────────────────┐    │
│  │ Logged in as: alice.bsky.social                       │   │
│  │ Session expires: 2025-10-06 14:23:00 UTC              │   │
│  │                              [Re-authenticate]         │   │
│  └───────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Features:

  • Display current identity (handle, DID, PDS)
  • Default hold configuration:
    • Dropdown showing user's io.atcr.hold records (query from PDS)
    • Option to select AppView's default storage endpoint
    • Manual entry for custom hold URL
    • "Save" button updates io.atcr.sailor.profile.defaultHold
  • OAuth session status
  • Re-authenticate button (redirects to OAuth flow)

API Endpoints:

GET /ui/api/profile
Auth: Required (session cookie)
Response:
{
  "did": "did:plc:alice123",
  "handle": "alice.bsky.social",
  "pds_endpoint": "https://bsky.social",
  "default_hold": "https://hold1.alice.com",
  "holds": [
    {
      "endpoint": "https://hold1.alice.com",
      "name": "My BYOS Storage",
      "public": false
    }
  ],
  "session_expires_at": "2025-10-06T14:23:00Z"
}

POST /ui/api/profile/default-hold
Auth: Required
Body:
{
  "hold_endpoint": "https://hold1.alice.com"
}
Response:
{
  "success": true
}

3. Personal Page - Your Images

URL: /ui/images or /ui/@{handle}

Auth: Requires login (OAuth)

Purpose: Manage your container images and tags.

Layout:

┌─────────────────────────────────────────────────────────────┐
│  ATCR                                         [@alice] [⚙️]  │
├─────────────────────────────────────────────────────────────┤
│  Your Images                                                  │
│                                                               │
│  ┌─ nginx ──────────────────────────────────────────────┐   │
│  │ 3 tags • 5 manifests • Last push: 2 hours ago        │   │
│  │                                                        │   │
│  │ Tags:                                                  │   │
│  │ ┌────────────────────────────────────────────────┐   │   │
│  │ │ latest  → sha256:abc123... (2 hours ago) [✏️][🗑️]│   │   │
│  │ │ v1.25   → sha256:def456... (1 day ago)   [✏️][🗑️]│   │   │
│  │ │ alpine  → sha256:ghi789... (3 days ago)  [✏️][🗑️]│   │   │
│  │ └────────────────────────────────────────────────┘   │   │
│  │                                                        │   │
│  │ Manifests:                                             │   │
│  │ ┌────────────────────────────────────────────────┐   │   │
│  │ │ sha256:abc123... • 45MB • hold1.alice.com      │   │   │
│  │ │   linux/amd64 • 5 layers • [View] [Delete]     │   │   │
│  │ │ sha256:def456... • 42MB • hold1.alice.com      │   │   │
│  │ │   linux/amd64 • 5 layers • [View] [Delete]     │   │   │
│  │ └────────────────────────────────────────────────┘   │   │
│  └───────────────────────────────────────────────────────┘   │
│                                                               │
│  ┌─ myapp ──────────────────────────────────────────────┐   │
│  │ 2 tags • 2 manifests • Last push: 1 day ago          │   │
│  │ [Expand ▼]                                             │   │
│  └───────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Features:

Repository List:

  • Group manifests by repository name
  • Show: tag count, manifest count, last push time
  • Collapsible/expandable repository cards

Repository Details (Expanded):

  • Tags: Table showing tag → manifest digest → timestamp
    • Edit tag: Modal to re-point tag to different manifest digest
    • Delete tag: Confirm dialog, removes io.atcr.tag record from PDS
  • Manifests: List of all manifests in repository
    • Show: digest (truncated), size, hold endpoint, architecture, layer count
    • View: Open manifest details modal (same as front page)
    • Delete: Confirm dialog with warning if manifest is tagged

Actions:

  • Copy pull command for each tag
  • Edit tag (re-point to different digest)
  • Delete tag
  • Delete manifest (with validation)

API Endpoints:

GET /ui/api/images
Auth: Required
Response:
{
  "repositories": [
    {
      "name": "nginx",
      "tag_count": 3,
      "manifest_count": 5,
      "last_push": "2025-10-05T10:23:45Z",
      "tags": [
        {
          "tag": "latest",
          "digest": "sha256:abc123...",
          "created_at": "2025-10-05T10:23:45Z"
        }
      ],
      "manifests": [
        {
          "digest": "sha256:abc123...",
          "size": 47185920,
          "hold_endpoint": "https://hold1.alice.com",
          "architecture": "amd64",
          "os": "linux",
          "layer_count": 5,
          "created_at": "2025-10-05T10:23:45Z",
          "tagged": true
        }
      ]
    }
  ]
}

PUT /ui/api/images/{repository}/tags/{tag}
Auth: Required
Body:
{
  "digest": "sha256:new-digest..."
}
Response:
{
  "success": true
}

DELETE /ui/api/images/{repository}/tags/{tag}
Auth: Required
Response:
{
  "success": true
}

DELETE /ui/api/images/{repository}/manifests/{digest}
Auth: Required
Response:
{
  "success": true
}

Authentication

OAuth Login Flow

Reuse existing OAuth implementation from credential helper and AppView.

Login Endpoint: /auth/oauth/login

Flow:

  1. User clicks "Login" on UI
  2. Redirects to /auth/oauth/login?return_to=/ui/images
  3. User enters handle (e.g., "alice.bsky.social")
  4. Server resolves handle → DID → PDS → OAuth server
  5. Server initiates OAuth flow with PAR + DPoP
  6. User redirected to PDS for authorization
  7. OAuth callback to /auth/oauth/callback
  8. Server exchanges code for token, validates with PDS
  9. Server creates session cookie (secure, httpOnly, SameSite)
  10. Redirects to return_to URL or default /ui/images

Session Management:

  • Session cookie: atcr_session (JWT or opaque token)
  • Session storage: In-memory map or SQLite table
  • Session duration: 24 hours (or match OAuth token expiry)
  • Refresh: Auto-refresh OAuth token when needed

Middleware:

// pkg/ui/middleware/auth.go

func RequireAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session := getSession(r)
        if session == nil {
            http.Redirect(w, r, "/auth/oauth/login?return_to="+r.URL.Path, http.StatusFound)
            return
        }

        // Add session info to context
        ctx := context.WithValue(r.Context(), "session", session)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Implementation Roadmap

Phase 1: Database & Firehose

  1. Define SQLite schema
  2. Implement database layer (pkg/ui/db/)
  3. Implement firehose worker (pkg/ui/firehose/)
  4. Test worker with real firehose

Phase 2: API Endpoints

  1. Implement /ui/api/recent-pushes (front page data)
  2. Implement /ui/api/profile (settings page data)
  3. Implement /ui/api/images (personal page data)
  4. Implement tag/manifest mutation endpoints

Phase 3: Authentication

  1. Implement OAuth login endpoint
  2. Implement session management
  3. Add auth middleware
  4. Test login flow

Phase 4: Frontend

  1. Choose framework (templates vs SPA)
  2. Implement front page
  3. Implement settings page
  4. Implement personal page
  5. Add styling

Phase 5: Polish

  1. Error handling
  2. Loading states
  3. Responsive design
  4. Testing

Open Questions

  1. Framework choice: Go templates (Templ?), HTMX, or SPA (React/Vue)?
  2. Styling: Tailwind, plain CSS, or component library?
  3. Manifest details: Modal vs dedicated page?
  4. Search: Full-text search on repository/tag names? Requires FTS in SQLite.
  5. Real-time updates: WebSocket for firehose events, or polling?
  6. Image size calculation: Sum of layer sizes, or read from manifest?
  7. Public profiles: Should /ui/@alice show public view of alice's images?
  8. Firehose resilience: Reconnect logic, backfill on downtime?

Dependencies

New Go packages needed:

  • github.com/mattn/go-sqlite3 - SQLite driver
  • github.com/bluesky-social/jetstream - Firehose client (or direct websocket)
  • Session management library (or custom implementation)
  • Frontend framework (TBD)

Configuration

Add to config/config.yml:

ui:
  enabled: true
  database_path: /var/lib/atcr/ui.db
  firehose:
    enabled: true
    endpoint: wss://jetstream.atproto.tools/subscribe
    collections:
      - io.atcr.manifest
      - io.atcr.tag
  session:
    duration: 24h
    cookie_name: atcr_session
    cookie_secure: true

Security Considerations

  1. Session cookies: Secure, HttpOnly, SameSite=Lax
  2. CSRF protection: For mutation endpoints (tag/manifest delete)
  3. Rate limiting: On API endpoints
  4. Input validation: Sanitize user input for search/filters
  5. Authorization: Verify authenticated user owns resources before mutation
  6. SQL injection: Use parameterized queries

Performance Considerations

  1. Database indexes: On DID, repository, created_at, digest
  2. Pagination: Limit query results to avoid large payloads
  3. Caching: Cache profile data, hold list, manifest details
  4. Firehose buffering: Batch database inserts
  5. Connection pooling: For SQLite and HTTP clients

Testing Strategy

  1. Unit tests: Database layer, API handlers
  2. Integration tests: Firehose worker with mock events
  3. E2E tests: Full login → browse → manage flow
  4. Load testing: Firehose worker with high event volume
  5. Manual testing: Real PDS, real images, real firehose