04/07 updated timezone, report via email, etc

This commit is contained in:
2026-04-07 11:24:08 -04:00
parent 56c65bfe4c
commit fed51f8c28
14 changed files with 1022 additions and 29 deletions
+87 -1
View File
@@ -117,6 +117,33 @@ def create_app(config_name=None):
# object is already in session), then fall back to a SELECT by PK.
return db.session.get(User, int(user_id))
# ── Timezone filter ──────────────────────────────────────────────────────
# All datetimes in the DB are stored as UTC (naive). The localtime filter
# converts them to the admin-configured display timezone for templates.
# Python routes should call app_localtime(dt) when they need a local datetime.
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
def _get_tz(app_obj):
"""Return the configured ZoneInfo, falling back to UTC on bad input."""
from app.models import SystemSetting
tz_name = SystemSetting.get('app_timezone', 'America/New_York') or 'America/New_York'
try:
return ZoneInfo(tz_name)
except (ZoneInfoNotFoundError, KeyError):
app_obj.logger.warning(f'[TZ] Unknown timezone {tz_name!r}, falling back to UTC')
return ZoneInfo('UTC')
def localtime_filter(dt, fmt='%b %d, %Y %H:%M %Z'):
"""Jinja2 filter: convert a naive UTC datetime to local display time."""
if dt is None:
return ''
from datetime import timezone as _tz
tz = _get_tz(app)
aware = dt.replace(tzinfo=_tz.utc)
return aware.astimezone(tz).strftime(fmt)
app.jinja_env.filters['localtime'] = localtime_filter
# ── Context processors ────────────────────────────────────────────────────
@app.context_processor
def inject_globals():
@@ -136,7 +163,17 @@ def create_app(config_name=None):
'logo_initials' : SystemSetting.get('logo_initials', 'TD'),
'primary_color' : SystemSetting.get('primary_color', '#2563eb'),
}
return dict(unread_notifications=unread, branding=branding)
from datetime import datetime as _dt, timezone as _tz
from zoneinfo import ZoneInfo as _ZI
from app.models import SystemSetting as _SS
_tz_name = _SS.get('app_timezone', 'America/New_York') or 'America/New_York'
try:
_zone = _ZI(_tz_name)
except Exception:
_zone = _ZI('UTC')
now_local = _dt.now(_tz.utc).astimezone(_zone)
return dict(unread_notifications=unread, branding=branding,
now_local=now_local, app_tz_name=_tz_name)
# ── DB initialisation (first run) ─────────────────────────────────────────
with app.app_context():
@@ -144,6 +181,9 @@ def create_app(config_name=None):
_seed_admin(app)
_seed_settings()
# ── Email ingestion background scheduler ────────────────────────────────────
_start_email_ingestion_scheduler(app)
# ── SLA background scheduler ──────────────────────────────────────────────
# APScheduler runs inside the gunicorn worker process (single-worker
# eventlet setup), so no cross-process coordination is needed.
@@ -188,12 +228,58 @@ def _start_sla_scheduler(app):
app.logger.error(f'[SLA] Failed to start APScheduler: {exc}')
def _start_email_ingestion_scheduler(app):
"""Start the APScheduler job that polls the inbound mailbox for new emails.
The interval is read from SystemSetting at job creation time (default 5 min).
The job is a no-op when email_ingestion_enabled = '0', so it is safe to
always register it — no credentials are required until the admin enables it.
"""
import os as _os
if _os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
return # skip reloader child process
try:
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from app.services.email_ingestion_service import check_inbound_email
from app.models import SystemSetting
with app.app_context():
interval = int(SystemSetting.get('email_ingestion_interval', '5') or '5')
scheduler = BackgroundScheduler(daemon=True)
scheduler.add_job(
func = check_inbound_email,
trigger = IntervalTrigger(minutes=interval),
id = 'email_ingest',
name = 'Inbound Email Ingestion',
replace_existing = True,
args = [app],
)
scheduler.start()
app.logger.info(
f'[EMAIL INGEST] APScheduler started — polling every {interval} minute(s)'
)
except Exception as exc:
app.logger.error(f'[EMAIL INGEST] Failed to start scheduler: {exc}')
def _seed_settings():
"""Ensure all required system settings exist with safe defaults."""
from app.models import SystemSetting
defaults = [
('registration_enabled', 'true', 'Allow new users to self-register via /auth/register'),
('app_timezone', 'America/New_York', 'Display timezone for all dates and times in the UI'),
('sla_critical_hours', '4', 'Hours before a CRITICAL ticket is considered overdue'),
('email_ingestion_enabled', '0', 'Enable automatic ticket creation from inbound email (1=on, 0=off)'),
('email_ingestion_host', '', 'IMAP server hostname (e.g. imap.gmail.com)'),
('email_ingestion_port', '993', 'IMAP SSL port'),
('email_ingestion_user', '', 'Mailbox username / email address'),
('email_ingestion_password', '', 'Mailbox password (stored in plaintext — use a dedicated app password)'),
('email_ingestion_folder', 'INBOX', 'IMAP folder to watch for new mail'),
('email_ingestion_move_to', 'Processed', 'IMAP folder to move processed mail into'),
('email_ingestion_interval', '5', 'Poll interval in minutes'),
('sla_high_hours', '8', 'Hours before a HIGH ticket is considered overdue'),
('sla_medium_hours', '48', 'Hours before a MEDIUM ticket is considered overdue'),
('sla_low_hours', '120', 'Hours before a LOW ticket is considered overdue'),