Files
WGSecure/app/ui/theme.py
T
tuxgyverandClaude Opus 5 cc9f521a95 feat(security): retire le MFA local, le code d'accès est celui du serveur (v0.8.1)
La v0.8.0 laissait cohabiter deux vérifications du code à 6 chiffres : la
fenêtre MFA locale, qui contrôlait un code contre un secret détenu par le
poste lui-même — et ne verrouillait donc que sa propre interface — puis la
validation par le serveur, seule à décider de l'accès réseau. L'ancienne
disparaît, avec le secret TOTP qu'elle stockait en clair dans config.json
(effacé au premier lancement) et l'onglet MFA du panneau Administrateur.

La saisie passe par une fenêtre dédiée qui relaie le refus exact du serveur
et laisse réessayer, la requête partant dans un thread pour qu'un serveur
muet ne fige pas l'application. Une ligne « Accès distant » distingue
désormais l'état de l'autorisation de celui du tunnel : monté sans code
validé, celui-ci ne mène nulle part.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 13:06:17 +02:00

234 lines
9.4 KiB
Python

"""Jetons de couleur et fragments de feuille de style partagés.
Les couleurs étaient écrites en dur au fil des widgets : vingt-quatre valeurs
distinctes dans le seul panneau d'administration, cinq bandeaux d'onglets sans
rapport de teinte entre eux, et des blocs de résultat dont le fond ne
correspondait ni à celui de la page ni à celui de leurs voisins. Tout passe
désormais par ce module — une couleur ne se choisit plus au cas par cas.
"""
# ── Fonds ────────────────────────────────────────────────────────────────────
BG = "#1c2833" # fond de l'application et de toutes les pages
BG_SUNKEN = "#17202a" # zones en creux : configurations, blocs de code
BG_RAISED = "#253545" # éléments posés sur le fond : listes, en-têtes secondaires
BORDER = "rgba(255,255,255,0.18)"
BORDER_SOFT = "rgba(255,255,255,0.10)"
INPUT_BG = "rgba(255,255,255,0.10)" # fond des champs de saisie et listes
# ── Textes ───────────────────────────────────────────────────────────────────
TEXT = "white"
TEXT_MUTED = "rgba(255,255,255,0.70)"
TEXT_FAINT = "rgba(255,255,255,0.50)"
TEXT_CODE = "#a8d8ea" # texte à chasse fixe (aperçus de config, rapports)
# ── Accent ───────────────────────────────────────────────────────────────────
ACCENT = "#2471a3"
ACCENT_HOVER = "#1a5276"
ACCENT_ACTIVE = "#154360"
ACCENT_LIGHT = "#5dade2"
# ── États ────────────────────────────────────────────────────────────────────
# Chaque état porte un trio : teinte du texte, fond translucide, bouton plein.
OK_TEXT, OK_BG, OK_SOLID = "#a9dfbf", "rgba(39,174,96,0.18)", "#1e8449"
WARN_TEXT, WARN_BG, WARN_SOLID = "#f7dc6f", "rgba(241,196,15,0.15)", "#b7950b"
FAIL_TEXT, FAIL_BG, FAIL_SOLID = "#f1948a", "rgba(231,76,60,0.18)", "#c0392b"
SKIP_TEXT, SKIP_BG = "rgba(255,255,255,0.45)", "rgba(255,255,255,0.05)"
# Teintes de survol des boutons pleins ok/fail — reprises telles quelles à
# plusieurs endroits (panneau admin, fenêtre principale) : un seul nom leur
# évite de dériver l'une de l'autre au fil des modifications.
OK_HOVER = "#27ae60"
FAIL_HOVER = "#e74c3c"
_STATE = {
"ok": (OK_TEXT, OK_BG),
"warn": (WARN_TEXT, WARN_BG),
"fail": (FAIL_TEXT, FAIL_BG),
"skip": (SKIP_TEXT, SKIP_BG),
"idle": (TEXT_MUTED, "transparent"),
}
# ── Badge de ping (fenêtre principale) ──────────────────────────────────────
PING_CSS = {
"good": "background:#1e8449; color:white;",
"medium": "background:#d4ac0d; color:black;",
"bad": "background:#922b21; color:white;",
"offline": f"background:{BG_RAISED}; color:{TEXT_FAINT};",
}
# ── Entrées de journal (fenêtre principale) ─────────────────────────────────
LOG_COLORS = {
"success": OK_TEXT,
"error": FAIL_TEXT,
"warning": WARN_TEXT,
"info": ACCENT_LIGHT,
}
# ── Bandeaux d'onglets ───────────────────────────────────────────────────────
# Six teintes calées sur la même luminosité et la même saturation : les
# onglets restent distinguables sans que l'un paraisse plus clair ou plus
# saturé que les autres, ce qui était le cas des valeurs choisies à la main.
BANNER = {
"wireguard": "#1a5276",
"keys": "#1a4a6e",
"test": "#1a4a7a",
"settings": "#155e52",
"security": "#7a3b1a",
"about": "#243b53",
}
def result_style(state: str, font_size: int = 11) -> str:
"""Feuille de style d'un bloc de résultat, selon son état.
Les blocs affichaient chacun leur propre fond rgba, recopié à la main :
même état, teintes différentes d'un widget à l'autre.
"""
color, bg = _STATE.get(state, _STATE["idle"])
return (f"padding: 8px; border-radius: 6px; font-size: {font_size}px;"
f" background: {bg}; color: {color};")
def result_view_style(state: str = "idle", font_size: int = 12) -> str:
"""Même chose pour un QTextEdit en lecture seule (rapport défilant)."""
color, bg = _STATE.get(state, _STATE["idle"])
return (
f"QTextEdit {{ background: {bg}; color: {color};"
f" border: 1px solid {BORDER_SOFT}; border-radius: 6px;"
f" padding: 8px; font-size: {font_size}px; }}"
f"QScrollBar:vertical {{ background: transparent; width: 8px;"
f" border: none; }}"
f"QScrollBar::handle:vertical {{ background: {BG_RAISED};"
f" border-radius: 4px; }}"
"QScrollBar::add-line:vertical,"
"QScrollBar::sub-line:vertical { height: 0; }"
)
def scrollarea_style() -> str:
return (
f"QScrollArea {{ border: none; background: {BG}; }}"
f"QScrollBar:vertical {{ background: {BG}; width: 8px; border: none; }}"
f"QScrollBar::handle:vertical {{ background: {BG_RAISED};"
f" border-radius: 4px; }}"
"QScrollBar::add-line:vertical,"
"QScrollBar::sub-line:vertical { height: 0; }"
)
def banner_style(color: str) -> str:
return (f"background: {color}; color: {TEXT}; font-size: 13px;"
" font-weight: bold; padding-left: 8px;")
#: CSS sombre commun à toutes les pages.
TAB_CSS = f"""
QWidget {{ background-color: {BG}; color: {TEXT}; }}
QLabel {{ color: {TEXT}; background: transparent; }}
QCheckBox {{ color: {TEXT}; background: transparent; }}
QGroupBox {{
color: {TEXT};
background: transparent;
border: 1px solid {BORDER};
border-radius: 5px;
margin-top: 10px;
font-weight: bold;
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: 10px;
padding: 0 4px;
color: {TEXT};
}}
QLineEdit {{
background: {INPUT_BG};
color: {TEXT};
border: 1px solid {BORDER};
border-radius: 4px;
padding: 4px 6px;
}}
QLineEdit:read-only {{ background: rgba(255,255,255,0.06); }}
QLineEdit:disabled {{ background: rgba(255,255,255,0.04);
color: {TEXT_FAINT}; }}
QSpinBox {{
background: {INPUT_BG};
color: {TEXT};
border: 1px solid {BORDER};
border-radius: 4px;
padding: 3px 6px;
}}
QSpinBox::up-button, QSpinBox::down-button {{
background: rgba(255,255,255,0.15);
}}
QComboBox {{
background: {INPUT_BG};
color: {TEXT};
border: 1px solid {BORDER};
border-radius: 4px;
padding: 3px 8px;
}}
QComboBox::drop-down {{ border: none; }}
QComboBox QAbstractItemView {{
background: {BG_RAISED};
color: {TEXT};
selection-background-color: {ACCENT};
}}
QTextEdit {{
background: {BG_SUNKEN};
color: {TEXT_CODE};
border: 1px solid {BORDER};
border-radius: 4px;
}}
QPushButton {{
background: {ACCENT};
color: {TEXT};
border-radius: 5px;
padding: 7px 12px;
border: none;
}}
QPushButton:hover {{ background: {ACCENT_HOVER}; }}
QPushButton:checked {{ background: {ACCENT_ACTIVE}; }}
QPushButton:disabled {{ background: rgba(255,255,255,0.08);
color: {TEXT_FAINT}; }}
"""
#: Barre d'onglets, utilisée par le panneau principal et les sous-onglets.
TABBAR_CSS = f"""
QTabWidget::pane {{ border: none; background: {BG}; }}
QTabBar::tab {{
background: {BG_RAISED};
color: {TEXT_MUTED};
padding: 6px 14px;
border: none;
}}
QTabBar::tab:selected {{ background: {ACCENT}; color: {TEXT}; }}
QTabBar::tab:hover:!selected {{ background: {ACCENT_HOVER};
color: {TEXT}; }}
"""
def primary_button_style(solid: str = OK_SOLID, hover: str = OK_HOVER) -> str:
"""Bouton d'action principale d'une page (lancer un test, par exemple)."""
return (f"QPushButton {{ padding: 9px; background: {solid}; color: {TEXT};"
f" border-radius: 5px; font-weight: bold; border: none; }}"
f"QPushButton:hover {{ background: {hover}; }}")
def secondary_button_style() -> str:
"""Bouton d'action secondaire d'une page (à côté d'une action principale)."""
return (f"QPushButton {{ padding: 9px; background: {BG_RAISED}; color: {TEXT};"
f" border-radius: 5px; border: none; }}"
f"QPushButton:hover {{ background: {ACCENT_HOVER}; }}")
def hint_style() -> str:
"""Encart d'introduction posé en tête de chaque onglet.
Assez contrasté pour se détacher du fond de page, assez sobre pour ne pas
concurrencer le contenu : un liseré d'accent à gauche, pas de cadre plein.
"""
return (f"background: rgba(36,113,163,0.16);"
f" border-left: 3px solid {ACCENT_LIGHT};"
f" border-radius: 4px; padding: 9px 11px;"
f" color: {TEXT_MUTED}; font-size: 11px;")