feat: docker-compose stacks with Forge-themed UI
Build / build (push) Successful in 10m42s

Adds a new Stacks feature: upload/edit docker-compose YAML,
deploy as atomic units, browse revisions, roll back, and
stream logs. Backend in internal/stack + internal/api/stacks.go,
persistent storage in internal/store/stacks.go.

Stacks pages (list, new, detail) use a modern Forge aesthetic —
Instrument Serif display type, JetBrains Mono for meta/code,
indigo ember accents, dot-grid hero, registration marks on
hover, terminal panel for logs. Palette is sourced from the
app's existing design tokens so the feature remains consistent
with the rest of Tinyforge.

Fonts self-hosted via @fontsource/instrument-serif and
@fontsource/jetbrains-mono to satisfy the strict CSP.
This commit is contained in:
2026-04-16 03:48:37 +03:00
parent b622384774
commit 75424a5f25
23 changed files with 3603 additions and 18 deletions
+44
View File
@@ -130,6 +130,48 @@ func (s *Store) runMigrations() error {
`ALTER TABLE static_sites ADD COLUMN storage_limit_mb INTEGER NOT NULL DEFAULT 0`,
}
// Additive stack tables (2026-04-16). Created here rather than in the
// schema constant so older databases pick them up on restart.
stackTables := []string{
`CREATE TABLE IF NOT EXISTS stacks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
compose_project_name TEXT NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'stopped',
error TEXT NOT NULL DEFAULT '',
current_revision_id TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)`,
`CREATE TABLE IF NOT EXISTS stack_revisions (
id TEXT PRIMARY KEY,
stack_id TEXT NOT NULL REFERENCES stacks(id) ON DELETE CASCADE,
revision INTEGER NOT NULL,
yaml TEXT NOT NULL,
author TEXT NOT NULL DEFAULT '',
deploy_id TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE(stack_id, revision)
)`,
`CREATE TABLE IF NOT EXISTS stack_deploys (
id TEXT PRIMARY KEY,
stack_id TEXT NOT NULL REFERENCES stacks(id) ON DELETE CASCADE,
revision_id TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'pending',
log TEXT NOT NULL DEFAULT '',
error TEXT NOT NULL DEFAULT '',
started_at TEXT NOT NULL DEFAULT (datetime('now')),
finished_at TEXT NOT NULL DEFAULT ''
)`,
}
for _, t := range stackTables {
if _, err := s.db.Exec(t); err != nil {
return fmt.Errorf("create stack table: %w", err)
}
}
for _, m := range migrations {
// Ignore errors from already-applied migrations (duplicate column).
_, _ = s.db.Exec(m)
@@ -150,6 +192,8 @@ func (s *Store) runMigrations() error {
`CREATE INDEX IF NOT EXISTS idx_event_log_created_at ON event_log(created_at)`,
`CREATE INDEX IF NOT EXISTS idx_dns_records_consumer ON dns_records(consumer_type, consumer_id)`,
`CREATE INDEX IF NOT EXISTS idx_static_site_secrets_site_id ON static_site_secrets(site_id)`,
`CREATE INDEX IF NOT EXISTS idx_stack_revisions_stack_id ON stack_revisions(stack_id)`,
`CREATE INDEX IF NOT EXISTS idx_stack_deploys_stack_id ON stack_deploys(stack_id)`,
}
for _, idx := range indexes {
if _, err := s.db.Exec(idx); err != nil {