fix scheduler orario start, immagine parcheggio, added more logs function
This commit is contained in:
@@ -26,6 +26,9 @@ router = APIRouter(prefix="/api/offices", tags=["offices"])
|
||||
class ValidOfficeCreate(BaseModel):
|
||||
name: str
|
||||
parking_quota: int = 0
|
||||
booking_window_enabled: bool = True
|
||||
booking_window_end_hour: int = 18
|
||||
booking_window_end_minute: int = 0
|
||||
|
||||
|
||||
class ClosingDayCreate(BaseModel):
|
||||
@@ -121,6 +124,9 @@ def create_office(data: ValidOfficeCreate, db: Session = Depends(get_db), user=D
|
||||
name=data.name,
|
||||
parking_quota=data.parking_quota,
|
||||
spot_prefix=get_next_available_prefix(db),
|
||||
booking_window_enabled=data.booking_window_enabled,
|
||||
booking_window_end_hour=data.booking_window_end_hour,
|
||||
booking_window_end_minute=data.booking_window_end_minute,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(office)
|
||||
@@ -180,12 +186,12 @@ def update_office_settings(office_id: str, data: OfficeSettingsUpdate, db: Sessi
|
||||
|
||||
if data.booking_window_end_hour is not None:
|
||||
if not (0 <= data.booking_window_end_hour <= 23):
|
||||
raise HTTPException(status_code=400, detail="Hour must be 0-23")
|
||||
raise HTTPException(status_code=400, detail="Hour must be 0-23")
|
||||
office.booking_window_end_hour = data.booking_window_end_hour
|
||||
|
||||
if data.booking_window_end_minute is not None:
|
||||
if not (0 <= data.booking_window_end_minute <= 59):
|
||||
raise HTTPException(status_code=400, detail="Minute must be 0-59")
|
||||
raise HTTPException(status_code=400, detail="Minute must be 0-59")
|
||||
office.booking_window_end_minute = data.booking_window_end_minute
|
||||
|
||||
office.updated_at = datetime.utcnow()
|
||||
|
||||
@@ -26,10 +26,10 @@ services:
|
||||
networks:
|
||||
- org-network
|
||||
labels:
|
||||
- "caddy=parking.lvh.me"
|
||||
- "caddy=parcheggio.lvh.me"
|
||||
- "caddy.reverse_proxy={{upstreams 8000}}"
|
||||
- "caddy.forward_auth=authelia:9091"
|
||||
- "caddy.forward_auth.uri=/api/verify?rd=https://parking.lvh.me/"
|
||||
- "caddy.forward_auth.uri=/api/verify?rd=https://parcheggio.lvh.me/"
|
||||
- "caddy.forward_auth.copy_headers=Remote-User Remote-Groups Remote-Name Remote-Email"
|
||||
# cambiare l'url delle label per il reverse proxy
|
||||
|
||||
|
||||
@@ -241,15 +241,7 @@ class NotificationLog(Base):
|
||||
)
|
||||
|
||||
|
||||
notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # parking_change
|
||||
subject = Column(Text, nullable=False)
|
||||
body = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
sent_at = Column(DateTime) # null = not sent yet
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_queue_pending', 'sent_at'),
|
||||
)
|
||||
|
||||
|
||||
class OfficeSpot(Base):
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 786 KiB After Width: | Height: | Size: 829 KiB |
@@ -17,8 +17,36 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
await loadOffices();
|
||||
setupEventListeners();
|
||||
populateTimeSelects();
|
||||
});
|
||||
|
||||
function populateTimeSelects() {
|
||||
const hoursSelect = document.getElementById('officeCutoffHour');
|
||||
const minutesSelect = document.getElementById('officeCutoffMinute');
|
||||
|
||||
// Clear existing
|
||||
hoursSelect.innerHTML = '';
|
||||
minutesSelect.innerHTML = ''; // Re-creating to allow 0-59 range
|
||||
|
||||
// Populate Hours 0-23
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const val = i.toString().padStart(2, '0');
|
||||
const opt = document.createElement('option');
|
||||
opt.value = i;
|
||||
opt.textContent = val;
|
||||
hoursSelect.appendChild(opt);
|
||||
}
|
||||
|
||||
// Populate Minutes 0-59
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const val = i.toString().padStart(2, '0');
|
||||
const opt = document.createElement('option');
|
||||
opt.value = i;
|
||||
opt.textContent = val;
|
||||
minutesSelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOffices() {
|
||||
const response = await api.get('/api/offices');
|
||||
if (response && response.ok) {
|
||||
@@ -70,6 +98,11 @@ async function editOffice(officeId) {
|
||||
document.getElementById('officeName').value = office.name;
|
||||
document.getElementById('officeQuota').value = office.parking_quota;
|
||||
|
||||
// Set booking window settings
|
||||
document.getElementById('officeWindowEnabled').checked = office.booking_window_enabled !== false;
|
||||
document.getElementById('officeCutoffHour').value = office.booking_window_end_hour != null ? office.booking_window_end_hour : 18;
|
||||
document.getElementById('officeCutoffMinute').value = office.booking_window_end_minute != null ? office.booking_window_end_minute : 0;
|
||||
|
||||
openModal('Modifica Ufficio');
|
||||
}
|
||||
|
||||
@@ -123,7 +156,10 @@ async function handleOfficeSubmit(e) {
|
||||
const officeId = document.getElementById('officeId').value;
|
||||
const data = {
|
||||
name: document.getElementById('officeName').value,
|
||||
parking_quota: parseInt(document.getElementById('officeQuota').value) || 0
|
||||
parking_quota: parseInt(document.getElementById('officeQuota').value) || 0,
|
||||
booking_window_enabled: document.getElementById('officeWindowEnabled').checked,
|
||||
booking_window_end_hour: parseInt(document.getElementById('officeCutoffHour').value),
|
||||
booking_window_end_minute: parseInt(document.getElementById('officeCutoffMinute').value)
|
||||
};
|
||||
|
||||
console.log('Payload:', data);
|
||||
|
||||
@@ -72,6 +72,15 @@ function populateHourSelect() {
|
||||
option.textContent = h.toString().padStart(2, '0');
|
||||
select.appendChild(option);
|
||||
}
|
||||
|
||||
const minuteSelect = document.getElementById('bookingWindowMinute');
|
||||
minuteSelect.innerHTML = '';
|
||||
for (let m = 0; m < 60; m++) {
|
||||
const option = document.createElement('option');
|
||||
option.value = m;
|
||||
option.textContent = m.toString().padStart(2, '0');
|
||||
minuteSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOfficeSettings(id) {
|
||||
|
||||
@@ -93,6 +93,27 @@
|
||||
<small class="text-muted">Numero totale di posti auto assegnati a questo ufficio</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Orario di Cut-off (Giorno Precedente)</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center;">
|
||||
<select id="officeCutoffHour" class="form-control" style="width: 80px;">
|
||||
<!-- Hours 0-23 generated by JS -->
|
||||
</select>
|
||||
<span>:</span>
|
||||
<select id="officeCutoffMinute" class="form-control" style="width: 80px;">
|
||||
<option value="0">00</option>
|
||||
<option value="15">15</option>
|
||||
<option value="30">30</option>
|
||||
<option value="45">45</option>
|
||||
</select>
|
||||
<label style="margin-left: 10px; display: flex; align-items: center; gap: 5px;">
|
||||
<input type="checkbox" id="officeWindowEnabled">
|
||||
Abilita Assegnazione Automatica
|
||||
</label>
|
||||
</div>
|
||||
<small class="text-muted">Orario limite per la prenotazione del giorno successivo</small>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-actions">
|
||||
|
||||
@@ -88,10 +88,7 @@
|
||||
</select>
|
||||
<span>:</span>
|
||||
<select id="bookingWindowMinute" style="width: 80px;">
|
||||
<option value="0">00</option>
|
||||
<option value="15">15</option>
|
||||
<option value="30">30</option>
|
||||
<option value="45">45</option>
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
</div>
|
||||
<small class="text-muted">Le presenze inserite prima di questo orario saranno messe in
|
||||
@@ -159,7 +156,8 @@
|
||||
<button id="testEmailBtn" class="btn btn-secondary">
|
||||
Test (Solo a Me)
|
||||
</button>
|
||||
<button id="bulkEmailBtn" class="btn btn-warning" title="Invia mail reale a tutti gli assegnatari">
|
||||
<button id="bulkEmailBtn" class="btn btn-warning"
|
||||
title="Invia mail reale a tutti gli assegnatari">
|
||||
Test (A Tutti)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
2
main.py
2
main.py
@@ -24,6 +24,7 @@ from app.routes.parking import router as parking_router
|
||||
from app.routes.parking import router as parking_router
|
||||
from database.connection import init_db, get_db_session
|
||||
from services.notifications import run_scheduled_notifications
|
||||
from services.parking import process_daily_allocations
|
||||
import asyncio
|
||||
|
||||
# Rate limiter setup
|
||||
@@ -37,6 +38,7 @@ async def scheduler_task():
|
||||
try:
|
||||
with get_db_session() as db:
|
||||
run_scheduled_notifications(db)
|
||||
process_daily_allocations(db)
|
||||
except Exception as e:
|
||||
config.logger.error(f"Scheduler error: {e}")
|
||||
|
||||
|
||||
@@ -228,7 +228,10 @@ def send_daily_parking_reminder(user: "User", date_obj: datetime, db: "Session")
|
||||
from database.models import DailyParkingAssignment, NotificationLog
|
||||
from services.parking import get_spot_display_name
|
||||
|
||||
config.logger.info(f"[SCHEDULER] Checking daily parking reminder for user {user.email}")
|
||||
|
||||
if not user.notify_daily_parking:
|
||||
config.logger.debug(f"[SCHEDULER] User {user.email} has disabled daily parking notifications")
|
||||
return False
|
||||
|
||||
date_str = date_obj.strftime("%Y-%m-%d")
|
||||
@@ -308,10 +311,10 @@ def run_scheduled_notifications(db: "Session"):
|
||||
users = db.query(User).all()
|
||||
|
||||
for user in users:
|
||||
# Thursday at 12: Presence reminder
|
||||
if current_weekday == 3 and current_hour == 12 and current_minute < 5:
|
||||
next_week = get_next_week_dates(today_date)
|
||||
send_presence_reminder(user, next_week, db)
|
||||
# Thursday Reminder: DISABLED as per user request
|
||||
# if current_weekday == 3 and current_hour == 12 and current_minute < 5:
|
||||
# next_week = get_next_week_dates(today_date)
|
||||
# send_presence_reminder(user, next_week, db)
|
||||
|
||||
# Daily parking reminder at user's preferred time
|
||||
user_hour = user.notify_daily_parking_hour or 8
|
||||
@@ -319,6 +322,7 @@ def run_scheduled_notifications(db: "Session"):
|
||||
|
||||
# Check if it's the right time for this user
|
||||
if current_hour == user_hour and abs(current_minute - user_minute) < 5:
|
||||
config.logger.info(f"[SCHEDULER] Triggering Daily Parking Reminder check for user {user.email} (Scheduled: {user_hour}:{user_minute})")
|
||||
# Check if Office is OPEN today
|
||||
is_office_open = True
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from database.models import (
|
||||
)
|
||||
from utils.helpers import generate_uuid
|
||||
from app import config
|
||||
from services.notifications import notify_parking_assigned
|
||||
|
||||
|
||||
def get_spot_prefix(office: Office, db: Session) -> str:
|
||||
@@ -252,6 +253,20 @@ def assign_parking_fairly(office_id: str, pool_date: date, db: Session) -> dict:
|
||||
waitlist.append(candidate["user_id"])
|
||||
|
||||
db.commit()
|
||||
|
||||
# Send notifications to successful assignees
|
||||
for user_id in assigned:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if user:
|
||||
# Re-fetch the assignment to get the spot details
|
||||
assignment = db.query(DailyParkingAssignment).filter(
|
||||
DailyParkingAssignment.user_id == user_id,
|
||||
DailyParkingAssignment.date == pool_date
|
||||
).first()
|
||||
if assignment:
|
||||
spot_name = get_spot_display_name(assignment.spot_id, office_id, db)
|
||||
notify_parking_assigned(user, pool_date, spot_name)
|
||||
|
||||
return {"assigned": assigned, "waitlist": waitlist}
|
||||
|
||||
|
||||
@@ -317,15 +332,20 @@ def handle_presence_change(user_id: str, change_date: date, old_status: Presence
|
||||
# Check booking window
|
||||
should_assign = True
|
||||
if office.booking_window_enabled:
|
||||
from zoneinfo import ZoneInfo
|
||||
tz = ZoneInfo(config.TIMEZONE)
|
||||
now = datetime.now(tz)
|
||||
|
||||
# Allocation time is Day-1 at cutoff hour
|
||||
cutoff_dt = datetime.combine(change_date - timedelta(days=1), datetime.min.time())
|
||||
cutoff_dt = cutoff_dt.replace(
|
||||
hour=office.booking_window_end_hour,
|
||||
minute=office.booking_window_end_minute
|
||||
minute=office.booking_window_end_minute,
|
||||
tzinfo=tz
|
||||
)
|
||||
|
||||
# If now is before cutoff, do not assign yet (wait for batch job)
|
||||
if datetime.utcnow() < cutoff_dt:
|
||||
if now < cutoff_dt:
|
||||
should_assign = False
|
||||
config.logger.debug(f"Queuing parking request for user {user_id} on {change_date} (Window open until {cutoff_dt})")
|
||||
|
||||
@@ -362,3 +382,35 @@ def run_batch_allocation(office_id: str, pool_date: date, db: Session) -> dict:
|
||||
|
||||
# 2. Run fair allocation
|
||||
return assign_parking_fairly(office_id, pool_date, db)
|
||||
|
||||
|
||||
def process_daily_allocations(db: Session):
|
||||
"""
|
||||
Check if any office's booking window has just closed and run batch allocation.
|
||||
Run by scheduler every minute.
|
||||
HALT: Checks if the cutoff time for TOMORROW has been reached.
|
||||
"""
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# Use configured timezone
|
||||
tz = ZoneInfo(config.TIMEZONE)
|
||||
now = datetime.now(tz)
|
||||
|
||||
# Check all offices with window enabled
|
||||
offices = db.query(Office).filter(Office.booking_window_enabled == True).all()
|
||||
|
||||
config.logger.debug(f"[SCHEDULER] Checking booking windows for {len(offices)} offices - Current Time: {now.strftime('%H:%M')}")
|
||||
|
||||
for office in offices:
|
||||
# Cutoff is defined as "Previous Day" (today) at Booking End Hour
|
||||
# If NOW matches the cutoff time, we run allocation for TOMORROW
|
||||
if now.hour == office.booking_window_end_hour and now.minute == office.booking_window_end_minute:
|
||||
target_date = now.date() + timedelta(days=1)
|
||||
config.logger.info(f"[SCHEDULER] CUTOFF REACHED for {office.name} (Cutoff: {office.booking_window_end_hour}:{office.booking_window_end_minute}). Starting Assegnazione Giornaliera parcheggi for {target_date}")
|
||||
|
||||
try:
|
||||
run_batch_allocation(office.id, target_date, db)
|
||||
config.logger.info(f"[SCHEDULER] Assegnazione Giornaliera parcheggi completed for {office.name} on {target_date}")
|
||||
except Exception as e:
|
||||
config.logger.error(f"[SCHEDULER] Failed Assegnazione Giornaliera parcheggi for {office.name}: {e}")
|
||||
|
||||
|
||||
48
upgrade_db_v1.py
Normal file
48
upgrade_db_v1.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Default path inside Docker container
|
||||
DEFAULT_DB_PATH = "/app/data/parking.db"
|
||||
|
||||
def migrate():
|
||||
# Allow overriding DB path via env var or argument
|
||||
db_path = os.getenv("DATABASE_PATH", DEFAULT_DB_PATH)
|
||||
if len(sys.argv) > 1:
|
||||
db_path = sys.argv[1]
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print(f"Error: Database file not found at {db_path}")
|
||||
print("Usage: python upgrade_db_v1.py [path_to_db]")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Migrating database at: {db_path}")
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
columns_to_add = [
|
||||
("booking_window_enabled", "BOOLEAN", "0"),
|
||||
("booking_window_end_hour", "INTEGER", "18"),
|
||||
("booking_window_end_minute", "INTEGER", "0")
|
||||
]
|
||||
|
||||
for col_name, col_type, default_val in columns_to_add:
|
||||
try:
|
||||
# Check if column exists
|
||||
cursor.execute(f"SELECT {col_name} FROM offices LIMIT 1")
|
||||
except sqlite3.OperationalError:
|
||||
# Column doesn't exist, add it
|
||||
print(f"Adding column: {col_name} ({col_type})")
|
||||
try:
|
||||
cursor.execute(f"ALTER TABLE offices ADD COLUMN {col_name} {col_type} DEFAULT {default_val}")
|
||||
except Exception as e:
|
||||
print(f"Failed to add column {col_name}: {e}")
|
||||
else:
|
||||
print(f"Column {col_name} already exists. Skipping.")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print("Migration complete.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
Reference in New Issue
Block a user