From 0748ce9b1e580b414c6f1a079333f0b9d304f1e5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 23 May 2026 14:36:02 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20timer=20intervallo=20misura=20su=20rice?= =?UTF-8?q?tta=20+=20auto-logout=20per=20inattivit=C3=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Recipe: nuovo campo measurement_interval_minutes (migration 003) con UI nel recipe editor - Auto-logout: impostazione di sistema configurabile da admin/settings, timer inattività JS con warning modale 60s prima del logout, tracking eventi utente throttled - Navbar: aggiunto link "Impostazioni" per admin (desktop + mobile) Co-Authored-By: Claude Opus 4.7 (1M context) --- ...dd_measurement_interval_and_auto_logout.py | 24 ++++ src/backend/models/api/recipe.py | 3 + src/backend/models/orm/recipe.py | 3 +- src/backend/services/recipe_service.py | 5 + src/frontend/flask_app/app.py | 1 + src/frontend/flask_app/blueprints/admin.py | 22 ++++ src/frontend/flask_app/blueprints/auth.py | 8 +- .../flask_app/templates/admin/settings.html | 114 ++++++++++++++++++ src/frontend/flask_app/templates/base.html | 68 +++++++++++ .../templates/components/navbar.html | 25 ++++ .../templates/maker/recipe_editor.html | 25 ++++ .../translations/en/LC_MESSAGES/messages.po | 45 +++++++ .../translations/it/LC_MESSAGES/messages.po | 48 +++++++- 13 files changed, 387 insertions(+), 4 deletions(-) create mode 100644 src/backend/migrations/versions/003_add_measurement_interval_and_auto_logout.py create mode 100644 src/frontend/flask_app/templates/admin/settings.html diff --git a/src/backend/migrations/versions/003_add_measurement_interval_and_auto_logout.py b/src/backend/migrations/versions/003_add_measurement_interval_and_auto_logout.py new file mode 100644 index 0000000..47f211b --- /dev/null +++ b/src/backend/migrations/versions/003_add_measurement_interval_and_auto_logout.py @@ -0,0 +1,24 @@ +"""add measurement_interval_minutes to recipes + +Revision ID: 003_measurement_interval +Revises: 002_add_stations +Create Date: 2026-05-23 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = '003_measurement_interval' +down_revision: Union[str, None] = '002_add_stations' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('recipes', sa.Column('measurement_interval_minutes', sa.SmallInteger(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('recipes', 'measurement_interval_minutes') diff --git a/src/backend/models/api/recipe.py b/src/backend/models/api/recipe.py index 2e235eb..2f31e47 100644 --- a/src/backend/models/api/recipe.py +++ b/src/backend/models/api/recipe.py @@ -14,6 +14,7 @@ class RecipeCreate(BaseModel): name: str = Field(..., min_length=1, max_length=255) description: Optional[str] = None image_path: Optional[str] = Field(None, max_length=500) + measurement_interval_minutes: Optional[int] = Field(None, ge=1, le=1440) # Optional task-level fields for the initial technical drawing file_path: Optional[str] = Field(None, max_length=500) file_type: Optional[str] = Field(None, pattern="^(image|pdf)$") @@ -25,6 +26,7 @@ class RecipeUpdate(BaseModel): name: Optional[str] = Field(None, min_length=1, max_length=255) description: Optional[str] = None image_path: Optional[str] = Field(None, max_length=500) + measurement_interval_minutes: Optional[int] = Field(None, ge=1, le=1440) change_notes: Optional[str] = None # Task-level fields: saved to the first task of the new version file_path: Optional[str] = Field(None, max_length=500) @@ -55,6 +57,7 @@ class RecipeResponse(BaseModel): name: str description: Optional[str] = None image_path: Optional[str] = None + measurement_interval_minutes: Optional[int] = None created_by: int created_at: datetime active: bool diff --git a/src/backend/models/orm/recipe.py b/src/backend/models/orm/recipe.py index 360f072..95faa12 100644 --- a/src/backend/models/orm/recipe.py +++ b/src/backend/models/orm/recipe.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import TYPE_CHECKING, Optional -from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint, func +from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, SmallInteger, String, Text, UniqueConstraint, func from sqlalchemy.orm import Mapped, mapped_column, relationship from src.backend.database import Base @@ -24,6 +24,7 @@ class Recipe(Base): DateTime, nullable=False, server_default=func.now() ) active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) + measurement_interval_minutes: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) # Relationships versions: Mapped[list["RecipeVersion"]] = relationship( diff --git a/src/backend/services/recipe_service.py b/src/backend/services/recipe_service.py index 03577de..8065804 100644 --- a/src/backend/services/recipe_service.py +++ b/src/backend/services/recipe_service.py @@ -134,6 +134,7 @@ async def create_recipe( name=data.name, description=data.description, image_path=data.image_path, + measurement_interval_minutes=data.measurement_interval_minutes, created_by=user.id, ) db.add(recipe) @@ -273,6 +274,8 @@ async def create_new_version( update_fields["description"] = data.description if data.image_path is not None: update_fields["image_path"] = data.image_path + if data.measurement_interval_minutes is not None: + update_fields["measurement_interval_minutes"] = data.measurement_interval_minutes if update_fields: await db.execute( update(Recipe).where(Recipe.id == recipe_id).values(**update_fields) @@ -369,6 +372,8 @@ async def update_current_version( update_fields["description"] = data.description if data.image_path is not None: update_fields["image_path"] = data.image_path + if data.measurement_interval_minutes is not None: + update_fields["measurement_interval_minutes"] = data.measurement_interval_minutes if update_fields: await db.execute( update(Recipe).where(Recipe.id == recipe_id).values(**update_fields) diff --git a/src/frontend/flask_app/app.py b/src/frontend/flask_app/app.py index f213f62..20cc278 100644 --- a/src/frontend/flask_app/app.py +++ b/src/frontend/flask_app/app.py @@ -92,6 +92,7 @@ def create_app() -> Flask: "current_language": get_locale(), "languages": Config.LANGUAGES, "company_logo": session.get("company_logo"), + "auto_logout_minutes": session.get("auto_logout_minutes"), } return app diff --git a/src/frontend/flask_app/blueprints/admin.py b/src/frontend/flask_app/blueprints/admin.py index 2b38fe3..5adc0bc 100644 --- a/src/frontend/flask_app/blueprints/admin.py +++ b/src/frontend/flask_app/blueprints/admin.py @@ -43,6 +43,28 @@ def user_list(): return render_template("admin/users.html", users=users) +@admin_bp.route("/settings") +@login_required +@admin_required +def settings_page(): + """System settings page.""" + resp = api_client.get("/api/settings") + settings = resp if not isinstance(resp, dict) or not resp.get("error") else {} + return render_template("admin/settings.html", settings=settings) + + +@admin_bp.route("/api/settings", methods=["PUT"]) +@login_required +@admin_required +def api_update_settings(): + """Proxy: Update system settings.""" + data = request.get_json(silent=True) or {} + resp = api_client.put("/api/settings", data=data) + if isinstance(resp, dict) and resp.get("error"): + return jsonify(resp), resp.get("status_code", 500) + return jsonify(resp), 200 + + @admin_bp.route("/stations") @login_required @admin_required diff --git a/src/frontend/flask_app/blueprints/auth.py b/src/frontend/flask_app/blueprints/auth.py index 8a7a1d8..8534a56 100644 --- a/src/frontend/flask_app/blueprints/auth.py +++ b/src/frontend/flask_app/blueprints/auth.py @@ -76,12 +76,18 @@ def login(): if user.get("theme_pref"): session["theme"] = user["theme_pref"] - # Carica logo aziendale dalle impostazioni di sistema + # Carica impostazioni di sistema settings_resp = api_client.get("/api/settings") if not settings_resp.get("error"): logo_path = settings_resp.get("company_logo_path") if logo_path: session["company_logo"] = logo_path + auto_logout = settings_resp.get("auto_logout_minutes") + if auto_logout: + try: + session["auto_logout_minutes"] = int(auto_logout) + except (ValueError, TypeError): + pass flash(_("Benvenuto, %(name)s!", name=user.get("display_name", username)), "success") diff --git a/src/frontend/flask_app/templates/admin/settings.html b/src/frontend/flask_app/templates/admin/settings.html new file mode 100644 index 0000000..1aaa79e --- /dev/null +++ b/src/frontend/flask_app/templates/admin/settings.html @@ -0,0 +1,114 @@ +{% extends "base.html" %} + +{% block title %}{{ _('Impostazioni') }} - TieMeasureFlow{% endblock %} + +{% block content %} +
+ + +
+

{{ _('Impostazioni di Sistema') }}

+

{{ _('Configura i parametri generali del sistema') }}

+
+ + +
+
+ + + + {{ _('Sessione e Sicurezza') }} +
+ +
+ +
+ +
+ + +
+

+ {{ _('L\'operatore verrà disconnesso automaticamente dopo il periodo di inattività indicato. Lasciare vuoto per disabilitare.') }} +

+
+
+ + +
+
+{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/src/frontend/flask_app/templates/base.html b/src/frontend/flask_app/templates/base.html index 8f31ddb..10b0a96 100644 --- a/src/frontend/flask_app/templates/base.html +++ b/src/frontend/flask_app/templates/base.html @@ -115,6 +115,74 @@ {% block extra_js %}{% endblock %} + {% if current_user and auto_logout_minutes %} + +
+ +
+ {% endif %} + diff --git a/src/frontend/flask_app/templates/components/navbar.html b/src/frontend/flask_app/templates/components/navbar.html index 0cdec13..17331a8 100644 --- a/src/frontend/flask_app/templates/components/navbar.html +++ b/src/frontend/flask_app/templates/components/navbar.html @@ -102,6 +102,20 @@ {{ _('Stazioni') }} + + + + + + + {{ _('Impostazioni') }} + {% endif %} @@ -299,6 +313,17 @@ {{ _('Stazioni') }} + + + + + + + {{ _('Impostazioni') }} + {% endif %} diff --git a/src/frontend/flask_app/templates/maker/recipe_editor.html b/src/frontend/flask_app/templates/maker/recipe_editor.html index 29993e5..e61387c 100644 --- a/src/frontend/flask_app/templates/maker/recipe_editor.html +++ b/src/frontend/flask_app/templates/maker/recipe_editor.html @@ -231,6 +231,23 @@ rows="3" placeholder="{{ _('Descrizione opzionale della ricetta...') }}"> + + +
+ + +

+ {{ _('Timer cicalino per ricordare la misurazione periodica') }} +

+
@@ -466,6 +483,9 @@ function recipeEditor() { versions: {{ (recipe.versions if recipe and recipe.versions else [])|tojson }}, currentVersion: {{ (recipe.current_version.version_number if recipe and recipe.current_version else 0)|tojson }}, + // ---- Measurement interval ---- + measurementIntervalMinutes: {{ (recipe.measurement_interval_minutes if recipe and recipe.measurement_interval_minutes else 'null')|tojson }}, + // ---- File upload (preview image) ---- currentFilePath: {{ (recipe.image_path if recipe and recipe.image_path else '')|tojson }}, uploadingFile: false, @@ -487,6 +507,11 @@ function recipeEditor() { description: this.description.trim(), }; + // Measurement interval + if (this.measurementIntervalMinutes) { + payload.measurement_interval_minutes = parseInt(this.measurementIntervalMinutes, 10); + } + // Include image_path for preview thumbnail if (this.currentFilePath) { payload.image_path = this.currentFilePath; diff --git a/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po b/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po index 7056f84..ac8b390 100644 --- a/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po +++ b/src/frontend/flask_app/translations/en/LC_MESSAGES/messages.po @@ -672,6 +672,51 @@ msgstr "Users" msgid "Stazioni" msgstr "Stations" +msgid "Impostazioni" +msgstr "Settings" + +msgid "Impostazioni di Sistema" +msgstr "System Settings" + +msgid "Configura i parametri generali del sistema" +msgstr "Configure general system parameters" + +msgid "Sessione e Sicurezza" +msgstr "Session & Security" + +msgid "Auto-logout per inattività (minuti)" +msgstr "Auto-logout for inactivity (minutes)" + +msgid "Disabilita" +msgstr "Disable" + +msgid "L'operatore verrà disconnesso automaticamente dopo il periodo di inattività indicato. Lasciare vuoto per disabilitare." +msgstr "The operator will be automatically logged out after the specified inactivity period. Leave empty to disable." + +msgid "Salva impostazioni" +msgstr "Save settings" + +msgid "Salvato" +msgstr "Saved" + +msgid "Sessione in scadenza" +msgstr "Session expiring" + +msgid "Sarai disconnesso tra" +msgstr "You will be logged out in" + +msgid "secondi per inattività." +msgstr "seconds due to inactivity." + +msgid "Continua a lavorare" +msgstr "Continue working" + +msgid "Intervallo misura (minuti)" +msgstr "Measurement interval (minutes)" + +msgid "Timer cicalino per ricordare la misurazione periodica" +msgstr "Buzzer timer to remind periodic measurement" + #: templates/components/next_measurement.html:19 msgid "Prossima misura" msgstr "Next measurement" diff --git a/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po b/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po index 1e8a501..5cf1eb8 100644 --- a/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po +++ b/src/frontend/flask_app/translations/it/LC_MESSAGES/messages.po @@ -699,9 +699,53 @@ msgid "Utenti" msgstr "Utenti" #: templates/components/navbar.html:103 templates/components/navbar.html:300 -#, fuzzy msgid "Stazioni" -msgstr "Azioni" +msgstr "Stazioni" + +msgid "Impostazioni" +msgstr "Impostazioni" + +msgid "Impostazioni di Sistema" +msgstr "Impostazioni di Sistema" + +msgid "Configura i parametri generali del sistema" +msgstr "Configura i parametri generali del sistema" + +msgid "Sessione e Sicurezza" +msgstr "Sessione e Sicurezza" + +msgid "Auto-logout per inattività (minuti)" +msgstr "Auto-logout per inattività (minuti)" + +msgid "Disabilita" +msgstr "Disabilita" + +msgid "L'operatore verrà disconnesso automaticamente dopo il periodo di inattività indicato. Lasciare vuoto per disabilitare." +msgstr "L'operatore verrà disconnesso automaticamente dopo il periodo di inattività indicato. Lasciare vuoto per disabilitare." + +msgid "Salva impostazioni" +msgstr "Salva impostazioni" + +msgid "Salvato" +msgstr "Salvato" + +msgid "Sessione in scadenza" +msgstr "Sessione in scadenza" + +msgid "Sarai disconnesso tra" +msgstr "Sarai disconnesso tra" + +msgid "secondi per inattività." +msgstr "secondi per inattività." + +msgid "Continua a lavorare" +msgstr "Continua a lavorare" + +msgid "Intervallo misura (minuti)" +msgstr "Intervallo misura (minuti)" + +msgid "Timer cicalino per ricordare la misurazione periodica" +msgstr "Timer cicalino per ricordare la misurazione periodica" #: templates/components/next_measurement.html:19 msgid "Prossima misura"