fix: improve SPC dashboard UX — hover toolbar, visible report errors
- Change Plotly modebar to hover-only mode to avoid overlapping chart content - Remove redundant Plotly titles (container headers already provide them) - Add Content-Type check and inline error display for report downloads - Fix setup login validation and seed idempotency for existing data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,14 +35,14 @@ const PLOTLY_LAYOUT_DEFAULTS = {
|
||||
font: { family: 'Inter, system-ui, sans-serif', color: SPC_COLORS.textColor },
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
margin: { t: 40, r: 30, b: 50, l: 60 },
|
||||
margin: { t: 20, r: 30, b: 50, l: 60 },
|
||||
xaxis: { gridcolor: SPC_COLORS.gridColor, zeroline: false },
|
||||
yaxis: { gridcolor: SPC_COLORS.gridColor, zeroline: false },
|
||||
};
|
||||
|
||||
const PLOTLY_CONFIG = {
|
||||
responsive: true,
|
||||
displayModeBar: true,
|
||||
displayModeBar: 'hover',
|
||||
modeBarButtonsToRemove: ['lasso2d', 'select2d'],
|
||||
displaylogo: false,
|
||||
};
|
||||
@@ -154,7 +154,6 @@ function renderControlChart(containerId, data) {
|
||||
|
||||
const layout = {
|
||||
...PLOTLY_LAYOUT_DEFAULTS,
|
||||
title: { text: _t('controlChart'), font: { size: 14 } },
|
||||
xaxis: { ...PLOTLY_LAYOUT_DEFAULTS.xaxis, title: _t('measureNum') },
|
||||
yaxis: { ...PLOTLY_LAYOUT_DEFAULTS.yaxis, title: _t('value') },
|
||||
shapes,
|
||||
@@ -231,7 +230,6 @@ function renderHistogram(containerId, data, tol) {
|
||||
|
||||
const layout = {
|
||||
...PLOTLY_LAYOUT_DEFAULTS,
|
||||
title: { text: _t('histogram'), font: { size: 14 } },
|
||||
xaxis: { ...PLOTLY_LAYOUT_DEFAULTS.xaxis, title: _t('value') },
|
||||
yaxis: { ...PLOTLY_LAYOUT_DEFAULTS.yaxis, title: _t('frequency') },
|
||||
shapes,
|
||||
|
||||
@@ -108,6 +108,12 @@
|
||||
{{ _("Applica filtri") }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- Inline report error (shown below buttons row) -->
|
||||
<div x-show="reportError" x-cloak
|
||||
class="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1"
|
||||
x-text="reportError"
|
||||
x-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nessuna ricetta selezionata -->
|
||||
@@ -266,6 +272,7 @@ function spcDashboard() {
|
||||
loading: false,
|
||||
hasData: false,
|
||||
errorMessage: '',
|
||||
reportError: '',
|
||||
downloadingReport: false,
|
||||
|
||||
init() {
|
||||
@@ -387,7 +394,7 @@ function spcDashboard() {
|
||||
renderHistogram('histogram-chart', this.histogram, tol);
|
||||
} else {
|
||||
const el = document.getElementById('histogram-chart');
|
||||
if (el) el.innerHTML = '<p class="text-center text-steel py-12">{{ _("Seleziona un punto di misura per l\'istogramma") }}</p>';
|
||||
if (el) el.innerHTML = '<p class="text-center text-steel py-12">' + {{ _("Seleziona un punto di misura per l'istogramma")|tojson }} + '</p>';
|
||||
}
|
||||
|
||||
// Capability gauge
|
||||
@@ -404,6 +411,7 @@ function spcDashboard() {
|
||||
},
|
||||
|
||||
async downloadReport(type) {
|
||||
this.reportError = '';
|
||||
this.downloadingReport = type;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
@@ -417,7 +425,19 @@ function spcDashboard() {
|
||||
|
||||
if (!resp.ok) {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
this.errorMessage = errData.detail || '{{ _("Errore nella generazione del report") }}';
|
||||
const msg = errData.detail || '{{ _("Errore nella generazione del report") }}';
|
||||
console.error('Report download failed (' + resp.status + '):', msg, errData);
|
||||
this.reportError = msg;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check Content-Type: server may return JSON error with 200 status
|
||||
const contentType = resp.headers.get('Content-Type') || '';
|
||||
if (!contentType.includes('application/pdf')) {
|
||||
const errData = await resp.json().catch(() => ({}));
|
||||
const msg = errData.detail || '{{ _("Errore nella generazione del report") }}';
|
||||
console.error('Report returned non-PDF content-type "' + contentType + '":', msg, errData);
|
||||
this.reportError = msg;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -434,7 +454,7 @@ function spcDashboard() {
|
||||
URL.revokeObjectURL(a.href);
|
||||
} catch (e) {
|
||||
console.error('Error downloading report:', e);
|
||||
this.errorMessage = '{{ _("Errore di connessione al server") }}';
|
||||
this.reportError = '{{ _("Errore di connessione al server") }}';
|
||||
} finally {
|
||||
this.downloadingReport = false;
|
||||
}
|
||||
|
||||
@@ -268,6 +268,14 @@ async def seed_demo_data(body: PasswordBody):
|
||||
|
||||
created_users: list[User] = []
|
||||
for u in users_data:
|
||||
# Skip users that already exist
|
||||
existing = await session.execute(
|
||||
select(User).where(User.username == u["username"])
|
||||
)
|
||||
existing_user = existing.scalar_one_or_none()
|
||||
if existing_user:
|
||||
created_users.append(existing_user)
|
||||
continue
|
||||
user = User(
|
||||
username=u["username"],
|
||||
password_hash=hash_password(u["password"]),
|
||||
@@ -286,6 +294,19 @@ async def seed_demo_data(body: PasswordBody):
|
||||
admin_user = created_users[0]
|
||||
|
||||
# ---- Recipe -------------------------------------------------------
|
||||
# Skip if recipe already exists
|
||||
existing_recipe = await session.execute(
|
||||
select(Recipe).where(Recipe.code == "DEMO-001")
|
||||
)
|
||||
if existing_recipe.scalar_one_or_none():
|
||||
return {
|
||||
"status": "ok",
|
||||
"message": "Demo data already exists, skipped",
|
||||
"users_created": [u["username"] for u in users_data],
|
||||
"recipe_created": "DEMO-001",
|
||||
"measurements_created": 0,
|
||||
}
|
||||
|
||||
recipe = Recipe(
|
||||
code="DEMO-001",
|
||||
name="Demo Measurement Recipe",
|
||||
|
||||
@@ -603,7 +603,8 @@
|
||||
showToast('Setup is disabled. Set SETUP_PASSWORD in .env', 'error');
|
||||
return;
|
||||
}
|
||||
// Password accepted (status endpoint is accessible)
|
||||
// Validate password via init-db (idempotent, no tables needed)
|
||||
await apiPost('/init-db', { password: pwd });
|
||||
setupPassword = pwd;
|
||||
document.getElementById('login-screen').classList.add('hidden');
|
||||
document.getElementById('setup-panel').classList.remove('hidden');
|
||||
@@ -611,7 +612,11 @@
|
||||
await loadUsers();
|
||||
showToast('Setup panel unlocked', 'success');
|
||||
} catch (e) {
|
||||
if (e.message.indexOf('403') !== -1 || e.message.indexOf('Invalid') !== -1) {
|
||||
showToast('Invalid setup password', 'error');
|
||||
} else {
|
||||
showToast('Connection error: ' + e.message, 'error');
|
||||
}
|
||||
} finally {
|
||||
setLoading('btn-login', false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user