feat: timer intervallo misura su ricetta + auto-logout per inattività

- 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) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-05-23 14:36:02 +00:00
parent b9c767fb0c
commit 0748ce9b1e
13 changed files with 387 additions and 4 deletions
@@ -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')
+3
View File
@@ -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
+2 -1
View File
@@ -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(
+5
View File
@@ -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)