SysOps config backup 2026-06-09 01:38 UTC

This commit is contained in:
SysOps Agent
2026-06-09 01:38:01 +00:00
parent 5d60d33db1
commit 69fe67cc0e
28 changed files with 1999 additions and 0 deletions
+23
View File
@@ -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=
+42
View File
@@ -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
+6
View File
@@ -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
}
@@ -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
+63
View File
@@ -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 <<REMOTE
set -euo pipefail
cd "$REMOTE_DIR"
for mig in migrations/*.sql; do
echo "Applying \$(basename "\$mig")..."
docker exec -i foodlinkk_db psql -U aissa -d foodlinkk < "\$mig" || true
done
docker compose build tools-api cockpit email-agent browser-agent
docker compose up -d
echo "Waiting for services..."
sleep 15
curl -sf http://127.0.0.1:8700/health && echo " tools-api OK" || echo " tools-api FAIL"
curl -sf -o /dev/null -w "cockpit:%{http_code}\n" http://127.0.0.1:8600/
curl -sf -o /dev/null -w "marketing:%{http_code}\n" http://127.0.0.1:8600/marketing
curl -sf -o /dev/null -w "ops:%{http_code}\n" http://127.0.0.1:8600/ops
curl -sf -o /dev/null -w "packaging:%{http_code}\n" http://127.0.0.1:8600/packaging
curl -sf -o /dev/null -w "agents:%{http_code}\n" http://127.0.0.1:8600/agents
echo "=== SysOps daily cron (07:00) ==="
(crontab -l 2>/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"
+144
View File
@@ -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:
@@ -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');
@@ -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');
@@ -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;
@@ -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'
);
@@ -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;
@@ -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;
@@ -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);
@@ -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;
@@ -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');
@@ -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;
@@ -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;
@@ -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%'
]);
@@ -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;
@@ -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;
@@ -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);
@@ -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';
@@ -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;
@@ -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';
@@ -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';
@@ -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;
@@ -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
@@ -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