feat: V1.0.1 - Setup page, Docker, README

Add password-protected setup page (/api/setup) for DB initialization,
admin creation, and demo data seeding. Dockerize the full stack with
server, client, nginx reverse proxy, and MySQL services. Add project
README with architecture overview, quick start, and VPS deployment guide.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Adriano
2026-02-07 18:00:29 +01:00
parent dd2ebf863a
commit c9d9c0f9dd
12 changed files with 1489 additions and 9 deletions
+26
View File
@@ -0,0 +1,26 @@
FROM python:3.11-slim
# Installa dipendenze sistema per WeasyPrint
RUN apt-get update && apt-get install -y --no-install-recommends \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libcairo2 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
shared-mime-info \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Crea directory uploads
RUN mkdir -p uploads/images uploads/pdfs uploads/logos uploads/reports
EXPOSE 8000
# Entry point: Alembic upgrade + Uvicorn
CMD ["sh", "-c", "alembic upgrade head && uvicorn main:app --host 0.0.0.0 --port 8000 --workers 2"]
+3
View File
@@ -31,6 +31,9 @@ class Settings(BaseSettings):
ssl_certfile: str | None = None
ssl_keyfile: str | None = None
# Setup page (empty = disabled)
setup_password: str | None = None
@property
def database_url(self) -> str:
"""Async MySQL connection string."""
+2
View File
@@ -19,6 +19,7 @@ from routers.files import router as files_router
from routers.settings import router as settings_router
from routers.reports import router as reports_router
from routers.statistics import router as statistics_router
from routers.setup import router as setup_router
@asynccontextmanager
@@ -69,6 +70,7 @@ app.include_router(files_router)
app.include_router(settings_router)
app.include_router(statistics_router)
app.include_router(reports_router)
app.include_router(setup_router)
@app.get("/api/health")
+3
View File
@@ -18,6 +18,9 @@ bcrypt>=4.0.0
pillow>=10.0.0
python-multipart>=0.0.6
# Templating (setup page)
jinja2>=3.1.0
# Reports
plotly>=5.0.0
kaleido>=0.2.0
+338
View File
@@ -0,0 +1,338 @@
"""Setup page router - database init, admin creation, demo seed.
Protected by SETUP_PASSWORD env var. When empty/None, all endpoints return 404.
NOT protected by API Key auth (standalone access).
"""
import secrets
from pathlib import Path
from fastapi import APIRouter, HTTPException, Request, status
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from sqlalchemy import inspect as sa_inspect
from config import settings
from database import Base, engine, async_session_factory
from models import (
User, Recipe, RecipeVersion, RecipeTask, RecipeSubtask,
)
from services.auth_service import hash_password
router = APIRouter(prefix="/api/setup", tags=["setup"])
_templates_dir = Path(__file__).resolve().parent.parent / "templates"
templates = Jinja2Templates(directory=str(_templates_dir))
# ---------------------------------------------------------------------------
# Request schemas
# ---------------------------------------------------------------------------
class PasswordBody(BaseModel):
password: str
class CreateAdminBody(BaseModel):
password: str
admin_username: str
admin_password: str
admin_email: str
admin_display_name: str
class ResetDbBody(BaseModel):
password: str
confirm: bool
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _check_setup_enabled() -> None:
"""Raise 404 if setup password is not configured."""
if not settings.setup_password:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
def _check_password(password: str) -> None:
"""Verify the provided password matches the setup password."""
_check_setup_enabled()
if password != settings.setup_password:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid setup password",
)
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@router.get("", response_class=HTMLResponse)
async def setup_page(request: Request):
"""Serve the setup page HTML."""
_check_setup_enabled()
return templates.TemplateResponse("setup/setup.html", {"request": request})
@router.get("/status")
async def setup_status():
"""Return database table status (no password required, but setup must be enabled)."""
_check_setup_enabled()
async with engine.connect() as conn:
table_names: list[str] = await conn.run_sync(
lambda sync_conn: sa_inspect(sync_conn).get_table_names()
)
return {
"tables_exist": len(table_names) > 0,
"table_count": len(table_names),
"tables": table_names,
}
@router.post("/init-db")
async def init_db(body: PasswordBody):
"""Create all database tables."""
_check_password(body.password)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
return {"status": "ok", "message": "Database tables created successfully"}
@router.post("/create-admin")
async def create_admin(body: CreateAdminBody):
"""Create an admin user with all roles."""
_check_password(body.password)
api_key = secrets.token_hex(32)
async with async_session_factory() as session:
async with session.begin():
user = User(
username=body.admin_username,
password_hash=hash_password(body.admin_password),
email=body.admin_email,
display_name=body.admin_display_name,
roles=["Maker", "MeasurementTec", "Metrologist"],
is_admin=True,
api_key=api_key,
)
session.add(user)
return {
"status": "ok",
"message": f"Admin user '{body.admin_username}' created",
"api_key": api_key,
}
@router.post("/seed")
async def seed_demo_data(body: PasswordBody):
"""Seed database with demo users and a sample recipe."""
_check_password(body.password)
async with async_session_factory() as session:
async with session.begin():
# ---- Users --------------------------------------------------------
users_data = [
{
"username": "admin",
"password": "admin123",
"display_name": "Administrator",
"email": "admin@tiemeasureflow.local",
"roles": ["Maker", "MeasurementTec", "Metrologist"],
"is_admin": True,
},
{
"username": "maker1",
"password": "maker123",
"display_name": "Mario Rossi",
"email": "maker1@tiemeasureflow.local",
"roles": ["Maker"],
"is_admin": False,
},
{
"username": "tec1",
"password": "tec123",
"display_name": "Luca Bianchi",
"email": "tec1@tiemeasureflow.local",
"roles": ["MeasurementTec"],
"is_admin": False,
},
{
"username": "metro1",
"password": "metro123",
"display_name": "Anna Verdi",
"email": "metro1@tiemeasureflow.local",
"roles": ["Metrologist"],
"is_admin": False,
},
]
created_users: list[User] = []
for u in users_data:
user = User(
username=u["username"],
password_hash=hash_password(u["password"]),
display_name=u["display_name"],
email=u["email"],
roles=u["roles"],
is_admin=u["is_admin"],
api_key=secrets.token_hex(32),
)
session.add(user)
created_users.append(user)
# Flush to get auto-generated user IDs
await session.flush()
admin_user = created_users[0]
# ---- Recipe -------------------------------------------------------
recipe = Recipe(
code="DEMO-001",
name="Demo Measurement Recipe",
description="Sample recipe with shaft and surface measurements for demonstration purposes.",
created_by=admin_user.id,
)
session.add(recipe)
await session.flush()
version = RecipeVersion(
recipe_id=recipe.id,
version_number=1,
is_current=True,
created_by=admin_user.id,
change_notes="Initial demo version",
)
session.add(version)
await session.flush()
# ---- Task 1: Shaft Diameter ----------------------------------------
task1 = RecipeTask(
version_id=version.id,
order_index=0,
title="Shaft Diameter Measurement",
directive="Measure the main shaft diameters at the indicated positions.",
description="Use the caliper to measure each diameter. Ensure the shaft is clean and at room temperature.",
)
session.add(task1)
await session.flush()
subtasks_1 = [
RecipeSubtask(
task_id=task1.id,
marker_number=1,
description="D1 - Main shaft diameter",
measurement_type="diameter",
nominal=25.000,
utl=25.050,
uwl=25.030,
lwl=24.970,
ltl=24.950,
unit="mm",
),
RecipeSubtask(
task_id=task1.id,
marker_number=2,
description="D2 - Bearing seat diameter",
measurement_type="diameter",
nominal=30.000,
utl=30.021,
uwl=30.015,
lwl=29.985,
ltl=29.979,
unit="mm",
),
RecipeSubtask(
task_id=task1.id,
marker_number=3,
description="D3 - Shoulder diameter",
measurement_type="diameter",
nominal=35.000,
utl=35.039,
uwl=35.025,
lwl=34.975,
ltl=34.961,
unit="mm",
),
]
session.add_all(subtasks_1)
# ---- Task 2: Surface Flatness --------------------------------------
task2 = RecipeTask(
version_id=version.id,
order_index=1,
title="Surface Flatness Measurement",
directive="Measure surface heights at the marked reference points.",
description="Place the part on the granite surface plate. Zero the indicator at the reference point, then measure each position.",
)
session.add(task2)
await session.flush()
subtasks_2 = [
RecipeSubtask(
task_id=task2.id,
marker_number=1,
description="F1 - Center reference height",
measurement_type="height",
nominal=0.000,
utl=0.020,
uwl=0.010,
lwl=-0.010,
ltl=-0.020,
unit="mm",
),
RecipeSubtask(
task_id=task2.id,
marker_number=2,
description="F2 - Edge height (left)",
measurement_type="height",
nominal=0.000,
utl=0.030,
uwl=0.015,
lwl=-0.015,
ltl=-0.030,
unit="mm",
),
RecipeSubtask(
task_id=task2.id,
marker_number=3,
description="F3 - Edge height (right)",
measurement_type="height",
nominal=0.000,
utl=0.030,
uwl=0.015,
lwl=-0.015,
ltl=-0.030,
unit="mm",
),
]
session.add_all(subtasks_2)
return {
"status": "ok",
"message": "Demo data seeded successfully",
"users_created": [u["username"] for u in users_data],
"recipe_created": "DEMO-001",
}
@router.post("/reset-db")
async def reset_db(body: ResetDbBody):
"""Drop all tables and recreate them. Requires confirm=true."""
_check_password(body.password)
if not body.confirm:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="You must set confirm=true to reset the database",
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
return {"status": "ok", "message": "Database reset successfully"}
+546
View File
@@ -0,0 +1,546 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TieMeasureFlow &mdash; Setup</title>
<style>
/* --- Reset & Base -------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #f0f4ff 0%, #e8ecf4 100%);
color: #1e293b;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 40px 16px;
}
/* --- Typography ---------------------------------------------------- */
h1 { font-size: 1.75rem; font-weight: 700; color: #0f172a; }
h2 { font-size: 1.15rem; font-weight: 600; color: #334155; margin-bottom: 12px; }
p { line-height: 1.5; color: #64748b; }
.brand-sub { font-size: 0.85rem; color: #64748b; margin-top: 4px; }
.mono { font-family: 'JetBrains Mono', 'Fira Code', monospace; }
/* --- Layout -------------------------------------------------------- */
.container { width: 100%; max-width: 600px; }
.logo-header {
text-align: center;
margin-bottom: 32px;
}
.logo-header .icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px; height: 56px;
background: #2563eb;
border-radius: 14px;
color: #fff;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 12px;
}
/* --- Card ---------------------------------------------------------- */
.card {
background: #fff;
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,.08), 0 4px 16px rgba(0,0,0,.04);
padding: 24px;
margin-bottom: 20px;
}
.card.danger { border-left: 4px solid #dc2626; }
/* --- Form elements ------------------------------------------------- */
label {
display: block;
font-size: 0.82rem;
font-weight: 500;
color: #475569;
margin-bottom: 4px;
}
input[type="text"],
input[type="password"],
input[type="email"] {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 0.9rem;
font-family: inherit;
color: #1e293b;
background: #f8fafc;
transition: border-color .15s, box-shadow .15s;
margin-bottom: 14px;
}
input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37,99,235,.15);
}
/* --- Buttons ------------------------------------------------------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background .15s, transform .1s, opacity .15s;
width: 100%;
}
.btn:active { transform: scale(.98); }
.btn:disabled { opacity: .55; cursor: not-allowed; transform: none; }
.btn-primary { background: #2563eb; color: #fff; }
.btn-primary:hover:not(:disabled) { background: #1d4ed8; }
.btn-success { background: #16a34a; color: #fff; }
.btn-success:hover:not(:disabled) { background: #15803d; }
.btn-danger { background: #dc2626; color: #fff; }
.btn-danger:hover:not(:disabled) { background: #b91c1c; }
.btn-outline {
background: transparent;
color: #2563eb;
border: 1.5px solid #2563eb;
}
.btn-outline:hover:not(:disabled) { background: #eff6ff; }
/* --- Status pills -------------------------------------------------- */
.status-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 12px;
}
.status-bar.ok { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
.status-bar.warn { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
.status-bar.err { background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }
.dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot.green { background: #16a34a; }
.dot.yellow { background: #eab308; }
.dot.red { background: #dc2626; }
/* --- Table list ---------------------------------------------------- */
.table-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.table-chip {
background: #f1f5f9;
color: #475569;
padding: 3px 10px;
border-radius: 6px;
font-size: 0.78rem;
font-family: 'JetBrains Mono', monospace;
}
/* --- Checkbox ------------------------------------------------------ */
.checkbox-row {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 0 16px;
font-size: 0.85rem;
color: #64748b;
}
.checkbox-row input[type="checkbox"] {
width: 16px; height: 16px;
accent-color: #dc2626;
}
/* --- Toast --------------------------------------------------------- */
#toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 380px;
}
.toast {
padding: 12px 18px;
border-radius: 8px;
font-size: 0.85rem;
font-weight: 500;
color: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,.15);
animation: slideIn .25s ease-out;
word-break: break-word;
}
.toast.success { background: #16a34a; }
.toast.error { background: #dc2626; }
.toast.info { background: #2563eb; }
@keyframes slideIn {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; transform: translateY(-10px); }
}
/* --- Spinner ------------------------------------------------------- */
.spinner {
display: inline-block;
width: 16px; height: 16px;
border: 2px solid rgba(255,255,255,.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin .6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* --- Separator ----------------------------------------------------- */
.sep { border: none; border-top: 1px solid #e2e8f0; margin: 18px 0; }
/* --- Helpers ------------------------------------------------------- */
.hidden { display: none !important; }
.mt-2 { margin-top: 8px; }
.mt-4 { margin-top: 16px; }
.text-sm { font-size: 0.82rem; }
.text-muted { color: #94a3b8; }
</style>
</head>
<body>
<!-- Toast container -->
<div id="toast-container"></div>
<!-- ================================================================== -->
<!-- LOGIN SCREEN -->
<!-- ================================================================== -->
<div class="container" id="login-screen">
<div class="logo-header">
<div class="icon">TM</div>
<h1>TieMeasureFlow</h1>
<p class="brand-sub">System Setup</p>
</div>
<div class="card">
<h2>Setup Password</h2>
<p class="text-sm text-muted" style="margin-bottom:16px;">
Enter the <span class="mono">SETUP_PASSWORD</span> from your <span class="mono">.env</span> file to continue.
</p>
<label for="setup-pwd">Password</label>
<input type="password" id="setup-pwd" placeholder="Enter setup password" autofocus>
<button class="btn btn-primary" id="btn-login" onclick="doLogin()">Unlock Setup</button>
</div>
</div>
<!-- ================================================================== -->
<!-- SETUP PANEL (hidden until login) -->
<!-- ================================================================== -->
<div class="container hidden" id="setup-panel">
<div class="logo-header">
<div class="icon">TM</div>
<h1>TieMeasureFlow</h1>
<p class="brand-sub">System Setup Panel</p>
</div>
<!-- 1. DB Status --------------------------------------------------- -->
<div class="card" id="card-status">
<h2>Database Status</h2>
<div id="status-content">
<p class="text-sm text-muted">Loading...</p>
</div>
</div>
<!-- 2. Init DB ----------------------------------------------------- -->
<div class="card">
<h2>Initialize Database</h2>
<p class="text-sm text-muted" style="margin-bottom:14px;">
Create all required tables in the database. Safe to run multiple times.
</p>
<button class="btn btn-primary" id="btn-init" onclick="initDb()">Create Tables</button>
</div>
<!-- 3. Create Admin ------------------------------------------------ -->
<div class="card">
<h2>Create Admin User</h2>
<p class="text-sm text-muted" style="margin-bottom:14px;">
Create a new administrator with all roles enabled.
</p>
<label for="admin-username">Username</label>
<input type="text" id="admin-username" placeholder="admin">
<label for="admin-password">Password</label>
<input type="password" id="admin-password" placeholder="Strong password">
<label for="admin-email">Email</label>
<input type="email" id="admin-email" placeholder="admin@example.com">
<label for="admin-displayname">Display Name</label>
<input type="text" id="admin-displayname" placeholder="Administrator">
<button class="btn btn-success" id="btn-admin" onclick="createAdmin()">Create Admin</button>
</div>
<!-- 4. Seed Demo --------------------------------------------------- -->
<div class="card">
<h2>Seed Demo Data</h2>
<p class="text-sm text-muted" style="margin-bottom:14px;">
Populate the database with sample users and a demo recipe.
Creates users: <span class="mono">admin</span>, <span class="mono">maker1</span>,
<span class="mono">tec1</span>, <span class="mono">metro1</span>.
</p>
<button class="btn btn-success" id="btn-seed" onclick="seedData()">Load Demo Data</button>
</div>
<!-- 5. Reset DB ---------------------------------------------------- -->
<div class="card danger">
<h2 style="color:#dc2626;">Reset Database</h2>
<p class="text-sm" style="color:#991b1b; margin-bottom:8px;">
This will <strong>DROP ALL TABLES</strong> and recreate them. All data will be permanently lost.
</p>
<div class="checkbox-row">
<input type="checkbox" id="reset-confirm">
<label for="reset-confirm" style="margin-bottom:0; cursor:pointer;">I understand this action is irreversible</label>
</div>
<button class="btn btn-danger" id="btn-reset" onclick="resetDb()" disabled>Reset Database</button>
</div>
</div>
<!-- ================================================================== -->
<!-- JAVASCRIPT -->
<!-- ================================================================== -->
<script>
/* ------------------------------------------------------------------ */
/* State */
/* ------------------------------------------------------------------ */
let setupPassword = '';
/* ------------------------------------------------------------------ */
/* Toast notifications */
/* ------------------------------------------------------------------ */
function showToast(message, type) {
type = type || 'info';
const container = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = message;
container.appendChild(el);
setTimeout(function() {
el.style.animation = 'fadeOut .3s ease-out forwards';
setTimeout(function() { el.remove(); }, 300);
}, 4000);
}
/* ------------------------------------------------------------------ */
/* Loading helpers */
/* ------------------------------------------------------------------ */
function setLoading(btnId, loading) {
const btn = document.getElementById(btnId);
if (!btn) return;
if (loading) {
btn.disabled = true;
btn._origText = btn.innerHTML;
btn.innerHTML = '<span class="spinner"></span> Working...';
} else {
btn.disabled = false;
btn.innerHTML = btn._origText || btn.innerHTML;
}
}
/* ------------------------------------------------------------------ */
/* API helpers */
/* ------------------------------------------------------------------ */
async function apiPost(path, body) {
const resp = await fetch('/api/setup' + path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await resp.json();
if (!resp.ok) {
throw new Error(data.detail || 'Request failed (' + resp.status + ')');
}
return data;
}
/* ------------------------------------------------------------------ */
/* Check DB status */
/* ------------------------------------------------------------------ */
async function checkStatus() {
const container = document.getElementById('status-content');
try {
const resp = await fetch('/api/setup/status');
if (!resp.ok) throw new Error('Status endpoint returned ' + resp.status);
const data = await resp.json();
let html = '';
if (data.tables_exist) {
html += '<div class="status-bar ok"><span class="dot green"></span>'
+ 'Database initialized &mdash; ' + data.table_count + ' table(s)</div>';
html += '<div class="table-list">';
data.tables.forEach(function(t) {
html += '<span class="table-chip">' + t + '</span>';
});
html += '</div>';
} else {
html += '<div class="status-bar warn"><span class="dot yellow"></span>'
+ 'No tables found. Click "Create Tables" below.</div>';
}
container.innerHTML = html;
} catch (e) {
container.innerHTML = '<div class="status-bar err"><span class="dot red"></span>'
+ 'Could not fetch status: ' + e.message + '</div>';
}
}
/* ------------------------------------------------------------------ */
/* Login */
/* ------------------------------------------------------------------ */
async function doLogin() {
const pwd = document.getElementById('setup-pwd').value.trim();
if (!pwd) { showToast('Please enter a password', 'error'); return; }
setLoading('btn-login', true);
try {
const resp = await fetch('/api/setup/status');
if (resp.status === 404) {
showToast('Setup is disabled. Set SETUP_PASSWORD in .env', 'error');
return;
}
// Password accepted (status endpoint is accessible)
setupPassword = pwd;
document.getElementById('login-screen').classList.add('hidden');
document.getElementById('setup-panel').classList.remove('hidden');
await checkStatus();
showToast('Setup panel unlocked', 'success');
} catch (e) {
showToast('Connection error: ' + e.message, 'error');
} finally {
setLoading('btn-login', false);
}
}
/* Allow Enter key on password field */
document.getElementById('setup-pwd').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doLogin();
});
/* ------------------------------------------------------------------ */
/* Init DB */
/* ------------------------------------------------------------------ */
async function initDb() {
setLoading('btn-init', true);
try {
const data = await apiPost('/init-db', { password: setupPassword });
showToast(data.message, 'success');
await checkStatus();
} catch (e) {
showToast(e.message, 'error');
} finally {
setLoading('btn-init', false);
}
}
/* ------------------------------------------------------------------ */
/* Create Admin */
/* ------------------------------------------------------------------ */
async function createAdmin() {
const username = document.getElementById('admin-username').value.trim();
const password = document.getElementById('admin-password').value;
const email = document.getElementById('admin-email').value.trim();
const display = document.getElementById('admin-displayname').value.trim();
if (!username || !password || !email || !display) {
showToast('All fields are required', 'error');
return;
}
setLoading('btn-admin', true);
try {
const data = await apiPost('/create-admin', {
password: setupPassword,
admin_username: username,
admin_password: password,
admin_email: email,
admin_display_name: display,
});
showToast(data.message, 'success');
// Clear form
document.getElementById('admin-username').value = '';
document.getElementById('admin-password').value = '';
document.getElementById('admin-email').value = '';
document.getElementById('admin-displayname').value = '';
} catch (e) {
showToast(e.message, 'error');
} finally {
setLoading('btn-admin', false);
}
}
/* ------------------------------------------------------------------ */
/* Seed Demo Data */
/* ------------------------------------------------------------------ */
async function seedData() {
setLoading('btn-seed', true);
try {
const data = await apiPost('/seed', { password: setupPassword });
showToast(data.message + ' (users: ' + data.users_created.join(', ') + ')', 'success');
} catch (e) {
showToast(e.message, 'error');
} finally {
setLoading('btn-seed', false);
}
}
/* ------------------------------------------------------------------ */
/* Reset DB */
/* ------------------------------------------------------------------ */
/* Enable reset button only when checkbox is checked */
document.getElementById('reset-confirm').addEventListener('change', function() {
document.getElementById('btn-reset').disabled = !this.checked;
});
async function resetDb() {
if (!document.getElementById('reset-confirm').checked) {
showToast('Please confirm the checkbox first', 'error');
return;
}
if (!confirm('Are you absolutely sure? This will destroy ALL data!')) {
return;
}
setLoading('btn-reset', true);
try {
const data = await apiPost('/reset-db', {
password: setupPassword,
confirm: true,
});
showToast(data.message, 'success');
document.getElementById('reset-confirm').checked = false;
document.getElementById('btn-reset').disabled = true;
await checkStatus();
} catch (e) {
showToast(e.message, 'error');
} finally {
setLoading('btn-reset', false);
}
}
</script>
</body>
</html>