From 99a25cb10e69de87c4a2afc53ea35ac856acb86c Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Fri, 9 Oct 2026 00:33:27 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(templates):=20catalog=20v2=20=E2=80=94?= =?UTF-8?q?=205=20new=20managed-DB=20stacks=20+=20seed=20validation=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ghost+MySQL, Gitea+PostgreSQL, Nextcloud+PG+Redis, Paperless+PG+Redis, Plausible+PG+ClickHouse. TestOfficialTemplatesAllParse parses every seed, topo-sorts graphs, and dry-resolves with no unresolved refs. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../internal/api/templategraph_test.go | 43 ++++++++++++ app/backend/internal/api/templates.go | 68 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/app/backend/internal/api/templategraph_test.go b/app/backend/internal/api/templategraph_test.go index d701dad..45ffdb5 100644 --- a/app/backend/internal/api/templategraph_test.go +++ b/app/backend/internal/api/templategraph_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "strings" "testing" ) @@ -157,3 +158,45 @@ func TestValidateTemplateGraph(t *testing.T) { t.Fatal("expected missing runtime error") } } + +// TestOfficialTemplatesAllParse walks every seeded template and asserts the +// config parses, graph templates topo-sort + validate, and a dry-run resolve +// produces no unresolved refs (defaults cover all expressions). +func TestOfficialTemplatesAllParse(t *testing.T) { + for _, tpl := range SeedTemplates() { + var cfg TemplateConfig + if err := json.Unmarshal([]byte(tpl.Config), &cfg); err != nil { + t.Errorf("%s: config parse: %v", tpl.ID, err) + continue + } + if len(cfg.Services) == 0 { + continue + } + sorted, err := topoSortServices(cfg.Services) + if err != nil { + t.Errorf("%s: topo sort: %v", tpl.ID, err) + continue + } + if err := validateTemplateGraph(sorted); err != nil { + t.Errorf("%s: validate: %v", tpl.ID, err) + continue + } + var vars []TemplateVariable + if tpl.Variables != "" { + if err := json.Unmarshal([]byte(tpl.Variables), &vars); err != nil { + t.Errorf("%s: variables parse: %v", tpl.ID, err) + continue + } + } + userVars := map[string]string{} + for _, v := range vars { + if v.Default != "" { + userVars[v.Key] = v.Default + } + } + graph := resolveTemplateGraph(sorted, userVars) + if len(graph.unresolved) > 0 { + t.Errorf("%s: unresolved refs: %v", tpl.ID, graph.unresolved) + } + } +} diff --git a/app/backend/internal/api/templates.go b/app/backend/internal/api/templates.go index 966cb9a..46d8fd1 100644 --- a/app/backend/internal/api/templates.go +++ b/app/backend/internal/api/templates.go @@ -1006,6 +1006,74 @@ func SeedTemplates() []ServiceTemplate { Variables: `[]`, IsOfficial: true, }, + { + ID: "tpl-ghost-stack", + Name: "Ghost + MySQL", + Description: "Ghost publishing platform backed by a managed MySQL database", + Category: "stack", + Logo: "https://cdn.simpleicons.org/ghost", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"mysql","environment":{"MYSQL_DATABASE":"ghost"}}, + {"key":"web","name":"ghost","type":"web","runtime":"ghost:5-alpine","port":2368,"depends_on":["db"], + "environment":{"url":"{{APP_URL:-http://localhost:2368}}","database__client":"mysql","database__connection__host":"{{service.db.host}}","database__connection__user":"{{service.db.user}}","database__connection__password":"{{service.db.password}}","database__connection__database":"{{service.db.database}}"}}]}`, + Variables: `[{"key":"APP_URL","label":"Public URL","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, + { + ID: "tpl-gitea-stack", + Name: "Gitea + PostgreSQL", + Description: "Self-hosted Git service backed by a managed PostgreSQL database", + Category: "stack", + Logo: "https://cdn.simpleicons.org/gitea", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"gitea"}}, + {"key":"web","name":"gitea","type":"web","runtime":"gitea/gitea:1","port":3000,"health_check":"/api/healthz","depends_on":["db"], + "environment":{"GITEA__database__DB_TYPE":"postgres","GITEA__database__HOST":"{{service.db.host}}:{{service.db.port}}","GITEA__database__NAME":"{{service.db.database}}","GITEA__database__USER":"{{service.db.user}}","GITEA__database__PASSWD":"{{service.db.password}}","GITEA__security__SECRET_KEY":"{{secret}}","GITEA__security__INTERNAL_TOKEN":"{{secret(64)}}"}}]}`, + Variables: `[]`, + IsOfficial: true, + }, + { + ID: "tpl-nextcloud-stack", + Name: "Nextcloud + PostgreSQL + Redis", + Description: "Nextcloud with a managed PostgreSQL database and Redis cache", + Category: "stack", + Logo: "https://cdn.simpleicons.org/nextcloud", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"nextcloud"}}, + {"key":"cache","type":"database","runtime":"redis"}, + {"key":"web","name":"nextcloud","type":"web","runtime":"nextcloud:30-apache","port":80,"health_check":"/status.php","depends_on":["db","cache"], + "environment":{"POSTGRES_HOST":"{{service.db.host}}","POSTGRES_DB":"{{service.db.database}}","POSTGRES_USER":"{{service.db.user}}","POSTGRES_PASSWORD":"{{service.db.password}}","REDIS_HOST":"{{service.cache.host}}","REDIS_HOST_PASSWORD":"{{service.cache.password}}","NEXTCLOUD_ADMIN_USER":"admin","NEXTCLOUD_ADMIN_PASSWORD":"{{secret}}","NEXTCLOUD_TRUSTED_DOMAINS":"{{APP_DOMAIN:-localhost}}"}}]}`, + Variables: `[{"key":"APP_DOMAIN","label":"Trusted domain","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, + { + ID: "tpl-paperless-stack", + Name: "Paperless-ngx + PostgreSQL + Redis", + Description: "Document management with managed PostgreSQL and Redis", + Category: "stack", + Logo: "https://cdn.simpleicons.org/paperlessngx", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"paperless"}}, + {"key":"cache","type":"database","runtime":"redis"}, + {"key":"web","name":"paperless","type":"web","runtime":"ghcr.io/paperless-ngx/paperless-ngx:latest","port":8000,"depends_on":["db","cache"], + "environment":{"PAPERLESS_DBHOST":"{{service.db.host}}","PAPERLESS_DBPORT":"{{service.db.port}}","PAPERLESS_DBNAME":"{{service.db.database}}","PAPERLESS_DBUSER":"{{service.db.user}}","PAPERLESS_DBPASS":"{{service.db.password}}","PAPERLESS_REDIS":"{{service.cache.url}}","PAPERLESS_ADMIN_USER":"admin","PAPERLESS_ADMIN_PASSWORD":"{{secret}}","PAPERLESS_SECRET_KEY":"{{secret(48)}}","PAPERLESS_URL":"{{APP_URL:-http://localhost:8000}}"}}]}`, + Variables: `[{"key":"APP_URL","label":"Public URL","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, + { + ID: "tpl-plausible-stack", + Name: "Plausible + PostgreSQL + ClickHouse", + Description: "Privacy-friendly analytics with managed PostgreSQL and ClickHouse", + Category: "stack", + Logo: "https://cdn.simpleicons.org/plausibleanalytics", + Config: `{"version":2,"services":[ + {"key":"db","type":"database","runtime":"postgresql","environment":{"POSTGRES_DB":"plausible"}}, + {"key":"events","type":"database","runtime":"clickhouse","environment":{"CLICKHOUSE_DB":"plausible"}}, + {"key":"web","name":"plausible","type":"web","runtime":"ghcr.io/plausible/community-edition:v2","port":8000,"health_check":"/api/health","depends_on":["db","events"], + "environment":{"BASE_URL":"{{APP_URL:-http://localhost:8000}}","SECRET_KEY_BASE":"{{secret(64)}}","DATABASE_URL":"{{service.db.url}}","CLICKHOUSE_DATABASE_URL":"http://{{service.events.host}}:{{service.events.port}}/{{service.events.database}}","TOTP_VAULT_KEY":"{{secret(32)}}"}}]}`, + Variables: `[{"key":"APP_URL","label":"Public URL","default":"","required":false,"secret":false}]`, + IsOfficial: true, + }, } return templates } From a50260e857429ba0c996430ef1c6830ab389149f Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Fri, 9 Oct 2026 05:21:09 +0200 Subject: [PATCH 2/2] fix: resolve ROADMAP2 merge markers --- ROADMAP2.md | 45 --------------------------------------------- 1 file changed, 45 deletions(-) diff --git a/ROADMAP2.md b/ROADMAP2.md index ec72ed1..5e2e9de 100644 --- a/ROADMAP2.md +++ b/ROADMAP2.md @@ -308,28 +308,6 @@ and wired refs, one click or one CLI call. audit logs, deployments, notifications tables. - [ ] **Notification producers round-out** — backups, agent offline, upgrade available, security findings. -<<<<<<< HEAD -- [ ] **Sleep-on-idle / wake-on-traffic** (serverless-lite) — - `services.sleep_after_idle` (duration, empty=off). Reaper loop stops - containers past idle threshold (last-seen from Traefik access logs or - request-tracking middleware); edge middleware returns a "waking" - page/503-with-retry, starts the container, then routes normally. - Per-service toggle + status badge (`sleeping`). Railway calls this - "app sleep"; it is the single biggest cost saver for hobby services. -- [ ] **Env doctor** — template/service required-var manifest: - `POST /templates/:id/plan` and service env UI surface *missing* and - *auto-generated* vars before deploy; never silently deploy with - empty required values. Secrets get `{{secret(len)}}` defaults. -- [ ] **One-click install** — `install.sh` (curl|sh): detects docker, - writes compose + .env with generated secrets, pulls images, runs - migrations, prints admin URL. Companion `docker-compose.prod.yml` - + `Dockerfile` images published to GHCR so installs never build - from source. -- [ ] **CI/CD v2** — required-checks workflow (backend test/vet, - frontend build, sqlc-diff check), path-filtered jobs for speed, - and a release pipeline: merge to main → tag → GoReleaser binaries - (CLI + MCP) → GHCR images (api, web, agent) → GitHub Release notes. -======= - [x] **Sleep-on-idle / wake-on-traffic** (serverless-lite) — `services.sleep_enabled` + `sleep_idle_minutes` (PR #29). Sweeper polls container NetworkIO deltas; idle past threshold → workload @@ -356,7 +334,6 @@ and wired refs, one click or one CLI call. conventional-commit version bump → tag → GHCR images (backend, frontend) → cross-platform CLI + MCP binaries → SHA-256 checksums → GitHub Release → Trivy scans (PR #33). ->>>>>>> origin/main - [ ] **Custom roles/RBAC** — defer until demanded; PAT scopes designed forward-compatible now. @@ -376,27 +353,6 @@ and wired refs, one click or one CLI call. ## 9b. Phase I — Companion surfaces -<<<<<<< HEAD -- [ ] **Phone app** — fully-featured companion, not a read-only viewer. - The target: a user on the go can see a problem, understand it, and - fix it without a laptop. PAT + profile auth (multi-instance like the - CLI). - - **Dashboard** — project/service status at a glance, deploy - activity, node health, resource usage (cpu/mem from existing - metrics endpoints). - - **Operate** — deploy, redeploy, restart/stop/start, rollback to a - prior deployment, cancel a running deploy, scale replicas. - - **Observe** — live log tail (SSE), deployment step logs, backup - status, cron execution history. - - **Fix** — env var edit + apply-redeploy, domain add/remove, - maintenance-mode toggle, database backup/restore trigger. - - **Notifications** — push via existing notification rows; zero-infra - path = self-hosted ntfy/Gotify relay (no Firebase dependency), - FCM/APNs optional. - - **Stack** — React Native (Expo) thin client over the agent-shaped - API; dark UI mirroring the web design system; deploy status as the - first-class view. Ships with `app/` workspace in this repo. -======= - [x] **Phone app (v1)** — `app/mobile`, Expo/React Native thin client over the agent-shaped API; dark UI mirroring the design system. PAT auth against any instance URL, stored in platform secure storage. @@ -412,7 +368,6 @@ and wired refs, one click or one CLI call. ntfy/Gotify relay (no Firebase dependency), FCM/APNs optional. - [ ] **Offline resilience** — cached last-known state + unreachable banner. ->>>>>>> origin/main - [ ] **Status/offline resilience** — app + CLI degrade gracefully when the instance is unreachable (cached last-known state, clear banner).