SysOps: deploy-all — 2026-06-09 10:41 UTC
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"""Unified cockpit projects and cross-module assets."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.db import execute, fetch_all, fetch_one
|
||||
from app.services import nas_folders
|
||||
|
||||
|
||||
def _row(row: dict | None) -> dict | None:
|
||||
if not row:
|
||||
return None
|
||||
out = dict(row)
|
||||
for k, v in list(out.items()):
|
||||
if hasattr(v, "isoformat"):
|
||||
out[k] = v.isoformat()
|
||||
return out
|
||||
|
||||
|
||||
def list_projects(client_id: Optional[int] = None, limit: int = 50) -> list[dict[str, Any]]:
|
||||
clauses, params = [], []
|
||||
if client_id:
|
||||
clauses.append("p.client_id = %s")
|
||||
params.append(client_id)
|
||||
where = (" WHERE " + " AND ".join(clauses)) if clauses else ""
|
||||
safe = max(1, min(limit, 200))
|
||||
rows = fetch_all(
|
||||
f"""
|
||||
SELECT p.*, c.name AS client_name,
|
||||
(SELECT COUNT(*) FROM project_assets a WHERE a.project_id = p.id) AS asset_count
|
||||
FROM cockpit_projects p
|
||||
LEFT JOIN clients c ON c.id = p.client_id
|
||||
{where}
|
||||
ORDER BY p.updated_at DESC, p.id DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
tuple(params + [safe]),
|
||||
)
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
def get_project(project_id: int) -> dict[str, Any] | None:
|
||||
row = fetch_one(
|
||||
"""
|
||||
SELECT p.*, c.name AS client_name
|
||||
FROM cockpit_projects p
|
||||
LEFT JOIN clients c ON c.id = p.client_id
|
||||
WHERE p.id = %s
|
||||
""",
|
||||
(project_id,),
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
out = _row(row) or {}
|
||||
assets = fetch_all(
|
||||
"SELECT * FROM project_assets WHERE project_id = %s ORDER BY created_at DESC",
|
||||
(project_id,),
|
||||
)
|
||||
out["assets"] = [_row(a) for a in assets]
|
||||
return out
|
||||
|
||||
|
||||
def create_project(
|
||||
name: str,
|
||||
client_id: Optional[int] = None,
|
||||
description: str = "",
|
||||
created_by: str = "ceo",
|
||||
metadata: dict | None = None,
|
||||
project_type: str = "general",
|
||||
priority: str = "normal",
|
||||
ensure_nas: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
row = fetch_one(
|
||||
"""
|
||||
INSERT INTO cockpit_projects (name, client_id, description, created_by, metadata, project_type, priority, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s::jsonb, %s, %s, NOW())
|
||||
RETURNING *
|
||||
""",
|
||||
(name.strip(), client_id, description or None, created_by, json.dumps(metadata or {}), project_type, priority),
|
||||
)
|
||||
out = _row(row) or {}
|
||||
if ensure_nas and out.get("id"):
|
||||
client_name = None
|
||||
if client_id:
|
||||
c = fetch_one("SELECT name FROM clients WHERE id = %s", (client_id,))
|
||||
client_name = c["name"] if c else None
|
||||
try:
|
||||
paths = nas_folders.ensure_project_folder(
|
||||
int(out["id"]), name, client_id, client_name, project_type
|
||||
)
|
||||
out.update(paths)
|
||||
except Exception as exc:
|
||||
out["nas_error"] = str(exc)
|
||||
return out
|
||||
|
||||
|
||||
def update_project(project_id: int, **fields: Any) -> dict[str, Any] | None:
|
||||
allowed = ("name", "description", "status", "client_id", "project_type", "priority")
|
||||
sets, params = [], []
|
||||
for k, v in fields.items():
|
||||
if k in allowed and v is not None:
|
||||
sets.append(f"{k} = %s")
|
||||
params.append(v)
|
||||
if not sets:
|
||||
return get_project(project_id)
|
||||
params.append(project_id)
|
||||
execute(f"UPDATE cockpit_projects SET {', '.join(sets)}, updated_at = NOW() WHERE id = %s", tuple(params))
|
||||
return get_project(project_id)
|
||||
|
||||
|
||||
def project_stats() -> dict[str, Any]:
|
||||
total = fetch_one("SELECT COUNT(*) AS n FROM cockpit_projects") or {"n": 0}
|
||||
by_type = fetch_all(
|
||||
"SELECT COALESCE(project_type, 'general') AS t, COUNT(*) AS n FROM cockpit_projects GROUP BY t"
|
||||
)
|
||||
with_nas = fetch_one("SELECT COUNT(*) AS n FROM cockpit_projects WHERE nas_path IS NOT NULL")
|
||||
with_client = fetch_one("SELECT COUNT(*) AS n FROM cockpit_projects WHERE client_id IS NOT NULL")
|
||||
assets = fetch_one("SELECT COUNT(*) AS n FROM project_assets")
|
||||
return {
|
||||
"total": int(total.get("n") or 0),
|
||||
"with_nas": int((with_nas or {}).get("n") or 0),
|
||||
"with_client": int((with_client or {}).get("n") or 0),
|
||||
"assets": int((assets or {}).get("n") or 0),
|
||||
"by_type": {r["t"]: int(r["n"]) for r in by_type},
|
||||
}
|
||||
|
||||
|
||||
def add_asset(
|
||||
project_id: int,
|
||||
asset_type: str,
|
||||
title: str,
|
||||
ref_id: str | None = None,
|
||||
file_path: str | None = None,
|
||||
payload: dict | None = None,
|
||||
created_by: str = "ceo",
|
||||
source_agent: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
row = fetch_one(
|
||||
"""
|
||||
INSERT INTO project_assets (project_id, asset_type, ref_id, title, file_path, payload, created_by, source_agent)
|
||||
VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(
|
||||
project_id,
|
||||
asset_type,
|
||||
ref_id,
|
||||
title,
|
||||
file_path,
|
||||
json.dumps(payload or {}),
|
||||
created_by,
|
||||
source_agent,
|
||||
),
|
||||
)
|
||||
execute("UPDATE cockpit_projects SET updated_at = NOW() WHERE id = %s", (project_id,))
|
||||
return _row(row) or {}
|
||||
|
||||
|
||||
def get_or_create_agent_project(agent_key: str, client_id: Optional[int] = None) -> int:
|
||||
key = (agent_key or "agent").strip().lower()
|
||||
row = fetch_one(
|
||||
"""
|
||||
SELECT id FROM cockpit_projects
|
||||
WHERE metadata->>'auto_agent' = %s AND status = 'active'
|
||||
ORDER BY updated_at DESC LIMIT 1
|
||||
""",
|
||||
(key,),
|
||||
)
|
||||
if row:
|
||||
return int(row["id"])
|
||||
created = create_project(
|
||||
f"Agent · {key}",
|
||||
client_id=client_id,
|
||||
description=f"Automatisch project voor {key} output",
|
||||
created_by=key,
|
||||
metadata={"auto_agent": key},
|
||||
)
|
||||
return int(created["id"])
|
||||
|
||||
|
||||
def register_agent_output(
|
||||
asset_type: str,
|
||||
title: str,
|
||||
ref_id: str | None = None,
|
||||
payload: dict | None = None,
|
||||
project_id: Optional[int] = None,
|
||||
source_agent: str | None = None,
|
||||
created_by: str = "ceo",
|
||||
file_path: str | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
agent = (source_agent or created_by or "agent").strip().lower()
|
||||
pid = project_id or get_or_create_agent_project(agent)
|
||||
|
||||
if ref_id:
|
||||
existing = fetch_one(
|
||||
"""
|
||||
SELECT id FROM project_assets
|
||||
WHERE project_id = %s AND asset_type = %s AND ref_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(pid, asset_type, ref_id),
|
||||
)
|
||||
if existing:
|
||||
return {"id": existing["id"], "project_id": pid, "dedup": True}
|
||||
|
||||
return add_asset(
|
||||
pid,
|
||||
asset_type,
|
||||
title,
|
||||
ref_id=ref_id,
|
||||
file_path=file_path,
|
||||
payload=payload,
|
||||
created_by=created_by or agent,
|
||||
source_agent=agent,
|
||||
)
|
||||
|
||||
|
||||
def link_photo_to_project(photo_id: int, project_id: int, created_by: str = "ceo") -> None:
|
||||
photo = fetch_one("SELECT id, filename, source FROM photo_imports WHERE id = %s", (photo_id,))
|
||||
if not photo:
|
||||
raise ValueError("Photo not found")
|
||||
execute(
|
||||
"UPDATE photo_imports SET project_id = %s, created_by = COALESCE(created_by, %s) WHERE id = %s",
|
||||
(project_id, created_by, photo_id),
|
||||
)
|
||||
add_asset(
|
||||
project_id,
|
||||
"photo",
|
||||
photo.get("filename") or f"Foto #{photo_id}",
|
||||
ref_id=str(photo_id),
|
||||
created_by=created_by,
|
||||
source_agent=photo.get("source"),
|
||||
)
|
||||
Reference in New Issue
Block a user