feat(cutover): hard legacy cutover — drop projects/stacks/sites/deploys
Build / build (push) Successful in 10m39s
Build / build (push) Successful in 10m39s
The clean-break delete that closes the workload-first refactor arc.
Net diff: ~30 backend files deleted, ~20 modified, ~12k LOC removed
on the Go side; entire /projects /stacks /sites /deploy frontend
trees gone; ~6.7k LOC removed on the Svelte/TypeScript side.
Backend
- API handlers gone: internal/api/{projects,stages,stage_env,stacks,
static_sites,deploys,instances,volume_browser}.go
- Store CRUD + tests gone: internal/store/{projects,stages,stage_env,
stacks,static_sites,static_site_secrets,deploys,poll_state,volumes,
workload_sync}.go (+ _test.go siblings)
- Legacy deployer pipeline gone: internal/deployer/{bluegreen,promote,
rollback,subdomain,resolver_test}.go; deployer.go trimmed to just the
dispatch surface used by the plugin pipeline
- internal/staticsite/{manager,healthcheck}.go and
internal/stack/manager.go gone (the rest of those packages stay as
helpers imported by the static + compose plugins)
- internal/registry/poller.go gone (legacy registry poller)
- internal/volume.ResolvePath gone; ResolveWorkloadPath stays
- internal/webhook: handleWebhook (project) + handleSiteWebhook (site)
gone; only POST /api/webhook/triggers/{secret} remains
- workload-side webhook URL handlers (getWorkloadWebhook +
regenerateWorkloadWebhook + EnsureWorkloadWebhookSecret +
SetWorkloadWebhookSecret + GetWorkloadByWebhookSecret) gone — they
minted URLs that would 404 against the new trigger-only ingress
- cmd/server/main.go: dropped staticsite.Manager, stack.Manager,
staticsite.HealthChecker, registry poller, SetSiteSyncTriggerer,
SetStaticSiteManager, SetStackManager, wireStaticBackend
- store/store.go: idempotent DROP TABLE IF EXISTS for every legacy
table (projects, stages, stage_env, volumes, deploys, deploy_logs,
poll_states, stacks, stack_revisions, stack_deploys, static_sites,
static_site_secrets); FK order children-then-parents
- store/models.go: dropped Project, Stage, Deploy, DeployLog, StageEnv,
Volume, StaticSite, StaticSiteSecret, Stack, StackRevision,
StackDeploy types; kept WorkloadKind constants as documented strings
- internal/store/helpers.go (new): BoolToInt, rowScanner,
GenerateWebhookSecret extracted from deleted CRUD files
- internal/api/secrets.go (new): forwards to store.GenerateWebhookSecret
so api + store paths share one secret-generation impl (no
panic-vs-UUID-fallback divergence)
- internal/reconciler/reconciler.go: dropped legacy stack-by-compose
+ static-site label paths; only canonical tinyforge.workload.id
dispatch remains
- providers (gitea_content/github_provider/gitlab_provider) gained
path-traversal rejection on every tree entry
- internal/webhook ParsedImage / ParseImageRef demoted to package-
private (no external callers)
Frontend
- /projects /stacks /sites /deploy routes deleted (entire trees)
- ProjectCard / InstanceCard / StaleContainerCard components deleted
- api.ts: dropped every project/stage/stack/site/deploy/instance
helper + types (Project, Stage, Stack, StaticSite, Deploy,
Instance, Volume, etc.); kept Workload, Container, App, Settings,
Registry, EventTrigger, LogScanRule, webhook envelopes
- WorkloadWebhook type + getWorkloadWebhook/regenerateWorkloadWebhook
api functions gone (mirror of the backend deletion above)
- web/src/routes/+layout.svelte: dropped /projects /sites /stacks
/deploy nav entries, trimmed quick-nav keymap
- web/src/routes/+page.svelte: dashboard rewrite — reads
listWorkloads + listContainers only; 4-card stat grid
(workloads/running/failed/stale) + recent workloads strip
- navCounts.ts, SystemHealthCard.svelte, ContainerLogs.svelte,
ContainerStats.svelte, StatusBadge.svelte, TagCombobox.svelte,
proxies/+page.svelte, containers/+page.svelte all rewired to the
workload-first surface
- AbortController plumbing on dashboard, nav-counts, stale page,
SystemHealthCard so navigation doesn't leave dangling fetches
- i18n: dropped projects.*, projectDetail.*, envEditor.*,
volumeEditor.*, volumeBrowser.*, quickDeploy.*, sites.*, stacks.*,
instance.*, confirm.* namespaces; en/ru parity preserved (1042
keys each)
Hardening from go-reviewer + security-reviewer + typescript-reviewer
subagent passes (0 CRITICAL across all three; 1 HIGH + ~12 MEDIUM
addressed inline before commit):
- Sec H1: dead-end workload webhook URL handlers (would mint URLs
that 404 the new trigger-only ingress) deleted across backend +
frontend
- Go M1: IsTerminalDeployStatus dropped (no production callers)
- Go M2: ParsedImage/ParseImageRef lowercased (in-package only)
- Go M6: generateWebhookSecret unified — api shim forwards to
store.GenerateWebhookSecret
- Doc/comment freshness: stage_id (no longer FK), ProxyRoute legacy
field names, workloadIDRow rationale, webhook_deliveries.target_type
enum, WebhookDeliveryLog component header
Doc
- WORKLOAD_REFACTOR_TODO: cutover marked DONE; all three Priority 1
items are now shipped. Next focus is Priority 3 polish (apps.* i18n
+ codemap entries) and Priority 4 tests.
Behavioral notes for operators upgrading from a pre-cutover build
- Existing rows in the dropped tables disappear on first boot.
- Legacy webhook URLs at /api/webhook/{secret} and
/api/webhook/sites/{secret} return 404; CI configs must repoint to
/api/webhook/triggers/{secret} (the trigger-split boot backfill
lifted any embedded workload secret onto a Trigger row, so the
secret value itself carries over).
- Frontend routes /projects /stacks /sites /deploy are gone; nav
links replaced with /apps and /triggers.
This commit is contained in:
+39
-269
@@ -97,97 +97,44 @@ func (s *Store) migrate() error {
|
||||
}
|
||||
|
||||
// runMigrations applies additive schema changes that cannot be expressed
|
||||
// with CREATE TABLE IF NOT EXISTS.
|
||||
// with CREATE TABLE IF NOT EXISTS, plus the hard-cutover drops that
|
||||
// remove every legacy project/stage/stack/static_site/deploy table.
|
||||
func (s *Store) runMigrations() error {
|
||||
migrations := []string{
|
||||
// Add owner column to registries (2026-03-28).
|
||||
`ALTER TABLE registries ADD COLUMN owner TEXT NOT NULL DEFAULT ''`,
|
||||
// Add base_volume_path to settings (2026-03-28).
|
||||
// Set default network for existing databases with empty network.
|
||||
`UPDATE settings SET network = 'tinyforge' WHERE network = ''`,
|
||||
// Settings column adds that survive the cutover. SQLite is tolerant
|
||||
// of "duplicate column" errors at the apply step, so re-running on
|
||||
// a fully-migrated DB is a no-op.
|
||||
`ALTER TABLE settings ADD COLUMN base_volume_path TEXT NOT NULL DEFAULT ''`,
|
||||
// Add enable_proxy to stages (2026-03-29). Default true for backwards compat.
|
||||
`ALTER TABLE stages ADD COLUMN enable_proxy INTEGER NOT NULL DEFAULT 1`,
|
||||
// Add ssl_certificate_id to settings (2026-03-29).
|
||||
`ALTER TABLE settings ADD COLUMN ssl_certificate_id INTEGER NOT NULL DEFAULT 0`,
|
||||
// Add stale_threshold_days to settings (2026-03-30).
|
||||
`ALTER TABLE settings ADD COLUMN stale_threshold_days INTEGER NOT NULL DEFAULT 7`,
|
||||
// Add last_alive_at to instances for stale container detection (2026-03-30).
|
||||
`ALTER TABLE instances ADD COLUMN last_alive_at TEXT NOT NULL DEFAULT ''`,
|
||||
// Add name column and rename mode→scope for volume scopes redesign (2026-03-31).
|
||||
`ALTER TABLE volumes ADD COLUMN name TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE volumes ADD COLUMN scope TEXT NOT NULL DEFAULT ''`,
|
||||
// Add allowed_volume_paths to settings for absolute volume scope allowlist (2026-04-01).
|
||||
`ALTER TABLE settings ADD COLUMN allowed_volume_paths TEXT NOT NULL DEFAULT '[]'`,
|
||||
// Add DNS management fields to settings (2026-04-02).
|
||||
`ALTER TABLE settings ADD COLUMN wildcard_dns INTEGER NOT NULL DEFAULT 1`,
|
||||
`ALTER TABLE settings ADD COLUMN dns_provider TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE settings ADD COLUMN cloudflare_api_token TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE settings ADD COLUMN cloudflare_zone_id TEXT NOT NULL DEFAULT ''`,
|
||||
// Add backup management fields to settings (2026-04-02).
|
||||
`ALTER TABLE settings ADD COLUMN backup_enabled INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE settings ADD COLUMN backup_interval_hours INTEGER NOT NULL DEFAULT 24`,
|
||||
`ALTER TABLE settings ADD COLUMN backup_retention_count INTEGER NOT NULL DEFAULT 10`,
|
||||
`ALTER TABLE stages ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
||||
// Add proxy_route_id to instances for provider-agnostic route tracking (2026-04-04).
|
||||
`ALTER TABLE instances ADD COLUMN proxy_route_id TEXT NOT NULL DEFAULT ''`,
|
||||
// Add proxy_provider to settings (2026-04-04). Default to npm for backward compat.
|
||||
`ALTER TABLE settings ADD COLUMN proxy_provider TEXT NOT NULL DEFAULT 'npm'`,
|
||||
// Add Traefik provider settings (2026-04-04).
|
||||
`ALTER TABLE settings ADD COLUMN traefik_entrypoint TEXT NOT NULL DEFAULT 'websecure'`,
|
||||
`ALTER TABLE settings ADD COLUMN traefik_cert_resolver TEXT NOT NULL DEFAULT 'letsencrypt'`,
|
||||
`ALTER TABLE settings ADD COLUMN traefik_network TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE settings ADD COLUMN traefik_api_url TEXT NOT NULL DEFAULT ''`,
|
||||
// Set default network for existing databases with empty network.
|
||||
`UPDATE settings SET network = 'tinyforge' WHERE network = ''`,
|
||||
// NPM remote mode: forward to server_ip instead of container name.
|
||||
`ALTER TABLE settings ADD COLUMN npm_remote INTEGER NOT NULL DEFAULT 0`,
|
||||
// Resource limits per stage.
|
||||
`ALTER TABLE stages ADD COLUMN cpu_limit REAL NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE stages ADD COLUMN memory_limit INTEGER NOT NULL DEFAULT 0`,
|
||||
// NPM access list support (global default + per-project override).
|
||||
`ALTER TABLE settings ADD COLUMN npm_access_list_id INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE projects ADD COLUMN npm_access_list_id INTEGER NOT NULL DEFAULT 0`,
|
||||
// Separate public IP for DNS A records.
|
||||
`ALTER TABLE settings ADD COLUMN public_ip TEXT NOT NULL DEFAULT ''`,
|
||||
// Image prune threshold (MB). Warn on dashboard when exceeded. 0 = disabled.
|
||||
`ALTER TABLE settings ADD COLUMN image_prune_threshold_mb INTEGER NOT NULL DEFAULT 1024`,
|
||||
// Add provider column to static_sites (2026-04-11).
|
||||
`ALTER TABLE static_sites ADD COLUMN provider TEXT NOT NULL DEFAULT ''`,
|
||||
// Add persistent storage columns to static_sites (2026-04-12).
|
||||
`ALTER TABLE static_sites ADD COLUMN storage_enabled INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE static_sites ADD COLUMN storage_limit_mb INTEGER NOT NULL DEFAULT 0`,
|
||||
// Per-project + per-site webhook secrets (2026-04-23). Global
|
||||
// settings.webhook_secret is deprecated; its column is retained to
|
||||
// avoid a destructive migration on SQLite.
|
||||
`ALTER TABLE projects ADD COLUMN webhook_secret TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE static_sites ADD COLUMN webhook_secret TEXT NOT NULL DEFAULT ''`,
|
||||
// Resource metrics collection (2026-04-24). Interval in seconds,
|
||||
// retention in hours. 0 in either disables collection.
|
||||
`ALTER TABLE settings ADD COLUMN stats_interval_seconds INTEGER NOT NULL DEFAULT 15`,
|
||||
`ALTER TABLE settings ADD COLUMN stats_retention_hours INTEGER NOT NULL DEFAULT 2`,
|
||||
// Outgoing-webhook signing secrets per tier (2026-05-07). Plain hex
|
||||
// tokens (matches the inbound webhook_secret pattern). Empty = no
|
||||
// signing; existing rows stay unsigned on upgrade for back-compat.
|
||||
`ALTER TABLE settings ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE projects ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE projects ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE stages ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE static_sites ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE static_sites ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
||||
// Auto-backup before deploy (2026-05-07). When enabled, the deployer
|
||||
// triggers a "pre-deploy" Tinyforge DB backup before any project deploy
|
||||
// so a corrupted deploy is recoverable without data loss.
|
||||
`ALTER TABLE settings ADD COLUMN auto_backup_before_deploy INTEGER NOT NULL DEFAULT 0`,
|
||||
// Per-entity inbound HMAC signing (2026-05-07). webhook_signing_secret
|
||||
// is the HMAC-SHA256 key separate from the URL secret so a leaked URL
|
||||
// alone is not sufficient to forge a valid request. require_signature
|
||||
// rejects unsigned requests when set (defense-in-depth opt-in).
|
||||
`ALTER TABLE projects ADD COLUMN webhook_signing_secret TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE projects ADD COLUMN webhook_require_signature INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE static_sites ADD COLUMN webhook_signing_secret TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE static_sites ADD COLUMN webhook_require_signature INTEGER NOT NULL DEFAULT 0`,
|
||||
// Webhook delivery audit log (2026-05-07). Persists every inbound
|
||||
// webhook request (project or site) with its outcome so users can
|
||||
// debug "why didn't my deploy fire?" without grepping daemon logs.
|
||||
// Registries — owner column.
|
||||
`ALTER TABLE registries ADD COLUMN owner TEXT NOT NULL DEFAULT ''`,
|
||||
// Webhook delivery audit log persists every inbound webhook
|
||||
// request so operators can debug "why didn't my deploy fire?"
|
||||
// without grepping daemon logs.
|
||||
`CREATE TABLE IF NOT EXISTS webhook_deliveries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
target_type TEXT NOT NULL,
|
||||
@@ -203,19 +150,36 @@ func (s *Store) runMigrations() error {
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_target ON webhook_deliveries(target_type, target_id, received_at)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_received_at ON webhook_deliveries(received_at)`,
|
||||
// Add stage_id to containers (2026-05-09). Backfill via the deployer
|
||||
// re-write path; the LEFT JOIN in ListContainersByStageID falls back
|
||||
// to (project_id, role=stage_name) so legacy rows still resolve.
|
||||
// Containers — stage_id is now an opaque string set by the source
|
||||
// plugin (image plugin uses it for the deploy-target tag). No FK
|
||||
// semantics: the legacy `stages` table this column once joined to
|
||||
// is gone; the column is just a free-form discriminator the
|
||||
// proxies / dashboard views read to disambiguate sibling rows.
|
||||
`ALTER TABLE containers ADD COLUMN stage_id TEXT NOT NULL DEFAULT ''`,
|
||||
// Workload-first refactor columns (2026-05-10). Land additively so
|
||||
// the legacy kind/ref_id columns continue to serve existing
|
||||
// project/stack/site rows during cutover.
|
||||
// Workload-first refactor columns. Land additively so old databases
|
||||
// (which have a bare workloads table) pick them up on the next boot.
|
||||
`ALTER TABLE workloads ADD COLUMN source_kind TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE workloads ADD COLUMN source_config TEXT NOT NULL DEFAULT '{}'`,
|
||||
`ALTER TABLE workloads ADD COLUMN trigger_kind TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE workloads ADD COLUMN trigger_config TEXT NOT NULL DEFAULT '{}'`,
|
||||
`ALTER TABLE workloads ADD COLUMN public_faces TEXT NOT NULL DEFAULT '[]'`,
|
||||
`ALTER TABLE workloads ADD COLUMN parent_workload_id TEXT NOT NULL DEFAULT ''`,
|
||||
// Hard cutover: drop every legacy table. Idempotent — DROP TABLE
|
||||
// IF EXISTS is a no-op once the table is gone. Operators upgrading
|
||||
// from a pre-cutover build will lose any project / stack / static
|
||||
// site rows; the upgrade notes call this out explicitly.
|
||||
`DROP TABLE IF EXISTS deploy_logs`,
|
||||
`DROP TABLE IF EXISTS deploys`,
|
||||
`DROP TABLE IF EXISTS stage_env`,
|
||||
`DROP TABLE IF EXISTS stages`,
|
||||
`DROP TABLE IF EXISTS poll_states`,
|
||||
`DROP TABLE IF EXISTS volumes`,
|
||||
`DROP TABLE IF EXISTS static_site_secrets`,
|
||||
`DROP TABLE IF EXISTS static_sites`,
|
||||
`DROP TABLE IF EXISTS stack_deploys`,
|
||||
`DROP TABLE IF EXISTS stack_revisions`,
|
||||
`DROP TABLE IF EXISTS stacks`,
|
||||
`DROP TABLE IF EXISTS projects`,
|
||||
}
|
||||
|
||||
// Workload refactor tables (2026-05-09). Workload is the unifying primitive
|
||||
@@ -369,46 +333,6 @@ func (s *Store) runMigrations() error {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Observability: event_triggers — consume EventLog entries off the
|
||||
// bus and dispatch webhook actions. Schema kept flat (comma-list
|
||||
// filters, single optional regex) — see LOGSCAN_AND_TRIGGERS_TODO.md.
|
||||
@@ -469,34 +393,18 @@ func (s *Store) runMigrations() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Create indexes on foreign key columns for query performance.
|
||||
// Create indexes on foreign key columns for query performance. Only
|
||||
// indexes targeting tables that still exist after the hard cutover.
|
||||
indexes := []string{
|
||||
// instances table dropped 2026-05-09 (workload refactor) — no indexes
|
||||
// needed; containers replaces it with idx_containers_workload below.
|
||||
`CREATE INDEX IF NOT EXISTS idx_deploys_project_id ON deploys(project_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_deploys_stage_id ON deploys(stage_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_deploy_logs_deploy_id ON deploy_logs(deploy_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_stages_project_id ON stages(project_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_stage_env_stage_id ON stage_env(stage_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_volumes_project_id ON volumes(project_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_log_severity ON event_log(severity)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_event_log_source ON event_log(source)`,
|
||||
`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)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_webhook_secret ON projects(webhook_secret) WHERE webhook_secret != ''`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_static_sites_webhook_secret ON static_sites(webhook_secret) WHERE webhook_secret != ''`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_container_stats_owner_ts ON container_stats_samples(owner_type, owner_id, ts)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_container_stats_container_ts ON container_stats_samples(container_id, ts)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_container_stats_ts ON container_stats_samples(ts)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_system_stats_ts ON system_stats_samples(ts)`,
|
||||
// Drop the legacy instances table — containers is the canonical index
|
||||
// after the workload refactor (2026-05-09). Idempotent: SQLite's
|
||||
// DROP TABLE IF EXISTS is a no-op on databases that already shed it.
|
||||
`DROP TABLE IF EXISTS instances`,
|
||||
// Workload refactor indexes (2026-05-09).
|
||||
// Workload refactor indexes.
|
||||
`CREATE INDEX IF NOT EXISTS idx_workloads_kind ON workloads(kind)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_workloads_app_id ON workloads(app_id) WHERE app_id != ''`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_workloads_ref ON workloads(kind, ref_id)`,
|
||||
@@ -508,7 +416,7 @@ func (s *Store) runMigrations() error {
|
||||
`CREATE INDEX IF NOT EXISTS idx_containers_stage_id ON containers(stage_id) WHERE stage_id != ''`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_workload_env_workload ON workload_env(workload_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_workload_volumes_workload ON workload_volumes(workload_id)`,
|
||||
// Trigger-split indexes (2026-05-16).
|
||||
// Trigger-split indexes.
|
||||
`CREATE INDEX IF NOT EXISTS idx_triggers_kind ON triggers(kind)`,
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_triggers_webhook_secret ON triggers(webhook_secret) WHERE webhook_secret != ''`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_bindings_workload ON workload_trigger_bindings(workload_id)`,
|
||||
@@ -520,19 +428,6 @@ func (s *Store) runMigrations() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Data migration: copy mode→scope for volumes that have scope still empty.
|
||||
// shared→project, isolated→instance. Log errors but don't fail startup.
|
||||
dataMigrations := []struct{ query, desc string }{
|
||||
{`UPDATE volumes SET scope = 'project' WHERE scope = '' AND mode = 'shared'`, "migrate shared→project"},
|
||||
{`UPDATE volumes SET scope = 'instance' WHERE scope = '' AND mode = 'isolated'`, "migrate isolated→instance"},
|
||||
{`UPDATE volumes SET scope = 'project' WHERE scope = '' AND mode = ''`, "migrate empty→project"},
|
||||
}
|
||||
for _, dm := range dataMigrations {
|
||||
if _, err := s.db.Exec(dm.query); err != nil {
|
||||
fmt.Printf("volume scope migration warning (%s): %v\n", dm.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.backfillTriggersFromWorkloads(); err != nil {
|
||||
slog.Warn("trigger backfill", "error", err)
|
||||
}
|
||||
@@ -658,42 +553,6 @@ func (s *Store) backfillOneTrigger(workloadID, workloadName, kind, config,
|
||||
}
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
registry TEXT NOT NULL DEFAULT '',
|
||||
image TEXT NOT NULL,
|
||||
port INTEGER NOT NULL DEFAULT 0,
|
||||
healthcheck TEXT NOT NULL DEFAULT '',
|
||||
env TEXT NOT NULL DEFAULT '{}',
|
||||
volumes TEXT NOT NULL DEFAULT '{}',
|
||||
npm_access_list_id INTEGER NOT NULL DEFAULT 0,
|
||||
notification_url TEXT NOT NULL DEFAULT '',
|
||||
notification_secret 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 stages (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
tag_pattern TEXT NOT NULL DEFAULT '*',
|
||||
auto_deploy INTEGER NOT NULL DEFAULT 0,
|
||||
max_instances INTEGER NOT NULL DEFAULT 1,
|
||||
confirm INTEGER NOT NULL DEFAULT 0,
|
||||
enable_proxy INTEGER NOT NULL DEFAULT 1,
|
||||
promote_from TEXT NOT NULL DEFAULT '',
|
||||
subdomain TEXT NOT NULL DEFAULT '',
|
||||
notification_url TEXT NOT NULL DEFAULT '',
|
||||
notification_secret TEXT NOT NULL DEFAULT '',
|
||||
cpu_limit REAL NOT NULL DEFAULT 0,
|
||||
memory_limit INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(project_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS registries (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
@@ -730,36 +589,6 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- The instances table was removed in the workload refactor (2026-05-09).
|
||||
-- Container state lives in the containers table; see runMigrations for the
|
||||
-- current schema. The DROP TABLE migration runs unconditionally on boot.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deploys (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
||||
instance_id TEXT NOT NULL DEFAULT '',
|
||||
image_tag TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
finished_at TEXT NOT NULL DEFAULT '',
|
||||
error TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS deploy_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
deploy_id TEXT NOT NULL REFERENCES deploys(id) ON DELETE CASCADE,
|
||||
message TEXT NOT NULL,
|
||||
level TEXT NOT NULL DEFAULT 'info',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS poll_states (
|
||||
stage_id TEXT PRIMARY KEY REFERENCES stages(id) ON DELETE CASCADE,
|
||||
last_tag TEXT NOT NULL DEFAULT '',
|
||||
last_polled TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
@@ -785,27 +614,6 @@ INSERT OR IGNORE INTO settings (id) VALUES (1);
|
||||
-- Seed the auth_settings row if it does not exist.
|
||||
INSERT OR IGNORE INTO auth_settings (id) VALUES (1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stage_env (
|
||||
id TEXT PRIMARY KEY,
|
||||
stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
encrypted INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(stage_id, key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS volumes (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
source TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
mode TEXT NOT NULL DEFAULT 'shared',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS event_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
@@ -845,44 +653,6 @@ CREATE TABLE IF NOT EXISTS backups (
|
||||
backup_type TEXT NOT NULL DEFAULT 'manual',
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS static_sites (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
provider TEXT NOT NULL DEFAULT '',
|
||||
gitea_url TEXT NOT NULL DEFAULT '',
|
||||
repo_owner TEXT NOT NULL DEFAULT '',
|
||||
repo_name TEXT NOT NULL DEFAULT '',
|
||||
branch TEXT NOT NULL DEFAULT 'main',
|
||||
folder_path TEXT NOT NULL DEFAULT '',
|
||||
access_token TEXT NOT NULL DEFAULT '',
|
||||
domain TEXT NOT NULL DEFAULT '',
|
||||
mode TEXT NOT NULL DEFAULT 'static',
|
||||
render_markdown INTEGER NOT NULL DEFAULT 0,
|
||||
sync_trigger TEXT NOT NULL DEFAULT 'manual',
|
||||
tag_pattern TEXT NOT NULL DEFAULT '',
|
||||
container_id TEXT NOT NULL DEFAULT '',
|
||||
proxy_route_id TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'idle',
|
||||
last_sync_at TEXT NOT NULL DEFAULT '',
|
||||
last_commit_sha TEXT NOT NULL DEFAULT '',
|
||||
error TEXT NOT NULL DEFAULT '',
|
||||
notification_url TEXT NOT NULL DEFAULT '',
|
||||
notification_secret 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 static_site_secrets (
|
||||
id TEXT PRIMARY KEY,
|
||||
site_id TEXT NOT NULL REFERENCES static_sites(id) ON DELETE CASCADE,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL DEFAULT '',
|
||||
encrypted INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE(site_id, key)
|
||||
);
|
||||
`
|
||||
|
||||
// Now returns the current time formatted for SQLite storage.
|
||||
|
||||
Reference in New Issue
Block a user