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 %} +
{{ _('Configura i parametri generali del sistema') }}
++ {{ _('L\'operatore verrà disconnesso automaticamente dopo il periodo di inattività indicato. Lasciare vuoto per disabilitare.') }} +
++ {{ _('Sarai disconnesso tra') }} {{ _('secondi per inattività.') }} +
+ +