···11+# ATCR AppView UI - Future Features
22+33+This document outlines potential features for future versions of the ATCR AppView UI, beyond the V1 MVP. These are ideas to consider as the project matures and user needs evolve.
44+55+## Advanced Image Management
66+77+### Multi-Architecture Image Support
88+99+**Display image indexes:**
1010+- Show when a tag points to an image index (multi-arch manifest)
1111+- Display all architectures/platforms in the index (linux/amd64, linux/arm64, darwin/arm64, etc.)
1212+- Allow viewing individual manifests within the index
1313+- Show platform-specific layer details
1414+1515+**Image index creation:**
1616+- UI for combining multiple single-arch manifests into an image index
1717+- Automatic platform detection from manifest metadata
1818+- Validate that all manifests are for the same image (different platforms)
1919+2020+### Layer Inspection & Visualization
2121+2222+**Layer details page:**
2323+- Show Dockerfile command that created each layer (if available in history)
2424+- Display layer size and compression ratio
2525+- Show file changes in each layer (added/modified/deleted files)
2626+- Visualize layer hierarchy (parent-child relationships)
2727+2828+**Layer deduplication stats:**
2929+- Show which layers are shared across images
3030+- Calculate storage savings from layer sharing
3131+- Identify duplicate layers with different digests (potential optimization)
3232+3333+### Image Operations
3434+3535+**Tag Management:**
3636+- **Tag promotion workflow:** dev → staging → prod with one click
3737+- **Tag aliases:** Create multiple tags pointing to same digest
3838+- **Tag patterns:** Auto-tag based on git commit, semantic version, date
3939+- **Tag protection:** Mark tags as immutable (prevent deletion/re-pointing)
4040+4141+**Image Copying:**
4242+- Copy image from one repository to another
4343+- Copy image from another user's repository (fork)
4444+- Bulk copy operations (copy all tags, copy all manifests)
4545+4646+**Image History:**
4747+- Timeline view of tag changes (what digest did "latest" point to over time)
4848+- Rollback functionality (revert tag to previous digest)
4949+- Audit log of all image operations (push, delete, tag changes)
5050+5151+### Vulnerability Scanning
5252+5353+**Integration with security scanners:**
5454+- **Trivy** - Comprehensive vulnerability scanner
5555+- **Grype** - Anchore's vulnerability scanner
5656+- **Clair** - CoreOS vulnerability scanner
5757+5858+**Features:**
5959+- Automatic scanning on image push
6060+- Display CVE count by severity (critical, high, medium, low)
6161+- Show detailed CVE information (description, CVSS score, affected packages)
6262+- Filter images by vulnerability status
6363+- Subscribe to CVE notifications for your images
6464+- Compare vulnerability status across tags/versions
6565+6666+### Image Signing & Verification
6767+6868+**Cosign/Sigstore integration:**
6969+- Sign images with Cosign
7070+- Display signature verification status
7171+- Show keyless signing certificate chains
7272+- Integrate with transparency log (Rekor)
7373+7474+**Features:**
7575+- UI for signing images (generate key, sign manifest)
7676+- Verify signatures before pull (browser-based verification)
7777+- Display signature metadata (signer, timestamp, transparency log entry)
7878+- Require signatures for protected repositories
7979+8080+### SBOM (Software Bill of Materials)
8181+8282+**SBOM generation and display:**
8383+- Generate SBOM on push (SPDX or CycloneDX format)
8484+- Display package list from SBOM
8585+- Show license information
8686+- Link to upstream package sources
8787+- Compare SBOMs across versions (what packages changed)
8888+8989+**SBOM attestation:**
9090+- Store SBOM as attestation (in-toto format)
9191+- Link SBOM to image signature
9292+- Verify SBOM integrity
9393+9494+## Hold Management Dashboard
9595+9696+### Hold Discovery & Registration
9797+9898+**Create hold:**
9999+- UI wizard for deploying hold service
100100+- One-click deployment to Fly.io, Railway, Render
101101+- Configuration generator (environment variables, docker-compose)
102102+- Test connectivity after deployment
103103+104104+**Hold registration:**
105105+- Automatic registration via OAuth (already implemented)
106106+- Manual registration form (for existing holds)
107107+- Bulk import holds from JSON/YAML
108108+109109+### Hold Configuration
110110+111111+**Hold settings page:**
112112+- Edit hold metadata (name, description, icon)
113113+- Toggle public/private flag
114114+- Configure storage backend (S3, Storj, Minio, filesystem)
115115+- Set storage quotas and limits
116116+- Configure retention policies (auto-delete old blobs)
117117+118118+**Hold credentials:**
119119+- Rotate S3 access keys
120120+- Test hold connectivity
121121+- View hold service logs (if accessible)
122122+123123+### Crew Management
124124+125125+**Invite crew members:**
126126+- Send invitation links (OAuth-based)
127127+- Invite by handle or DID
128128+- Set crew permissions (read-only, read-write, admin)
129129+- Bulk invite (upload CSV)
130130+131131+**Crew list:**
132132+- Display all crew members
133133+- Show last activity (last push, last pull)
134134+- Remove crew members
135135+- Change crew permissions
136136+137137+**Crew request workflow:**
138138+- Allow users to request access to a hold
139139+- Hold owner approves/rejects requests
140140+- Notification system for requests
141141+142142+### Hold Analytics
143143+144144+**Storage metrics:**
145145+- Total storage used (bytes)
146146+- Blob count
147147+- Largest blobs
148148+- Growth over time (chart)
149149+- Deduplication savings
150150+151151+**Access metrics:**
152152+- Total downloads (pulls)
153153+- Bandwidth used
154154+- Popular images (most pulled)
155155+- Geographic distribution (if available)
156156+- Access logs (who pulled what, when)
157157+158158+**Cost estimation:**
159159+- Calculate S3 storage costs
160160+- Calculate bandwidth costs
161161+- Compare costs across storage backends
162162+- Budget alerts (notify when approaching limit)
163163+164164+## Discovery & Social Features
165165+166166+### Federated Browse & Search
167167+168168+**Enhanced discovery:**
169169+- Full-text search across all ATCR images (repository name, tag, description)
170170+- Filter by user, hold, architecture, date range
171171+- Sort by popularity, recency, size
172172+- Advanced query syntax (e.g., "user:alice tag:latest arch:arm64")
173173+174174+**Popular/Trending:**
175175+- Most pulled images (past day, week, month)
176176+- Fastest growing images (new pulls)
177177+- Recently updated images (new tags)
178178+- Community favorites (curated list)
179179+180180+**Categories & Tags:**
181181+- User-defined categories (web, database, ml, etc.)
182182+- Tag images with keywords (nginx, proxy, reverse-proxy)
183183+- Browse by category
184184+- Tag cloud visualization
185185+186186+### Sailor Profiles (Public)
187187+188188+**Public profile page:**
189189+- `/ui/@alice` shows alice's public repositories
190190+- Bio, avatar, website links
191191+- Statistics (total images, total pulls, joined date)
192192+- Pinned repositories (showcase best images)
193193+194194+**Social features:**
195195+- Follow other sailors (get notified of their pushes)
196196+- Star repositories (bookmark favorites)
197197+- Comment on images (feedback, questions)
198198+- Like/upvote images
199199+200200+**Activity feed:**
201201+- Timeline of followed sailors' activity
202202+- Recent pushes from community
203203+- Popular images from followed users
204204+205205+### Federated Timeline
206206+207207+**ATProto-native feed:**
208208+- Real-time feed of container pushes (like Bluesky's timeline)
209209+- Filter by follows, community, or global
210210+- React to pushes (like, share, comment)
211211+- Share images to Bluesky/ATProto social apps
212212+213213+**Custom feeds:**
214214+- Create algorithmic feeds (e.g., "Show me all ML images")
215215+- Subscribe to curated feeds
216216+- Publish feeds for others to subscribe
217217+218218+## Access Control & Permissions
219219+220220+### Repository-Level Permissions
221221+222222+**Private repositories:**
223223+- Mark repositories as private (only owner + collaborators can pull)
224224+- Invite collaborators by handle/DID
225225+- Set permissions (read-only, read-write, admin)
226226+227227+**Public repositories:**
228228+- Default: public (anyone can pull)
229229+- Require authentication for private repos
230230+- Generate read-only tokens (for CI/CD)
231231+232232+**Implementation challenge:**
233233+- ATProto doesn't support private records yet
234234+- May require proxy layer for access control
235235+- Or use encrypted blobs with shared keys
236236+237237+### Team/Organization Accounts
238238+239239+**Multi-user organizations:**
240240+- Create organization account (e.g., `@acme-corp`)
241241+- Add members with roles (owner, maintainer, member)
242242+- Organization-owned repositories
243243+- Billing and quotas at org level
244244+245245+**Features:**
246246+- Team-based access control
247247+- Shared hold for organization
248248+- Audit logs for all org activity
249249+- Single sign-on (SSO) integration
250250+251251+## Analytics & Monitoring
252252+253253+### Dashboard
254254+255255+**Personal dashboard:**
256256+- Overview of your images, holds, activity
257257+- Quick stats (total size, pull count, last push)
258258+- Recent activity (your pushes, pulls)
259259+- Alerts and notifications
260260+261261+**Hold dashboard:**
262262+- Storage usage, bandwidth, costs
263263+- Active crew members
264264+- Recent uploads/downloads
265265+- Health status of hold service
266266+267267+### Pull Analytics
268268+269269+**Detailed metrics:**
270270+- Pull count per image/tag
271271+- Pull count by client (Docker, containerd, podman)
272272+- Pull count by geography (country, region)
273273+- Pull count over time (chart)
274274+- Failed pulls (errors, retries)
275275+276276+**User analytics:**
277277+- Who is pulling your images (if authenticated)
278278+- Anonymous vs authenticated pulls
279279+- Repeat users vs new users
280280+281281+### Alerts & Notifications
282282+283283+**Alert types:**
284284+- Storage quota exceeded
285285+- High bandwidth usage
286286+- New vulnerability detected
287287+- Image signature invalid
288288+- Hold service down
289289+- Crew member joined/left
290290+291291+**Notification channels:**
292292+- Email
293293+- Webhook (POST to custom URL)
294294+- ATProto app notification (future: in-app notifications in Bluesky)
295295+- Slack, Discord, Telegram integrations
296296+297297+## Developer Tools & Integrations
298298+299299+### API Documentation
300300+301301+**Interactive API docs:**
302302+- Swagger/OpenAPI spec for OCI API
303303+- Swagger/OpenAPI spec for UI API
304304+- Interactive API explorer (try API calls in browser)
305305+- Code examples in multiple languages (curl, Go, Python, JavaScript)
306306+307307+**SDK/Client Libraries:**
308308+- Official Go client library
309309+- JavaScript/TypeScript client
310310+- Python client
311311+- Rust client
312312+313313+### Webhooks
314314+315315+**Webhook configuration:**
316316+- Register webhook URLs per repository
317317+- Select events to trigger (push, delete, tag update)
318318+- Test webhooks (send test payload)
319319+- View webhook delivery history
320320+- Retry failed deliveries
321321+322322+**Webhook events:**
323323+- `manifest.pushed`
324324+- `manifest.deleted`
325325+- `tag.created`
326326+- `tag.updated`
327327+- `tag.deleted`
328328+- `scan.completed` (vulnerability scan finished)
329329+330330+### CI/CD Integration Guides
331331+332332+**Documentation for popular CI/CD platforms:**
333333+- GitHub Actions (example workflows)
334334+- GitLab CI (.gitlab-ci.yml examples)
335335+- CircleCI (config.yml examples)
336336+- Jenkins (Jenkinsfile examples)
337337+- Drone CI
338338+339339+**Features:**
340340+- One-click workflow generation
341341+- Pre-built actions/plugins for ATCR
342342+- Cache layer optimization for faster builds
343343+- Build status badges (show build status in README)
344344+345345+### Infrastructure as Code
346346+347347+**IaC examples:**
348348+- Terraform module for deploying hold service
349349+- Pulumi program for ATCR infrastructure
350350+- Kubernetes manifests for hold service
351351+- Docker Compose for local development
352352+- Helm chart for AppView + hold
353353+354354+**GitOps workflows:**
355355+- ArgoCD integration (deploy images from ATCR)
356356+- FluxCD integration
357357+- Automated deployments on tag push
358358+359359+## Documentation & Onboarding
360360+361361+### Interactive Getting Started
362362+363363+**Onboarding wizard:**
364364+- Step-by-step guide for first-time users
365365+- Interactive tutorial (push your first image)
366366+- Verify setup (test authentication, test push/pull)
367367+- Completion checklist
368368+369369+**Guided tours:**
370370+- Product tour of UI features
371371+- Tooltips and hints for new users
372372+- Help center with FAQs
373373+374374+### Comprehensive Documentation
375375+376376+**Documentation sections:**
377377+- Quickstart guide
378378+- Detailed user manual
379379+- API reference
380380+- ATProto record schemas
381381+- Deployment guides (hold service, AppView)
382382+- Troubleshooting guide
383383+- Security best practices
384384+385385+**Video tutorials:**
386386+- YouTube channel with how-to videos
387387+- Screen recordings of common tasks
388388+- Conference talks and demos
389389+390390+### Community & Support
391391+392392+**Community features:**
393393+- Discussion forum (or integrate with Discourse)
394394+- GitHub Discussions for ATCR project
395395+- Discord/Slack community
396396+- Monthly community calls
397397+398398+**Support channels:**
399399+- Email support
400400+- Live chat (for paid tiers)
401401+- Priority support (for enterprise)
402402+403403+## Advanced ATProto Integration
404404+405405+### Record Viewer
406406+407407+**ATProto record browser:**
408408+- Browse all your `io.atcr.*` records
409409+- Raw JSON view with ATProto metadata (CID, commit info, timestamp)
410410+- Diff viewer for record updates
411411+- History view (see all versions of a record)
412412+- Link to ATP URI (`at://did/collection/rkey`)
413413+414414+**Export/Import:**
415415+- Export all records as JSON (backup)
416416+- Import records from JSON (restore, migration)
417417+- CAR file export (ATProto native format)
418418+419419+### PDS Integration
420420+421421+**Multi-PDS support:**
422422+- Switch between multiple PDS accounts
423423+- Manage images across different PDSs
424424+- Unified view of all your images (across PDSs)
425425+426426+**PDS health monitoring:**
427427+- Show PDS connection status
428428+- Alert if PDS is unreachable
429429+- Fallback to alternate PDS (if configured)
430430+431431+**PDS migration tools:**
432432+- Migrate images from one PDS to another
433433+- Bulk update hold endpoints
434434+- Re-sign OAuth tokens for new PDS
435435+436436+### Decentralization Features
437437+438438+**Data sovereignty:**
439439+- "Verify on PDS" button (proves manifest is in your PDS)
440440+- "Clone my registry" guide (backup to another PDS)
441441+- "Export registry" (download all manifests + metadata)
442442+443443+**Federation:**
444444+- Cross-AppView image pulls (pull from other ATCR AppViews)
445445+- AppView discovery (find other ATCR instances)
446446+- Federated search (search across multiple AppViews)
447447+448448+## Enterprise Features (Future Commercial Offering)
449449+450450+### Team Collaboration
451451+452452+**Organizations:**
453453+- Enterprise org accounts with unlimited members
454454+- RBAC (role-based access control)
455455+- SSO integration (SAML, OIDC)
456456+- Audit logs for compliance
457457+458458+### Compliance & Security
459459+460460+**Compliance tools:**
461461+- SOC 2 compliance reporting
462462+- HIPAA-compliant storage options
463463+- GDPR data export/deletion
464464+- Retention policies (auto-delete after N days)
465465+466466+**Security features:**
467467+- Image scanning with policy enforcement (block vulnerable images)
468468+- Malware scanning (scan blobs for malware)
469469+- Secrets scanning (detect leaked credentials in layers)
470470+- Content trust (require signed images)
471471+472472+### SLA & Support
473473+474474+**Paid tiers:**
475475+- Free tier: 5GB storage, community support
476476+- Pro tier: 100GB storage, email support, SLA
477477+- Enterprise tier: Unlimited storage, priority support, dedicated instance
478478+479479+**Features:**
480480+- Guaranteed uptime (99.9%)
481481+- Premium support (24/7, faster response)
482482+- Dedicated account manager
483483+- Custom contract terms
484484+485485+## UI/UX Enhancements
486486+487487+### Design System
488488+489489+**Theming:**
490490+- Light and dark modes (system preference)
491491+- Custom themes (nautical, cyberpunk, minimalist)
492492+- Accessibility (WCAG 2.1 AA compliance)
493493+- High contrast mode
494494+495495+**Responsive design:**
496496+- Mobile-first design
497497+- Progressive web app (PWA) with offline support
498498+- Native mobile apps (iOS, Android)
499499+500500+### Performance Optimizations
501501+502502+**Frontend optimizations:**
503503+- Lazy loading for images and data
504504+- Virtual scrolling for large lists
505505+- Service worker for caching
506506+- Code splitting (load only what's needed)
507507+508508+**Backend optimizations:**
509509+- GraphQL API (fetch only required fields)
510510+- Real-time updates via WebSocket
511511+- Server-sent events for firehose
512512+- Edge caching (CloudFlare, Fastly)
513513+514514+### Internationalization
515515+516516+**Multi-language support:**
517517+- UI translations (English, Spanish, French, German, Japanese, Chinese, etc.)
518518+- RTL (right-to-left) language support
519519+- Localized date/time formats
520520+- Locale-specific formatting (numbers, currencies)
521521+522522+## Miscellaneous Ideas
523523+524524+### Image Build Service
525525+526526+**Cloud-based builds:**
527527+- Build images from Dockerfile in the UI
528528+- Multi-stage build support
529529+- Build cache optimization
530530+- Build logs and status
531531+532532+**Automated builds:**
533533+- Connect GitHub/GitLab repository
534534+- Auto-build on git push
535535+- Build matrix (multiple architectures, versions)
536536+- Build notifications
537537+538538+### Image Registry Mirroring
539539+540540+**Mirror external registries:**
541541+- Cache images from Docker Hub, ghcr.io, quay.io
542542+- Transparent proxy (pull-through cache)
543543+- Reduce external bandwidth costs
544544+- Faster pulls (cache locally)
545545+546546+**Features:**
547547+- Configurable cache retention
548548+- Whitelist/blacklist registries
549549+- Statistics (cache hit rate, savings)
550550+551551+### Deployment Tools
552552+553553+**One-click deployments:**
554554+- Deploy image to Kubernetes
555555+- Deploy to Docker Swarm
556556+- Deploy to AWS ECS/Fargate
557557+- Deploy to Fly.io, Railway, Render
558558+559559+**Deployment tracking:**
560560+- Track where images are deployed
561561+- Show running versions (which environments use which tags)
562562+- Notify on new deployments
563563+564564+### Image Recommendations
565565+566566+**ML-based recommendations:**
567567+- "Similar images" (based on layers, packages, tags)
568568+- "People who pulled this also pulled..." (collaborative filtering)
569569+- "Recommended for you" (personalized based on history)
570570+571571+### Gamification
572572+573573+**Achievements:**
574574+- Badges for milestones (first push, 100 pulls, 1GB storage, etc.)
575575+- Leaderboards (most popular images, most active sailors)
576576+- Community contributions (points for helping others)
577577+578578+### Advanced Search
579579+580580+**Semantic search:**
581581+- Search by description, README, labels
582582+- Natural language queries ("show me nginx images with SSL")
583583+- AI-powered search (GPT-based understanding)
584584+585585+**Saved searches:**
586586+- Save frequently used queries
587587+- Subscribe to search results (get notified of new matches)
588588+- Share searches with team
589589+590590+## Implementation Priority
591591+592592+If implementing these features, suggested priority order:
593593+594594+**High Priority (Next 6 months):**
595595+1. Multi-architecture image support
596596+2. Vulnerability scanning integration
597597+3. Hold management dashboard
598598+4. Enhanced search and filtering
599599+5. Webhooks for CI/CD integration
600600+601601+**Medium Priority (6-12 months):**
602602+1. Team/organization accounts
603603+2. Repository-level permissions
604604+3. Image signing and verification
605605+4. Pull analytics and monitoring
606606+5. API documentation and SDKs
607607+608608+**Low Priority (12+ months):**
609609+1. Enterprise features (SSO, compliance, SLA)
610610+2. Image build service
611611+3. Registry mirroring
612612+4. Mobile apps
613613+5. ML-based recommendations
614614+615615+**Research/Experimental:**
616616+1. Private repositories (requires ATProto private records)
617617+2. Federated timeline (requires ATProto feed infrastructure)
618618+3. Deployment tools integration
619619+4. Semantic search
620620+621621+---
622622+623623+**Note:** This is a living document. Features may be added, removed, or reprioritized based on user feedback, technical feasibility, and ATProto ecosystem evolution.
···11+package db
22+33+import "time"
44+55+// User represents a user in the system
66+type User struct {
77+ DID string
88+ Handle string
99+ PDSEndpoint string
1010+ LastSeen time.Time
1111+}
1212+1313+// Manifest represents an OCI manifest stored in the cache
1414+type Manifest struct {
1515+ ID int64
1616+ DID string
1717+ Repository string
1818+ Digest string
1919+ HoldEndpoint string
2020+ SchemaVersion int
2121+ MediaType string
2222+ ConfigDigest string
2323+ ConfigSize int64
2424+ RawManifest string // JSON
2525+ CreatedAt time.Time
2626+}
2727+2828+// Layer represents a layer in a manifest
2929+type Layer struct {
3030+ ManifestID int64
3131+ Digest string
3232+ Size int64
3333+ MediaType string
3434+ LayerIndex int
3535+}
3636+3737+// Tag represents a tag pointing to a manifest
3838+type Tag struct {
3939+ ID int64
4040+ DID string
4141+ Repository string
4242+ Tag string
4343+ Digest string
4444+ CreatedAt time.Time
4545+}
4646+4747+// Push represents a combined tag and manifest for the recent pushes view
4848+type Push struct {
4949+ DID string
5050+ Handle string
5151+ Repository string
5252+ Tag string
5353+ Digest string
5454+ HoldEndpoint string
5555+ CreatedAt time.Time
5656+}
5757+5858+// Repository represents an aggregated view of a user's repository
5959+type Repository struct {
6060+ Name string
6161+ TagCount int
6262+ ManifestCount int
6363+ LastPush time.Time
6464+ Tags []Tag
6565+ Manifests []Manifest
6666+}
+292
pkg/appview/db/queries.go
···11+package db
22+33+import (
44+ "database/sql"
55+)
66+77+// GetRecentPushes fetches recent pushes with pagination
88+func GetRecentPushes(db *sql.DB, limit, offset int, userFilter string) ([]Push, int, error) {
99+ query := `
1010+ SELECT u.did, u.handle, t.repository, t.tag, t.digest, m.hold_endpoint, t.created_at
1111+ FROM tags t
1212+ JOIN users u ON t.did = u.did
1313+ JOIN manifests m ON t.did = m.did AND t.repository = m.repository AND t.digest = m.digest
1414+ `
1515+1616+ args := []interface{}{}
1717+1818+ if userFilter != "" {
1919+ query += " WHERE u.handle = ? OR u.did = ?"
2020+ args = append(args, userFilter, userFilter)
2121+ }
2222+2323+ query += " ORDER BY t.created_at DESC LIMIT ? OFFSET ?"
2424+ args = append(args, limit, offset)
2525+2626+ rows, err := db.Query(query, args...)
2727+ if err != nil {
2828+ return nil, 0, err
2929+ }
3030+ defer rows.Close()
3131+3232+ var pushes []Push
3333+ for rows.Next() {
3434+ var p Push
3535+ if err := rows.Scan(&p.DID, &p.Handle, &p.Repository, &p.Tag, &p.Digest, &p.HoldEndpoint, &p.CreatedAt); err != nil {
3636+ return nil, 0, err
3737+ }
3838+ pushes = append(pushes, p)
3939+ }
4040+4141+ // Get total count
4242+ countQuery := "SELECT COUNT(*) FROM tags t JOIN users u ON t.did = u.did"
4343+ countArgs := []interface{}{}
4444+4545+ if userFilter != "" {
4646+ countQuery += " WHERE u.handle = ? OR u.did = ?"
4747+ countArgs = append(countArgs, userFilter, userFilter)
4848+ }
4949+5050+ var total int
5151+ if err := db.QueryRow(countQuery, countArgs...).Scan(&total); err != nil {
5252+ return nil, 0, err
5353+ }
5454+5555+ return pushes, total, nil
5656+}
5757+5858+// GetUserRepositories fetches all repositories for a user
5959+func GetUserRepositories(db *sql.DB, did string) ([]Repository, error) {
6060+ // Get repository summary
6161+ rows, err := db.Query(`
6262+ SELECT
6363+ repository,
6464+ COUNT(DISTINCT tag) as tag_count,
6565+ COUNT(DISTINCT digest) as manifest_count,
6666+ MAX(created_at) as last_push
6767+ FROM (
6868+ SELECT repository, tag, digest, created_at FROM tags WHERE did = ?
6969+ UNION
7070+ SELECT repository, NULL, digest, created_at FROM manifests WHERE did = ?
7171+ )
7272+ GROUP BY repository
7373+ ORDER BY last_push DESC
7474+ `, did, did)
7575+7676+ if err != nil {
7777+ return nil, err
7878+ }
7979+ defer rows.Close()
8080+8181+ var repos []Repository
8282+ for rows.Next() {
8383+ var r Repository
8484+ if err := rows.Scan(&r.Name, &r.TagCount, &r.ManifestCount, &r.LastPush); err != nil {
8585+ return nil, err
8686+ }
8787+8888+ // Get tags for this repo
8989+ tagRows, err := db.Query(`
9090+ SELECT id, tag, digest, created_at
9191+ FROM tags
9292+ WHERE did = ? AND repository = ?
9393+ ORDER BY created_at DESC
9494+ `, did, r.Name)
9595+9696+ if err != nil {
9797+ return nil, err
9898+ }
9999+100100+ for tagRows.Next() {
101101+ var t Tag
102102+ t.DID = did
103103+ t.Repository = r.Name
104104+ if err := tagRows.Scan(&t.ID, &t.Tag, &t.Digest, &t.CreatedAt); err != nil {
105105+ tagRows.Close()
106106+ return nil, err
107107+ }
108108+ r.Tags = append(r.Tags, t)
109109+ }
110110+ tagRows.Close()
111111+112112+ // Get manifests for this repo
113113+ manifestRows, err := db.Query(`
114114+ SELECT id, digest, hold_endpoint, schema_version, media_type,
115115+ config_digest, config_size, raw_manifest, created_at
116116+ FROM manifests
117117+ WHERE did = ? AND repository = ?
118118+ ORDER BY created_at DESC
119119+ `, did, r.Name)
120120+121121+ if err != nil {
122122+ return nil, err
123123+ }
124124+125125+ for manifestRows.Next() {
126126+ var m Manifest
127127+ m.DID = did
128128+ m.Repository = r.Name
129129+ if err := manifestRows.Scan(&m.ID, &m.Digest, &m.HoldEndpoint, &m.SchemaVersion,
130130+ &m.MediaType, &m.ConfigDigest, &m.ConfigSize, &m.RawManifest, &m.CreatedAt); err != nil {
131131+ manifestRows.Close()
132132+ return nil, err
133133+ }
134134+ r.Manifests = append(r.Manifests, m)
135135+ }
136136+ manifestRows.Close()
137137+138138+ repos = append(repos, r)
139139+ }
140140+141141+ return repos, nil
142142+}
143143+144144+// UpsertUser inserts or updates a user record
145145+func UpsertUser(db *sql.DB, user *User) error {
146146+ _, err := db.Exec(`
147147+ INSERT INTO users (did, handle, pds_endpoint, last_seen)
148148+ VALUES (?, ?, ?, ?)
149149+ ON CONFLICT(did) DO UPDATE SET
150150+ handle = excluded.handle,
151151+ pds_endpoint = excluded.pds_endpoint,
152152+ last_seen = excluded.last_seen
153153+ `, user.DID, user.Handle, user.PDSEndpoint, user.LastSeen)
154154+ return err
155155+}
156156+157157+// InsertManifest inserts a new manifest record
158158+func InsertManifest(db *sql.DB, manifest *Manifest) (int64, error) {
159159+ result, err := db.Exec(`
160160+ INSERT OR IGNORE INTO manifests
161161+ (did, repository, digest, hold_endpoint, schema_version, media_type,
162162+ config_digest, config_size, raw_manifest, created_at)
163163+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
164164+ `, manifest.DID, manifest.Repository, manifest.Digest, manifest.HoldEndpoint,
165165+ manifest.SchemaVersion, manifest.MediaType, manifest.ConfigDigest,
166166+ manifest.ConfigSize, manifest.RawManifest, manifest.CreatedAt)
167167+168168+ if err != nil {
169169+ return 0, err
170170+ }
171171+172172+ return result.LastInsertId()
173173+}
174174+175175+// InsertLayer inserts a new layer record
176176+func InsertLayer(db *sql.DB, layer *Layer) error {
177177+ _, err := db.Exec(`
178178+ INSERT INTO layers (manifest_id, digest, size, media_type, layer_index)
179179+ VALUES (?, ?, ?, ?, ?)
180180+ `, layer.ManifestID, layer.Digest, layer.Size, layer.MediaType, layer.LayerIndex)
181181+ return err
182182+}
183183+184184+// UpsertTag inserts or updates a tag record
185185+func UpsertTag(db *sql.DB, tag *Tag) error {
186186+ _, err := db.Exec(`
187187+ INSERT INTO tags (did, repository, tag, digest, created_at)
188188+ VALUES (?, ?, ?, ?, ?)
189189+ ON CONFLICT(did, repository, tag) DO UPDATE SET
190190+ digest = excluded.digest,
191191+ created_at = excluded.created_at
192192+ `, tag.DID, tag.Repository, tag.Tag, tag.Digest, tag.CreatedAt)
193193+ return err
194194+}
195195+196196+// DeleteTag deletes a tag record
197197+func DeleteTag(db *sql.DB, did, repository, tag string) error {
198198+ _, err := db.Exec(`
199199+ DELETE FROM tags WHERE did = ? AND repository = ? AND tag = ?
200200+ `, did, repository, tag)
201201+ return err
202202+}
203203+204204+// DeleteManifest deletes a manifest and its associated layers
205205+func DeleteManifest(db *sql.DB, did, repository, digest string) error {
206206+ _, err := db.Exec(`
207207+ DELETE FROM manifests WHERE did = ? AND repository = ? AND digest = ?
208208+ `, did, repository, digest)
209209+ return err
210210+}
211211+212212+// GetManifest fetches a single manifest by digest
213213+func GetManifest(db *sql.DB, digest string) (*Manifest, error) {
214214+ var m Manifest
215215+ err := db.QueryRow(`
216216+ SELECT id, did, repository, digest, hold_endpoint, schema_version,
217217+ media_type, config_digest, config_size, raw_manifest, created_at
218218+ FROM manifests
219219+ WHERE digest = ?
220220+ `, digest).Scan(&m.ID, &m.DID, &m.Repository, &m.Digest, &m.HoldEndpoint,
221221+ &m.SchemaVersion, &m.MediaType, &m.ConfigDigest, &m.ConfigSize,
222222+ &m.RawManifest, &m.CreatedAt)
223223+224224+ if err != nil {
225225+ return nil, err
226226+ }
227227+228228+ return &m, nil
229229+}
230230+231231+// GetLayersForManifest fetches all layers for a manifest
232232+func GetLayersForManifest(db *sql.DB, manifestID int64) ([]Layer, error) {
233233+ rows, err := db.Query(`
234234+ SELECT manifest_id, digest, size, media_type, layer_index
235235+ FROM layers
236236+ WHERE manifest_id = ?
237237+ ORDER BY layer_index
238238+ `, manifestID)
239239+240240+ if err != nil {
241241+ return nil, err
242242+ }
243243+ defer rows.Close()
244244+245245+ var layers []Layer
246246+ for rows.Next() {
247247+ var l Layer
248248+ if err := rows.Scan(&l.ManifestID, &l.Digest, &l.Size, &l.MediaType, &l.LayerIndex); err != nil {
249249+ return nil, err
250250+ }
251251+ layers = append(layers, l)
252252+ }
253253+254254+ return layers, nil
255255+}
256256+257257+// GetFirehoseCursor retrieves the current firehose cursor
258258+func GetFirehoseCursor(db *sql.DB) (int64, error) {
259259+ var cursor int64
260260+ err := db.QueryRow("SELECT cursor FROM firehose_cursor WHERE id = 1").Scan(&cursor)
261261+ if err == sql.ErrNoRows {
262262+ return 0, nil
263263+ }
264264+ return cursor, err
265265+}
266266+267267+// UpdateFirehoseCursor updates the firehose cursor
268268+func UpdateFirehoseCursor(db *sql.DB, cursor int64) error {
269269+ _, err := db.Exec(`
270270+ INSERT INTO firehose_cursor (id, cursor, updated_at)
271271+ VALUES (1, ?, datetime('now'))
272272+ ON CONFLICT(id) DO UPDATE SET
273273+ cursor = excluded.cursor,
274274+ updated_at = excluded.updated_at
275275+ `, cursor)
276276+ return err
277277+}
278278+279279+// IsManifestTagged checks if a manifest has any tags
280280+func IsManifestTagged(db *sql.DB, did, repository, digest string) (bool, error) {
281281+ var count int
282282+ err := db.QueryRow(`
283283+ SELECT COUNT(*) FROM tags
284284+ WHERE did = ? AND repository = ? AND digest = ?
285285+ `, did, repository, digest).Scan(&count)
286286+287287+ if err != nil {
288288+ return false, err
289289+ }
290290+291291+ return count > 0, nil
292292+}
+86
pkg/appview/db/schema.go
···11+package db
22+33+import (
44+ "database/sql"
55+66+ _ "github.com/mattn/go-sqlite3"
77+)
88+99+const schema = `
1010+CREATE TABLE IF NOT EXISTS users (
1111+ did TEXT PRIMARY KEY,
1212+ handle TEXT NOT NULL,
1313+ pds_endpoint TEXT NOT NULL,
1414+ last_seen TIMESTAMP NOT NULL,
1515+ UNIQUE(handle)
1616+);
1717+CREATE INDEX IF NOT EXISTS idx_users_handle ON users(handle);
1818+1919+CREATE TABLE IF NOT EXISTS manifests (
2020+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2121+ did TEXT NOT NULL,
2222+ repository TEXT NOT NULL,
2323+ digest TEXT NOT NULL,
2424+ hold_endpoint TEXT NOT NULL,
2525+ schema_version INTEGER NOT NULL,
2626+ media_type TEXT NOT NULL,
2727+ config_digest TEXT,
2828+ config_size INTEGER,
2929+ raw_manifest TEXT NOT NULL,
3030+ created_at TIMESTAMP NOT NULL,
3131+ UNIQUE(did, repository, digest),
3232+ FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
3333+);
3434+CREATE INDEX IF NOT EXISTS idx_manifests_did_repo ON manifests(did, repository);
3535+CREATE INDEX IF NOT EXISTS idx_manifests_created_at ON manifests(created_at DESC);
3636+CREATE INDEX IF NOT EXISTS idx_manifests_digest ON manifests(digest);
3737+3838+CREATE TABLE IF NOT EXISTS layers (
3939+ manifest_id INTEGER NOT NULL,
4040+ digest TEXT NOT NULL,
4141+ size INTEGER NOT NULL,
4242+ media_type TEXT NOT NULL,
4343+ layer_index INTEGER NOT NULL,
4444+ PRIMARY KEY(manifest_id, layer_index),
4545+ FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
4646+);
4747+CREATE INDEX IF NOT EXISTS idx_layers_digest ON layers(digest);
4848+4949+CREATE TABLE IF NOT EXISTS tags (
5050+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5151+ did TEXT NOT NULL,
5252+ repository TEXT NOT NULL,
5353+ tag TEXT NOT NULL,
5454+ digest TEXT NOT NULL,
5555+ created_at TIMESTAMP NOT NULL,
5656+ UNIQUE(did, repository, tag),
5757+ FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE
5858+);
5959+CREATE INDEX IF NOT EXISTS idx_tags_did_repo ON tags(did, repository);
6060+6161+CREATE TABLE IF NOT EXISTS firehose_cursor (
6262+ id INTEGER PRIMARY KEY CHECK (id = 1),
6363+ cursor INTEGER NOT NULL,
6464+ updated_at TIMESTAMP NOT NULL
6565+);
6666+`
6767+6868+// InitDB initializes the SQLite database with the schema
6969+func InitDB(path string) (*sql.DB, error) {
7070+ db, err := sql.Open("sqlite3", path)
7171+ if err != nil {
7272+ return nil, err
7373+ }
7474+7575+ // Enable foreign keys
7676+ if _, err := db.Exec("PRAGMA foreign_keys = ON"); err != nil {
7777+ return nil, err
7878+ }
7979+8080+ // Create schema
8181+ if _, err := db.Exec(schema); err != nil {
8282+ return nil, err
8383+ }
8484+8585+ return db, nil
8686+}
+63
pkg/appview/handlers/auth.go
···11+package handlers
22+33+import (
44+ "html/template"
55+ "net/http"
66+)
77+88+// LoginHandler shows the OAuth login form
99+type LoginHandler struct {
1010+ Templates *template.Template
1111+}
1212+1313+func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
1414+ returnTo := r.URL.Query().Get("return_to")
1515+ if returnTo == "" {
1616+ returnTo = "/"
1717+ }
1818+1919+ data := struct {
2020+ ReturnTo string
2121+ Error string
2222+ }{
2323+ ReturnTo: returnTo,
2424+ Error: r.URL.Query().Get("error"),
2525+ }
2626+2727+ if err := h.Templates.ExecuteTemplate(w, "login", data); err != nil {
2828+ http.Error(w, err.Error(), http.StatusInternalServerError)
2929+ return
3030+ }
3131+}
3232+3333+// LoginSubmitHandler processes the login form submission
3434+type LoginSubmitHandler struct{}
3535+3636+func (h *LoginSubmitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3737+ if r.Method != http.MethodPost {
3838+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
3939+ return
4040+ }
4141+4242+ handle := r.FormValue("handle")
4343+ returnTo := r.FormValue("return_to")
4444+4545+ if handle == "" {
4646+ http.Redirect(w, r, "/auth/oauth/login?return_to="+returnTo+"&error=handle_required", http.StatusFound)
4747+ return
4848+ }
4949+5050+ // Store return_to in cookie so callback can use it
5151+ http.SetCookie(w, &http.Cookie{
5252+ Name: "oauth_return_to",
5353+ Value: returnTo,
5454+ Path: "/",
5555+ MaxAge: 600, // 10 minutes
5656+ HttpOnly: true,
5757+ Secure: true,
5858+ SameSite: http.SameSiteLaxMode,
5959+ })
6060+6161+ // Redirect to OAuth authorize with handle
6262+ http.Redirect(w, r, "/auth/oauth/authorize?handle="+handle, http.StatusFound)
6363+}