SysOps: deploy-all — 2026-06-09 10:59 UTC

This commit is contained in:
sysops
2026-06-09 10:59:45 +00:00
parent 21ea3a2c81
commit 7f973b7203
9 changed files with 669 additions and 94 deletions
+11
View File
@@ -68,6 +68,17 @@ async def herman_briefing_stats():
bookmarks = []
stats["rss_bookmarks"] = bookmarks
stats["rss_bookmark_ids"] = list(bookmark_map.keys())
if not stats.get("rss_live") and not stats.get("trending_food"):
try:
stats["rss_live"] = fetch_all(
"""SELECT i.id, i.title, i.link, i.summary, f.name AS feed_name,
f.category, i.published_at
FROM rss_items i
JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE
ORDER BY i.published_at DESC NULLS LAST, i.fetched_at DESC LIMIT 25"""
)
except Exception:
stats["rss_live"] = []
return {"ok": True, "stats": stats, "bookmarks": bookmarks, "at": datetime.utcnow().isoformat()}
+4
View File
@@ -52,6 +52,8 @@ class LinkNasFileBody(BaseModel):
class PreferencesBody(BaseModel):
dashboard_layout: Optional[list[str]] = None
dashboard_widgets: Optional[dict[str, bool]] = None
dashboard_kpis: Optional[list[str]] = None
global_viz_mode: Optional[str] = None
viz_modes: Optional[dict[str, str]] = None
locale: Optional[str] = None
@@ -190,6 +192,8 @@ def api_save_ui_preferences(body: PreferencesBody) -> dict[str, Any]:
prefs = ui_preferences.save_preferences(
"ceo",
dashboard_layout=body.dashboard_layout,
dashboard_widgets=body.dashboard_widgets,
dashboard_kpis=body.dashboard_kpis,
global_viz_mode=body.global_viz_mode,
viz_modes=body.viz_modes,
locale=body.locale,
+28
View File
@@ -285,6 +285,18 @@ def collect_briefing_data() -> dict[str, Any]:
except Exception:
data["trending_food"] = []
if not data.get("trending_food"):
try:
data["trending_food"] = fetch_all(
"""SELECT i.id, i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url,
f.category, i.published_at
FROM rss_items i
JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE
ORDER BY i.published_at DESC NULLS LAST, i.fetched_at DESC LIMIT 12"""
)
except Exception:
data["trending_food"] = []
try:
data["food_market_highlights"] = fetch_all(
"""SELECT i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url, f.category
@@ -296,6 +308,22 @@ def collect_briefing_data() -> dict[str, Any]:
except Exception:
data["food_market_highlights"] = []
if not data.get("food_market_highlights"):
data["food_market_highlights"] = list(data.get("trending_food") or [])[:10]
try:
data["rss_live"] = fetch_all(
"""SELECT i.id, i.title, i.link, i.summary, f.name AS feed_name, f.url AS feed_url,
f.category, i.published_at
FROM rss_items i
JOIN rss_feeds f ON f.id = i.feed_id AND f.is_active = TRUE
ORDER BY i.published_at DESC NULLS LAST, i.fetched_at DESC LIMIT 25"""
)
except Exception:
data["rss_live"] = list(data.get("trending_food") or [])
data["rss_items"] = _safe_count("rss_items")
data["activity_log"] = _build_activity_log(data)
return data
+141 -21
View File
@@ -1,14 +1,62 @@
"""User UI preferences — dashboard layout and visualization modes."""
"""User UI preferences — dashboard layout, widgets, KPIs, visualization modes."""
from __future__ import annotations
import json
from typing import Any
from app.db import execute, fetch_one
from app.db import fetch_one
DEFAULT_LAYOUT = ["kpis", "executive", "briefing", "retail", "analytics", "approvals", "feed"]
DEFAULT_LAYOUT = [
"kpis",
"executive",
"briefing",
"rss",
"retail",
"analytics",
"approvals",
"feed",
]
DEFAULT_VIZ = "neo-bars"
DEFAULT_KPI_LAYOUT = [
"pipeline",
"clients_active",
"crm_partnerships",
"supermarkets",
"trending_food",
"pending_approvals",
]
WIDGET_CATALOG = [
{"id": "kpis", "label": "CEO KPI's", "icon": "📊"},
{"id": "executive", "label": "Alles op een rij", "icon": "📋"},
{"id": "briefing", "label": "Herman briefing", "icon": "📝"},
{"id": "rss", "label": "RSS live feed", "icon": "📰"},
{"id": "retail", "label": "Retail operatie", "icon": "🏪"},
{"id": "analytics", "label": "Data & analytics", "icon": "📈"},
{"id": "approvals", "label": "Agent goedkeuringen", "icon": ""},
{"id": "feed", "label": "Agent feed & projecten", "icon": ""},
]
KPI_CATALOG = [
{"id": "pipeline", "label": "Pipeline", "sub": "actieve deals", "icon": "💰"},
{"id": "clients_active", "label": "Actieve klanten", "sub": "CRM · zaken mee", "icon": "🤝"},
{"id": "clients_total", "label": "Totaal klanten", "sub": "CRM database", "icon": "👥"},
{"id": "crm_partnerships", "label": "CRM partnerships", "sub": "actieve filialen", "icon": "🏪"},
{"id": "supermarkets", "label": "Supermarkten", "sub": "Retail 360 DB", "icon": "🛒"},
{"id": "wholesalers", "label": "Groothandels", "sub": "Retail 360 DB", "icon": "📦"},
{"id": "trending_food", "label": "Food trends", "sub": "RSS live", "icon": "📰"},
{"id": "rss_items", "label": "RSS items", "sub": "totaal in DB", "icon": "📡"},
{"id": "rss_bookmarks", "label": "RSS bookmarks", "sub": "opgeslagen", "icon": ""},
{"id": "pending_approvals", "label": "Goedkeuringen", "sub": "wacht op OK", "icon": ""},
{"id": "deals", "label": "Deals", "sub": "totaal CRM", "icon": "💼"},
{"id": "products", "label": "Producten", "sub": "catalogus", "icon": "🥫"},
{"id": "suppliers", "label": "Leveranciers", "sub": "sourcing", "icon": "🚚"},
{"id": "nas_docs", "label": "NAS documenten", "sub": "geanalyseerd", "icon": "📊"},
{"id": "promo_campaigns", "label": "Promo campagnes", "sub": "actief", "icon": "📁"},
{"id": "market_best", "label": "Aandeel top", "sub": "retail markt", "icon": "📈"},
]
VIZ_MODES = [
{"id": "neo-bars", "label": "Neo staafdiagram", "icon": "📊"},
{"id": "neo-rings", "label": "Neo ringen", "icon": "🍩"},
@@ -17,49 +65,108 @@ VIZ_MODES = [
{"id": "neo-table", "label": "Neo tabel", "icon": "📋"},
]
_ALL_WIDGET_IDS = {w["id"] for w in WIDGET_CATALOG}
_ALL_KPI_IDS = {k["id"] for k in KPI_CATALOG}
def _default_widgets() -> dict[str, bool]:
return {w["id"]: True for w in WIDGET_CATALOG}
def _parse_json(val: Any, fallback: Any) -> Any:
if val is None:
return fallback
if isinstance(val, str):
try:
return json.loads(val)
except Exception:
return fallback
return val
def _normalize_layout(layout: list[str]) -> list[str]:
seen: set[str] = set()
out: list[str] = []
for wid in layout:
if wid in _ALL_WIDGET_IDS and wid not in seen:
out.append(wid)
seen.add(wid)
for w in WIDGET_CATALOG:
if w["id"] not in seen:
out.append(w["id"])
return out
def _normalize_widgets(widgets: dict[str, Any] | None) -> dict[str, bool]:
base = _default_widgets()
if not widgets:
return base
for wid in _ALL_WIDGET_IDS:
if wid in widgets:
base[wid] = bool(widgets[wid])
return base
def _normalize_kpis(kpis: list[str] | None) -> list[str]:
if not kpis:
return list(DEFAULT_KPI_LAYOUT)
seen: set[str] = set()
out: list[str] = []
for kid in kpis:
if kid in _ALL_KPI_IDS and kid not in seen:
out.append(kid)
seen.add(kid)
return out or list(DEFAULT_KPI_LAYOUT)
def get_preferences(user_key: str = "ceo") -> dict[str, Any]:
row = fetch_one("SELECT * FROM user_ui_preferences WHERE user_key = %s", (user_key,))
if not row:
return {
"user_key": user_key,
"dashboard_layout": DEFAULT_LAYOUT,
"dashboard_layout": list(DEFAULT_LAYOUT),
"dashboard_widgets": _default_widgets(),
"dashboard_kpis": list(DEFAULT_KPI_LAYOUT),
"viz_modes": {},
"global_viz_mode": DEFAULT_VIZ,
"locale": "nl",
"viz_options": VIZ_MODES,
"widget_catalog": WIDGET_CATALOG,
"kpi_catalog": KPI_CATALOG,
}
layout = row.get("dashboard_layout") or DEFAULT_LAYOUT
if isinstance(layout, str):
try:
layout = json.loads(layout)
except Exception:
layout = DEFAULT_LAYOUT
viz_modes = row.get("viz_modes") or {}
if isinstance(viz_modes, str):
try:
viz_modes = json.loads(viz_modes)
except Exception:
viz_modes = {}
layout = _normalize_layout(_parse_json(row.get("dashboard_layout"), DEFAULT_LAYOUT))
widgets = _normalize_widgets(_parse_json(row.get("dashboard_widgets"), None))
kpis = _normalize_kpis(_parse_json(row.get("dashboard_kpis"), None))
viz_modes = _parse_json(row.get("viz_modes"), {})
return {
"user_key": user_key,
"dashboard_layout": layout,
"viz_modes": viz_modes,
"dashboard_widgets": widgets,
"dashboard_kpis": kpis,
"viz_modes": viz_modes if isinstance(viz_modes, dict) else {},
"global_viz_mode": row.get("global_viz_mode") or DEFAULT_VIZ,
"locale": row.get("locale") or "nl",
"viz_options": VIZ_MODES,
"widget_catalog": WIDGET_CATALOG,
"kpi_catalog": KPI_CATALOG,
}
def save_preferences(
user_key: str,
dashboard_layout: list[str] | None = None,
dashboard_widgets: dict[str, bool] | None = None,
dashboard_kpis: list[str] | None = None,
global_viz_mode: str | None = None,
viz_modes: dict[str, str] | None = None,
locale: str | None = None,
) -> dict[str, Any]:
current = get_preferences(user_key)
layout = dashboard_layout if dashboard_layout is not None else current["dashboard_layout"]
layout = _normalize_layout(dashboard_layout if dashboard_layout is not None else current["dashboard_layout"])
widgets = _normalize_widgets(
dashboard_widgets if dashboard_widgets is not None else current["dashboard_widgets"]
)
kpis = _normalize_kpis(dashboard_kpis if dashboard_kpis is not None else current["dashboard_kpis"])
gviz = global_viz_mode if global_viz_mode is not None else current["global_viz_mode"]
vmodes = viz_modes if viz_modes is not None else current["viz_modes"]
loc = locale if locale is not None else current.get("locale", "nl")
@@ -67,16 +174,29 @@ def save_preferences(
loc = "nl"
fetch_one(
"""
INSERT INTO user_ui_preferences (user_key, dashboard_layout, global_viz_mode, viz_modes, locale, updated_at)
VALUES (%s, %s::jsonb, %s, %s::jsonb, %s, NOW())
INSERT INTO user_ui_preferences (
user_key, dashboard_layout, dashboard_widgets, dashboard_kpis,
global_viz_mode, viz_modes, locale, updated_at
)
VALUES (%s, %s::jsonb, %s::jsonb, %s::jsonb, %s, %s::jsonb, %s, NOW())
ON CONFLICT (user_key) DO UPDATE SET
dashboard_layout = EXCLUDED.dashboard_layout,
dashboard_widgets = EXCLUDED.dashboard_widgets,
dashboard_kpis = EXCLUDED.dashboard_kpis,
global_viz_mode = EXCLUDED.global_viz_mode,
viz_modes = EXCLUDED.viz_modes,
locale = EXCLUDED.locale,
updated_at = NOW()
RETURNING user_key
""",
(user_key, json.dumps(layout), gviz, json.dumps(vmodes), loc),
(
user_key,
json.dumps(layout),
json.dumps(widgets),
json.dumps(kpis),
gviz,
json.dumps(vmodes),
loc,
),
)
return get_preferences(user_key)