Files
novacpx/db/schema.sql
T
myron fbc445dad2 Migrate panel DB from MySQL to SQLite
Panel no longer depends on the user-managed MariaDB service.
SQLite at /var/lib/novacpx/panel.db runs independently so the
control panel stays up even when MariaDB is stopped.

- DB.php: switch to sqlite: DSN, add SQL translator (ON DUPLICATE KEY,
  DATE_ADD/DATE_SUB INTERVAL, NOW(), UNIX_TIMESTAMP(), IFNULL)
- Core.php: replace DB_HOST/NAME/USER/PASS with DB_PATH constant
- schema.sql: full SQLite syntax, add TOTP columns to users table
- _branding.php: use sqlite: PDO, datetime('now') for session check
- install.sh: apt install sqlite3, create SQLite DB instead of MySQL DB
- tools/migrate-to-sqlite.sh: one-shot migration script for existing installs
2026-06-09 14:52:02 +00:00

532 lines
24 KiB
SQL

-- NovaCPX Database Schema v1.1.0
-- Engine: SQLite 3.35+
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = OFF;
-- ── Version tracking ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS novacpx_version (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL,
installed_at TEXT DEFAULT (datetime('now')),
notes TEXT,
git_commit TEXT
);
INSERT OR IGNORE INTO novacpx_version (version, notes, git_commit)
VALUES ('1.1.0', 'Initial installation', 'HEAD');
-- ── Audit log ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username TEXT,
action TEXT NOT NULL,
resource TEXT,
detail TEXT,
ip_address TEXT,
user_agent TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_audit_user ON audit_log (user_id);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action);
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log (created_at);
-- ── Users ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('admin','reseller','user')),
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','suspended','pending')),
reseller_id INTEGER DEFAULT NULL,
package_id INTEGER DEFAULT NULL,
theme TEXT DEFAULT 'nova-dark',
language TEXT DEFAULT 'en',
contact_name TEXT,
contact_phone TEXT,
last_login TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
totp_secret TEXT,
totp_enabled INTEGER DEFAULT 0,
totp_backup_codes TEXT,
FOREIGN KEY (reseller_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_users_role ON users (role);
CREATE INDEX IF NOT EXISTS idx_users_status ON users (status);
CREATE INDEX IF NOT EXISTS idx_users_reseller ON users (reseller_id);
-- ── Sessions ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id INTEGER NOT NULL,
ip_address TEXT,
user_agent TEXT,
data TEXT,
expires_at TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions (expires_at);
-- ── Packages / Hosting Plans ──────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS packages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
owner_id INTEGER DEFAULT NULL,
disk_mb INTEGER DEFAULT 1024,
bandwidth_mb INTEGER DEFAULT 10240,
max_domains INTEGER DEFAULT 1,
max_subdomains INTEGER DEFAULT 10,
max_addon_domains INTEGER DEFAULT 0,
max_parked_domains INTEGER DEFAULT 5,
max_email INTEGER DEFAULT 10,
max_ftp INTEGER DEFAULT 5,
max_databases INTEGER DEFAULT 5,
php_version TEXT DEFAULT '8.3',
ssl_enabled INTEGER DEFAULT 1,
is_default INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_packages_owner ON packages (owner_id);
INSERT OR IGNORE INTO packages (id, name, disk_mb, bandwidth_mb, max_domains, max_email, max_databases, is_default)
VALUES (1, 'Default', 5120, 51200, 5, 25, 10, 1);
-- ── Hosting Accounts ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE,
domain TEXT NOT NULL,
home_dir TEXT NOT NULL,
document_root TEXT,
system_user TEXT DEFAULT 'www-data',
package_id INTEGER,
disk_used_mb INTEGER DEFAULT 0,
bw_used_mb INTEGER DEFAULT 0,
php_version TEXT DEFAULT '8.3',
web_server TEXT DEFAULT 'apache' CHECK(web_server IN ('apache','nginx')),
status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended','terminated')),
cf_api_key TEXT,
cf_api_email TEXT,
cf_zone_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
suspended_at TEXT DEFAULT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_domain ON accounts (domain);
CREATE INDEX IF NOT EXISTS idx_accounts_status ON accounts (status);
-- ── Domains ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS domains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
domain TEXT NOT NULL UNIQUE,
type TEXT DEFAULT 'main' CHECK(type IN ('main','addon','subdomain','parked','alias')),
document_root TEXT,
php_version TEXT,
ssl_enabled INTEGER DEFAULT 0,
ssl_cert TEXT,
ssl_key TEXT,
ssl_chain TEXT,
ssl_expires TEXT,
redirect_to TEXT DEFAULT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_domains_account ON domains (account_id);
CREATE INDEX IF NOT EXISTS idx_domains_type ON domains (type);
-- ── DNS Zones & Records ───────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS dns_zones (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
domain TEXT NOT NULL UNIQUE,
serial INTEGER DEFAULT 1,
primary_ns TEXT DEFAULT 'ns1.localhost',
secondary_ns TEXT DEFAULT 'ns2.localhost',
admin_email TEXT DEFAULT 'hostmaster@localhost',
ttl INTEGER DEFAULT 3600,
refresh INTEGER DEFAULT 86400,
retry INTEGER DEFAULT 7200,
expire INTEGER DEFAULT 2419200,
minimum INTEGER DEFAULT 86400,
status TEXT DEFAULT 'active' CHECK(status IN ('active','disabled')),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_dns_zones_account ON dns_zones (account_id);
CREATE TABLE IF NOT EXISTS dns_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
zone_id INTEGER NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('A','AAAA','CNAME','MX','TXT','SRV','NS','PTR','CAA','DKIM','SPF','DMARC')),
content TEXT NOT NULL,
ttl INTEGER DEFAULT 3600,
priority INTEGER DEFAULT NULL,
proxied INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (zone_id) REFERENCES dns_zones(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_dns_records_zone ON dns_records (zone_id);
CREATE INDEX IF NOT EXISTS idx_dns_records_type ON dns_records (type);
-- ── Email ─────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS email_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
quota_mb INTEGER DEFAULT 500,
used_mb INTEGER DEFAULT 0,
status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended')),
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_email_accounts_account ON email_accounts (account_id);
CREATE TABLE IF NOT EXISTS email_forwarders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
source TEXT NOT NULL,
destination TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS email_autoresponders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
email TEXT NOT NULL,
subject TEXT,
body TEXT,
is_active INTEGER DEFAULT 1,
start_at TEXT,
stop_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── Databases ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS databases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
db_name TEXT NOT NULL,
db_user TEXT NOT NULL,
db_pass TEXT NOT NULL,
db_type TEXT DEFAULT 'mysql' CHECK(db_type IN ('mysql','postgresql')),
size_mb REAL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_databases_account ON databases (account_id);
-- ── FTP Accounts ──────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ftp_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
home_dir TEXT NOT NULL,
quota_mb INTEGER DEFAULT 0,
status TEXT DEFAULT 'active' CHECK(status IN ('active','suspended')),
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── SSL Certificates ──────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS ssl_certs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
domain TEXT NOT NULL,
type TEXT DEFAULT 'lets_encrypt' CHECK(type IN ('lets_encrypt','self_signed','custom')),
cert TEXT,
private_key TEXT,
chain TEXT,
issued_at TEXT,
expires_at TEXT,
auto_renew INTEGER DEFAULT 1,
status TEXT DEFAULT 'pending' CHECK(status IN ('active','expired','pending','failed')),
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_ssl_certs_domain ON ssl_certs (domain);
CREATE INDEX IF NOT EXISTS idx_ssl_certs_expires ON ssl_certs (expires_at);
-- ── Cron Jobs ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS cron_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
command TEXT NOT NULL,
minute TEXT DEFAULT '*',
hour TEXT DEFAULT '*',
day TEXT DEFAULT '*',
month TEXT DEFAULT '*',
weekday TEXT DEFAULT '*',
is_active INTEGER DEFAULT 1,
last_run TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── PHP Configuration ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS php_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL UNIQUE,
php_version TEXT DEFAULT '8.3',
memory_limit TEXT DEFAULT '256M',
max_execution_time INTEGER DEFAULT 30,
upload_max_filesize TEXT DEFAULT '64M',
post_max_size TEXT DEFAULT '64M',
max_input_vars INTEGER DEFAULT 1000,
display_errors INTEGER DEFAULT 0,
error_reporting TEXT DEFAULT 'E_ALL & ~E_NOTICE',
extensions TEXT,
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── Backups ───────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS backups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
filename TEXT NOT NULL,
size_mb REAL DEFAULT 0,
type TEXT DEFAULT 'full' CHECK(type IN ('full','partial','db_only','files_only')),
status TEXT DEFAULT 'pending' CHECK(status IN ('pending','running','complete','failed')),
storage TEXT DEFAULT 'local' CHECK(storage IN ('local','s3','ftp','sftp')),
created_at TEXT DEFAULT (datetime('now')),
completed_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_backups_account ON backups (account_id);
CREATE INDEX IF NOT EXISTS idx_backups_status ON backups (status);
CREATE TABLE IF NOT EXISTS backup_schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL UNIQUE,
frequency TEXT DEFAULT 'daily' CHECK(frequency IN ('hourly','daily','weekly','monthly')),
type TEXT DEFAULT 'full' CHECK(type IN ('full','files','database')),
retain_count INTEGER DEFAULT 7,
last_run TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── Server Stats ──────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS server_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cpu_pct REAL,
ram_pct REAL,
disk_pct REAL,
load_1m REAL,
load_5m REAL,
load_15m REAL,
net_in_kb INTEGER DEFAULT 0,
net_out_kb INTEGER DEFAULT 0,
recorded_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_server_stats_recorded ON server_stats (recorded_at);
-- ── Notifications ─────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
title TEXT NOT NULL,
message TEXT NOT NULL,
type TEXT DEFAULT 'info' CHECK(type IN ('info','success','warning','error')),
is_read INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications (is_read);
-- ── API Tokens ────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS api_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
permissions TEXT,
last_used TEXT,
expires_at TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_api_tokens_token ON api_tokens (token);
-- ── Settings ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS settings (
`key` TEXT PRIMARY KEY,
`value` TEXT,
updated_at TEXT
);
INSERT OR IGNORE INTO settings (`key`, `value`) VALUES
('panel_name', 'NovaCPX'),
('panel_version', '1.1.0'),
('default_nameserver1', 'ns1.localhost'),
('default_nameserver2', 'ns2.localhost'),
('default_php', '8.3'),
('mail_enabled', '1'),
('ftp_enabled', '1'),
('dns_enabled', '1'),
('backup_dir', '/var/novacpx/backups'),
('update_channel', 'stable'),
('git_remote', 'https://github.com/myronblair/novacpx.git'),
('proxy_mode', 'disabled'),
('proxy_apache_port', '80');
-- ── DKIM Keys ─────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS dkim_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
domain TEXT NOT NULL UNIQUE,
selector TEXT NOT NULL DEFAULT 'mail',
public_key TEXT NOT NULL,
private_key_path TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
-- ── Rate Limits ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS api_rate_limits (
ip TEXT NOT NULL,
endpoint TEXT NOT NULL,
hits INTEGER NOT NULL DEFAULT 1,
window_start INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (ip, endpoint)
);
-- ── Proxy Hosts ───────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS proxy_hosts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
domain TEXT NOT NULL UNIQUE,
upstream TEXT NOT NULL,
ssl_enabled INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
custom_config TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL
);
-- ── Reseller Branding ─────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS reseller_branding (
user_id INTEGER PRIMARY KEY,
panel_name TEXT NOT NULL DEFAULT 'NovaCPX',
logo_url TEXT,
favicon_url TEXT,
primary_color TEXT NOT NULL DEFAULT '#6366f1',
accent_color TEXT NOT NULL DEFAULT '#0ea5e9',
support_email TEXT,
support_url TEXT,
hide_powered_by INTEGER NOT NULL DEFAULT 0,
custom_css TEXT,
updated_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- ── Webmail SSO Tokens ────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS webmail_sso_tokens (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
enc_pass TEXT NOT NULL,
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_webmail_sso_expires ON webmail_sso_tokens (expires_at);
-- ── WordPress Installs ────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS wordpress_installs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
domain TEXT NOT NULL,
path TEXT DEFAULT '/',
db_name TEXT,
db_user TEXT,
db_pass TEXT,
wp_version TEXT,
admin_user TEXT,
admin_email TEXT,
status TEXT DEFAULT 'active' CHECK(status IN ('active','updating','suspended')),
staging_of INTEGER,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_wp_installs_account ON wordpress_installs (account_id);
-- ── Docker ────────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS docker_containers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
container_id TEXT,
name TEXT NOT NULL,
image TEXT NOT NULL,
app_key TEXT,
status TEXT DEFAULT 'pending' CHECK(status IN ('running','stopped','error','pending')),
ports TEXT,
memory_mb INTEGER,
cpus REAL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_docker_containers_account ON docker_containers (account_id);
CREATE TABLE IF NOT EXISTS docker_compose_stacks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
name TEXT NOT NULL,
stack_dir TEXT NOT NULL,
compose_file TEXT NOT NULL,
status TEXT DEFAULT 'pending' CHECK(status IN ('running','stopped','error','pending')),
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS docker_quotas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
max_containers INTEGER DEFAULT 2,
max_memory_mb INTEGER DEFAULT 512,
max_cpus REAL DEFAULT 1.0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- ── Features ──────────────────────────────────────────────────────────────────
CREATE TABLE IF NOT EXISTS features (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
enabled INTEGER DEFAULT 0,
installed INTEGER DEFAULT 0,
install_cmd TEXT,
uninstall_cmd TEXT,
config_keys TEXT,
install_pid INTEGER,
install_log TEXT,
requires TEXT,
requires_restart INTEGER DEFAULT 0,
min_ram_mb INTEGER DEFAULT 0,
updated_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_features_category ON features (category);
CREATE INDEX IF NOT EXISTS idx_features_enabled ON features (enabled);
PRAGMA foreign_keys = ON;