# PostgreSQL + TimescaleDB Setup

Einrichtung von PostgreSQL mit TimescaleDB auf dem RMM-Backend-Server (Debian 13 Trixie).

> **Hinweis:** Das Backend legt alle Tabellen beim ersten Start **automatisch an** (Auto-Migration inkl. ALTER TABLE für neue Spalten). Das Schema muss **nicht** manuell erstellt werden. Diese Datei dient als Referenz und Installationsanleitung.

---

## 1. PostgreSQL installieren

> **Wichtig für Debian 13 (Trixie):** Das Standard-Debian-Repository liefert PostgreSQL 17.8,
> TimescaleDB benötigt aber mindestens 17.9. Deshalb muss zuerst das **offizielle
> PostgreSQL-Apt-Repository** eingerichtet werden — das liefert immer die aktuelle Version.

```bash
apt update
apt install -y curl gnupg

# Offizielles PostgreSQL-Repository einrichten
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
  | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg

echo "deb https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \
  > /etc/apt/sources.list.d/pgdg.list

apt update
apt install -y postgresql-17

systemctl enable postgresql
systemctl start postgresql

# Version prüfen — muss 17.9 oder neuer zeigen
psql --version
```

---

## 2. TimescaleDB installieren

```bash
# TimescaleDB-Repository einrichten
echo "deb https://packagecloud.io/timescale/timescaledb/debian/ bookworm main" \
  > /etc/apt/sources.list.d/timescaledb.list
curl -sL https://packagecloud.io/timescale/timescaledb/gpgkey \
  | gpg --dearmor > /etc/apt/trusted.gpg.d/timescaledb.gpg
apt update

# TimescaleDB für PostgreSQL 17 installieren
apt install -y timescaledb-2-postgresql-17

# In postgresql.conf aktivieren
sed -i "s/#shared_preload_libraries = ''/shared_preload_libraries = 'timescaledb'/" \
  /etc/postgresql/17/main/postgresql.conf

systemctl restart postgresql
```

> **Falls der Install mit einem Abhängigkeitsfehler fehlschlägt** (`postgresql-17 >= 17.9 required`):
> Das bedeutet, dass noch das Debian-eigene PostgreSQL 17.8 installiert ist.
> Lösung:
> ```bash
> apt remove -y postgresql* && apt autoremove -y
> # Dann Schritt 1 (PostgreSQL aus dem pgdg-Repo) wiederholen
> ```

---

## 3. Datenbank und User anlegen

```bash
sudo -u postgres psql << 'EOF'
CREATE DATABASE rmm;
CREATE USER rmm WITH PASSWORD 'SICHERES_PASSWORT';
GRANT ALL PRIVILEGES ON DATABASE rmm TO rmm;
\c rmm
GRANT ALL ON SCHEMA public TO rmm;
CREATE EXTENSION IF NOT EXISTS timescaledb;
\dx
EOF
```

---

## 4. pg_hba.conf anpassen

```bash
echo "host rmm rmm 127.0.0.1/32 scram-sha-256" >> /etc/postgresql/17/main/pg_hba.conf
systemctl reload postgresql
```

---

## 5. Verbindung testen

```bash
psql -h 127.0.0.1 -U rmm -d rmm -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'timescaledb';"
```

---

## 6. Schema (Referenz — wird automatisch angelegt)

### `agents` — Agent-Registry

```sql
CREATE TABLE agents (
    id                TEXT        PRIMARY KEY,
    name              TEXT        NOT NULL,
    hostname          TEXT        NOT NULL,
    ip                TEXT        NOT NULL,
    opnsense_version  TEXT        NOT NULL DEFAULT '',
    agent_version     TEXT        NOT NULL DEFAULT '',
    platform          TEXT        NOT NULL DEFAULT 'freebsd',
    registered_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_heartbeat    TIMESTAMPTZ,
    customer_id       INTEGER     REFERENCES customers(id),
    update_requested  BOOLEAN     NOT NULL DEFAULT FALSE
);
CREATE INDEX idx_agents_customer ON agents(customer_id);
```

### `customers` — Kundenstammdaten

```sql
CREATE TABLE customers (
    id         SERIAL      PRIMARY KEY,
    number     TEXT        NOT NULL UNIQUE,   -- z.B. "K05068"
    name       TEXT        NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### `users` — Frontend-Benutzer

```sql
CREATE TABLE users (
    id            SERIAL      PRIMARY KEY,
    username      TEXT        NOT NULL UNIQUE,
    password_hash TEXT        NOT NULL,       -- bcrypt
    display_name  TEXT        NOT NULL DEFAULT '',
    role          TEXT        NOT NULL DEFAULT 'admin',
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_login    TIMESTAMPTZ
);
-- Standard-Admin: admin / Start!123 (beim ersten Backend-Start angelegt)
```

### `system_data` — Aktueller Systemzustand pro Agent

```sql
CREATE TABLE system_data (
    agent_id   TEXT        PRIMARY KEY REFERENCES agents(id) ON DELETE CASCADE,
    data_json  JSONB       NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### `config_backups` — Versionierte OPNsense-Configs

```sql
CREATE TABLE config_backups (
    id         SERIAL      PRIMARY KEY,
    agent_id   TEXT        NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    hash       TEXT        NOT NULL,   -- SHA256 (Deduplizierung)
    size       INTEGER     NOT NULL,
    config_xml TEXT        NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_config_backups_agent ON config_backups(agent_id, created_at DESC);
CREATE INDEX idx_config_backups_hash  ON config_backups(agent_id, hash);
```

### `agent_events` — Online/Offline-Ereignisse

```sql
CREATE TABLE agent_events (
    id         SERIAL      PRIMARY KEY,
    agent_id   TEXT        NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    event_type TEXT        NOT NULL,   -- "online","offline","stale","connected","disconnected"
    message    TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_agent_events_agent ON agent_events(agent_id, created_at DESC);
```

### `agent_firmware` — Agent-Binaries

```sql
CREATE TABLE agent_firmware (
    version     TEXT        NOT NULL,
    platform    TEXT        NOT NULL DEFAULT 'freebsd',  -- "freebsd","linux","windows"
    hash        TEXT        NOT NULL,   -- SHA256
    binary_data BYTEA       NOT NULL,
    uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (version, platform)
);
```

### `tasks` — Task-Scheduler

```sql
CREATE TABLE tasks (
    id                   SERIAL      PRIMARY KEY,
    agent_id             TEXT        NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    name                 TEXT        DEFAULT '',
    action               TEXT        NOT NULL,             -- "update","backup","reboot",...
    status               TEXT        NOT NULL DEFAULT 'scheduled',
    schedule_type        TEXT        DEFAULT 'once',       -- "once","daily","weekly","monthly"
    schedule_time        TEXT        DEFAULT '02:00',
    schedule_weekday     INT,                              -- 0=So..6=Sa
    schedule_monthday    INT,                              -- 1-31
    reboot               BOOLEAN     DEFAULT FALSE,
    health_check         BOOLEAN     DEFAULT FALSE,
    health_check_timeout INT         DEFAULT 600,
    active               BOOLEAN     DEFAULT TRUE,
    scheduled_at         TIMESTAMPTZ NOT NULL,
    started_at           TIMESTAMPTZ,
    finished_at          TIMESTAMPTZ,
    last_run             TIMESTAMPTZ,
    next_run             TIMESTAMPTZ,
    run_count            INT         DEFAULT 0,
    result               JSONB,
    webhook_url          TEXT,
    webhook_sent         BOOLEAN     DEFAULT FALSE,
    webhook_response     TEXT,
    created_by           TEXT,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tasks_status_scheduled ON tasks(status, scheduled_at);
CREATE INDEX idx_tasks_active_next_run  ON tasks(active, next_run) WHERE active = TRUE;
```

### `api_keys` — API-Keys

```sql
CREATE TABLE api_keys (
    id         SERIAL      PRIMARY KEY,
    name       TEXT        NOT NULL,
    key        TEXT        NOT NULL UNIQUE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    last_used  TIMESTAMPTZ
);
```

### `installers` — OPNsense Plugin-Pakete

```sql
CREATE TABLE installers (
    id          SERIAL      PRIMARY KEY,
    platform    TEXT        NOT NULL UNIQUE,
    filename    TEXT        NOT NULL,
    data        BYTEA       NOT NULL,
    sha256      TEXT        NOT NULL,
    size        INTEGER     NOT NULL,
    uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### `audit_log` — Audit-Protokoll

```sql
CREATE TABLE audit_log (
    id          SERIAL      PRIMARY KEY,
    timestamp   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    user_name   TEXT        NOT NULL,
    action      TEXT        NOT NULL,
    target_type TEXT,
    target_id   TEXT,
    target_name TEXT,
    details     TEXT,
    source_ip   TEXT
);
CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp DESC);
```

### `system_settings` — Backend-Einstellungen

```sql
CREATE TABLE system_settings (
    key        TEXT PRIMARY KEY,   -- "backend_url_internal","backend_url_external","api_key"
    value      TEXT NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### `metrics` — TimescaleDB Hypertable

```sql
CREATE TABLE metrics (
    time     TIMESTAMPTZ      NOT NULL,
    agent_id TEXT             NOT NULL,
    metric   TEXT             NOT NULL,
    value    DOUBLE PRECISION NOT NULL,
    tags     JSONB
);
SELECT create_hypertable('metrics', 'time', if_not_exists => TRUE);
CREATE INDEX idx_metrics_agent_metric ON metrics(agent_id, metric, time DESC);

-- Retention: Rohdaten nach 90 Tagen löschen
SELECT add_retention_policy('metrics', INTERVAL '90 days');

-- Compression: Daten älter als 7 Tage komprimieren
ALTER TABLE metrics SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'agent_id, metric',
    timescaledb.compress_orderby = 'time DESC'
);
SELECT add_compression_policy('metrics', INTERVAL '7 days');
```

---

## 7. Nützliche Abfragen

```sql
-- Alle Agents mit Status
SELECT id, name, hostname, ip, opnsense_version, agent_version,
       last_heartbeat,
       CASE
         WHEN last_heartbeat > NOW() - INTERVAL '2 min'  THEN 'online'
         WHEN last_heartbeat > NOW() - INTERVAL '5 min'  THEN 'stale'
         ELSE 'offline'
       END AS status
FROM agents ORDER BY name;

-- CPU-Auslastung letzte 24h (5-Minuten-Durchschnitt)
SELECT time_bucket('5 minutes', time) AS t, agent_id, AVG(value) AS cpu_avg
FROM metrics
WHERE metric = 'cpu_usage' AND time > NOW() - INTERVAL '24 hours'
GROUP BY t, agent_id ORDER BY t;

-- Disk-Warnung: > 80% belegt
SELECT agent_id, tags->>'mountpoint' AS mount, value AS used_pct, time
FROM metrics
WHERE metric = 'disk_used_percent' AND value > 80
  AND time > NOW() - INTERVAL '1 hour'
ORDER BY value DESC;

-- Offene Tasks
SELECT t.id, a.name AS agent, t.action, t.status, t.scheduled_at, t.webhook_url
FROM tasks t JOIN agents a ON t.agent_id = a.id
WHERE t.status IN ('scheduled','running')
ORDER BY t.scheduled_at;

-- Datenbankgrösse
SELECT pg_size_pretty(pg_database_size('rmm')) AS db_size;
SELECT relname AS tabelle, pg_size_pretty(pg_total_relation_size(relid)) AS groesse
FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC;
```

---

## Übersicht

| Tabelle | Typ | Beschreibung |
|---------|-----|-------------|
| `agents` | Relational | Agent-Registry |
| `customers` | Relational | Kundenstammdaten |
| `users` | Relational | Frontend-Benutzer |
| `system_data` | Relational | Aktueller Systemzustand (JSONB) |
| `config_backups` | Relational | Versionierte OPNsense-Configs |
| `agent_events` | Relational | Online/Offline-Ereignisse |
| `agent_firmware` | Relational | Agent-Binaries pro Plattform |
| `tasks` | Relational | Task-Scheduler mit Webhook |
| `api_keys` | Relational | API-Keys |
| `installers` | Relational | OPNsense Plugin-Pakete |
| `audit_log` | Relational | Audit-Protokoll |
| `system_settings` | Relational | Backend-Einstellungen (Key/Value) |
| `metrics` | TimescaleDB | Zeitreihen (CPU, RAM, Disk, Network, Gateway) |
