fix(setup,ui): risolve errori setup + logo SVG theme-aware
- setup: corregge path templates (sibling di api/, non figlio) e firma TemplateResponse - setup: invalida sessione client dopo operazioni che ruotano l'API key (init DB, creazione admin, cambio password, attivazione utente) - api_client: include il nome del campo nei messaggi di errore 422 (FastAPI validation) - ui: nuovo componente _app_logo.html (SVG inline theme-aware) usato su login e navbar
This commit is contained in:
@@ -26,7 +26,7 @@ from src.backend.services.measurement_service import calculate_pass_fail
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api/setup", tags=["setup"])
|
router = APIRouter(prefix="/api/setup", tags=["setup"])
|
||||||
|
|
||||||
_templates_dir = Path(__file__).resolve().parent.parent / "templates"
|
_templates_dir = Path(__file__).resolve().parent.parent.parent / "templates"
|
||||||
templates = Jinja2Templates(directory=str(_templates_dir))
|
templates = Jinja2Templates(directory=str(_templates_dir))
|
||||||
|
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ async def _seed_default_station(session: AsyncSession, admin_user: User) -> None
|
|||||||
async def setup_page(request: Request):
|
async def setup_page(request: Request):
|
||||||
"""Serve the setup page HTML."""
|
"""Serve the setup page HTML."""
|
||||||
_check_setup_enabled()
|
_check_setup_enabled()
|
||||||
return templates.TemplateResponse("setup/setup.html", {"request": request})
|
return templates.TemplateResponse(request, "setup/setup.html")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
|
|||||||
@@ -559,6 +559,19 @@
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Setup operations rotate api_keys (DB init, admin creation, password
|
||||||
|
* change, user (de)activation). Any Flask session held in this browser
|
||||||
|
* still carries the old key, so we clear it to force a fresh /auth/login. */
|
||||||
|
async function invalidateClientSession() {
|
||||||
|
try {
|
||||||
|
await fetch('/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
redirect: 'manual',
|
||||||
|
});
|
||||||
|
} catch (_) { /* best-effort */ }
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Check DB status */
|
/* Check DB status */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
@@ -635,6 +648,7 @@
|
|||||||
try {
|
try {
|
||||||
const data = await apiPost('/init-db', { password: setupPassword });
|
const data = await apiPost('/init-db', { password: setupPassword });
|
||||||
showToast(data.message, 'success');
|
showToast(data.message, 'success');
|
||||||
|
await invalidateClientSession();
|
||||||
await checkStatus();
|
await checkStatus();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message, 'error');
|
showToast(e.message, 'error');
|
||||||
@@ -667,6 +681,7 @@
|
|||||||
admin_display_name: display,
|
admin_display_name: display,
|
||||||
});
|
});
|
||||||
showToast(data.message, 'success');
|
showToast(data.message, 'success');
|
||||||
|
await invalidateClientSession();
|
||||||
// Clear form
|
// Clear form
|
||||||
document.getElementById('admin-username').value = '';
|
document.getElementById('admin-username').value = '';
|
||||||
document.getElementById('admin-password').value = '';
|
document.getElementById('admin-password').value = '';
|
||||||
@@ -690,6 +705,7 @@
|
|||||||
if (data.measurements_created) msg += ', measurements: ' + data.measurements_created;
|
if (data.measurements_created) msg += ', measurements: ' + data.measurements_created;
|
||||||
msg += ')';
|
msg += ')';
|
||||||
showToast(msg, 'success');
|
showToast(msg, 'success');
|
||||||
|
await invalidateClientSession();
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message, 'error');
|
showToast(e.message, 'error');
|
||||||
@@ -876,6 +892,7 @@
|
|||||||
if (pwd) body.user_password = pwd;
|
if (pwd) body.user_password = pwd;
|
||||||
var data = await apiPost('/manage-user', body);
|
var data = await apiPost('/manage-user', body);
|
||||||
showToast(data.message, 'success');
|
showToast(data.message, 'success');
|
||||||
|
await invalidateClientSession();
|
||||||
closeModal('modal-user');
|
closeModal('modal-user');
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -908,6 +925,7 @@
|
|||||||
new_password: newPwd,
|
new_password: newPwd,
|
||||||
});
|
});
|
||||||
showToast(data.message, 'success');
|
showToast(data.message, 'success');
|
||||||
|
await invalidateClientSession();
|
||||||
closeModal('modal-password');
|
closeModal('modal-password');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message, 'error');
|
showToast(e.message, 'error');
|
||||||
@@ -924,6 +942,7 @@
|
|||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
showToast(data.message, 'success');
|
showToast(data.message, 'success');
|
||||||
|
await invalidateClientSession();
|
||||||
await loadUsers();
|
await loadUsers();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message, 'error');
|
showToast(e.message, 'error');
|
||||||
|
|||||||
@@ -56,11 +56,21 @@ class APIClient:
|
|||||||
try:
|
try:
|
||||||
error_body = response.json()
|
error_body = response.json()
|
||||||
detail = error_body.get("detail", str(error_body))
|
detail = error_body.get("detail", str(error_body))
|
||||||
# FastAPI 422 returns detail as a list of validation errors
|
# FastAPI 422 returns detail as a list of validation errors:
|
||||||
|
# [{"type": ..., "loc": ["body", "password"], "msg": "...", ...}, ...]
|
||||||
if isinstance(detail, list):
|
if isinstance(detail, list):
|
||||||
detail = "; ".join(
|
parts = []
|
||||||
e.get("msg", str(e)) for e in detail if isinstance(e, dict)
|
for e in detail:
|
||||||
) or str(detail)
|
if not isinstance(e, dict):
|
||||||
|
parts.append(str(e))
|
||||||
|
continue
|
||||||
|
msg = e.get("msg", str(e))
|
||||||
|
loc = e.get("loc") or []
|
||||||
|
# Strip leading "body"/"query"/"path" segments; keep field path.
|
||||||
|
field_path = [str(p) for p in loc if p not in ("body", "query", "path")]
|
||||||
|
field = ".".join(field_path) if field_path else None
|
||||||
|
parts.append(f"{field}: {msg}" if field else msg)
|
||||||
|
detail = "; ".join(parts) or str(detail)
|
||||||
except Exception:
|
except Exception:
|
||||||
detail = response.text or f"HTTP {response.status_code}"
|
detail = response.text or f"HTTP {response.status_code}"
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,10 @@
|
|||||||
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-xl p-8">
|
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-xl p-8">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="text-center mb-8">
|
<div class="text-center mb-8">
|
||||||
<img src="{{ url_for('static', filename='img/tmflow-logo.svg') }}"
|
<div class="mx-auto mb-4 inline-flex justify-center text-slate-900 dark:text-white">
|
||||||
alt="TieMeasureFlow Logo"
|
{% set logo_class = 'h-16 w-auto' %}
|
||||||
class="mx-auto h-16 w-auto mb-4"
|
{% include 'components/_app_logo.html' %}
|
||||||
onerror="this.style.display='none'">
|
</div>
|
||||||
<h2 class="text-3xl font-bold text-slate-900 dark:text-white mb-2">
|
|
||||||
TieMeasureFlow
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||||
{{ _('Accedi al sistema') }}
|
{{ _('Accedi al sistema') }}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!--
|
||||||
|
Inline TieMeasureFlow logo. Text fills use `currentColor`, so wrap with a
|
||||||
|
parent that sets `color` (e.g. `text-slate-900 dark:text-white`) to make the
|
||||||
|
wordmark adapt to the active theme. The caliper/flow icon keeps its brand
|
||||||
|
blue regardless of theme.
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 260 48" fill="none"
|
||||||
|
class="{{ logo_class|default('h-10 w-auto') }}"
|
||||||
|
aria-label="TieMeasureFlow">
|
||||||
|
<!-- Caliper + flow icon (always brand color) -->
|
||||||
|
<g transform="translate(2, 6)">
|
||||||
|
<rect x="0" y="4" width="6" height="32" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="0" y="4" width="24" height="6" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="0" y="30" width="24" height="6" rx="1.5" fill="#2563EB"/>
|
||||||
|
<rect x="14" y="10" width="5" height="8" rx="1" fill="#1E40AF"/>
|
||||||
|
<rect x="14" y="22" width="5" height="8" rx="1" fill="#1E40AF"/>
|
||||||
|
<rect x="2" y="18" width="16" height="4" rx="1" fill="#3B82F6" opacity="0.6"/>
|
||||||
|
<rect x="7" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="10" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="13" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="16" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<rect x="19" y="7" width="1" height="3" rx="0.5" fill="#FFFFFF" opacity="0.7"/>
|
||||||
|
<path d="M26 20 C30 20, 32 14, 36 14 C40 14, 40 20, 36 20"
|
||||||
|
stroke="#2563EB" stroke-width="2.5" stroke-linecap="round" fill="none"/>
|
||||||
|
<path d="M34 17 L37 20 L34 23"
|
||||||
|
stroke="#2563EB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Wordmark: themed via currentColor; "Measure" stays brand-tinted -->
|
||||||
|
<g transform="translate(52, 0)" font-family="Inter, system-ui, sans-serif" letter-spacing="-0.5">
|
||||||
|
<text x="0" y="34" font-size="28" font-weight="700" fill="currentColor">Tie</text>
|
||||||
|
<text x="44" y="34" font-size="28" font-weight="500" fill="#2563EB">Measure</text>
|
||||||
|
<text x="166" y="34" font-size="28" font-weight="700" fill="currentColor">Flow</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -6,14 +6,14 @@
|
|||||||
|
|
||||||
<!-- Left: Logo -->
|
<!-- Left: Logo -->
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<a href="{{ url_for('index') }}" class="flex items-center gap-2.5 group">
|
<a href="{{ url_for('index') }}"
|
||||||
|
class="flex items-center gap-2.5 group text-slate-900 dark:text-white">
|
||||||
{% if company_logo %}
|
{% if company_logo %}
|
||||||
<img src="{{ url_for('static', filename='img/' ~ company_logo) }}" alt="Logo" class="h-8 w-auto"
|
<img src="{{ url_for('static', filename='img/' ~ company_logo) }}" alt="Logo" class="h-10 w-auto"
|
||||||
onerror="this.src='{{ url_for('static', filename='img/tmflow-logo.svg') }}'">
|
onerror="this.style.display='none'">
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="{{ url_for('static', filename='img/tmflow-logo.svg') }}"
|
{% set logo_class = 'h-10 w-auto' %}
|
||||||
alt="TieMeasureFlow"
|
{% include 'components/_app_logo.html' %}
|
||||||
class="h-8 w-auto">
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user