Files

182 lines
9.7 KiB
Plaintext

════════════════════════════════════════════════════════════════════════════
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/<same path>.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 <target>` 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.