refactor(workload): extract Instance entirely; Container is canonical
Build / build (push) Successful in 10m41s
Build / build (push) Successful in 10m41s
End-to-end extraction of the Instance concept. After this commit:
* internal/store/instances.go — DELETED
* internal/store/models.go — Instance struct gone, ProxyRoute moved here
* containers table is the single source of truth for project/stack/site
container state. instances table is dropped via DROP TABLE migration
(idempotent; re-runnable on every boot).
* Legacy tinyforge.project / tinyforge.stage / tinyforge.instance-id
Docker labels are no longer emitted; only tinyforge.workload.{id,kind},
tinyforge.role, and tinyforge.managed are stamped on new containers.
Backend rewrites:
- internal/deployer: executeDeploy + blueGreenDeploy + rollback +
promote use store.Container natively. New
removeContainer() replaces removeInstance().
enforceMaxInstances reads via
ListContainersByStageID.
- internal/reconciler: legacy tinyforge.instance-id dispatch removed;
upsertByWorkloadLabel now finds existing rows
by docker container ID first and falls back to
the deterministic workloadID:role key.
- internal/stale/scanner: Scan + new FindStaleContainers walk the
containers table; emit StaleContainer JSON.
- internal/stats/collector: ListContainers replaces ListAllInstances.
- internal/webhook/handler: workload-secret lookup tried first; falls back
to project / static_site secret column.
- internal/api: instances.go, stale.go, stats.go, stats_history.go,
projects.go, settings.go, docker.go, dns.go all read /
write through Container.
Docker layer:
- ManagedContainer exposes WorkloadID/Kind/Role from the canonical labels.
- ListContainers filters by tinyforge.managed=true.
- Network creation uses LabelManaged instead of LabelProject.
Frontend:
- Instance type is now a Container alias; .status → .state,
.last_alive_at → .last_seen_at.
- InstanceCard takes stageId as a prop (no longer derived from Instance).
- StaleContainer JSON shape rewritten: { container, workload_name, role,
days_stale }. StaleContainerCard + /containers/stale page updated.
- ProjectCard / homepage / SystemHealthCard filter by .state.
The migration loop now tolerates "no such table" alongside "duplicate
column" / "already exists" so obsolete ALTER TABLE entries targeting the
dropped instances table no-op cleanly on first boot.
Tests: store + deployer + reconciler + webhook + staticsite + notify all
still pass. Frontend svelte-check: zero errors.
This commit is contained in:
@@ -135,6 +135,77 @@ func (s *Store) GetContainerByDockerID(dockerID string) (Container, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ListProxyRoutes returns proxy-enabled project containers joined with
|
||||
// project + stage names. Reads from the normalized containers index. Stage
|
||||
// ID is resolved through a (project_id, role=stage_name) join, which is
|
||||
// uniquely indexed via UNIQUE(project_id, name) on stages.
|
||||
//
|
||||
// Source is reported as "instance" for back-compat with the Proxies page
|
||||
// filter (the frontend keys off the literal string).
|
||||
func (s *Store) ListProxyRoutes(domain string) ([]ProxyRoute, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT c.id, p.id, p.name, s.id, s.name,
|
||||
c.image_tag, c.subdomain, c.container_id, c.port,
|
||||
c.proxy_route_id, c.npm_proxy_id, c.state, c.created_at
|
||||
FROM containers c
|
||||
JOIN workloads w ON w.id = c.workload_id AND w.kind = 'project'
|
||||
JOIN projects p ON p.id = w.ref_id
|
||||
JOIN stages s ON s.project_id = p.id AND s.name = c.role
|
||||
WHERE c.subdomain != '' AND (c.proxy_route_id != '' OR c.npm_proxy_id > 0)
|
||||
ORDER BY p.name, s.name, c.created_at DESC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query proxy routes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
routes := []ProxyRoute{}
|
||||
for rows.Next() {
|
||||
var r ProxyRoute
|
||||
if err := rows.Scan(
|
||||
&r.InstanceID, &r.ProjectID, &r.ProjectName, &r.StageID, &r.StageName,
|
||||
&r.ImageTag, &r.Subdomain, &r.ContainerID, &r.Port,
|
||||
&r.ProxyRouteID, &r.NpmProxyID, &r.Status, &r.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan proxy route: %w", err)
|
||||
}
|
||||
r.Source = "instance"
|
||||
if domain != "" && r.Subdomain != "" {
|
||||
r.Domain = r.Subdomain + "." + domain
|
||||
}
|
||||
routes = append(routes, r)
|
||||
}
|
||||
return routes, rows.Err()
|
||||
}
|
||||
|
||||
// ListContainersByStageID returns project containers for the given stage,
|
||||
// newest first. Resolves stage → project_id → workload(kind=project) →
|
||||
// containers with role = stage.name. Replaces GetInstancesByStageID for
|
||||
// callers in the deployer / API layer.
|
||||
func (s *Store) ListContainersByStageID(stageID string) ([]Container, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT `+prefixCols(containerColumns, "c.")+`
|
||||
FROM containers c
|
||||
JOIN workloads w ON w.id = c.workload_id AND w.kind = 'project'
|
||||
JOIN stages s ON s.project_id = w.ref_id AND s.name = c.role
|
||||
WHERE s.id = ?
|
||||
ORDER BY c.created_at DESC`, stageID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query containers by stage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := []Container{}
|
||||
for rows.Next() {
|
||||
c, err := scanContainer(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan container: %w", err)
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ListContainersByWorkload returns all containers for a given workload, newest first.
|
||||
func (s *Store) ListContainersByWorkload(workloadID string) ([]Container, error) {
|
||||
rows, err := s.db.Query(
|
||||
|
||||
@@ -1,251 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// instanceColumns is the canonical column list for instance queries.
|
||||
const instanceColumns = `id, stage_id, project_id, container_id, image_tag, subdomain, npm_proxy_id, proxy_route_id, status, port, last_alive_at, created_at, updated_at`
|
||||
|
||||
// scanInstance scans a row into an Instance struct using the canonical column order.
|
||||
func scanInstance(scanner interface{ Scan(...any) error }) (Instance, error) {
|
||||
var inst Instance
|
||||
err := scanner.Scan(
|
||||
&inst.ID, &inst.StageID, &inst.ProjectID, &inst.ContainerID, &inst.ImageTag,
|
||||
&inst.Subdomain, &inst.NpmProxyID, &inst.ProxyRouteID, &inst.Status, &inst.Port,
|
||||
&inst.LastAliveAt, &inst.CreatedAt, &inst.UpdatedAt,
|
||||
)
|
||||
return inst, err
|
||||
}
|
||||
|
||||
// CreateInstance inserts a new instance record.
|
||||
func (s *Store) CreateInstance(inst Instance) (Instance, error) {
|
||||
inst.ID = uuid.New().String()
|
||||
inst.CreatedAt = Now()
|
||||
inst.UpdatedAt = inst.CreatedAt
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO instances (`+instanceColumns+`)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
||||
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
||||
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Instance{}, fmt.Errorf("insert instance: %w", err)
|
||||
}
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// CreateInstanceWithID inserts a new instance using a pre-generated ID.
|
||||
// Use this when the ID must be known before creation (e.g., for container labels).
|
||||
func (s *Store) CreateInstanceWithID(inst Instance) (Instance, error) {
|
||||
if inst.ID == "" {
|
||||
return Instance{}, fmt.Errorf("instance ID is required")
|
||||
}
|
||||
inst.CreatedAt = Now()
|
||||
inst.UpdatedAt = inst.CreatedAt
|
||||
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO instances (`+instanceColumns+`)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
||||
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
||||
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return Instance{}, fmt.Errorf("insert instance: %w", err)
|
||||
}
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// GetInstanceByID returns a single instance by its ID.
|
||||
func (s *Store) GetInstanceByID(id string) (Instance, error) {
|
||||
inst, err := scanInstance(s.db.QueryRow(
|
||||
`SELECT `+instanceColumns+` FROM instances WHERE id = ?`, id,
|
||||
))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return Instance{}, fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
||||
}
|
||||
if err != nil {
|
||||
return Instance{}, fmt.Errorf("query instance: %w", err)
|
||||
}
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// GetInstancesByStageID returns all instances for a given stage.
|
||||
func (s *Store) GetInstancesByStageID(stageID string) ([]Instance, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT `+instanceColumns+` FROM instances WHERE stage_id = ? ORDER BY created_at DESC`, stageID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query instances: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
instances := []Instance{}
|
||||
for rows.Next() {
|
||||
inst, err := scanInstance(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan instance: %w", err)
|
||||
}
|
||||
instances = append(instances, inst)
|
||||
}
|
||||
return instances, rows.Err()
|
||||
}
|
||||
|
||||
// ListAllInstances returns all instances across all stages.
|
||||
func (s *Store) ListAllInstances() ([]Instance, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT ` + instanceColumns + ` FROM instances ORDER BY created_at DESC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query all instances: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
instances := []Instance{}
|
||||
for rows.Next() {
|
||||
inst, err := scanInstance(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan instance: %w", err)
|
||||
}
|
||||
instances = append(instances, inst)
|
||||
}
|
||||
return instances, rows.Err()
|
||||
}
|
||||
|
||||
// ProxyRoute represents a proxy-enabled resource (Docker instance or static site)
|
||||
// joined with the human-readable names needed to render the Proxies page.
|
||||
type ProxyRoute struct {
|
||||
Source string `json:"source"` // "instance" or "static_site"
|
||||
InstanceID string `json:"instance_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
StageID string `json:"stage_id"`
|
||||
StageName string `json:"stage_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
Domain string `json:"domain"`
|
||||
ContainerID string `json:"container_id"`
|
||||
Port int `json:"port"`
|
||||
ProxyRouteID string `json:"proxy_route_id"`
|
||||
NpmProxyID int `json:"npm_proxy_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// ListProxyRoutes returns proxy-enabled project containers joined with
|
||||
// project + stage names. Reads from the normalized containers index — the
|
||||
// instances table is no longer queried. Stage ID is resolved through a
|
||||
// (project_id, role=stage_name) join, which is uniquely indexed.
|
||||
//
|
||||
// Source is reported as "instance" for back-compat with the Proxies page
|
||||
// filter (which keys off the literal string).
|
||||
func (s *Store) ListProxyRoutes(domain string) ([]ProxyRoute, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT c.id, p.id, p.name, s.id, s.name,
|
||||
c.image_tag, c.subdomain, c.container_id, c.port,
|
||||
c.proxy_route_id, c.npm_proxy_id, c.state, c.created_at
|
||||
FROM containers c
|
||||
JOIN workloads w ON w.id = c.workload_id AND w.kind = 'project'
|
||||
JOIN projects p ON p.id = w.ref_id
|
||||
JOIN stages s ON s.project_id = p.id AND s.name = c.role
|
||||
WHERE c.subdomain != '' AND (c.proxy_route_id != '' OR c.npm_proxy_id > 0)
|
||||
ORDER BY p.name, s.name, c.created_at DESC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query proxy routes: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
routes := []ProxyRoute{}
|
||||
for rows.Next() {
|
||||
var r ProxyRoute
|
||||
if err := rows.Scan(
|
||||
&r.InstanceID, &r.ProjectID, &r.ProjectName, &r.StageID, &r.StageName,
|
||||
&r.ImageTag, &r.Subdomain, &r.ContainerID, &r.Port,
|
||||
&r.ProxyRouteID, &r.NpmProxyID, &r.Status, &r.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan proxy route: %w", err)
|
||||
}
|
||||
r.Source = "instance"
|
||||
if domain != "" && r.Subdomain != "" {
|
||||
r.Domain = r.Subdomain + "." + domain
|
||||
}
|
||||
routes = append(routes, r)
|
||||
}
|
||||
return routes, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateInstance updates an existing instance's mutable fields.
|
||||
func (s *Store) UpdateInstance(inst Instance) error {
|
||||
inst.UpdatedAt = Now()
|
||||
result, err := s.db.Exec(
|
||||
`UPDATE instances SET stage_id=?, project_id=?, container_id=?, image_tag=?, subdomain=?, npm_proxy_id=?, proxy_route_id=?, status=?, port=?, last_alive_at=?, updated_at=?
|
||||
WHERE id=?`,
|
||||
inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
||||
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
||||
inst.LastAliveAt, inst.UpdatedAt, inst.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update instance: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("instance %s: %w", inst.ID, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInstanceStatus sets only the status field on an instance.
|
||||
func (s *Store) UpdateInstanceStatus(id string, status string) error {
|
||||
ts := Now()
|
||||
result, err := s.db.Exec(
|
||||
`UPDATE instances SET status=?, updated_at=? WHERE id=?`,
|
||||
status, ts, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update instance status: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLastAliveAt sets the last_alive_at timestamp for an instance.
|
||||
// Called when an instance is seen running.
|
||||
func (s *Store) UpdateLastAliveAt(id string) error {
|
||||
ts := Now()
|
||||
result, err := s.db.Exec(
|
||||
`UPDATE instances SET last_alive_at=?, updated_at=? WHERE id=?`,
|
||||
ts, ts, id,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update last_alive_at: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteInstance removes an instance by ID.
|
||||
func (s *Store) DeleteInstance(id string) error {
|
||||
result, err := s.db.Exec(`DELETE FROM instances WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete instance: %w", err)
|
||||
}
|
||||
n, _ := result.RowsAffected()
|
||||
if n == 0 {
|
||||
return fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+19
-14
@@ -142,21 +142,26 @@ type DNSRecord struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Instance represents a running (or stopped) container for a project stage.
|
||||
type Instance struct {
|
||||
ID string `json:"id"`
|
||||
StageID string `json:"stage_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ContainerID string `json:"container_id"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
NpmProxyID int `json:"npm_proxy_id"`
|
||||
ProxyRouteID string `json:"proxy_route_id"`
|
||||
Status string `json:"status"` // running, stopped, failed, removing
|
||||
// ProxyRoute is a proxy-enabled container row joined with its project + stage
|
||||
// names, shaped for the Proxies page. Source is "instance" for project
|
||||
// containers and "static_site" for site rows — the names are historical
|
||||
// (the table itself was renamed to containers in the workload refactor).
|
||||
type ProxyRoute struct {
|
||||
Source string `json:"source"`
|
||||
InstanceID string `json:"instance_id"`
|
||||
ProjectID string `json:"project_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
StageID string `json:"stage_id"`
|
||||
StageName string `json:"stage_name"`
|
||||
ImageTag string `json:"image_tag"`
|
||||
Subdomain string `json:"subdomain"`
|
||||
Domain string `json:"domain"`
|
||||
ContainerID string `json:"container_id"`
|
||||
Port int `json:"port"`
|
||||
LastAliveAt string `json:"last_alive_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ProxyRouteID string `json:"proxy_route_id"`
|
||||
NpmProxyID int `json:"npm_proxy_id"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// Deploy represents a deployment attempt.
|
||||
|
||||
+16
-19
@@ -310,12 +310,15 @@ func (s *Store) runMigrations() error {
|
||||
for _, m := range migrations {
|
||||
if _, err := s.db.Exec(m); err != nil {
|
||||
// "duplicate column" / "already exists" are expected when a
|
||||
// migration has already been applied. Anything else (typo, FK
|
||||
// conflict, real schema bug) must surface, otherwise the store
|
||||
// silently runs against the wrong shape.
|
||||
// migration has already been applied. "no such table" is
|
||||
// expected for obsolete ALTER TABLE migrations targeting tables
|
||||
// the workload refactor dropped (e.g. instances). Anything
|
||||
// else must surface — silently running against the wrong shape
|
||||
// is worse than a startup failure.
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "duplicate column") &&
|
||||
!strings.Contains(msg, "already exists") {
|
||||
!strings.Contains(msg, "already exists") &&
|
||||
!strings.Contains(msg, "no such table") {
|
||||
return fmt.Errorf("apply migration %q: %w", m, err)
|
||||
}
|
||||
}
|
||||
@@ -323,8 +326,8 @@ func (s *Store) runMigrations() error {
|
||||
|
||||
// Create indexes on foreign key columns for query performance.
|
||||
indexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_instances_stage_id ON instances(stage_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_instances_project_id ON instances(project_id)`,
|
||||
// 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)`,
|
||||
@@ -344,6 +347,10 @@ func (s *Store) runMigrations() error {
|
||||
`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).
|
||||
`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 != ''`,
|
||||
@@ -449,19 +456,9 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS instances (
|
||||
id TEXT PRIMARY KEY,
|
||||
stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
container_id TEXT NOT NULL DEFAULT '',
|
||||
image_tag TEXT NOT NULL,
|
||||
subdomain TEXT NOT NULL DEFAULT '',
|
||||
npm_proxy_id INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'stopped',
|
||||
port INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user