Aug 7 - Update new design as default

This commit is contained in:
2026-08-07 11:24:22 -04:00
parent b12c019810
commit 16d5e8a6fa
6 changed files with 101 additions and 9 deletions
+17 -1
View File
@@ -166,6 +166,7 @@ part of the tree — see §7. Device registration on the API side lives in
| `GROQ_MODEL` | Optional. Groq model ID. Defaults to `llama-3.3-70b-versatile`. | | `GROQ_MODEL` | Optional. Groq model ID. Defaults to `llama-3.3-70b-versatile`. |
| `ENROLLMENT_NOTIFY_EMAILS` | Optional. Comma-separated extra addresses alerted on a new enrollment, **in addition to** every active `admin` account. For people who should be told but hold no JQC login. | | `ENROLLMENT_NOTIFY_EMAILS` | Optional. Comma-separated extra addresses alerted on a new enrollment, **in addition to** every active `admin` account. For people who should be told but hold no JQC login. |
| `ENROLLMENT_DIR` | Optional. Directory for enrollment-form JSON submissions. Defaults to `<instance_path>/enrollments` (git-ignored). Created at boot. | | `ENROLLMENT_DIR` | Optional. Directory for enrollment-form JSON submissions. Defaults to `<instance_path>/enrollments` (git-ignored). Created at boot. |
| `DEFAULT_UI_THEME` | Optional, default `modern` (phase50). The design shown when a user has no stored preference — i.e. new accounts and unauthenticated pages. A stored `users.ui_theme` always wins. Set `classic` to revert the default **without** touching anyone's saved choice. |
| `COMMENTS_VISIBLE_TO_ALL` | Optional, default `true`. **TEMPORARY (Aug 2026).** When true, customers see *every* comment on an issue, not only those ticked "Share with customer". Set `false` to restore the phase22 staff-only filtering — `is_customer_visible` is still written on every comment, so the revert needs no data repair. | | `COMMENTS_VISIBLE_TO_ALL` | Optional, default `true`. **TEMPORARY (Aug 2026).** When true, customers see *every* comment on an issue, not only those ticked "Share with customer". Set `false` to restore the phase22 staff-only filtering — `is_customer_visible` is still written on every comment, so the revert needs no data repair. |
| `PHOTO_STAMP_ENABLED` | Optional, default `true`. Burns a capture-time + geo overlay into photos uploaded via `POST /api/v1/photos/upload`. Set `false` to store raw uploads. | | `PHOTO_STAMP_ENABLED` | Optional, default `true`. Burns a capture-time + geo overlay into photos uploaded via `POST /api/v1/photos/upload`. Set `false` to store raw uploads. |
@@ -908,7 +909,22 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
→ phase46_followup_req_by → phase46_followup_req_by
→ phase47_sched_acknowledged → phase47_sched_acknowledged
→ phase48_user_ui_theme → phase48_user_ui_theme
→ phase49_external_inspector ← HEAD → phase49_external_inspector
→ phase50_default_modern ← HEAD
#### phase50 — modern design becomes the default
Revision id `phase50_default_modern`. Promotes the phase48 A/B-test design to the default. Two changes, **both required**: the `users.ui_theme` column default becomes `'modern'` (new accounts), and existing rows are moved `'classic'` → `'modern'`. phase48 stored a literal `'classic'` for everyone rather than NULL, so a default change alone would leave every current user on the old design.
**It overwrites a deliberate choice** — phase48 gave no way to tell "I picked classic" from "I never touched it", so anyone who actively preferred classic is moved too. They can switch back from the account menu and that choice then sticks; the switcher is unchanged. `/ui/theme-votes` reads the same column, so the tally reads 100% modern afterwards — capture it first if the numbers matter. `downgrade()` returns *everyone* to classic (individual prior choices were never recorded).
To change the default without touching saved preferences, set `DEFAULT_UI_THEME=classic` instead of running this.
**Deploy order:**
```bash
flask db upgrade
sudo systemctl restart gunicorn
```
#### phase49 — External Inspector role #### phase49 — External Inspector role
+11 -5
View File
@@ -167,22 +167,28 @@ def create_app(config_name='default'):
# The mobile API renders no templates and authenticates by JWT — skip it # The mobile API renders no templates and authenticates by JWT — skip it
# so this never touches the Flask-Login session loader on API traffic. # so this never touches the Flask-Login session loader on API traffic.
if request.path.startswith('/api/'): if request.path.startswith('/api/'):
# The API renders no templates; 'classic' here only means "never
# rewrite a template name" (see ThemedEnvironment.get_template).
g.jqc_theme = 'classic' g.jqc_theme = 'classic'
return return
from flask_login import current_user as _cu from flask_login import current_user as _cu
theme = 'classic' # phase50 — the fallback is configurable and now defaults to 'modern'.
# A stored users.ui_theme still wins, so an explicit choice is kept.
default = app.config.get('DEFAULT_UI_THEME', 'modern')
theme = default
try: try:
if _cu.is_authenticated: if _cu.is_authenticated:
theme = _cu.ui_theme or 'classic' theme = _cu.ui_theme or default
except Exception: # DB column missing (migration not yet run) except Exception: # DB column missing (migration not yet run)
theme = 'classic' theme = default
g.jqc_theme = theme if theme in ('classic', 'modern') else 'classic' g.jqc_theme = theme if theme in ('classic', 'modern') else default
@app.context_processor @app.context_processor
def inject_ui_theme(): def inject_ui_theme():
"""Give base.html the shell to extend.""" """Give base.html the shell to extend."""
from app.utils.time_utils import now_eastern from app.utils.time_utils import now_eastern
theme = getattr(g, 'jqc_theme', 'classic') theme = getattr(g, 'jqc_theme',
app.config.get('DEFAULT_UI_THEME', 'modern'))
_now = now_eastern() _now = now_eastern()
return { return {
'jqc_theme': theme, 'jqc_theme': theme,
+3 -1
View File
@@ -59,8 +59,10 @@ class User(UserMixin, db.Model):
# Drives base.html's layout dispatch via the inject_ui_theme() context # Drives base.html's layout dispatch via the inject_ui_theme() context
# processor. Persisted per user so the choice survives logout and can be # processor. Persisted per user so the choice survives logout and can be
# tallied as a vote (see /ui/theme-votes). # tallied as a vote (see /ui/theme-votes).
# phase50 — 'modern' is the default for new accounts. Existing rows were
# migrated in phase50; anyone who switches keeps their own choice.
ui_theme = db.Column(db.String(16), nullable=False, ui_theme = db.Column(db.String(16), nullable=False,
server_default='classic', default='classic') server_default='modern', default='modern')
# ── Customer password-setup workflow ────────────────────────────────── # ── Customer password-setup workflow ──────────────────────────────────
# password_set: False for newly created customer accounts until they # password_set: False for newly created customer accounts until they
+3 -2
View File
@@ -16,7 +16,7 @@ layout shell base.html extends.
import logging import logging
from flask import (Blueprint, render_template, redirect, request, from flask import (Blueprint, render_template, redirect, request,
url_for, flash) url_for, flash, current_app)
from flask_login import login_required, current_user from flask_login import login_required, current_user
from sqlalchemy import func from sqlalchemy import func
@@ -51,7 +51,8 @@ def switch_theme():
flash('Unknown design option.', 'warning') flash('Unknown design option.', 'warning')
return redirect(_safe_next(request.form.get('next'))) return redirect(_safe_next(request.form.get('next')))
previous = current_user.ui_theme or 'classic' previous = current_user.ui_theme or current_app.config.get(
'DEFAULT_UI_THEME', 'modern')
if previous != theme: if previous != theme:
current_user.ui_theme = theme current_user.ui_theme = theme
db.session.commit() db.session.commit()
+10
View File
@@ -58,6 +58,16 @@ class Config:
# bar into the image before storing it. Set false to store raw uploads. # bar into the image before storing it. Set false to store raw uploads.
PHOTO_STAMP_ENABLED = os.environ.get('PHOTO_STAMP_ENABLED', 'true').lower() == 'true' PHOTO_STAMP_ENABLED = os.environ.get('PHOTO_STAMP_ENABLED', 'true').lower() == 'true'
# ── Web portal design (phase50 — Aug 2026) ───────────────────────────────
# The design a user sees when they have never chosen one. phase48 shipped
# the modern design as an opt-in A/B test with 'classic' as the default;
# phase50 promotes 'modern' to the default now that the test is settled.
#
# This is the FALLBACK only — a stored users.ui_theme always wins, so anyone
# who switches keeps their choice. Set DEFAULT_UI_THEME=classic to revert
# the default without touching a single saved preference.
DEFAULT_UI_THEME = os.environ.get('DEFAULT_UI_THEME', 'modern')
# ── Issue comment visibility (TEMPORARY — Aug 2026) ────────────────────── # ── Issue comment visibility (TEMPORARY — Aug 2026) ──────────────────────
# True = every comment on an issue is visible to everyone, customers # True = every comment on an issue is visible to everyone, customers
# included; the per-comment is_customer_visible flag is ignored # included; the per-comment is_customer_visible flag is ignored
@@ -0,0 +1,57 @@
"""phase50 — make the modern design the default
phase48 shipped the modern design as an opt-in A/B test: `users.ui_theme`
defaulted to 'classic' and users switched themselves over. The test is settled,
so this promotes 'modern' to the default.
Two separate things have to change, and BOTH are needed:
1. The column default, so NEW accounts start on modern.
2. The existing rows. Every account created under phase48 has a literal
'classic' stored not a NULL so a default change alone would leave every
current user on the old design and the switch would look like it did nothing.
**This overwrites a deliberate choice.** phase48 gave no way to distinguish "I
picked classic" from "I never touched it", so anyone who actively preferred
classic is moved too. They can switch straight back from the account menu, and
that choice will then stick nothing here removes the switcher.
`/ui/theme-votes` reads this same column, so the A/B tally reads 100% modern
once this runs. Capture it before deploying if the numbers still matter.
To change the default WITHOUT touching saved preferences, set
DEFAULT_UI_THEME=classic in the environment instead of running this.
"""
revision = 'phase50_default_modern'
down_revision = 'phase49_external_inspector'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
# 1. New accounts start on modern.
op.execute(sa.text(
"ALTER TABLE users MODIFY COLUMN ui_theme VARCHAR(16) "
"NOT NULL DEFAULT 'modern'"
))
# 2. Move everyone still on the phase48 default. Idempotent: re-running
# matches nothing once every row is 'modern'.
op.execute(sa.text(
"UPDATE users SET ui_theme = 'modern' WHERE ui_theme = 'classic'"
))
def downgrade():
op.execute(sa.text(
"ALTER TABLE users MODIFY COLUMN ui_theme VARCHAR(16) "
"NOT NULL DEFAULT 'classic'"
))
# Individual pre-upgrade choices were not recorded, so this returns
# EVERYONE to classic rather than restoring who had what.
op.execute(sa.text(
"UPDATE users SET ui_theme = 'classic' WHERE ui_theme = 'modern'"
))