Files
mc-webui/app/schema.sql
T
MarekWo ca2a6bacf9 feat(repeaters): My Repeaters panel (stage 1)
New full-screen panel (main menu / FAB) for repeater administration:
- repeaters table (saved per-pubkey admin password, login metadata);
  plaintext per observer_brokers precedent (single-user LAN app)
- REST /api/repeaters: list merged with device contact truth, add
  (device REP contacts only), set/clear password, remove, login
- device_manager: _repeater_lock serializes all repeater ops (companion
  firmware has a single pending-request slot); repeater_login now
  filters LOGIN_SUCCESS by pubkey_prefix and captures is_admin/
  permissions into an in-memory session store
- login timeout UX: wrong password and unreachable repeater are
  indistinguishable (firmware stays silent) - error message names both
- panel UI: add-picker, password modal (remembered password), per-
  repeater path editor reusing /api/contacts/<pk>/paths + Leaflet
  repeater map picker (adapted from the DM panel)
- meshcore pin bumped to >=2.3.7 (send_login_sync era API, fixed
  LOGIN_SUCCESS/LOGIN_FAILED parsing; container already ships 2.3.7)

Stage 2 (management panel with Status/Telemetry/Neighbors/CLI/
Settings/Actions tools) follows after user review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:55:44 +02:00

298 lines
13 KiB
SQL

-- mc-webui v2 SQLite Schema
-- WAL mode and foreign keys are enabled programmatically in Database.__init__
-- Schema versioning for future migrations
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
-- Device identity and settings
CREATE TABLE IF NOT EXISTS device (
id INTEGER PRIMARY KEY CHECK (id = 1), -- singleton row
public_key TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
self_info TEXT -- JSON blob with full device info
);
-- All known contacts (replaces contacts_cache.jsonl)
CREATE TABLE IF NOT EXISTS contacts (
public_key TEXT PRIMARY KEY, -- hex, lowercase
name TEXT NOT NULL DEFAULT '',
type INTEGER DEFAULT 0, -- node type from device
flags INTEGER DEFAULT 0,
out_path TEXT DEFAULT '', -- outgoing path string
out_path_len INTEGER DEFAULT 0,
last_advert TEXT, -- ISO 8601 timestamp
adv_lat REAL, -- GPS latitude from advert
adv_lon REAL, -- GPS longitude from advert
first_seen TEXT NOT NULL DEFAULT (datetime('now')),
last_seen TEXT NOT NULL DEFAULT (datetime('now')),
source TEXT DEFAULT 'advert', -- 'advert', 'device', 'manual'
is_protected INTEGER DEFAULT 0, -- 1 = protected from cleanup
lastmod TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Channel configuration
CREATE TABLE IF NOT EXISTS channels (
idx INTEGER PRIMARY KEY, -- channel index (0-7)
name TEXT NOT NULL DEFAULT '',
secret TEXT, -- channel secret/key (hex)
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Region registry (user-curated MeshCore flood scopes)
CREATE TABLE IF NOT EXISTS regions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE, -- firmware-safe name, e.g. 'pl-ma'
key_hex TEXT NOT NULL, -- 32 hex chars = 16-byte scope key
is_default INTEGER NOT NULL DEFAULT 0, -- mirrors firmware CMD_GET_DEFAULT_FLOOD_SCOPE
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- User-configured MeshCore Analyzer services
CREATE TABLE IF NOT EXISTS analyzers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
url_template TEXT NOT NULL, -- must contain '{packetHash}'
is_default INTEGER NOT NULL DEFAULT 0,
is_disabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_analyzers_one_default
ON analyzers(is_default) WHERE is_default = 1;
-- Observer: MQTT brokers that receive captured mesh packets
-- (meshcore-packet-capture compatible publishing)
CREATE TABLE IF NOT EXISTS observer_brokers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
host TEXT NOT NULL,
port INTEGER NOT NULL DEFAULT 1883,
username TEXT NOT NULL DEFAULT '',
password TEXT NOT NULL DEFAULT '', -- plaintext (single-user LAN app)
use_tls INTEGER NOT NULL DEFAULT 0,
tls_verify INTEGER NOT NULL DEFAULT 1, -- 0 = accept self-signed certs
is_disabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Per-channel region mapping (absent row = no override; firmware default applies)
CREATE TABLE IF NOT EXISTS channel_scopes (
channel_idx INTEGER PRIMARY KEY,
region_id INTEGER NOT NULL REFERENCES regions(id) ON DELETE CASCADE,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Channel messages (replaces CHAN/SENT_CHAN from .msgs)
CREATE TABLE IF NOT EXISTS channel_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_idx INTEGER NOT NULL DEFAULT 0,
sender TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
timestamp INTEGER NOT NULL DEFAULT 0, -- unix epoch
sender_timestamp INTEGER, -- sender's clock
is_own INTEGER NOT NULL DEFAULT 0, -- 1 = sent by us
txt_type INTEGER DEFAULT 0,
snr REAL,
path_len INTEGER,
pkt_payload TEXT, -- for echo matching
raw_packet TEXT, -- full hex packet (header+codes+path_len+payload) for raw resend; NULL for received/legacy rows
raw_json TEXT, -- original JSON line
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Direct messages (replaces PRIV/SENT_MSG from .msgs)
CREATE TABLE IF NOT EXISTS direct_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_pubkey TEXT, -- FK to contacts (nullable for unknown)
direction TEXT NOT NULL CHECK (direction IN ('in', 'out')),
content TEXT NOT NULL DEFAULT '',
timestamp INTEGER NOT NULL DEFAULT 0, -- unix epoch
sender_timestamp INTEGER,
txt_type INTEGER DEFAULT 0,
snr REAL,
path_len INTEGER,
expected_ack TEXT, -- ACK code for delivery tracking
pkt_payload TEXT, -- raw packet payload for hash/analyzer
signature TEXT, -- dedup signature
raw_json TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (contact_pubkey) REFERENCES contacts(public_key) ON DELETE SET NULL
);
-- ACK tracking (replaces .acks.jsonl)
CREATE TABLE IF NOT EXISTS acks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
expected_ack TEXT NOT NULL, -- ACK code to match
received_at TEXT NOT NULL DEFAULT (datetime('now')),
snr REAL,
rssi REAL,
route_type TEXT, -- 'direct', 'flood', etc.
is_retry INTEGER DEFAULT 0,
dm_id INTEGER, -- FK to direct_messages (nullable)
FOREIGN KEY (dm_id) REFERENCES direct_messages(id) ON DELETE SET NULL
);
-- Echo tracking (replaces .echoes.jsonl)
CREATE TABLE IF NOT EXISTS echoes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pkt_payload TEXT NOT NULL, -- matches channel_messages.pkt_payload
path TEXT, -- relay path string
snr REAL,
received_at TEXT NOT NULL DEFAULT (datetime('now')),
direction TEXT DEFAULT 'incoming', -- 'sent' or 'incoming'
cm_id INTEGER, -- FK to channel_messages (nullable)
hash_size INTEGER NOT NULL DEFAULT 1, -- bytes per hop hash: 1, 2, or 3
FOREIGN KEY (cm_id) REFERENCES channel_messages(id) ON DELETE SET NULL
);
-- Path tracking (replaces .path.jsonl)
CREATE TABLE IF NOT EXISTS paths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_pubkey TEXT,
pkt_payload TEXT,
path TEXT,
snr REAL,
path_len INTEGER,
received_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- User-configured paths for DM retry rotation
CREATE TABLE IF NOT EXISTS contact_paths (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contact_pubkey TEXT NOT NULL REFERENCES contacts(public_key) ON DELETE CASCADE,
path_hex TEXT NOT NULL DEFAULT '', -- raw hex path bytes (e.g. "5e34e761")
hash_size INTEGER NOT NULL DEFAULT 1, -- bytes per hop: 1, 2, or 3
label TEXT NOT NULL DEFAULT '', -- friendly label (e.g. "via Zalesie")
is_primary INTEGER NOT NULL DEFAULT 0, -- 1 = priority/default path
sort_order INTEGER NOT NULL DEFAULT 0, -- lower = tried first during rotation
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Advertisements (replaces .adverts.jsonl)
CREATE TABLE IF NOT EXISTS advertisements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_key TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
type INTEGER DEFAULT 0,
lat REAL,
lon REAL,
timestamp INTEGER NOT NULL DEFAULT 0,
snr REAL,
raw_payload TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Read status tracking (replaces .read_status.json)
CREATE TABLE IF NOT EXISTS read_status (
key TEXT PRIMARY KEY, -- 'chan_0', 'dm_<pubkey>', etc.
last_seen_ts INTEGER DEFAULT 0, -- unix timestamp
is_muted INTEGER DEFAULT 0, -- 1 = muted (channels only)
is_favorite INTEGER DEFAULT 0, -- 1 = favorite (channels only)
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Ignored contacts (adverts cached but not pending/auto-added)
CREATE TABLE IF NOT EXISTS ignored_contacts (
public_key TEXT PRIMARY KEY REFERENCES contacts(public_key) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Blocked contacts (ignored + messages hidden from display)
CREATE TABLE IF NOT EXISTS blocked_contacts (
public_key TEXT PRIMARY KEY REFERENCES contacts(public_key) ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Blocked names (for bots/contacts without known public_key)
CREATE TABLE IF NOT EXISTS blocked_names (
name TEXT PRIMARY KEY,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Application settings (key-value store, replaces .webui_settings.json)
CREATE TABLE IF NOT EXISTS app_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT '', -- JSON-encoded value
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- My Repeaters: repeaters selected for administration, with saved login.
-- No FK to contacts: a saved password must survive contact cleanup/re-add.
CREATE TABLE IF NOT EXISTS repeaters (
public_key TEXT PRIMARY KEY, -- hex, lowercase (64 chars)
password TEXT NOT NULL DEFAULT '', -- plaintext (single-user LAN app)
added_at TEXT NOT NULL DEFAULT (datetime('now')),
last_login_at TEXT,
last_login_role TEXT -- 'admin' | 'guest'
);
-- ============================================================
-- Indexes
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_cm_channel_ts ON channel_messages(channel_idx, timestamp);
CREATE INDEX IF NOT EXISTS idx_cm_pkt ON channel_messages(pkt_payload);
CREATE INDEX IF NOT EXISTS idx_dm_contact ON direct_messages(contact_pubkey, timestamp);
CREATE INDEX IF NOT EXISTS idx_dm_ack ON direct_messages(expected_ack);
CREATE INDEX IF NOT EXISTS idx_acks_code ON acks(expected_ack);
CREATE INDEX IF NOT EXISTS idx_echoes_pkt ON echoes(pkt_payload);
CREATE INDEX IF NOT EXISTS idx_adv_pubkey ON advertisements(public_key, timestamp);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE INDEX IF NOT EXISTS idx_cp_contact ON contact_paths(contact_pubkey, sort_order);
CREATE INDEX IF NOT EXISTS idx_regions_default ON regions(is_default) WHERE is_default = 1;
-- ============================================================
-- Full-Text Search (FTS5)
-- ============================================================
CREATE VIRTUAL TABLE IF NOT EXISTS channel_messages_fts USING fts5(
content,
content=channel_messages,
content_rowid=id
);
CREATE VIRTUAL TABLE IF NOT EXISTS direct_messages_fts USING fts5(
content,
content=direct_messages,
content_rowid=id
);
-- FTS triggers: keep FTS index in sync with source tables
CREATE TRIGGER IF NOT EXISTS cm_fts_insert AFTER INSERT ON channel_messages BEGIN
INSERT INTO channel_messages_fts(rowid, content) VALUES (new.id, new.content);
END;
CREATE TRIGGER IF NOT EXISTS cm_fts_delete AFTER DELETE ON channel_messages BEGIN
INSERT INTO channel_messages_fts(channel_messages_fts, rowid, content)
VALUES ('delete', old.id, old.content);
END;
CREATE TRIGGER IF NOT EXISTS cm_fts_update AFTER UPDATE OF content ON channel_messages BEGIN
INSERT INTO channel_messages_fts(channel_messages_fts, rowid, content)
VALUES ('delete', old.id, old.content);
INSERT INTO channel_messages_fts(rowid, content) VALUES (new.id, new.content);
END;
CREATE TRIGGER IF NOT EXISTS dm_fts_insert AFTER INSERT ON direct_messages BEGIN
INSERT INTO direct_messages_fts(rowid, content) VALUES (new.id, new.content);
END;
CREATE TRIGGER IF NOT EXISTS dm_fts_delete AFTER DELETE ON direct_messages BEGIN
INSERT INTO direct_messages_fts(direct_messages_fts, rowid, content)
VALUES ('delete', old.id, old.content);
END;
CREATE TRIGGER IF NOT EXISTS dm_fts_update AFTER UPDATE OF content ON direct_messages BEGIN
INSERT INTO direct_messages_fts(direct_messages_fts, rowid, content)
VALUES ('delete', old.id, old.content);
INSERT INTO direct_messages_fts(rowid, content) VALUES (new.id, new.content);
END;