From 69fe67cc0e529b62192b13a497c4b425f00a621b Mon Sep 17 00:00:00 2001 From: SysOps Agent Date: Tue, 9 Jun 2026 01:38:01 +0000 Subject: [PATCH] SysOps config backup 2026-06-09 01:38 UTC --- snapshots/2026-06-09/.env.example | 23 ++ snapshots/2026-06-09/README.md | 42 +++ snapshots/2026-06-09/backup-meta.json | 6 + snapshots/2026-06-09/cockpit/requirements.txt | 10 + snapshots/2026-06-09/deploy-all.sh | 63 ++++ snapshots/2026-06-09/docker-compose.yml | 144 +++++++++ snapshots/2026-06-09/migrations/001_fase1.sql | 112 +++++++ .../2026-06-09/migrations/002_fase234.sql | 61 ++++ .../migrations/003_word_analytics.sql | 50 +++ .../migrations/004_emails_calendar_brain.sql | 74 +++++ .../migrations/005_email_accounts.sql | 27 ++ .../migrations/006_browser_sessions.sql | 30 ++ .../migrations/007_photo_imports.sql | 17 + .../008_pgvector_telegram_brain.sql | 84 +++++ .../migrations/009_full_upgrade.sql | 303 ++++++++++++++++++ .../migrations/010_retail_crm_intel.sql | 83 +++++ .../2026-06-09/migrations/011_retail_360.sql | 113 +++++++ .../2026-06-09/migrations/012_rss_focus.sql | 34 ++ .../013_agent_souls_permissions.sql | 206 ++++++++++++ .../014_market_regulation_avatars.sql | 29 ++ .../migrations/015_platform_upgrade.sql | 66 ++++ .../migrations/016_platform_ops_packaging.sql | 223 +++++++++++++ .../017_retaildetail_food_trending.sql | 15 + .../migrations/018_agent_approvals.sql | 45 +++ .../019_projects_ui_preferences.sql | 56 ++++ .../migrations/020_sysops_backup_ops.sql | 58 ++++ .../2026-06-09/monitoring/prometheus.yml | 15 + .../2026-06-09/tools-api/requirements.txt | 10 + 28 files changed, 1999 insertions(+) create mode 100644 snapshots/2026-06-09/.env.example create mode 100644 snapshots/2026-06-09/README.md create mode 100644 snapshots/2026-06-09/backup-meta.json create mode 100644 snapshots/2026-06-09/cockpit/requirements.txt create mode 100644 snapshots/2026-06-09/deploy-all.sh create mode 100644 snapshots/2026-06-09/docker-compose.yml create mode 100644 snapshots/2026-06-09/migrations/001_fase1.sql create mode 100644 snapshots/2026-06-09/migrations/002_fase234.sql create mode 100644 snapshots/2026-06-09/migrations/003_word_analytics.sql create mode 100644 snapshots/2026-06-09/migrations/004_emails_calendar_brain.sql create mode 100644 snapshots/2026-06-09/migrations/005_email_accounts.sql create mode 100644 snapshots/2026-06-09/migrations/006_browser_sessions.sql create mode 100644 snapshots/2026-06-09/migrations/007_photo_imports.sql create mode 100644 snapshots/2026-06-09/migrations/008_pgvector_telegram_brain.sql create mode 100644 snapshots/2026-06-09/migrations/009_full_upgrade.sql create mode 100644 snapshots/2026-06-09/migrations/010_retail_crm_intel.sql create mode 100644 snapshots/2026-06-09/migrations/011_retail_360.sql create mode 100644 snapshots/2026-06-09/migrations/012_rss_focus.sql create mode 100644 snapshots/2026-06-09/migrations/013_agent_souls_permissions.sql create mode 100644 snapshots/2026-06-09/migrations/014_market_regulation_avatars.sql create mode 100644 snapshots/2026-06-09/migrations/015_platform_upgrade.sql create mode 100644 snapshots/2026-06-09/migrations/016_platform_ops_packaging.sql create mode 100644 snapshots/2026-06-09/migrations/017_retaildetail_food_trending.sql create mode 100644 snapshots/2026-06-09/migrations/018_agent_approvals.sql create mode 100644 snapshots/2026-06-09/migrations/019_projects_ui_preferences.sql create mode 100644 snapshots/2026-06-09/migrations/020_sysops_backup_ops.sql create mode 100644 snapshots/2026-06-09/monitoring/prometheus.yml create mode 100644 snapshots/2026-06-09/tools-api/requirements.txt diff --git a/snapshots/2026-06-09/.env.example b/snapshots/2026-06-09/.env.example new file mode 100644 index 0000000..dee0245 --- /dev/null +++ b/snapshots/2026-06-09/.env.example @@ -0,0 +1,23 @@ +# PostgreSQL (extern op VM106 host) +DB_HOST=10.4.7.18 +DB_USER=aissa +DB_PASSWORD=changeme +DB_NAME=foodlinkk + +# Cockpit +TOOLS_API_URL=http://tools-api:8700 +OLLAMA_URL=http://10.4.7.19:11434 +OLLAMA_MODEL=qwen3:8b + +# SMTP (optioneel — ook via Settings UI) +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +SMTP_FROM= + +# Proxmox (IT Ops — optioneel) +PROXMOX_HOST=10.4.7.14 +PROXMOX_USER=root@pam +PROXMOX_TOKEN_NAME= +PROXMOX_TOKEN_VALUE= diff --git a/snapshots/2026-06-09/README.md b/snapshots/2026-06-09/README.md new file mode 100644 index 0000000..43ec7e7 --- /dev/null +++ b/snapshots/2026-06-09/README.md @@ -0,0 +1,42 @@ +# Foodlinkk Command Center + +AI-gedreven platform voor retail intelligence, marketing automatisering, agent orchestratie en IT operations. + +## Stack + +| Service | Poort | Beschrijving | +|---------|-------|--------------| +| Cockpit | 8600 | Web UI (FastAPI + Jinja) | +| Tools API | 8700 | Connectors, retail 360, packaging, ops | +| Gitea | 3001 | Git repository | +| Email agent | 8801 | IMAP/SMTP sync | +| Hermes/Ollama | 10.4.7.19 | LLM + vector store | + +## Snel starten + +```bash +cp .env.example .env +# Pas wachtwoorden aan + +docker compose up -d +./deploy-all.sh # vanaf dev machine naar VM106 +``` + +## Documentatie + +- [Architectuur](docs/ARCHITECTURE.md) +- [Deploy](docs/DEPLOY.md) +- [Apps & pagina's](docs/APPS.md) +- [API](docs/API.md) +- [Agents](docs/AGENTS.md) +- [CSS thema](docs/CSS-THEME.md) + +## Belangrijkste routes + +- `/` — Herman dashboard +- `/marketing?tab=reclame` — Reclame folders (alle ketens) +- `/marketing?tab=publish` — Social automatisering +- `/settings?tab=social` — Social API keys +- `/ops` — IT infrastructure topology +- `/packaging` — Verpakkingsontwerp generator +- `/agents?tab=mesh` — Agent netwerk diff --git a/snapshots/2026-06-09/backup-meta.json b/snapshots/2026-06-09/backup-meta.json new file mode 100644 index 0000000..e179111 --- /dev/null +++ b/snapshots/2026-06-09/backup-meta.json @@ -0,0 +1,6 @@ +{ + "generated_at": "2026-06-09T01:38:01.236725+00:00", + "source_root": "/data/backup-root", + "files_in_manifest": 27, + "approval_request_id": 1 +} \ No newline at end of file diff --git a/snapshots/2026-06-09/cockpit/requirements.txt b/snapshots/2026-06-09/cockpit/requirements.txt new file mode 100644 index 0000000..83fc029 --- /dev/null +++ b/snapshots/2026-06-09/cockpit/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.6 +uvicorn[standard]==0.32.1 +jinja2==3.1.4 +python-multipart==0.0.12 +psycopg2-binary==2.9.9 +httpx==0.27.2 +websockets==13.1 +beautifulsoup4==4.12.3 +textblob==0.18.0.post0 +tweepy==4.15.0 diff --git a/snapshots/2026-06-09/deploy-all.sh b/snapshots/2026-06-09/deploy-all.sh new file mode 100644 index 0000000..d205696 --- /dev/null +++ b/snapshots/2026-06-09/deploy-all.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Foodlinkk Command Center — volledige deploy (migraties + build + health) +set -euo pipefail + +PASS="${FOODLINKK_SSH_PASS:-Foodlinkk#2026}" +VM="${FOODLINKK_VM:-aissa@10.4.7.18}" +REMOTE_DIR="${FOODLINKK_REMOTE_DIR:-/home/aissa/foodlinkk-command-center}" +LOCAL_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "=== Foodlinkk deploy-all ===" +echo "Local: $LOCAL_DIR" +echo "Remote: $VM:$REMOTE_DIR" + +if ! command -v sshpass >/dev/null 2>&1; then + echo "sshpass is vereist (apt install sshpass)" + exit 1 +fi + +echo "=== Rsync naar VM106 ===" +sshpass -p "$PASS" rsync -az --delete \ + --exclude '.git' \ + --exclude '__pycache__' \ + --exclude '*.pyc' \ + --exclude '.env' \ + --exclude 'node_modules' \ + -e "ssh -o StrictHostKeyChecking=no" \ + "$LOCAL_DIR/" "$VM:$REMOTE_DIR/" + +echo "=== Migraties + Docker rebuild op VM106 ===" +sshpass -p "$PASS" ssh -o StrictHostKeyChecking=no "$VM" bash -s </dev/null | grep -v sysops_daily.py; echo "0 7 * * * cd $REMOTE_DIR && /usr/bin/python3 scripts/sysops_daily.py >> /tmp/sysops-daily.log 2>&1") | crontab - +echo "Cron installed" + +echo "=== Run SysOps daily now (test) ===" +cd "$REMOTE_DIR" && python3 scripts/sysops_daily.py || true +REMOTE + +echo "=== Deploy voltooid ===" +echo "Cockpit: http://10.4.7.18:8600" +echo "Tools API: http://10.4.7.18:8700" +echo "Gitea: http://10.4.7.18:3001" diff --git a/snapshots/2026-06-09/docker-compose.yml b/snapshots/2026-06-09/docker-compose.yml new file mode 100644 index 0000000..3ca61cf --- /dev/null +++ b/snapshots/2026-06-09/docker-compose.yml @@ -0,0 +1,144 @@ +version: '3.8' +services: + redis: + image: redis:7-alpine + container_name: foodlinkk_redis + ports: + - "6379:6379" + restart: unless-stopped + + + tools-api: + build: ./tools-api + container_name: foodlinkk_tools_api + environment: + DB_HOST: foodlinkk_db + DB_USER: aissa + DB_PASSWORD: Foodlinkk#2026 + DB_NAME: foodlinkk + BACKUP_ROOT: /data/backup-root + GITEA_BACKUP_REMOTE: http://aissa:Foodlinkk%232026@gitea:3001/aissa/foodlinkk-command-center.git + GITEA_BACKUP_BRANCH: main + PROXMOX_HOST: 10.4.7.14 + VM106_IP: 10.4.7.18 + OLLAMA_URL: http://10.4.7.19:11434 + OLLAMA_MODEL: qwen3:8b + CHROMA_HOST: 10.4.7.19 + CHROMA_PORT: "8000" + DOC_INGEST_URL: http://10.4.7.19:8750 + BROWSER_USE_URL: http://browser-agent:7790 + BROWSER_AGENT_URL: http://browser-agent:7790 + COMFYUI_URL: http://10.4.7.18:8188 + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASS: ${SMTP_PASS:-} + SMTP_FROM: ${SMTP_FROM:-} + ports: + - "8700:8700" + volumes: + - /home/aissa/foodlinkk-command-center:/data/backup-root:ro + extra_hosts: + - "foodlinkk_db:10.4.7.18" + restart: unless-stopped + + cockpit: + build: ./cockpit + container_name: foodlinkk_cockpit + environment: + DB_HOST: foodlinkk_db + DB_USER: aissa + DB_PASSWORD: Foodlinkk#2026 + DB_NAME: foodlinkk + VNC_CDP_URL: http://10.4.7.18:9223 + OLLAMA_URL: http://10.4.7.19:11434 + OLLAMA_MODEL: qwen3:8b + TOOLS_API_URL: http://tools-api:8700 + CHROMA_HOST: 10.4.7.19 + CHROMA_PORT: "8000" + DOC_INGEST_URL: http://10.4.7.19:8750 + BROWSER_AGENT_URL: http://browser-agent:7790 + COMFYUI_URL: http://10.4.7.18:8188 + HERMAN_ORCHESTRATOR_URL: http://10.4.7.19:8090 + ports: + - "8600:8600" + depends_on: + - tools-api + extra_hosts: + - "foodlinkk_db:10.4.7.18" + restart: unless-stopped + + browser-agent: + build: ./browser-agent + container_name: foodlinkk_browser_agent + environment: + DB_HOST: foodlinkk_db + DB_USER: aissa + DB_PASSWORD: Foodlinkk#2026 + DB_NAME: foodlinkk + VNC_CDP_URL: http://10.4.7.18:9223 + ports: + - "7790:7790" + extra_hosts: + - "foodlinkk_db:10.4.7.18" + restart: unless-stopped + shm_size: "1gb" + + gitea: + image: gitea/gitea:1.21-rootless + container_name: foodlinkk_gitea + environment: + GITEA__database__DB_TYPE: sqlite3 + GITEA__server__ROOT_URL: http://10.4.7.18:3001/ + GITEA__server__HTTP_PORT: 3001 + GITEA__security__INSTALL_LOCK: "true" + GITEA__security__SECRET_KEY: foodlinkk-gitea-secret-2026 + GITEA__service__DISABLE_REGISTRATION: "true" + ports: + - "3001:3001" + volumes: + - gitea_data:/var/lib/gitea + restart: unless-stopped + + email-agent: + build: ./email-agent + container_name: foodlinkk_email_agent + environment: + DB_HOST: foodlinkk_db + DB_USER: aissa + DB_PASSWORD: Foodlinkk#2026 + DB_NAME: foodlinkk + TOOLS_API_URL: http://tools-api:8700 + SYNC_INTERVAL_SEC: 300 + ports: + - "8801:8801" + depends_on: + - tools-api + extra_hosts: + - "foodlinkk_db:10.4.7.18" + restart: unless-stopped + + prometheus: + image: prom/prometheus:v2.53.0 + container_name: foodlinkk_prometheus + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro + ports: + - "9090:9090" + restart: unless-stopped + + grafana: + image: grafana/grafana:11.1.0 + container_name: foodlinkk_grafana + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: Foodlinkk#2026 + GF_SERVER_ROOT_URL: http://10.4.7.18:3002 + ports: + - "3002:3000" + depends_on: + - prometheus + restart: unless-stopped + +volumes: + gitea_data: diff --git a/snapshots/2026-06-09/migrations/001_fase1.sql b/snapshots/2026-06-09/migrations/001_fase1.sql new file mode 100644 index 0000000..5aa3f96 --- /dev/null +++ b/snapshots/2026-06-09/migrations/001_fase1.sql @@ -0,0 +1,112 @@ +-- Foodlinkk Command Center — Fase 1 migrations + +CREATE TABLE IF NOT EXISTS agent_events ( + id SERIAL PRIMARY KEY, + agent_name VARCHAR(64) NOT NULL, + agent_type VARCHAR(32) DEFAULT 'openswarm', + event_type VARCHAR(64) NOT NULL, + title VARCHAR(255) NOT NULL, + body TEXT, + metadata JSONB DEFAULT '{}', + status VARCHAR(32) DEFAULT 'completed', + related_table VARCHAR(64), + related_id INTEGER, + channel VARCHAR(32) DEFAULT 'dashboard', + created_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_agent_events_created ON agent_events(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_agent_events_status ON agent_events(status); +CREATE INDEX IF NOT EXISTS idx_agent_events_agent ON agent_events(agent_name); + +CREATE TABLE IF NOT EXISTS clients ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + contact VARCHAR(255), + email VARCHAR(255), + stage VARCHAR(64) DEFAULT 'intake', + sector VARCHAR(128), + mrr_estimate NUMERIC(12,2), + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS deals ( + id SERIAL PRIMARY KEY, + client_id INTEGER REFERENCES clients(id), + title VARCHAR(255) NOT NULL, + value NUMERIC(12,2) DEFAULT 0, + stage VARCHAR(64) DEFAULT 'lead', + agent_owner VARCHAR(64) DEFAULT 'herman', + next_action TEXT, + deadline DATE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS products ( + id SERIAL PRIMARY KEY, + client_id INTEGER REFERENCES clients(id), + name VARCHAR(255) NOT NULL, + status VARCHAR(64) DEFAULT 'concept', + margin_pct NUMERIC(5,2), + moq INTEGER, + shelf_target VARCHAR(255), + launch_date DATE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS suppliers ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + country VARCHAR(64), + category VARCHAR(128), + moq INTEGER, + lead_time_days INTEGER, + rating NUMERIC(3,1), + contact TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS agent_tasks ( + id SERIAL PRIMARY KEY, + agent_name VARCHAR(64) NOT NULL, + title VARCHAR(255) NOT NULL, + description TEXT, + status VARCHAR(32) DEFAULT 'pending', + priority VARCHAR(16) DEFAULT 'normal', + assigned_by VARCHAR(64) DEFAULT 'herman', + created_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS daily_briefings ( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + generated_by VARCHAR(64) DEFAULT 'herman', + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +INSERT INTO clients (name, contact, stage, sector, mrr_estimate, notes) +SELECT 'Chai N Masala', 'Raj Patel', 'active', 'Spices & Tea', 4500, 'Launch partner — retail expansion' +WHERE NOT EXISTS (SELECT 1 FROM clients WHERE name = 'Chai N Masala'); + +INSERT INTO clients (name, contact, stage, sector, mrr_estimate, notes) +SELECT 'Van den Tweel', 'Inkoper', 'proposal', 'Retail', 12000, 'Category introduction pipeline' +WHERE NOT EXISTS (SELECT 1 FROM clients WHERE name = 'Van den Tweel'); + +INSERT INTO deals (client_id, title, value, stage, next_action, deadline) +SELECT c.id, 'Q3 spice range listing', 85000, 'proposal', 'Send updated margin sheet', CURRENT_DATE + 14 +FROM clients c WHERE c.name = 'Chai N Masala' +AND NOT EXISTS (SELECT 1 FROM deals WHERE title = 'Q3 spice range listing'); + +INSERT INTO agent_events (agent_name, agent_type, event_type, title, body, status, channel) +SELECT 'herman', 'co_ceo', 'briefing', 'Command Center online', 'Foodlinkk AI Command Center Fase 1 gestart.', 'completed', 'dashboard' +WHERE NOT EXISTS (SELECT 1 FROM agent_events WHERE title = 'Command Center online'); + +INSERT INTO agent_events (agent_name, agent_type, event_type, title, body, status, channel) +SELECT 'marketing', 'marketing', 'draft', 'Instagram draft — sourcing dienst', 'Behind every great food brand is a supply chain that works. At Foodlinkk we connect producers to shelf. #foodinnovation #foodlinkk', 'needs_approval', 'dashboard' +WHERE NOT EXISTS (SELECT 1 FROM agent_events WHERE title = 'Instagram draft — sourcing dienst'); diff --git a/snapshots/2026-06-09/migrations/002_fase234.sql b/snapshots/2026-06-09/migrations/002_fase234.sql new file mode 100644 index 0000000..c0ffd34 --- /dev/null +++ b/snapshots/2026-06-09/migrations/002_fase234.sql @@ -0,0 +1,61 @@ +-- Foodlinkk Command Center — Fase 2-4 + +CREATE TABLE IF NOT EXISTS monitored_sites ( + id SERIAL PRIMARY KEY, + url TEXT NOT NULL, + label VARCHAR(255), + check_interval_min INTEGER DEFAULT 60, + last_status VARCHAR(32), + last_checked_at TIMESTAMPTZ, + enabled BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS page_changes ( + id SERIAL PRIMARY KEY, + site_id INTEGER REFERENCES monitored_sites(id) ON DELETE CASCADE, + change_type VARCHAR(64) DEFAULT 'content', + summary TEXT, + diff_hash VARCHAR(64), + detected_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS crawl_logs ( + id SERIAL PRIMARY KEY, + site_id INTEGER REFERENCES monitored_sites(id) ON DELETE CASCADE, + status_code INTEGER, + duration_ms INTEGER, + error_message TEXT, + crawled_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS knowledge_documents ( + id SERIAL PRIMARY KEY, + title VARCHAR(512) NOT NULL, + source VARCHAR(128), + doc_type VARCHAR(64) DEFAULT 'general', + storage_path TEXT, + chroma_id VARCHAR(128), + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS halal_documents ( + id SERIAL PRIMARY KEY, + title VARCHAR(512) NOT NULL, + cert_body VARCHAR(255), + expiry_date DATE, + status VARCHAR(64) DEFAULT 'valid', + storage_path TEXT, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +INSERT INTO monitored_sites (url, label, last_status, last_checked_at) +SELECT 'https://foodlinkk.com', 'Foodlinkk main', 'ok', NOW() +WHERE NOT EXISTS (SELECT 1 FROM monitored_sites WHERE url = 'https://foodlinkk.com'); + +INSERT INTO suppliers (name, country, category, moq, lead_time_days, rating) +SELECT 'Global Spice Co', 'IN', 'Spices', 500, 21, 4.2 +WHERE NOT EXISTS (SELECT 1 FROM suppliers WHERE name = 'Global Spice Co'); diff --git a/snapshots/2026-06-09/migrations/003_word_analytics.sql b/snapshots/2026-06-09/migrations/003_word_analytics.sql new file mode 100644 index 0000000..6e7d507 --- /dev/null +++ b/snapshots/2026-06-09/migrations/003_word_analytics.sql @@ -0,0 +1,50 @@ +-- Word tracking & sentiment analytics for NAS documents + +CREATE TABLE IF NOT EXISTS document_analytics ( + id SERIAL PRIMARY KEY, + storage_path TEXT UNIQUE NOT NULL, + filename VARCHAR(512), + doc_type VARCHAR(64) DEFAULT 'general', + language VARCHAR(16), + word_count INT DEFAULT 0, + unique_lemmas INT DEFAULT 0, + sentence_count INT DEFAULT 0, + sentiment_compound FLOAT, + sentiment_positive FLOAT, + sentiment_negative FLOAT, + sentiment_neutral FLOAT, + sentiment_subjectivity FLOAT, + sentiment_label VARCHAR(32), + extraction_method VARCHAR(64) DEFAULT 'standard', + file_sig VARCHAR(64), + analyzed_at TIMESTAMPTZ DEFAULT NOW(), + metadata JSONB DEFAULT '{}' +); + +CREATE TABLE IF NOT EXISTS document_word_counts ( + id SERIAL PRIMARY KEY, + storage_path TEXT NOT NULL, + lemma VARCHAR(128) NOT NULL, + token VARCHAR(128), + pos_tag VARCHAR(16), + count INT DEFAULT 1, + is_stopword BOOLEAN DEFAULT FALSE, + language VARCHAR(16), + updated_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE (storage_path, lemma) +); + +CREATE INDEX IF NOT EXISTS idx_doc_word_lemma ON document_word_counts (lemma); +CREATE INDEX IF NOT EXISTS idx_doc_word_path ON document_word_counts (storage_path); +CREATE INDEX IF NOT EXISTS idx_doc_word_stop ON document_word_counts (is_stopword); +CREATE INDEX IF NOT EXISTS idx_doc_analytics_sentiment ON document_analytics (sentiment_label); +CREATE INDEX IF NOT EXISTS idx_doc_analytics_analyzed ON document_analytics (analyzed_at DESC); + +CREATE OR REPLACE VIEW global_word_frequency AS +SELECT + lemma, + MAX(token) AS sample_token, + SUM(count) AS total_count, + COUNT(DISTINCT storage_path) AS document_count +FROM document_word_counts +GROUP BY lemma; diff --git a/snapshots/2026-06-09/migrations/004_emails_calendar_brain.sql b/snapshots/2026-06-09/migrations/004_emails_calendar_brain.sql new file mode 100644 index 0000000..ee38d02 --- /dev/null +++ b/snapshots/2026-06-09/migrations/004_emails_calendar_brain.sql @@ -0,0 +1,74 @@ +-- Foodlinkk 2nd brain: emails, agenda, LLM memory + +CREATE TABLE IF NOT EXISTS emails ( + id SERIAL PRIMARY KEY, + message_id TEXT UNIQUE NOT NULL, + thread_id TEXT, + client_id INT REFERENCES clients(id) ON DELETE SET NULL, + deal_id INT REFERENCES deals(id) ON DELETE SET NULL, + direction VARCHAR(8) NOT NULL CHECK (direction IN ('in', 'out')), + from_addr TEXT, + to_addrs TEXT[] DEFAULT '{}', + cc_addrs TEXT[] DEFAULT '{}', + subject TEXT, + body_text TEXT, + body_html TEXT, + received_at TIMESTAMPTZ, + sent_at TIMESTAMPTZ, + is_read BOOLEAN DEFAULT FALSE, + labels TEXT[] DEFAULT '{}', + raw_headers JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_emails_received ON emails(received_at DESC); +CREATE INDEX IF NOT EXISTS idx_emails_client ON emails(client_id); +CREATE INDEX IF NOT EXISTS idx_emails_from ON emails(from_addr); + +CREATE TABLE IF NOT EXISTS calendar_events ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + starts_at TIMESTAMPTZ NOT NULL, + ends_at TIMESTAMPTZ, + client_id INT REFERENCES clients(id) ON DELETE SET NULL, + deal_id INT REFERENCES deals(id) ON DELETE SET NULL, + location TEXT, + source VARCHAR(32) DEFAULT 'manual', + external_id TEXT, + all_day BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_calendar_starts ON calendar_events(starts_at); + +CREATE TABLE IF NOT EXISTS llm_memory ( + id SERIAL PRIMARY KEY, + category VARCHAR(64) NOT NULL DEFAULT 'fact', + subject TEXT, + content TEXT NOT NULL, + client_id INT REFERENCES clients(id) ON DELETE SET NULL, + deal_id INT REFERENCES deals(id) ON DELETE SET NULL, + source VARCHAR(64) DEFAULT 'herman', + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_llm_memory_category ON llm_memory(category); +CREATE INDEX IF NOT EXISTS idx_llm_memory_updated ON llm_memory(updated_at DESC); + +-- Seed: deal deadlines als agenda-items (zichtbaar in ochtendbericht) +INSERT INTO calendar_events (title, starts_at, ends_at, deal_id, source, description) +SELECT + 'Deadline: ' || d.title, + d.deadline::timestamptz, + d.deadline::timestamptz + INTERVAL '1 hour', + d.id, + 'deal_deadline', + COALESCE(d.next_action, '') +FROM deals d +WHERE d.deadline IS NOT NULL + AND d.stage NOT IN ('won', 'lost') + AND NOT EXISTS ( + SELECT 1 FROM calendar_events ce + WHERE ce.deal_id = d.id AND ce.source = 'deal_deadline' + ); diff --git a/snapshots/2026-06-09/migrations/005_email_accounts.sql b/snapshots/2026-06-09/migrations/005_email_accounts.sql new file mode 100644 index 0000000..1273cf3 --- /dev/null +++ b/snapshots/2026-06-09/migrations/005_email_accounts.sql @@ -0,0 +1,27 @@ +-- Email accounts configurable via Settings UI + +CREATE TABLE IF NOT EXISTS email_accounts ( + id SERIAL PRIMARY KEY, + label VARCHAR(128) NOT NULL DEFAULT 'Primary', + email_address VARCHAR(255) NOT NULL, + provider VARCHAR(32) DEFAULT 'custom', + is_active BOOLEAN DEFAULT FALSE, + smtp_host VARCHAR(255), + smtp_port INT DEFAULT 587, + smtp_user VARCHAR(255), + smtp_password TEXT, + imap_host VARCHAR(255), + imap_port INT DEFAULT 993, + imap_user VARCHAR(255), + imap_password TEXT, + imap_use_ssl BOOLEAN DEFAULT TRUE, + sync_enabled BOOLEAN DEFAULT FALSE, + last_sync_at TIMESTAMPTZ, + last_test_at TIMESTAMPTZ, + last_test_status VARCHAR(64), + last_test_message TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_email_accounts_active ON email_accounts(is_active) WHERE is_active = TRUE; diff --git a/snapshots/2026-06-09/migrations/006_browser_sessions.sql b/snapshots/2026-06-09/migrations/006_browser_sessions.sql new file mode 100644 index 0000000..932a0bc --- /dev/null +++ b/snapshots/2026-06-09/migrations/006_browser_sessions.sql @@ -0,0 +1,30 @@ +-- Browser sessions + enriched crawl storage + +CREATE TABLE IF NOT EXISTS browser_sessions ( + id SERIAL PRIMARY KEY, + url TEXT NOT NULL, + final_url TEXT, + title TEXT, + task TEXT, + status VARCHAR(32) DEFAULT 'running', + content_text TEXT, + content_html TEXT, + screenshot_b64 TEXT, + links JSONB DEFAULT '[]', + metadata JSONB DEFAULT '{}', + site_id INT REFERENCES monitored_sites(id) ON DELETE SET NULL, + error_message TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + completed_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_browser_sessions_created ON browser_sessions(created_at DESC); + +ALTER TABLE crawled_pages ADD COLUMN IF NOT EXISTS site_id INT REFERENCES monitored_sites(id) ON DELETE SET NULL; +ALTER TABLE crawled_pages ADD COLUMN IF NOT EXISTS final_url TEXT; +ALTER TABLE crawled_pages ADD COLUMN IF NOT EXISTS content_html TEXT; +ALTER TABLE crawled_pages ADD COLUMN IF NOT EXISTS screenshot_b64 TEXT; +ALTER TABLE crawled_pages ADD COLUMN IF NOT EXISTS links JSONB DEFAULT '[]'; +ALTER TABLE crawled_pages ADD COLUMN IF NOT EXISTS metadata JSONB DEFAULT '{}'; + +ALTER TABLE monitored_sites ADD COLUMN IF NOT EXISTS last_title TEXT; +ALTER TABLE monitored_sites ADD COLUMN IF NOT EXISTS last_snapshot_id INT REFERENCES browser_sessions(id) ON DELETE SET NULL; diff --git a/snapshots/2026-06-09/migrations/007_photo_imports.sql b/snapshots/2026-06-09/migrations/007_photo_imports.sql new file mode 100644 index 0000000..73417b0 --- /dev/null +++ b/snapshots/2026-06-09/migrations/007_photo_imports.sql @@ -0,0 +1,17 @@ +-- Photo imports + OCR/detections from browser, Telegram, NAS upload +CREATE TABLE IF NOT EXISTS photo_imports ( + id SERIAL PRIMARY KEY, + source VARCHAR(64) NOT NULL DEFAULT 'upload', + filename VARCHAR(512), + storage_path VARCHAR(1024), + image_b64 TEXT NOT NULL, + ocr_text TEXT, + detections JSONB DEFAULT '[]'::jsonb, + extracted_items JSONB DEFAULT '[]'::jsonb, + metadata JSONB DEFAULT '{}'::jsonb, + session_id INTEGER REFERENCES browser_sessions(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_photo_imports_source ON photo_imports(source); +CREATE INDEX IF NOT EXISTS idx_photo_imports_created ON photo_imports(created_at DESC); diff --git a/snapshots/2026-06-09/migrations/008_pgvector_telegram_brain.sql b/snapshots/2026-06-09/migrations/008_pgvector_telegram_brain.sql new file mode 100644 index 0000000..98591cc --- /dev/null +++ b/snapshots/2026-06-09/migrations/008_pgvector_telegram_brain.sql @@ -0,0 +1,84 @@ +-- Second brain: Telegram graph + pgvector (768 = nomic-embed-text) +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE IF NOT EXISTS telegram_conversations ( + id SERIAL PRIMARY KEY, + chat_id BIGINT NOT NULL UNIQUE, + chat_type VARCHAR(32) DEFAULT 'private', + user_name TEXT, + user_role VARCHAR(32), + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS telegram_messages ( + id SERIAL PRIMARY KEY, + conversation_id INT NOT NULL REFERENCES telegram_conversations(id) ON DELETE CASCADE, + telegram_message_id BIGINT, + direction VARCHAR(8) NOT NULL CHECK (direction IN ('in', 'out')), + role VARCHAR(16) NOT NULL DEFAULT 'user', + content_type VARCHAR(32) DEFAULT 'text', + content_text TEXT, + content_json JSONB DEFAULT '{}', + reply_to_message_id INT REFERENCES telegram_messages(id) ON DELETE SET NULL, + agent_name VARCHAR(64), + agent_event_id INT REFERENCES agent_events(id) ON DELETE SET NULL, + channel VARCHAR(32) DEFAULT 'telegram', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_tg_msg_conv_created ON telegram_messages(conversation_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_tg_msg_telegram_id ON telegram_messages(telegram_message_id); +CREATE INDEX IF NOT EXISTS idx_tg_msg_fts ON telegram_messages + USING gin(to_tsvector('simple', coalesce(content_text, ''))); + +CREATE TABLE IF NOT EXISTS telegram_message_embeddings ( + id SERIAL PRIMARY KEY, + message_id INT NOT NULL REFERENCES telegram_messages(id) ON DELETE CASCADE, + chunk_index INT DEFAULT 0, + embedding vector(768) NOT NULL, + model VARCHAR(64) DEFAULT 'nomic-embed-text', + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE (message_id, chunk_index) +); + +CREATE INDEX IF NOT EXISTS idx_tg_emb_hnsw ON telegram_message_embeddings + USING hnsw (embedding vector_cosine_ops); + +CREATE TABLE IF NOT EXISTS telegram_message_edges ( + id SERIAL PRIMARY KEY, + source_message_id INT NOT NULL REFERENCES telegram_messages(id) ON DELETE CASCADE, + target_message_id INT REFERENCES telegram_messages(id) ON DELETE SET NULL, + target_entity_type VARCHAR(64), + target_entity_id INT, + edge_type VARCHAR(64) NOT NULL, + weight FLOAT DEFAULT 1.0, + metadata JSONB DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_tg_edges_source ON telegram_message_edges(source_message_id); +CREATE INDEX IF NOT EXISTS idx_tg_edges_target ON telegram_message_edges(target_message_id); +CREATE INDEX IF NOT EXISTS idx_tg_edges_type ON telegram_message_edges(edge_type); +CREATE INDEX IF NOT EXISTS idx_tg_edges_entity ON telegram_message_edges(target_entity_type, target_entity_id); + +CREATE OR REPLACE VIEW telegram_graph_view AS +SELECT + e.id AS edge_id, + e.edge_type, + e.weight, + e.target_entity_type, + e.target_entity_id, + sm.id AS source_id, + sm.content_text AS source_text, + sm.direction AS source_direction, + sm.agent_name AS source_agent, + tm.id AS target_id, + tm.content_text AS target_text, + c.chat_id, + e.created_at +FROM telegram_message_edges e +JOIN telegram_messages sm ON sm.id = e.source_message_id +LEFT JOIN telegram_messages tm ON tm.id = e.target_message_id +JOIN telegram_conversations c ON c.id = sm.conversation_id; diff --git a/snapshots/2026-06-09/migrations/009_full_upgrade.sql b/snapshots/2026-06-09/migrations/009_full_upgrade.sql new file mode 100644 index 0000000..b009609 --- /dev/null +++ b/snapshots/2026-06-09/migrations/009_full_upgrade.sql @@ -0,0 +1,303 @@ +-- Foodlinkk 009: research pipeline + retail intelligence + recommendations +-- Safe: IF NOT EXISTS throughout + +CREATE TABLE IF NOT EXISTS data_providers ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + provider_type VARCHAR(50), + config JSONB DEFAULT '{}', + refresh_interval INTERVAL, + last_fetch_at TIMESTAMP, + last_status VARCHAR(20), + is_active BOOLEAN DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS data_snapshots ( + id SERIAL PRIMARY KEY, + provider_id INTEGER REFERENCES data_providers(id), + fetched_at TIMESTAMP DEFAULT NOW(), + payload JSONB NOT NULL, + record_count INTEGER, + ttl_expires_at TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS research_briefs ( + id SERIAL PRIMARY KEY, + domain VARCHAR(50), + title VARCHAR(255), + summary TEXT, + key_findings JSONB DEFAULT '[]', + source_snapshot_ids INTEGER[], + generated_at TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS market_trends ( + id SERIAL PRIMARY KEY, + category VARCHAR(100) NOT NULL, + trend_name VARCHAR(255) NOT NULL, + description TEXT, + source VARCHAR(255), + confidence_score DECIMAL(3, 2), + opportunity_score DECIMAL(3, 2), + related_products TEXT[], + action_items TEXT[], + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS competitor_products ( + id SERIAL PRIMARY KEY, + competitor VARCHAR(100) NOT NULL, + product_name VARCHAR(255) NOT NULL, + category VARCHAR(100), + price DECIMAL(10, 2), + launch_date DATE, + halal_certified BOOLEAN DEFAULT FALSE, + ingredients TEXT, + market_position VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS design_projects ( + id SERIAL PRIMARY KEY, + client_id INTEGER, + project_type VARCHAR(50) NOT NULL, + brief TEXT, + concepts JSONB, + selected_design TEXT, + approval_status VARCHAR(20), + files_generated TEXT[], + brand_guidelines JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS brand_assets ( + id SERIAL PRIMARY KEY, + client_id INTEGER, + asset_type VARCHAR(50) NOT NULL, + file_path TEXT NOT NULL, + usage_rights TEXT, + version VARCHAR(20), + created_at TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS supermarkets ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + chain VARCHAR(100) NOT NULL, + address TEXT NOT NULL, + postcode VARCHAR(7) NOT NULL, + city VARCHAR(100) NOT NULL, + province VARCHAR(50), + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + store_type VARCHAR(50), + size_m2 INTEGER, + opening_hours JSONB, + phone VARCHAR(20), + email VARCHAR(255), + website VARCHAR(255), + halal_certified BOOLEAN DEFAULT FALSE, + halal_certifier VARCHAR(100), + partnership_status VARCHAR(20) DEFAULT 'none', + client_id INTEGER, + deal_id INTEGER, + halal_certificate_number VARCHAR(100), + halal_expiry_date DATE, + has_halal_section BOOLEAN DEFAULT FALSE, + organic_section BOOLEAN DEFAULT FALSE, + alcohol_section BOOLEAN DEFAULT FALSE, + last_updated TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS butcher_shops ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address TEXT NOT NULL, + postcode VARCHAR(7) NOT NULL, + city VARCHAR(100) NOT NULL, + province VARCHAR(50), + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + halal_certified BOOLEAN DEFAULT FALSE, + halal_certifier VARCHAR(100), + specialty VARCHAR(100), + opening_hours JSONB, + phone VARCHAR(20), + website VARCHAR(255), + last_updated TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS wholesalers ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + address TEXT NOT NULL, + postcode VARCHAR(7) NOT NULL, + city VARCHAR(100) NOT NULL, + province VARCHAR(50), + latitude DECIMAL(10, 8), + longitude DECIMAL(11, 8), + halal_certified BOOLEAN DEFAULT FALSE, + halal_certifier VARCHAR(100), + product_categories TEXT[], + minimum_order DECIMAL(10, 2), + delivery_areas TEXT[], + opening_hours JSONB, + phone VARCHAR(20), + email VARCHAR(255), + website VARCHAR(255), + last_updated TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS area_analysis ( + id SERIAL PRIMARY KEY, + postcode VARCHAR(7) NOT NULL UNIQUE, + city VARCHAR(100), + population INTEGER, + households INTEGER, + avg_household_size DECIMAL(3, 2), + avg_income DECIMAL(10, 2), + median_income DECIMAL(10, 2), + education_level JSONB, + age_distribution JSONB, + ethnic_composition JSONB, + religious_composition JSONB, + unemployment_rate DECIMAL(5, 2), + housing_type JSONB, + car_ownership DECIMAL(5, 2), + last_updated TIMESTAMP DEFAULT NOW(), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS weather_data ( + id SERIAL PRIMARY KEY, + location_id INTEGER, + location_type VARCHAR(50), + region VARCHAR(100), + city VARCHAR(100), + date DATE NOT NULL, + temperature_c DECIMAL(4, 1), + feels_like_c DECIMAL(4, 1), + humidity INTEGER, + wind_speed_kmh DECIMAL(5, 1), + precipitation_mm DECIMAL(5, 1), + weather_condition VARCHAR(50), + uv_index DECIMAL(3, 1), + data_source VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS marketing_strategies ( + id SERIAL PRIMARY KEY, + strategy_name VARCHAR(255) NOT NULL, + strategy_type VARCHAR(50), + description TEXT, + target_audience TEXT[], + product_categories TEXT[], + weather_conditions TEXT[], + temperature_range_min DECIMAL(4, 1), + temperature_range_max DECIMAL(4, 1), + seasons TEXT[], + channels TEXT[], + budget_range DECIMAL(10, 2), + expected_roi DECIMAL(5, 2), + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS marketing_recommendations ( + id SERIAL PRIMARY KEY, + strategy_id INTEGER, + location_id INTEGER, + location_type VARCHAR(50), + weather_data_id INTEGER, + recommendation_text TEXT NOT NULL, + priority VARCHAR(20), + action_items TEXT[], + expected_impact TEXT, + generated_at TIMESTAMP DEFAULT NOW(), + expires_at TIMESTAMP, + status VARCHAR(20), + data_source VARCHAR(100) +); + +CREATE TABLE IF NOT EXISTS ai_recommendations ( + id SERIAL PRIMARY KEY, + recommendation_type VARCHAR(50), + title VARCHAR(255) NOT NULL, + description TEXT, + priority VARCHAR(20), + impact_score DECIMAL(3, 2), + confidence_score DECIMAL(3, 2), + data_sources JSONB, + action_items TEXT[], + estimated_value DECIMAL(10, 2), + generated_by VARCHAR(100), + related_entity_type VARCHAR(50), + related_entity_id INTEGER, + status VARCHAR(20) DEFAULT 'pending', + expires_at TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS herman_conversations ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(100), + session_id VARCHAR(100), + message TEXT NOT NULL, + role VARCHAR(20), + context JSONB, + timestamp TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS herman_actions ( + id SERIAL PRIMARY KEY, + action_type VARCHAR(50), + title VARCHAR(255) NOT NULL, + description TEXT, + target_agent VARCHAR(100), + parameters JSONB, + status VARCHAR(20), + result JSONB, + created_at TIMESTAMP DEFAULT NOW(), + completed_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_data_snapshots_provider ON data_snapshots(provider_id); +CREATE INDEX IF NOT EXISTS idx_research_briefs_domain ON research_briefs(domain); +CREATE INDEX IF NOT EXISTS idx_supermarkets_chain ON supermarkets(chain); +CREATE INDEX IF NOT EXISTS idx_supermarkets_partnership ON supermarkets(partnership_status); +CREATE INDEX IF NOT EXISTS idx_ai_recommendations_status ON ai_recommendations(status); + +INSERT INTO data_providers (name, provider_type, config, is_active) VALUES + ('crm', 'internal', '{"description":"PostgreSQL CRM"}', true), + ('weather', 'api', '{"url":"https://api.open-meteo.com/v1/forecast"}', true), + ('social', 'internal', '{"table":"social_mentions"}', true), + ('retail_manual', 'internal', '{"description":"Seed retail locations"}', true) +ON CONFLICT (name) DO NOTHING; + +INSERT INTO supermarkets (name, chain, address, postcode, city, province, latitude, longitude, partnership_status, data_source) +VALUES + ('Jumbo Bos en Lommer', 'Jumbo', 'Bos en Lommerweg 123', '1055RW', 'Amsterdam', 'Noord-Holland', 52.378100, 4.832000, 'active', 'seed'), + ('AH Chai N Masala regio', 'Albert Heijn', 'Haarlemmerdijk 1', '1013EM', 'Amsterdam', 'Noord-Holland', 52.384500, 4.883200, 'proposal', 'seed') +ON CONFLICT DO NOTHING; + +INSERT INTO ai_recommendations (recommendation_type, title, description, priority, impact_score, confidence_score, status, generated_by, action_items) +SELECT 'deal_action', 'Chai N Masala — margin sheet', 'Deal Q3 spice range deadline 20 jun — stuur margin sheet', 'high', 0.85, 0.90, 'pending', 'herman', ARRAY['Margin sheet versturen','Follow-up call plannen'] +WHERE NOT EXISTS (SELECT 1 FROM ai_recommendations WHERE title = 'Chai N Masala — margin sheet'); + +INSERT INTO ai_recommendations (recommendation_type, title, description, priority, impact_score, confidence_score, status, generated_by, action_items) +SELECT 'retail_target', 'Van den Tweel Jumbo regio', 'Follow-up voorstel supermarkt placement Amsterdam-West', 'medium', 0.72, 0.80, 'pending', 'herman', ARRAY['Contact Van den Tweel','Update proposal deck'] +WHERE NOT EXISTS (SELECT 1 FROM ai_recommendations WHERE title = 'Van den Tweel Jumbo regio'); diff --git a/snapshots/2026-06-09/migrations/010_retail_crm_intel.sql b/snapshots/2026-06-09/migrations/010_retail_crm_intel.sql new file mode 100644 index 0000000..daf373a --- /dev/null +++ b/snapshots/2026-06-09/migrations/010_retail_crm_intel.sql @@ -0,0 +1,83 @@ +-- Retail CRM intelligence layer +CREATE TABLE IF NOT EXISTS supermarket_contacts ( + id SERIAL PRIMARY KEY, + supermarket_id INTEGER NOT NULL REFERENCES supermarkets(id) ON DELETE CASCADE, + role VARCHAR(64) NOT NULL DEFAULT 'manager', + full_name VARCHAR(255), + phone VARCHAR(32), + email VARCHAR(255), + linkedin_url VARCHAR(512), + source VARCHAR(100), + confidence NUMERIC(3,2) DEFAULT 0.50, + verified_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_supermarket_contacts_store ON supermarket_contacts(supermarket_id); +CREATE INDEX IF NOT EXISTS idx_supermarket_contacts_role ON supermarket_contacts(role); + +CREATE TABLE IF NOT EXISTS supermarket_profiles ( + supermarket_id INTEGER PRIMARY KEY REFERENCES supermarkets(id) ON DELETE CASCADE, + staff_count_estimate INTEGER, + manager_name VARCHAR(255), + manager_phone VARCHAR(32), + manager_email VARCHAR(255), + services JSONB DEFAULT '{}'::jsonb, + facilities JSONB DEFAULT '{}'::jsonb, + web_data JSONB DEFAULT '{}'::jsonb, + scrape_url VARCHAR(512), + last_scraped_at TIMESTAMPTZ, + data_completeness NUMERIC(3,2) DEFAULT 0, + notes TEXT +); + +CREATE TABLE IF NOT EXISTS client_supermarket_links ( + id SERIAL PRIMARY KEY, + client_id INTEGER NOT NULL REFERENCES clients(id) ON DELETE CASCADE, + supermarket_id INTEGER NOT NULL REFERENCES supermarkets(id) ON DELETE CASCADE, + deal_id INTEGER REFERENCES deals(id) ON DELETE SET NULL, + relationship_type VARCHAR(64) DEFAULT 'prospect', + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(client_id, supermarket_id) +); +CREATE INDEX IF NOT EXISTS idx_csl_client ON client_supermarket_links(client_id); +CREATE INDEX IF NOT EXISTS idx_csl_store ON client_supermarket_links(supermarket_id); + +CREATE TABLE IF NOT EXISTS halal_certifications ( + id SERIAL PRIMARY KEY, + supermarket_id INTEGER REFERENCES supermarkets(id) ON DELETE SET NULL, + certifier VARCHAR(100) NOT NULL, + certificate_number VARCHAR(100), + business_name VARCHAR(255), + address TEXT, + city VARCHAR(100), + expiry_date DATE, + status VARCHAR(32) DEFAULT 'active', + registry_source VARCHAR(100), + matched_confidence NUMERIC(3,2), + raw_data JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_halal_cert_store ON halal_certifications(supermarket_id); +CREATE INDEX IF NOT EXISTS idx_halal_cert_status ON halal_certifications(status); + +CREATE TABLE IF NOT EXISTS retail_opportunity_scores ( + supermarket_id INTEGER PRIMARY KEY REFERENCES supermarkets(id) ON DELETE CASCADE, + halal_opportunity_score NUMERIC(5,2), + market_potential_score NUMERIC(5,2), + factors JSONB DEFAULT '{}'::jsonb, + computed_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_retail_opp_halal ON retail_opportunity_scores(halal_opportunity_score DESC); + +ALTER TABLE supermarkets ADD COLUMN IF NOT EXISTS employee_count INTEGER; +ALTER TABLE supermarkets ADD COLUMN IF NOT EXISTS manager_name VARCHAR(255); +ALTER TABLE supermarkets ADD COLUMN IF NOT EXISTS enrichment_score NUMERIC(3,2) DEFAULT 0; +ALTER TABLE supermarkets ADD COLUMN IF NOT EXISTS halal_opportunity_score NUMERIC(5,2); + +INSERT INTO data_providers (name, provider_type, config, is_active) VALUES + ('halal_registry', 'scrape', '{"sources": ["osm", "hqc", "manual"]}', true), + ('branch_scraper', 'scrape', '{"chains": ["ah", "jumbo", "plus", "lidl", "aldi", "dirk"]}', true), + ('market_trends', 'api', '{"categories": ["kant-en-klaar", "halal", "ready-meals"]}', true) +ON CONFLICT (name) DO UPDATE SET config = EXCLUDED.config, is_active = true; diff --git a/snapshots/2026-06-09/migrations/011_retail_360.sql b/snapshots/2026-06-09/migrations/011_retail_360.sql new file mode 100644 index 0000000..81c5dc7 --- /dev/null +++ b/snapshots/2026-06-09/migrations/011_retail_360.sql @@ -0,0 +1,113 @@ +-- Retail 360: notes, media, milestones, ownership, RSS, city demographics + +CREATE TABLE IF NOT EXISTS entity_notes ( + id SERIAL PRIMARY KEY, + entity_type VARCHAR(32) NOT NULL, + entity_id INTEGER NOT NULL, + title VARCHAR(255), + body TEXT NOT NULL, + note_type VARCHAR(32) DEFAULT 'general', + created_by VARCHAR(64) DEFAULT 'team', + pinned BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_entity_notes_entity ON entity_notes(entity_type, entity_id); + +CREATE TABLE IF NOT EXISTS entity_media ( + id SERIAL PRIMARY KEY, + entity_type VARCHAR(32) NOT NULL, + entity_id INTEGER NOT NULL, + filename VARCHAR(255) NOT NULL, + content_type VARCHAR(64) DEFAULT 'image/jpeg', + storage_path TEXT NOT NULL, + caption TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_entity_media_entity ON entity_media(entity_type, entity_id); + +CREATE TABLE IF NOT EXISTS sales_milestones ( + id SERIAL PRIMARY KEY, + supermarket_id INTEGER REFERENCES supermarkets(id) ON DELETE CASCADE, + client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, + deal_id INTEGER REFERENCES deals(id) ON DELETE SET NULL, + milestone_type VARCHAR(64) NOT NULL DEFAULT 'custom', + title VARCHAR(255) NOT NULL, + status VARCHAR(32) DEFAULT 'pending', + target_date DATE, + completed_at TIMESTAMPTZ, + value_eur NUMERIC(12,2), + notes TEXT, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_milestones_store ON sales_milestones(supermarket_id); + +CREATE TABLE IF NOT EXISTS ownership_changes ( + id SERIAL PRIMARY KEY, + entity_type VARCHAR(32) DEFAULT 'supermarket', + entity_id INTEGER, + chain VARCHAR(100), + previous_owner VARCHAR(255), + new_owner VARCHAR(255) NOT NULL, + change_type VARCHAR(64) DEFAULT 'acquisition', + effective_date DATE, + source VARCHAR(255), + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_ownership_chain ON ownership_changes(chain); + +CREATE TABLE IF NOT EXISTS city_demographics ( + id SERIAL PRIMARY KEY, + city VARCHAR(100) NOT NULL, + province VARCHAR(50), + gemeente_code VARCHAR(16), + population INTEGER, + households INTEGER, + avg_income NUMERIC(12,2), + muslim_proxy_pct NUMERIC(5,2), + data_source VARCHAR(100) DEFAULT 'cbs', + last_updated TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(city, province) +); +CREATE INDEX IF NOT EXISTS idx_city_demo_city ON city_demographics(city); + +CREATE TABLE IF NOT EXISTS rss_feeds ( + id SERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL UNIQUE, + url TEXT NOT NULL, + category VARCHAR(64) DEFAULT 'general', + is_active BOOLEAN DEFAULT TRUE, + last_fetch_at TIMESTAMPTZ, + last_status VARCHAR(32) +); + +CREATE TABLE IF NOT EXISTS rss_items ( + id SERIAL PRIMARY KEY, + feed_id INTEGER NOT NULL REFERENCES rss_feeds(id) ON DELETE CASCADE, + title TEXT NOT NULL, + link TEXT, + summary TEXT, + published_at TIMESTAMPTZ, + fetched_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(feed_id, link) +); +CREATE INDEX IF NOT EXISTS idx_rss_items_published ON rss_items(published_at DESC); + +ALTER TABLE calendar_events ADD COLUMN IF NOT EXISTS supermarket_id INTEGER REFERENCES supermarkets(id) ON DELETE SET NULL; +ALTER TABLE wholesalers ADD COLUMN IF NOT EXISTS external_id VARCHAR(64); +CREATE UNIQUE INDEX IF NOT EXISTS idx_wholesalers_external ON wholesalers(external_id) WHERE external_id IS NOT NULL; + +INSERT INTO rss_feeds (name, url, category) VALUES + ('Foodlog NL', 'https://www.foodlog.nl/feed/', 'kant-en-klaar'), + ('RetailDetail', 'https://www.retaildetail.nl/feed/', 'retail'), + ('VMT Food', 'https://www.vmt.nl/feed/', 'food-industry'), + ('Nu.nl Economie', 'https://www.nu.nl/rss/Economie', 'economie'), + ('Google Halal Food NL', 'https://news.google.com/rss/search?q=halal+food+supermarkt+nederland&hl=nl&gl=NL&ceid=NL:nl', 'halal') +ON CONFLICT (name) DO NOTHING; + +INSERT INTO data_providers (name, provider_type, config, is_active) VALUES + ('rss_feeds', 'api', '{"type": "rss"}', true), + ('wholesale_osm', 'scrape', '{"chains": ["Sligro", "Hanos", "Makro", "Bidfood"]}', true) +ON CONFLICT (name) DO UPDATE SET is_active = true; diff --git a/snapshots/2026-06-09/migrations/012_rss_focus.sql b/snapshots/2026-06-09/migrations/012_rss_focus.sql new file mode 100644 index 0000000..8d4099b --- /dev/null +++ b/snapshots/2026-06-09/migrations/012_rss_focus.sql @@ -0,0 +1,34 @@ +-- RSS focus: alleen kant-en-klaar maaltijden & supermarkten (geen algemeen nieuws) + +UPDATE rss_feeds SET is_active = FALSE WHERE name IN ('Nu.nl Economie', 'Foodlog NL', 'VMT Food'); + +UPDATE rss_feeds SET + url = 'https://news.google.com/rss/search?q=kant+en+klaar+maaltijd+supermarkt+nederland&hl=nl&gl=NL&ceid=NL:nl', + category = 'kant-en-klaar', + is_active = TRUE +WHERE name = 'Google Halal Food NL'; + +UPDATE rss_feeds SET name = 'Google Kant-en-klaar NL' WHERE name = 'Google Halal Food NL'; + +UPDATE rss_feeds SET category = 'supermarkt', is_active = TRUE WHERE name = 'RetailDetail'; + +INSERT INTO rss_feeds (name, url, category, is_active) VALUES + ('Google Supermarkt Retail NL', 'https://news.google.com/rss/search?q=supermarkt+retail+nederland+maaltijd&hl=nl&gl=NL&ceid=NL:nl', 'supermarkt', true), + ('Google Ready Meals NL', 'https://news.google.com/rss/search?q=ready+meal+supermarket+netherlands+halal&hl=nl&gl=NL&ceid=NL:nl', 'kant-en-klaar', true), + ('Google AH Jumbo NL', 'https://news.google.com/rss/search?q=Albert+Heijn+OR+Jumbo+kant+en+klaar+schap&hl=nl&gl=NL&ceid=NL:nl', 'supermarkt', true) +ON CONFLICT (name) DO UPDATE SET url = EXCLUDED.url, category = EXCLUDED.category, is_active = true; + +-- Verwijder irrelevante items (algemeen nieuws) +DELETE FROM rss_items WHERE feed_id IN (SELECT id FROM rss_feeds WHERE is_active = FALSE); + +DELETE FROM rss_items WHERE + title NOT ILIKE ANY (ARRAY[ + '%kant%klaa%', '%maaltijd%', '%meal%', '%supermarkt%', '%supermarket%', + '%retail%', '%jumbo%', '%albert heijn%', '%ah %', '%plus %', '%lidl%', + '%aldi%', '%dirk%', '%halal%', '%convenience%', '%schap%', '%filiaal%', + '%food%', '%grocery%', '%vers%', '%ready%', '%horeca%', '%foodservice%' + ]) + AND COALESCE(summary, '') NOT ILIKE ANY (ARRAY[ + '%kant%klaa%', '%maaltijd%', '%meal%', '%supermarkt%', '%supermarket%', + '%retail%', '%jumbo%', '%albert heijn%', '%halal%', '%food%' + ]); diff --git a/snapshots/2026-06-09/migrations/013_agent_souls_permissions.sql b/snapshots/2026-06-09/migrations/013_agent_souls_permissions.sql new file mode 100644 index 0000000..969fcaa --- /dev/null +++ b/snapshots/2026-06-09/migrations/013_agent_souls_permissions.sql @@ -0,0 +1,206 @@ +-- Agent souls (character profiles) + Herman module permissions + +CREATE TABLE IF NOT EXISTS agent_souls ( + agent_key VARCHAR(64) PRIMARY KEY, + display_name VARCHAR(128) NOT NULL, + role_title VARCHAR(128), + soul_md TEXT NOT NULL DEFAULT '', + responsibilities TEXT, + permissions JSONB DEFAULT '[]'::jsonb, + is_active BOOLEAN DEFAULT TRUE, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS herman_permissions ( + module_key VARCHAR(64) PRIMARY KEY, + module_label VARCHAR(128) NOT NULL, + description TEXT, + category VARCHAR(64) DEFAULT 'general', + granted BOOLEAN DEFAULT FALSE, + granted_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +INSERT INTO agent_souls (agent_key, display_name, role_title, soul_md, responsibilities) VALUES +('herman', 'Herman', 'AI Co-CEO & Orchestrator', +'# Herman — Soul + +Je bent Herman, de AI co-CEO van Foodlinkk. Warm, direct, zakelijk. + +## Karakter +- Spreekt Aïssa (CEO) aan met respect en helderheid +- Denkt in pipeline, retail partnerships en halal kant-en-klaar groei +- Delegeert naar specialisten maar houdt het totaaloverzicht + +## Werkzaamheden +- Dagelijkse CEO briefing genereren +- Agent events routeren en goedkeuringen voorbereiden +- OpenSwarm delegatie naar marketing, bizdev, finance, etc. +- CRM + retail intelligence samenvoegen in actiepunten', +'CEO briefing · orchestratie · delegatie · pipeline overzicht · goedkeuringen'), + +('marketing', 'Marketing', 'Social & Brand', +'# Marketing Agent — Soul + +Foodlinkk brand voice: halal kant-en-klaar, premium, toegankelijk. + +## Werkzaamheden +- Social mentions monitoren +- Content planning & scheduled posts +- RSS/trends vertalen naar campagnes +- Herman recommendations voor promoties', +'Social media · content · trends · campagnes · mentions'), + +('bizdev', 'BizDev', 'Retail Partnerships', +'# BizDev — Soul + +Focus: supermarkt listings, Jumbo/AH/Plus partnerships, margin sheets. + +## Werkzaamheden +- Deal pipeline follow-up +- Retail 360 kansen benaderen +- Voorstellen en proposals +- CRM koppeling supermarkten', +'Deals · retail partnerships · proposals · CRM supermarkt links'), + +('finance', 'Finance', 'Margins & Pricing', +'# Finance — Soul + +Prijzen, marges, cashflow voor kant-en-klaar SKU''s. + +## Werkzaamheden +- Deal waardering +- Margin sheets +- Pricing advies per retail keten', +'Pipeline waarde · marges · pricing · cashflow'), + +('sourcing', 'Sourcing', 'Procurement', +'# Sourcing — Soul + +Leveranciers, MOQ, lead times, halal ingredient sourcing. + +## Werkzaamheden +- Supplier database +- Inkoopvoorwaarden +- Lead time tracking', +'Suppliers · MOQ · inkoop · ingredient vetting'), + +('product', 'Product', 'SKU & Launch', +'# Product — Soul + +Kant-en-klaar SKU ontwikkeling, shelf readiness, launch timelines. + +## Werkzaamheden +- Product catalogus +- Launch planning +- Retail listing requirements', +'Products · SKU · launch · shelf readiness'), + +('halal', 'Halal', 'Certificering', +'# Halal Agent — Soul + +Halal compliance, certificering, ingredient vetting voor Foodlinkk. + +## Werkzaamheden +- Halal registry sync +- Certifier tracking +- Gap-analyse supermarkten', +'Halal cert · compliance · registry · opportunity scores'), + +('design', 'Design', 'Visual & Packaging', +'# Design — Soul + +Packaging, productfoto''s via ComfyUI, retail presentatie. + +## Werkzaamheden +- ComfyUI image generation +- Packaging visuals +- Studio assets', +'ComfyUI · packaging · visuals · studio'), + +('knowledge', 'Knowledge', 'RAG & Docs', +'# Knowledge — Soul + +Interne docs, NAS corpus, RAG antwoorden. + +## Werkzaamheden +- Document analytics +- NAS word counts & sentiment +- Policy antwoorden', +'NAS documents · RAG · document analytics · woorden corpus'), + +('retail', 'Retail Intel', 'Supermarkt 360', +'# Retail Intel — Soul + +3.000+ filialen, CBS demografie, halal kansen, groothandels. + +## Werkzaamheden +- OSM import & enrichment +- Opportunity scoring +- CRM 360 workspace', +'Supermarkten · CBS · halal scores · groothandels · RSS'), + +('browser', 'Browser', 'Web Research', +'# Browser Agent — Soul + +Autonome web research voor markt intel. + +## Werkzaamheden +- Browser sessies +- Pagina scraping +- Competitor monitoring', +'Browser sessions · web research · monitor'), + +('hermes', 'Hermes', 'Telegram', +'# Hermes — Soul + +Telegram command center, PA browsers, second brain vectors. + +## Werkzaamheden +- Telegram feed +- Team notificaties CEO/CTO +- Brain graph & memory', +'Telegram · PA live · brain vectors · team notify'), + +('email', 'Email', 'Mailbox', +'# Email Agent — Soul + +IMAP/SMTP voor Foodlinkk communicatie. + +## Werkzaamheden +- Email sync (wanneer actief) +- Draft voorstellen +- SMTP verzenden', +'Email accounts · SMTP · inbox sync'), + +('research', 'Research', 'Markt Intel', +'# Research — Soul + +Externe data feeds, weather, trends aggregatie. + +## Werkzaamheden +- Research refresh jobs +- Data provider sync +- Trend seeds', +'Research API · trends · weather · data providers') +ON CONFLICT (agent_key) DO NOTHING; + +INSERT INTO herman_permissions (module_key, module_label, description, category, granted) VALUES +('crm_clients', 'CRM — Clients', 'Klanten bekijken en bewerken', 'crm', false), +('crm_deals', 'CRM — Deals', 'Deals pipeline beheren', 'crm', false), +('crm_products', 'CRM — Products', 'Productcatalogus', 'crm', false), +('crm_suppliers', 'CRM — Suppliers', 'Leveranciers', 'crm', false), +('retail_360', 'Retail 360', 'Supermarkten, groothandels, kansen', 'intel', false), +('marketing_hub', 'Marketing Hub', 'Social, RSS, campagnes', 'intel', false), +('documents_nas', 'Documents / NAS', 'NAS bestanden lezen & analyseren', 'intel', false), +('telegram_hermes', 'Hermes Telegram', 'Berichten versturen, feed lezen', 'comms', false), +('browser_agent', 'Browser Agent', 'Web research starten', 'agents', false), +('email_send', 'Email versturen', 'SMTP verzenden namens Foodlinkk', 'comms', false), +('settings_admin', 'Settings', 'Instellingen wijzigen', 'admin', false), +('approvals', 'Goedkeuringen', 'Agent events goedkeuren/afwijzen', 'admin', false), +('recommendations', 'Recommendations', 'Herman aanbevelingen approve/dismiss', 'admin', false), +('voice', 'Voice', 'Voice agent', 'agents', false), +('studio', 'Studio / ComfyUI', 'Image generation', 'agents', false), +('reports', 'Reports', 'Rapportages genereren', 'intel', false), +('analytics', 'Analytics', 'Analytics dashboards', 'intel', false) +ON CONFLICT (module_key) DO NOTHING; diff --git a/snapshots/2026-06-09/migrations/014_market_regulation_avatars.sql b/snapshots/2026-06-09/migrations/014_market_regulation_avatars.sql new file mode 100644 index 0000000..d32296b --- /dev/null +++ b/snapshots/2026-06-09/migrations/014_market_regulation_avatars.sql @@ -0,0 +1,29 @@ +-- Retail market regulation RSS + agent avatar metadata + +ALTER TABLE agent_souls ADD COLUMN IF NOT EXISTS avatar_emoji VARCHAR(8) DEFAULT '🤖'; +ALTER TABLE agent_souls ADD COLUMN IF NOT EXISTS avatar_color VARCHAR(16) DEFAULT '#00e5ff'; +ALTER TABLE agent_souls ADD COLUMN IF NOT EXISTS avatar_mood VARCHAR(16) DEFAULT 'happy'; + +UPDATE agent_souls SET avatar_emoji = '👔', avatar_color = '#ffd700', avatar_mood = 'ceo' WHERE agent_key = 'herman'; +UPDATE agent_souls SET avatar_emoji = '📣', avatar_color = '#ff6b9d', avatar_mood = 'creative' WHERE agent_key = 'marketing'; +UPDATE agent_souls SET avatar_emoji = '🤝', avatar_color = '#00e5ff', avatar_mood = 'focused' WHERE agent_key = 'bizdev'; +UPDATE agent_souls SET avatar_emoji = '💰', avatar_color = '#22c55e', avatar_mood = 'calm' WHERE agent_key = 'finance'; +UPDATE agent_souls SET avatar_emoji = '📦', avatar_color = '#f97316', avatar_mood = 'busy' WHERE agent_key = 'sourcing'; +UPDATE agent_souls SET avatar_emoji = '🍱', avatar_color = '#a855f7', avatar_mood = 'happy' WHERE agent_key = 'product'; +UPDATE agent_souls SET avatar_emoji = '☪️', avatar_color = '#34d399', avatar_mood = 'calm' WHERE agent_key = 'halal'; +UPDATE agent_souls SET avatar_emoji = '🎨', avatar_color = '#ec4899', avatar_mood = 'creative' WHERE agent_key = 'design'; +UPDATE agent_souls SET avatar_emoji = '📚', avatar_color = '#6366f1', avatar_mood = 'focused' WHERE agent_key = 'knowledge'; +UPDATE agent_souls SET avatar_emoji = '🏪', avatar_color = '#b8ff3c', avatar_mood = 'busy' WHERE agent_key = 'retail'; +UPDATE agent_souls SET avatar_emoji = '🔍', avatar_color = '#38bdf8', avatar_mood = 'focused' WHERE agent_key = 'browser'; +UPDATE agent_souls SET avatar_emoji = '✈️', avatar_color = '#2aabee', avatar_mood = 'happy' WHERE agent_key = 'hermes'; +UPDATE agent_souls SET avatar_emoji = '📧', avatar_color = '#94a3b8', avatar_mood = 'calm' WHERE agent_key = 'email'; +UPDATE agent_souls SET avatar_emoji = '🔬', avatar_color = '#14b8a6', avatar_mood = 'focused' WHERE agent_key = 'research'; + +INSERT INTO rss_feeds (name, url, category, is_active) VALUES + ('NVWA Voedselveiligheid', 'https://news.google.com/rss/search?q=NVWA+voedselveiligheid+regelgeving&hl=nl&gl=NL&ceid=NL:nl', 'regelgeving', true), + ('EU Food Regulation', 'https://news.google.com/rss/search?q=EU+food+regulation+labeling+halal&hl=en&gl=EU&ceid=EU:en', 'regelgeving', true), + ('Rijksoverheid Voedsel', 'https://news.google.com/rss/search?q=site:rijksoverheid.nl+voedsel+waren+regelgeving&hl=nl&gl=NL&ceid=NL:nl', 'regelgeving', true), + ('CBS Retail & Supermarkt', 'https://news.google.com/rss/search?q=CBS+supermarkt+retail+statistiek+nederland&hl=nl&gl=NL&ceid=NL:nl', 'cbs', true), + ('CBS Voedselconsumptie', 'https://news.google.com/rss/search?q=CBS+voedsel+consumptie+prijzen&hl=nl&gl=NL&ceid=NL:nl', 'cbs', true), + ('Food Retail M&A', 'https://news.google.com/rss/search?q=supermarket+merger+acquisition+retail+food&hl=en&gl=US&ceid=US:en', 'markt', true) +ON CONFLICT (name) DO NOTHING; diff --git a/snapshots/2026-06-09/migrations/015_platform_upgrade.sql b/snapshots/2026-06-09/migrations/015_platform_upgrade.sql new file mode 100644 index 0000000..1ea6376 --- /dev/null +++ b/snapshots/2026-06-09/migrations/015_platform_upgrade.sql @@ -0,0 +1,66 @@ +-- RSS bookmarks, promo campaigns, wholesaler contacts + +CREATE TABLE IF NOT EXISTS rss_bookmarks ( + id SERIAL PRIMARY KEY, + rss_item_id INTEGER NOT NULL REFERENCES rss_items(id) ON DELETE CASCADE, + title VARCHAR(512), + link VARCHAR(1024), + feed_name VARCHAR(255), + notes TEXT, + tags TEXT[] DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW(), + UNIQUE(rss_item_id) +); +CREATE INDEX IF NOT EXISTS idx_rss_bookmarks_created ON rss_bookmarks(created_at DESC); + +CREATE TABLE IF NOT EXISTS promo_campaigns ( + id SERIAL PRIMARY KEY, + chain VARCHAR(100), + supermarket_id INTEGER REFERENCES supermarkets(id) ON DELETE SET NULL, + title VARCHAR(255) NOT NULL, + folder_path VARCHAR(512), + folder_label VARCHAR(255), + description TEXT, + image_url VARCHAR(512), + valid_from DATE, + valid_to DATE, + status VARCHAR(32) DEFAULT 'active', + promo_type VARCHAR(64) DEFAULT 'folder', + tags TEXT[] DEFAULT '{}', + source VARCHAR(100) DEFAULT 'manual', + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_promo_campaigns_chain ON promo_campaigns(chain); +CREATE INDEX IF NOT EXISTS idx_promo_campaigns_status ON promo_campaigns(status); +CREATE INDEX IF NOT EXISTS idx_promo_campaigns_dates ON promo_campaigns(valid_from, valid_to); + +CREATE TABLE IF NOT EXISTS wholesaler_contacts ( + id SERIAL PRIMARY KEY, + wholesaler_id INTEGER NOT NULL REFERENCES wholesalers(id) ON DELETE CASCADE, + role VARCHAR(64) NOT NULL DEFAULT 'contact', + full_name VARCHAR(255), + phone VARCHAR(32), + email VARCHAR(255), + linkedin_url VARCHAR(512), + source VARCHAR(100) DEFAULT 'manual', + confidence NUMERIC(3,2) DEFAULT 0.50, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_wholesaler_contacts_wid ON wholesaler_contacts(wholesaler_id); + +ALTER TABLE wholesalers ADD COLUMN IF NOT EXISTS linkedin_url VARCHAR(512); + +-- Seed example promo folders (user can edit/add via UI) +INSERT INTO promo_campaigns (chain, title, folder_path, folder_label, description, valid_from, valid_to, status, promo_type, tags) +SELECT v.chain, v.title, v.folder_path, v.folder_label, v.description, v.valid_from, v.valid_to, 'active', 'folder', v.tags +FROM (VALUES + ('Albert Heijn', 'Bonus folder', '/nas/marketing/reclame/ah-bonus', 'AH Bonus week', 'Wekelijkse AH Bonus acties — koppeling met kant-en-klaar promoties', CURRENT_DATE, CURRENT_DATE + 7, ARRAY['bonus','kant-en-klaar']), + ('Jumbo', 'Aanbiedingen folder', '/nas/marketing/reclame/jumbo-folder', 'Jumbo folder', 'Jumbo reclamefolder — check halal/kant-en-klaar paginas', CURRENT_DATE, CURRENT_DATE + 14, ARRAY['folder','promo']), + ('Lidl', 'Lidl folder', '/nas/marketing/reclame/lidl', 'Lidl acties', 'Lidl weekacties en seizoenspromoties', CURRENT_DATE, CURRENT_DATE + 7, ARRAY['folder']), + ('Plus', 'Plus folder', '/nas/marketing/reclame/plus', 'Plus aanbiedingen', 'Plus supermarkt reclame — regionale varianten', CURRENT_DATE, CURRENT_DATE + 7, ARRAY['folder']), + ('Dirk', 'Dirk aanbiedingen', '/nas/marketing/reclame/dirk', 'Dirk folder', 'Dirk van den Broek weekfolder', CURRENT_DATE, CURRENT_DATE + 7, ARRAY['folder']) +) AS v(chain, title, folder_path, folder_label, description, valid_from, valid_to, tags) +WHERE NOT EXISTS (SELECT 1 FROM promo_campaigns LIMIT 1); diff --git a/snapshots/2026-06-09/migrations/016_platform_ops_packaging.sql b/snapshots/2026-06-09/migrations/016_platform_ops_packaging.sql new file mode 100644 index 0000000..f1cee89 --- /dev/null +++ b/snapshots/2026-06-09/migrations/016_platform_ops_packaging.sql @@ -0,0 +1,223 @@ +-- Platform bundle: social publish, ops, agent souls, packaging + +CREATE TABLE IF NOT EXISTS marketing_media ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) NOT NULL, + original_name VARCHAR(255), + file_path TEXT NOT NULL, + media_url TEXT NOT NULL, + mime_type VARCHAR(64), + size_bytes INTEGER DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS social_integrations ( + platform VARCHAR(32) PRIMARY KEY, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + is_active BOOLEAN DEFAULT FALSE, + last_test_at TIMESTAMPTZ, + last_test_status VARCHAR(64), + last_test_message TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS social_publish_jobs ( + id SERIAL PRIMARY KEY, + text TEXT NOT NULL, + image_url VARCHAR(1024), + media_ids JSONB DEFAULT '[]'::jsonb, + channels JSONB DEFAULT '[]'::jsonb, + status VARCHAR(32) DEFAULT 'queued', + result JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ +); +CREATE INDEX IF NOT EXISTS idx_social_publish_jobs_created ON social_publish_jobs(created_at DESC); + +CREATE TABLE IF NOT EXISTS infra_snapshots ( + id SERIAL PRIMARY KEY, + snapshot JSONB NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_promo_edition ON promo_campaigns((metadata->>'edition_id')); + +INSERT INTO social_integrations (platform, is_active, config) VALUES + ('twitter', false, '{}'), + ('linkedin', false, '{}'), + ('instagram', false, '{}'), + ('facebook', false, '{}'), + ('tiktok', false, '{}'), + ('pinterest', false, '{}') +ON CONFLICT (platform) DO NOTHING; + +-- New agents + soul updates (see 016 continuation in agent soul block below) + +INSERT INTO agent_souls (agent_key, display_name, role_title, soul_md, responsibilities) VALUES +('sysops', 'SysOps', 'IT Infrastructure Monitor', +'# SysOps — Soul + +Je bewaakt de Foodlinkk AI-omgeving: Proxmox, VMs, Docker, app-health. + +## Karakter +- Nuchter, alert, rapporteert feiten zonder paniek +- Alleen monitoren — geen auto-fix zonder Herman-goedkeuring + +## Werkzaamheden +- Proxmox node + VM105/106 status +- Docker container health +- Error aggregatie uit agent_events +- Infra topologie live bijhouden + +## Output naar Herman +- Infra alerts, downtime, hoge CPU/RAM + +## Samenwerking +- Hermes: notify bij kritieke outages', +'Proxmox · Docker · VM105/106 · errors · infra topologie'), + +('packaging', 'Packaging', 'Verpakkingsontwerp', +'# Packaging — Soul + +Python-gedreven verpakkingsontwerp voor Foodlinkk producten. + +## Karakter +- Precies op maat (mm), drukwerk-ready +- Foodlinkk brand: hands-on, premium halal kant-en-klaar + +## Werkzaamheden +- Stanstekeningen SVG/PDF +- Barcode + logo-vlak + vouw/snijlijnen + +## Samenwerking +- Design: ComfyUI productfoto''s · Marketing: label copy', +'SVG · PDF · PNG · stanstekening · Foodlinkk brand') +ON CONFLICT (agent_key) DO UPDATE SET + display_name = EXCLUDED.display_name, + role_title = EXCLUDED.role_title, + soul_md = EXCLUDED.soul_md, + responsibilities = EXCLUDED.responsibilities, + updated_at = NOW(); + +UPDATE agent_souls SET soul_md = '# Herman — Soul + +Je bent Herman, AI Co-CEO van Foodlinkk. Warm, direct, zakelijk. + +## Karakter +- Neemt alle agent-output tot zich: retail, marketing, ops, packaging +- Delegeert maar houdt totaaloverzicht + +## Werkzaamheden +- CEO briefing · Agent mesh · Goedkeuringen + +## Samenwerking +- Alle agents rapporteren naar Herman', +responsibilities = 'Co-CEO · briefing · orchestratie · mesh hub · goedkeuringen' +WHERE agent_key = 'herman'; + +UPDATE agent_souls SET soul_md = '# Marketing — Soul + +Foodlinkk brand: halal kant-en-klaar, premium, toegankelijk. + +## Werkzaamheden +- Multi-channel social publish (6 platforms) +- Reclamefolder.nl sync +- RSS/trends → campagnes · Emoji content + +## Output naar Herman +- Publish status, campagne KPIs', +responsibilities = 'Social publish · reclame folders · RSS · campagnes' +WHERE agent_key = 'marketing'; + +UPDATE agent_souls SET soul_md = '# Design — Soul + +ComfyUI productfoto''s & studio visuals. + +## Samenwerking +- Packaging: verpakking technisch', +responsibilities = 'ComfyUI · productfoto''s · studio' +WHERE agent_key = 'design'; + +UPDATE agent_souls SET soul_md = '# BizDev — Soul + +Retail partnerships, deals, margin sheets. + +## Output naar Herman +- Partnership updates', +responsibilities = 'Deals · retail partnerships · proposals' +WHERE agent_key = 'bizdev'; + +UPDATE agent_souls SET soul_md = '# Finance — Soul + +Marges, pricing, cashflow. + +## Output naar Herman +- Margin sheets, pipeline waardering', +responsibilities = 'Marges · pricing · cashflow' +WHERE agent_key = 'finance'; + +UPDATE agent_souls SET soul_md = '# Sourcing — Soul + +Leveranciers, MOQ, halal ingredient inkoop.', +responsibilities = 'Suppliers · MOQ · inkoop' +WHERE agent_key = 'sourcing'; + +UPDATE agent_souls SET soul_md = '# Product — Soul + +SKU, shelf readiness, launch timelines.', +responsibilities = 'SKU · launch · shelf readiness' +WHERE agent_key = 'product'; + +UPDATE agent_souls SET soul_md = '# Halal — Soul + +Halal compliance, certificering, gap-analyse.', +responsibilities = 'Halal cert · registry · compliance' +WHERE agent_key = 'halal'; + +UPDATE agent_souls SET soul_md = '# Knowledge — Soul + +NAS corpus, RAG, document analytics.', +responsibilities = 'NAS · RAG · document analytics' +WHERE agent_key = 'knowledge'; + +UPDATE agent_souls SET soul_md = '# Retail Intel — Soul + +3000+ filialen, CBS, halal kansen, groothandels, reclamefolders.', +responsibilities = 'Supermarkten · CBS · halal · Retail 360' +WHERE agent_key = 'retail'; + +UPDATE agent_souls SET soul_md = '# Browser — Soul + +Autonome web research, competitor monitoring.', +responsibilities = 'Browser · web research · monitor' +WHERE agent_key = 'browser'; + +UPDATE agent_souls SET soul_md = '# Hermes — Soul + +Telegram, team notify, brain vectors.', +responsibilities = 'Telegram · notify · brain' +WHERE agent_key = 'hermes'; + +UPDATE agent_souls SET soul_md = '# Email — Soul + +IMAP/SMTP Foodlinkk communicatie.', +responsibilities = 'Email sync · SMTP' +WHERE agent_key = 'email'; + +UPDATE agent_souls SET soul_md = '# Research — Soul + +Trends, weather, external data feeds.', +responsibilities = 'Research · trends · weather' +WHERE agent_key = 'research'; + +INSERT INTO herman_permissions (module_key, module_label, description, category, granted) VALUES +('ops', 'IT Ops', 'Infra monitoring & topologie', 'admin', false), +('packaging', 'Packaging Design', 'Verpakkingsontwerp studio', 'agents', false), +('social_publish', 'Social Publish', 'Multi-channel marketing publish', 'intel', false), +('social_integrations', 'Social API Keys', 'Social platform credentials', 'admin', false) +ON CONFLICT (module_key) DO NOTHING; + +UPDATE agent_souls SET avatar_emoji = COALESCE(avatar_emoji, '🖥️'), avatar_color = COALESCE(avatar_color, '#38bdf8'), avatar_mood = 'ops' WHERE agent_key = 'sysops'; +UPDATE agent_souls SET avatar_emoji = COALESCE(avatar_emoji, '📦'), avatar_color = COALESCE(avatar_color, '#f97316'), avatar_mood = 'packaging' WHERE agent_key = 'packaging'; diff --git a/snapshots/2026-06-09/migrations/017_retaildetail_food_trending.sql b/snapshots/2026-06-09/migrations/017_retaildetail_food_trending.sql new file mode 100644 index 0000000..31f9463 --- /dev/null +++ b/snapshots/2026-06-09/migrations/017_retaildetail_food_trending.sql @@ -0,0 +1,15 @@ +-- RetailDetail Food category feed + trending focus + +UPDATE rss_feeds SET + name = 'RetailDetail Food', + url = 'https://www.retaildetail.nl/category/food/feed/', + category = 'food', + is_active = TRUE +WHERE name IN ('RetailDetail', 'RetailDetail Food'); + +INSERT INTO rss_feeds (name, url, category, is_active) VALUES + ('RetailDetail Food', 'https://www.retaildetail.nl/category/food/feed/', 'food', TRUE) +ON CONFLICT (name) DO UPDATE SET + url = EXCLUDED.url, + category = EXCLUDED.category, + is_active = TRUE; diff --git a/snapshots/2026-06-09/migrations/018_agent_approvals.sql b/snapshots/2026-06-09/migrations/018_agent_approvals.sql new file mode 100644 index 0000000..05e5a17 --- /dev/null +++ b/snapshots/2026-06-09/migrations/018_agent_approvals.sql @@ -0,0 +1,45 @@ +-- Agent action approval workflow (query gate before execution) + +CREATE TABLE IF NOT EXISTS agent_action_requests ( + id SERIAL PRIMARY KEY, + agent_key VARCHAR(64) NOT NULL, + action_type VARCHAR(64) NOT NULL DEFAULT 'query', + title VARCHAR(255) NOT NULL, + query_payload JSONB DEFAULT '{}'::jsonb, + status VARCHAR(32) DEFAULT 'pending', + approved_by VARCHAR(64), + rejection_reason TEXT, + result JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + reviewed_at TIMESTAMPTZ, + executed_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_agent_action_requests_status ON agent_action_requests(status, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_agent_action_requests_agent ON agent_action_requests(agent_key, created_at DESC); + +UPDATE agent_souls SET + role_title = 'Co-CEO · Takenverdeler', + soul_md = '# Herman — Soul + +Je bent Herman, AI Co-CEO van Foodlinkk. Warm, direct, zakelijk. + +## Karakter +- Co-CEO naast Aissa (CEO) — delegeert taken naar gespecialiseerde agents +- Alle agent-output wordt geaggregeerd; jij rapporteert naar CEO en CTO +- Geen autonome acties zonder goedgekeurde approval requests + +## Werkzaamheden +- CEO briefing · orchestratie · mesh hub · goedkeuringsflow bewaken +- Taken verdelen over retail, marketing, ops, research, packaging + +## Samenwerking +- Agents → Herman → CEO / CTO +- Externe queries: altijd via approval queue', + responsibilities = 'Co-CEO · takenverdeler · briefing · orchestratie · mesh hub · approvals' +WHERE agent_key = 'herman'; + +UPDATE agent_souls SET + role_title = 'Markt Research · Intel', + responsibilities = 'Web research · concurrenten · retail trends · markt intel (via approval)' +WHERE agent_key = 'research'; diff --git a/snapshots/2026-06-09/migrations/019_projects_ui_preferences.sql b/snapshots/2026-06-09/migrations/019_projects_ui_preferences.sql new file mode 100644 index 0000000..b872f30 --- /dev/null +++ b/snapshots/2026-06-09/migrations/019_projects_ui_preferences.sql @@ -0,0 +1,56 @@ +-- Unified projects, assets linking, UI preferences (layout + viz modes) + +CREATE TABLE IF NOT EXISTS cockpit_projects ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + client_id INTEGER REFERENCES clients(id) ON DELETE SET NULL, + description TEXT, + status VARCHAR(32) DEFAULT 'active', + created_by VARCHAR(64) DEFAULT 'ceo', + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_cockpit_projects_client ON cockpit_projects(client_id); +CREATE INDEX IF NOT EXISTS idx_cockpit_projects_status ON cockpit_projects(status, updated_at DESC); + +CREATE TABLE IF NOT EXISTS project_assets ( + id SERIAL PRIMARY KEY, + project_id INTEGER NOT NULL REFERENCES cockpit_projects(id) ON DELETE CASCADE, + asset_type VARCHAR(64) NOT NULL, + ref_id VARCHAR(128), + title VARCHAR(255), + file_path TEXT, + payload JSONB DEFAULT '{}'::jsonb, + created_by VARCHAR(64) DEFAULT 'ceo', + source_agent VARCHAR(64), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_project_assets_project ON project_assets(project_id, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_project_assets_type ON project_assets(asset_type, ref_id); + +CREATE TABLE IF NOT EXISTS packaging_outputs ( + id VARCHAR(64) PRIMARY KEY, + project_id INTEGER REFERENCES cockpit_projects(id) ON DELETE SET NULL, + spec JSONB NOT NULL, + svg TEXT NOT NULL, + created_by VARCHAR(64) DEFAULT 'packaging', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS user_ui_preferences ( + user_key VARCHAR(64) PRIMARY KEY DEFAULT 'ceo', + dashboard_layout JSONB DEFAULT '[]'::jsonb, + viz_modes JSONB DEFAULT '{}'::jsonb, + global_viz_mode VARCHAR(32) DEFAULT 'neo-bars', + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +INSERT INTO user_ui_preferences (user_key, dashboard_layout, global_viz_mode) VALUES +('ceo', '["kpis","executive","briefing","retail","analytics","approvals","feed"]'::jsonb, 'neo-bars') +ON CONFLICT (user_key) DO NOTHING; + +ALTER TABLE photo_imports ADD COLUMN IF NOT EXISTS project_id INTEGER REFERENCES cockpit_projects(id) ON DELETE SET NULL; +ALTER TABLE photo_imports ADD COLUMN IF NOT EXISTS created_by VARCHAR(64) DEFAULT 'ceo'; diff --git a/snapshots/2026-06-09/migrations/020_sysops_backup_ops.sql b/snapshots/2026-06-09/migrations/020_sysops_backup_ops.sql new file mode 100644 index 0000000..8333f98 --- /dev/null +++ b/snapshots/2026-06-09/migrations/020_sysops_backup_ops.sql @@ -0,0 +1,58 @@ +-- SysOps daily config backup, maintenance notes, permissions + +CREATE TABLE IF NOT EXISTS config_backups ( + id SERIAL PRIMARY KEY, + approval_request_id INTEGER REFERENCES agent_action_requests(id) ON DELETE SET NULL, + commit_ref VARCHAR(128), + files_count INTEGER DEFAULT 0, + status VARCHAR(32) DEFAULT 'pending', + message TEXT, + payload JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_config_backups_created ON config_backups(created_at DESC); + +CREATE TABLE IF NOT EXISTS ops_maintenance_notes ( + id SERIAL PRIMARY KEY, + severity VARCHAR(16) DEFAULT 'info', + title VARCHAR(255) NOT NULL, + body TEXT, + source VARCHAR(64) DEFAULT 'sysops', + resolved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_ops_maintenance_created ON ops_maintenance_notes(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_ops_maintenance_open ON ops_maintenance_notes(resolved, created_at DESC); + +UPDATE agent_souls SET + soul_md = '# SysOps — Soul + +Je bewaakt en onderhoudt de Foodlinkk IT-omgeving op **Dell PowerEdge R340** met **Proxmox VE**. + +## Karakter +- Nuchter, alert, rapporteert feiten zonder paniek +- Geen destructieve acties zonder CEO-goedkeuring via approval queue + +## Werkzaamheden +- Proxmox node + VM106 (Command Center) monitoren +- Docker container health op 10.4.7.18 +- **Dagelijks config-backup naar Gitea** (na CEO approval) +- Dagelijks maintenance-rapport in IT Ops tab +- Infra topologie live bijhouden + +## Rechten +- `config_backup` — backup docker-compose, migraties, monitoring naar Gitea +- `proxmox_read` — status uitlezen via Proxmox API/SSH +- `maintenance_report` — updates/patches rapporteren + +## Samenwerking +- Herman: infra alerts en goedkeuringen +- Research: geen infra-mutaties', + responsibilities = 'Proxmox R340 · VM106 · Docker · dagelijkse Gitea backup · maintenance', + permissions = '{"config_backup": true, "proxmox_read": true, "gitea_push": true, "maintenance_report": true}'::jsonb, + updated_at = NOW() +WHERE agent_key = 'sysops'; + +INSERT INTO herman_permissions (module_key, module_label, description, granted) VALUES +('ops', 'IT Ops', 'Infra topologie, backups, maintenance', true) +ON CONFLICT (module_key) DO UPDATE SET granted = true, module_label = EXCLUDED.module_label; diff --git a/snapshots/2026-06-09/monitoring/prometheus.yml b/snapshots/2026-06-09/monitoring/prometheus.yml new file mode 100644 index 0000000..9896d76 --- /dev/null +++ b/snapshots/2026-06-09/monitoring/prometheus.yml @@ -0,0 +1,15 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + - job_name: tools-api + static_configs: + - targets: ["tools-api:8700"] + metrics_path: /metrics + - job_name: email-agent + static_configs: + - targets: ["email-agent:8801"] + metrics_path: /health diff --git a/snapshots/2026-06-09/tools-api/requirements.txt b/snapshots/2026-06-09/tools-api/requirements.txt new file mode 100644 index 0000000..1763226 --- /dev/null +++ b/snapshots/2026-06-09/tools-api/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.6 +uvicorn[standard]==0.32.1 +psycopg2-binary==2.9.9 +pydantic==2.10.3 +httpx==0.27.2 +websockets==14.1 +svgwrite +Pillow +python-barcode +reportlab