TBM Configuration Management System

- Config versioning with approval workflow (Draft/Approved/Archived)
- Fleet management with real-time machine status
- Deploy configs to machines with status tracking
- Full audit logging for compliance
- React dashboard + FastAPI backend + machine simulator
This commit is contained in:
SpartanTech
2025-11-30 21:15:43 -06:00
commit f62ef2cf05
32 changed files with 9438 additions and 0 deletions

21
backend/.env.example Normal file
View File

@@ -0,0 +1,21 @@
# TBM Config Manager - Environment Variables
# Copy this file to .env and update values for your environment
# Demo credentials - CHANGE THESE FOR PRODUCTION
ADMIN_USERNAME=admin
ADMIN_PASSWORD=changeme
ADMIN_NAME=Admin User
ENGINEER_USERNAME=engineer
ENGINEER_PASSWORD=changeme
ENGINEER_NAME=Engineer User
OPERATOR_USERNAME=operator
OPERATOR_PASSWORD=changeme
OPERATOR_NAME=Operator User
# Database path (relative to backend directory)
DATABASE_PATH=tbm_config.db
# Online threshold in seconds
ONLINE_THRESHOLD_SECONDS=30

1157
backend/main.py Normal file

File diff suppressed because it is too large Load Diff

4
backend/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
python-dotenv==1.0.0

343
backend/seed_data.py Normal file
View File

@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
Seed the database with mock data for MVP testing.
Usage:
python seed_data.py # Reset database and seed with mock data
python seed_data.py --keep # Keep existing data, only add if empty
"""
import sqlite3
import json
import sys
import os
from uuid import uuid4
from datetime import datetime, timedelta
DB_PATH = "tbm_config.db"
def create_tables(cursor):
"""Create all tables with current schema"""
cursor.executescript("""
DROP TABLE IF EXISTS pending_configs;
DROP TABLE IF EXISTS audit_logs;
DROP TABLE IF EXISTS deployments;
DROP TABLE IF EXISTS configurations;
DROP TABLE IF EXISTS machines;
CREATE TABLE machines (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
machine_type TEXT NOT NULL,
operational_mode TEXT DEFAULT 'active',
current_version TEXT,
last_seen TEXT
);
CREATE TABLE configurations (
id TEXT PRIMARY KEY,
version TEXT NOT NULL,
name TEXT NOT NULL,
machine_type TEXT NOT NULL,
status TEXT DEFAULT 'draft',
config TEXT NOT NULL,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE deployments (
id TEXT PRIMARY KEY,
config_id TEXT NOT NULL,
config_version TEXT NOT NULL,
machine_id TEXT NOT NULL,
status TEXT DEFAULT 'pending',
initiated_by TEXT NOT NULL,
initiated_at TEXT NOT NULL,
completed_at TEXT,
error TEXT,
FOREIGN KEY (config_id) REFERENCES configurations(id),
FOREIGN KEY (machine_id) REFERENCES machines(id)
);
CREATE TABLE audit_logs (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
user TEXT NOT NULL,
action TEXT NOT NULL,
resource TEXT NOT NULL,
details TEXT NOT NULL
);
CREATE TABLE pending_configs (
machine_id TEXT PRIMARY KEY,
config_id TEXT NOT NULL,
deployment_id TEXT NOT NULL,
FOREIGN KEY (machine_id) REFERENCES machines(id),
FOREIGN KEY (config_id) REFERENCES configurations(id)
);
""")
print("✓ Database schema created")
def seed():
# Delete existing database file to ensure clean schema
if os.path.exists(DB_PATH):
os.remove(DB_PATH)
print(f"✓ Removed old database: {DB_PATH}")
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Create tables with current schema
create_tables(cursor)
conn.commit()
now = datetime.utcnow()
# ========================================================================
# MACHINES
# ========================================================================
# operational_mode: active (should be online), standby (intentionally idle),
# maintenance (under repair), decommissioned (not in use)
machines = [
# (id, name, machine_type, operational_mode, current_version, last_seen)
("PRF4-001", "Prufrock 4 - Alpha", "PRUFROCK_4", "active", "2.4.1", (now - timedelta(minutes=2)).isoformat()),
("PRF4-002", "Prufrock 4 - Beta", "PRUFROCK_4", "active", "2.4.0", (now - timedelta(minutes=5)).isoformat()),
("PRF4-003", "Prufrock 4 - Gamma", "PRUFROCK_4", "maintenance", "2.4.1", (now - timedelta(hours=12)).isoformat()),
("PRF5-001", "Prufrock 5 - Prototype", "PRUFROCK_5", "standby", "3.0.0", (now - timedelta(hours=48)).isoformat()),
("PRF5-002", "Prufrock 5 - Test Unit", "PRUFROCK_5", "decommissioned", "3.0.0", (now - timedelta(days=7)).isoformat()),
]
for m in machines:
cursor.execute(
"INSERT INTO machines (id, name, machine_type, operational_mode, current_version, last_seen) VALUES (?, ?, ?, ?, ?, ?)",
m
)
print(f"✓ Created {len(machines)} machines")
# ========================================================================
# CONFIGURATIONS
# ========================================================================
# Default config template
default_config = {
"excavation": {
"thrust_kn": 18500,
"cutterhead_rpm": 2.4,
"penetration_mm_rev": 8,
"advance_rate_mm_min": 45
},
"pressure": {
"face_pressure_bar": 2.8,
"min_pressure_bar": 2.4,
"max_pressure_bar": 3.4
},
"safety": {
"max_thrust_kn": 26000,
"torque_limit_pct": 85,
"emergency_stop_enabled": True
},
"navigation": {
"horizontal_tolerance_mm": 50,
"vertical_tolerance_mm": 40,
"steering_mode": "semi_auto"
}
}
# High performance config
high_perf_config = {
"excavation": {
"thrust_kn": 22000,
"cutterhead_rpm": 2.8,
"penetration_mm_rev": 12,
"advance_rate_mm_min": 65
},
"pressure": {
"face_pressure_bar": 3.2,
"min_pressure_bar": 2.8,
"max_pressure_bar": 3.8
},
"safety": {
"max_thrust_kn": 26000,
"torque_limit_pct": 90,
"emergency_stop_enabled": True
},
"navigation": {
"horizontal_tolerance_mm": 40,
"vertical_tolerance_mm": 30,
"steering_mode": "full_auto"
}
}
# Soft soil config
soft_soil_config = {
"excavation": {
"thrust_kn": 15000,
"cutterhead_rpm": 2.0,
"penetration_mm_rev": 6,
"advance_rate_mm_min": 35
},
"pressure": {
"face_pressure_bar": 2.4,
"min_pressure_bar": 2.0,
"max_pressure_bar": 3.0
},
"safety": {
"max_thrust_kn": 26000,
"torque_limit_pct": 75,
"emergency_stop_enabled": True
},
"navigation": {
"horizontal_tolerance_mm": 60,
"vertical_tolerance_mm": 50,
"steering_mode": "semi_auto"
}
}
configs = [
(str(uuid4()), "2.4.0", "Vegas Loop - Standard", "PRUFROCK_4", "approved", json.dumps(default_config), "engineer", (now - timedelta(days=30)).isoformat()),
(str(uuid4()), "2.4.1", "Vegas Loop - Optimized", "PRUFROCK_4", "approved", json.dumps(high_perf_config), "engineer", (now - timedelta(days=14)).isoformat()),
(str(uuid4()), "2.4.2", "Vegas Loop - Soft Soil Zone", "PRUFROCK_4", "approved", json.dumps(soft_soil_config), "admin", (now - timedelta(days=7)).isoformat()),
(str(uuid4()), "2.5.0", "Vegas Loop - Phase 2 Draft", "PRUFROCK_4", "draft", json.dumps(high_perf_config), "engineer", (now - timedelta(days=1)).isoformat()),
(str(uuid4()), "3.0.0", "Prufrock 5 - Initial", "PRUFROCK_5", "approved", json.dumps(default_config), "admin", (now - timedelta(days=60)).isoformat()),
]
for c in configs:
cursor.execute(
"INSERT INTO configurations (id, version, name, machine_type, status, config, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
c
)
print(f"✓ Created {len(configs)} configurations")
# ========================================================================
# DEPLOYMENTS (historical)
# ========================================================================
# Get config IDs
cursor.execute("SELECT id, version, machine_type FROM configurations")
config_map = {(row[1], row[2]): row[0] for row in cursor.fetchall()}
deployments = [
# PRF4-001 deployments
(str(uuid4()), config_map[("2.4.0", "PRUFROCK_4")], "2.4.0", "PRF4-001", "success", "engineer",
(now - timedelta(days=25)).isoformat(), (now - timedelta(days=25, minutes=-1)).isoformat(), None),
(str(uuid4()), config_map[("2.4.1", "PRUFROCK_4")], "2.4.1", "PRF4-001", "success", "admin",
(now - timedelta(days=10)).isoformat(), (now - timedelta(days=10, minutes=-1)).isoformat(), None),
# PRF4-002 deployments
(str(uuid4()), config_map[("2.4.0", "PRUFROCK_4")], "2.4.0", "PRF4-002", "success", "engineer",
(now - timedelta(days=28)).isoformat(), (now - timedelta(days=28, minutes=-1)).isoformat(), None),
# Failed deployment example
(str(uuid4()), config_map[("2.4.2", "PRUFROCK_4")], "2.4.2", "PRF4-002", "failed", "operator",
(now - timedelta(days=3)).isoformat(), (now - timedelta(days=3, minutes=-1)).isoformat(), "PLC communication timeout"),
# PRF5-001 deployment
(str(uuid4()), config_map[("3.0.0", "PRUFROCK_5")], "3.0.0", "PRF5-001", "success", "admin",
(now - timedelta(days=55)).isoformat(), (now - timedelta(days=55, minutes=-1)).isoformat(), None),
]
for d in deployments:
cursor.execute(
"""INSERT INTO deployments (id, config_id, config_version, machine_id, status, initiated_by, initiated_at, completed_at, error)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
d
)
print(f"✓ Created {len(deployments)} deployments")
# ========================================================================
# AUDIT LOGS
# ========================================================================
audit_logs = [
(str(uuid4()), (now - timedelta(days=30)).isoformat(), "engineer", "config.created", "config:2.4.0", json.dumps({"name": "Vegas Loop - Standard"})),
(str(uuid4()), (now - timedelta(days=30)).isoformat(), "engineer", "config.approved", "config:2.4.0", json.dumps({})),
(str(uuid4()), (now - timedelta(days=25)).isoformat(), "engineer", "deployment.initiated", "deployment:PRF4-001", json.dumps({"version": "2.4.0"})),
(str(uuid4()), (now - timedelta(days=25)).isoformat(), "PRF4-001", "deployment.success", "deployment:PRF4-001", json.dumps({"version": "2.4.0"})),
(str(uuid4()), (now - timedelta(days=14)).isoformat(), "engineer", "config.created", "config:2.4.1", json.dumps({"name": "Vegas Loop - Optimized"})),
(str(uuid4()), (now - timedelta(days=14)).isoformat(), "admin", "config.approved", "config:2.4.1", json.dumps({})),
(str(uuid4()), (now - timedelta(days=10)).isoformat(), "admin", "deployment.initiated", "deployment:PRF4-001", json.dumps({"version": "2.4.1"})),
(str(uuid4()), (now - timedelta(days=10)).isoformat(), "PRF4-001", "deployment.success", "deployment:PRF4-001", json.dumps({"version": "2.4.1"})),
(str(uuid4()), (now - timedelta(days=7)).isoformat(), "admin", "config.created", "config:2.4.2", json.dumps({"name": "Vegas Loop - Soft Soil Zone"})),
(str(uuid4()), (now - timedelta(days=7)).isoformat(), "admin", "config.approved", "config:2.4.2", json.dumps({})),
(str(uuid4()), (now - timedelta(days=3)).isoformat(), "operator", "deployment.initiated", "deployment:PRF4-002", json.dumps({"version": "2.4.2"})),
(str(uuid4()), (now - timedelta(days=3)).isoformat(), "PRF4-002", "deployment.failed", "deployment:PRF4-002", json.dumps({"error": "PLC communication timeout"})),
(str(uuid4()), (now - timedelta(days=1)).isoformat(), "engineer", "config.created", "config:2.5.0", json.dumps({"name": "Vegas Loop - Phase 2 Draft"})),
(str(uuid4()), (now - timedelta(hours=2)).isoformat(), "admin", "auth.login", "user:admin", json.dumps({})),
]
for a in audit_logs:
cursor.execute(
"INSERT INTO audit_logs (id, timestamp, user, action, resource, details) VALUES (?, ?, ?, ?, ?, ?)",
a
)
print(f"✓ Created {len(audit_logs)} audit log entries")
conn.commit()
conn.close()
print("\n✅ Database seeded successfully!")
print("\nTest credentials:")
print(" admin / admin123 (full access)")
print(" engineer / eng123 (engineer)")
print(" operator / op123 (operator)")
if __name__ == "__main__":
# Initialize tables first
conn = sqlite3.connect(DB_PATH)
conn.executescript("""
CREATE TABLE IF NOT EXISTS machines (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
machine_type TEXT NOT NULL,
status TEXT DEFAULT 'offline',
current_version TEXT,
last_seen TEXT
);
CREATE TABLE IF NOT EXISTS configurations (
id TEXT PRIMARY KEY,
version TEXT NOT NULL,
name TEXT NOT NULL,
machine_type TEXT NOT NULL,
status TEXT DEFAULT 'draft',
config TEXT NOT NULL,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS deployments (
id TEXT PRIMARY KEY,
config_id TEXT NOT NULL,
config_version TEXT NOT NULL,
machine_id TEXT NOT NULL,
status TEXT DEFAULT 'pending',
initiated_by TEXT NOT NULL,
initiated_at TEXT NOT NULL,
completed_at TEXT,
error TEXT
);
CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
timestamp TEXT NOT NULL,
user TEXT NOT NULL,
action TEXT NOT NULL,
resource TEXT NOT NULL,
details TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS pending_configs (
machine_id TEXT PRIMARY KEY,
config_id TEXT NOT NULL,
deployment_id TEXT NOT NULL
);
""")
conn.commit()
conn.close()
seed()