Phase 25: templates as stack folders (0.31.0)

Templates are now stack-shaped folders (compose.yaml + .env.example +
template.json) instead of DB rows + manifest.json + {{VAR}} rendering.
Pull copies the folder into a new stack; custom templates persist under
DATA_DIR/templates. Adds POST /api/templates/from-stack and a one-time
startup migration for pre-0.31 DB templates (drops the template table).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-12 07:29:54 +00:00
co-authored by Claude Fable 5
parent 34cb215266
commit 1609b8bcc3
29 changed files with 564 additions and 329 deletions
+1 -2
View File
@@ -6,10 +6,9 @@ from models.backup_destination import BackupDestination
from models.backup_schedule import BackupSchedule
from models.setting import Setting, Webhook
from models.stack import Stack
from models.template import Template
from models.user import User
__all__ = [
"User", "Stack", "AuditLog", "Template", "Setting", "Webhook", "Agent",
"User", "Stack", "AuditLog", "Setting", "Webhook", "Agent",
"BackupDestination", "BackupSchedule", "AutoUpdate",
]
+15 -30
View File
@@ -1,34 +1,11 @@
from __future__ import annotations
from datetime import datetime, timezone
from typing import Optional
from sqlmodel import Field, SQLModel
from sqlmodel import SQLModel
def _now() -> datetime:
return datetime.now(timezone.utc)
class Template(SQLModel, table=True):
"""User-saved custom template (bundled ones live on disk)."""
id: Optional[int] = Field(default=None, primary_key=True)
slug: str = Field(index=True, unique=True)
name: str
description: Optional[str] = None
tags: str = "" # comma separated
yaml: str = ""
created_at: datetime = Field(default_factory=_now)
# --- API schemas ---
class TemplateVariable(SQLModel):
name: str
description: str = ""
default: str = ""
# Templates are stored as stack-shaped folders on disk (see
# services/template_service.py), not in the database. These are API schemas only.
class TemplateSummary(SQLModel):
@@ -41,18 +18,26 @@ class TemplateSummary(SQLModel):
class TemplateDetail(TemplateSummary):
yaml: str
variables: list[TemplateVariable] = []
compose: str = ""
env: str = ""
files: list[str] = [] # relative paths the template ships
class TemplateSaveRequest(SQLModel):
name: str
description: Optional[str] = None
tags: list[str] = []
yaml: str
gpu: Optional[str] = None
compose: str
env: str = ""
class TemplateFromStackRequest(SQLModel):
stack_id: str
name: str
description: Optional[str] = None
class TemplateInstantiateRequest(SQLModel):
name: str # new stack name
values: dict[str, str] = {}
agent_id: int | None = None # None = local host; otherwise deploy to a remote agent