════════════════════════════════════════════════════════════════════════════ JQC — phase48: Modern web portal design (A/B test with user switch) ════════════════════════════════════════════════════════════════════════════ WHAT THIS DOES ────────────── Adds a second, complete web portal design (sidebar shell, teal-blue palette from JQC_design.pptx) alongside the existing one. Each user picks which design they see; the choice is stored on their account and doubles as their vote. The classic design is byte-for-byte unchanged apart from one added menu item ("Try the New Design"). No route, endpoint, function, model or column was renamed, and no functionality was removed. HOW IT WORKS (root of the design, not a workaround) ─────────────────────────────────────────────────── 1. `app/templates/base.html` used to hold the entire page chrome. That markup moved verbatim to `app/templates/layouts/classic.html`. 2. `base.html` is now a single line — `{% extends jqc_layout %}`. Jinja resolves `{% block %}` overrides through the whole inheritance chain, so all 69 page templates keep `{% extends "base.html" %}` and needed ZERO edits. 3. `jqc_layout` is supplied by the new `inject_ui_theme()` context processor in `app/__init__.py`, driven by the new `users.ui_theme` column ('classic' | 'modern'). 4. Pages whose layout genuinely differs in the deck get an override file under `app/templates/modern/.html`. `ThemedEnvironment.get_template()` (app/__init__.py) swaps `dashboard.html` → `modern/dashboard.html` only when `g.jqc_theme == 'modern'`. The swap happens in `get_template()` rather than in the loader **on purpose**: Jinja's template cache is keyed on the name that `get_template()` receives, so a cached modern template can never be served to a classic user or vice versa. A loader-level swap would have that bug. 5. Every other page renders its existing markup inside the modern shell and is restyled by `static/css/theme_modern.css`, which loads after `theme.css` and is scoped to `body.jqc-modern`. Classic pages never load that file. FILES — PLACEMENT MAP ───────────────────── NEW app/templates/layouts/classic.html ← old base.html verbatim + one "Try the New Design" menu item in the user dropdown app/templates/layouts/modern.html ← new sidebar shell (top bar, search, bell, avatar, sidebar nav, switch) app/static/css/theme_modern.css ← modern skin, scoped to .jqc-modern app/routes/ui.py ← blueprint `ui` POST /ui/theme switch_theme() GET /ui/about about() GET /ui/support-center support_center() GET /ui/theme-votes theme_votes() (admin) app/templates/modern/dashboard.html ← deck slide 1 app/templates/modern/facilities/list.html ← deck slide 5 (hub cards + the original list, unchanged, below) app/templates/ui/about.html ← new About Us page app/templates/ui/support_center.html ← deck slide 6 support hub app/templates/ui/theme_votes.html ← admin vote tally migrations/versions/phase48_user_ui_theme.py migrations/versions/0003_add_user_active.py NO-OP stub. Repairs a PRE-EXISTING break in the Alembic revision graph: phase1_projects_roles.py declares down_revision = '0003_add_user_active' but that script is not in the repo (the early 0001-0003 files were lost). Alembic warns while walking the graph but raises KeyError as soon as it builds the full revision map, which any `flask db upgrade ` does. The stub restores the node with down_revision = None and empty upgrade()/downgrade(). No schema effect. Do not delete it. MODIFIED app/templates/base.html Entire file replaced by the one-line dispatcher (old content now lives in layouts/classic.html). app/models/user.py class User — added `ui_theme` column after `active`. VARCHAR(16) NOT NULL DEFAULT 'classic'. app/__init__.py + `ThemedEnvironment` class above create_app() + `app.jinja_environment = ThemedEnvironment` as the FIRST statement in create_app() (must precede any touch of app.jinja_env — it is a cached property) + modern-template index built at boot, before_request `resolve_ui_theme()`, context processor `inject_ui_theme()` (also exposes `now_display`) + `from app.routes import ui` and `app.register_blueprint(ui.bp)` DEPLOY — STEP 1: CODE ───────────────────── cd /home/jqc/janitorial_qc # back up the two files being replaced wholesale cp app/templates/base.html /tmp/base.html.bak cp app/__init__.py /tmp/__init__.py.bak # unzip the package over the repo root (paths already match) unzip -o jqc_phase48_modern_design.zip -d /home/jqc/janitorial_qc chown -R jqc:jqc /home/jqc/janitorial_qc/app DEPLOY — STEP 2: MIGRATION (run separately, after the code is in place) ─────────────────────────────────────────────────────────────────────── cd /home/jqc/janitorial_qc source venv/bin/activate # adjust if your venv path differs flask db upgrade Expect: phase47_sched_acknowledged → phase48_user_ui_theme With the 0003 stub in place, `flask db heads` reports exactly one head (phase48_user_ui_theme) and no "Revision ... is not present" warning. The migration uses an INFORMATION_SCHEMA existence check and an idempotent backfill — safe to re-run. Verify: mysql -e "SHOW COLUMNS FROM users LIKE 'ui_theme';" janitorial_qc DEPLOY — STEP 3: RESTART ──────────────────────── sudo systemctl restart jqc # or your unit name journalctl -u jqc -n 40 --no-pager Look for: "UI themes | modern overrides indexed: 2" No Nginx change is required — no new external host, no CSP change. VERIFICATION ──────────── 1. Log in. Portal looks exactly as before (everyone starts on classic). 2. Account menu (top right) → "Try the New Design" → same page reloads in the sidebar design, flash message confirms. 3. Dashboard: 4 KPI tiles + Inspection / Open Issues / SLA Issues cards + Scheduled + Recent Activities. Click each number — it lands on the same filtered list the classic dashboard links to. 4. Facility: 4 hub cards, then the full grouped facility list underneath. Add Facility / Print All QR / Delete modal all still work. 5. Sidebar → Supports and About Us render. 6. Bell icon: badge count and dropdown behave as on classic. 7. Sidebar → "Classic Design" button (or account menu) → returns to classic. 8. Log out and back in — the design choice persists. 9. Admin account menu → "Design Vote Tally" shows the split. 10. Audit Trail shows UPDATE / User / "ui_theme=classic→modern" for each switch. 11. Narrow the browser below 992px — the sidebar becomes an off-canvas drawer behind the hamburger. ROLLBACK ──────── Fastest (no deploy): reset everyone to classic — mysql -e "UPDATE users SET ui_theme='classic';" janitorial_qc The modern design becomes unreachable; nothing else changes. Full rollback: cp /tmp/base.html.bak app/templates/base.html cp /tmp/__init__.py.bak app/__init__.py rm -rf app/templates/layouts app/templates/modern app/templates/ui \ app/static/css/theme_modern.css app/routes/ui.py flask db downgrade phase47_sched_acknowledged sudo systemctl restart jqc KNOWN SCOPE LIMITS (deliberate) ─────────────────────────────── • Deck slides 2 (Reports), 3 (Inspections) and 4 (Issues) are NOT rebuilt as separate templates. Their existing structure already matches the deck (title + subtitle, filter row, KPI row, cards, table) and theme_modern.css restyles them — dark-teal table headers, pill filters, rounded cards. Building parallel copies of those three templates would duplicate several hundred lines of filter/permission/export logic and double the maintenance surface during a vote. Say the word after the vote and I will rebuild whichever ones you keep. • Slide 3's "Scheduled Inspection In Progress" panel appears on the modern DASHBOARD (where the route already supplies that data). Putting it on the Inspections page as well needs an additive query in `inspections.index` — small, but it is a route change, so it is not in this package. • The deck's "Overall Score" and "Avg. Score" KPI tiles are not on the modern dashboard: `dashboard.index` does not compute either value today. Adding them means new aggregate queries in the route — flag it and I will send that separately. • "Customize" on the Facility hub points at Templates (inspection templates), the closest existing feature. There is no facility field/tag configuration screen in the app yet.