Move the ~130-line $extraJsInline heredoc from admin/system.php into a
static file public/assets/js/system.js, loaded via $extraJs so the
template footer emits a normal <script src=…>.
Replace 4 inline style= attributes with named CSS modifier classes in
system.css:
- style="margin:0;border:none;padding:0" on .srv-section-title
→ .srv-section-title--compact
- style="margin-bottom:.75rem" on sub-heading <h3>
→ .srv-section-title--sub
- style="margin-bottom:0" on .php-grid
→ .php-grid--flush
- style="font-size:.84rem;color:var(--text-secondary)" on <label>
→ .log-toolbar label rule in system.css
The one remaining inline style (--disk-pct / --disk-color CSS custom
properties on .disk-bar) is intentionally kept: it carries PHP runtime
values that cannot be expressed in a static stylesheet.
App::consumeFlash() had 18-line legacy fallback chains reading from seven old
session keys (error, admin_error, edit_error, form_error, success,
admin_success, edit_success) that were written by no code in the codebase.
All action handlers have used App::flash() -> _flash_error / _flash_success
since the App class was introduced. Removed the dead fallbacks; consumeFlash()
is now 4 lines.
admin/import.php was the last admin template with inline style= attributes.
Extracted four of them to named CSS classes in admin.css:
- admin-error-list — error <ul> spacing (was style="margin:.5rem 0 0;padding-left:1.2rem")
- admin-file-hint — <small> display + margin (was style="margin-top:.5rem")
- admin-import-results — results panel margin (was style="margin-top:2rem")
- admin-import-results__title — <h2> typography (was multi-property inline style)
Closes the 'unify flash message keys' item in todo/02-php-components.md and
the import.php inline style item in todo/01-css-semantic-refactor.md.
3.3.2 Labels or instructions
- Replace bare <label>Lecteur·ices :</label> (no 'for', no associated control)
with <fieldset class="admin-jury-lecteurs"><legend>Lecteur·ices</legend>
giving AT a proper programmatic label for the entire lecteur group
4.1.2 Name, role, value — Externe checkboxes lacked group context
- Add aria-label="Promoteur·ice — externe" on the promoteur Externe checkbox
- Add aria-label="Lecteur·ice N — nom" on every lecteur name input
- Add aria-label="Lecteur·ice N — externe" on every lecteur Externe checkbox
- All three attributes added to both PHP-rendered rows and the addJuryRow() JS
that builds new rows dynamically
2.1.1 Keyboard — remove buttons already had aria-label; verified and updated
label text to "Supprimer le lecteur·ice N" (consistent with new numbering)
CSS (admin.css)
- Add .admin-body fieldset fieldset.admin-jury-lecteurs rule: removes
border/padding/background from the nested fieldset so it reads as a
sub-group inside the outer jury fieldset, not a double-bordered card
Audit (no code change)
- WCAG 1.4.4: all font-size values use rem — no px text sizing anywhere
- WCAG 1.4.12: only overflow:hidden on media containers and .sr-only utility;
no essential text content is clipped by text-spacing overrides
- WCAG 4.1.2 bulk JS: result is a redirect to flash-messages.php which already
emits role="alert"/role="status" — no additional JS announcement needed
- Updated 6 admin templates: add.php, edit.php, login.php, account.php,
import.php, pages-edit.php — replaced <div class="admin-submit-wrap">
with <div class="admin-form-footer">
- Updated 8 CSS selectors in admin.css:
- .admin-form-footer { margin-top/padding-top } (was .admin-submit-wrap)
- .admin-form > div:not(.admin-form-footer) grid exclusion guard (×3)
- .admin-login-box .admin-form > div:not(.admin-form-footer) overrides (×2)
- .admin-login-box .admin-form-footer compact spacing override
- No visual change; purely a semantic rename to a descriptive class name
- Also marked status-badge.php partial and WCAG 1.3.1 badge tasks as
already-done in todo/02-php-components.md and todo/04-accessibility.md
(partial + CSS were fully implemented but todo had not been updated)
WCAG 1.4.1 — Active nav link had no non-colour indicator in the admin panel.
Public nav already had border-bottom via common.css; admin nav had nothing.
admin.css:
- Add `[aria-current="page"]` rule on admin nav links:
border-bottom: 2px solid currentColor; padding-bottom: 1px
This gives a visible underline as a non-colour signal for the active page.
- Fix `--admin-purple` undefined CSS variable in pagination button hover.
The variable was referenced but never defined in variables.css (which was
refactored to use --accent-primary / --accent-secondary). Replaced both
border-color and color usages with var(--accent-primary) (#9557b5 — same
value), restoring the intended purple hover tint on pagination buttons.
todo/01-css-semantic-refactor.md:
- Audited ~15 pending CSS/HTML tasks; all were already implemented.
Marked as done: .admin-main, .admin-page-title, .admin-form-row,
.admin-label, .admin-input/select/textarea, .admin-table, .admin-fieldset,
tfe.css class replacements, search.css h2 selector, admin-alert replacement,
login.php/edit.php inline style removal, form partial hints (<small>).
todo/04-accessibility.md:
- Marked WCAG 1.4.1 admin nav and --admin-purple audit items as completed.
- Add Database::getThesesListCount(array $filters) — runs the same WHERE
clauses as getThesesList() but with COUNT(DISTINCT t.id); used to compute
total pages without loading all rows.
- Extend Database::getThesesList() with $limit/$offset parameters; when
$limit > 0 appends LIMIT/OFFSET and re-binds positional params individually
to avoid the PDO mixed-style restriction.
- Fix getThesesList() SELECT: add LEFT JOIN access_types + at.name as
access_type — the column was referenced in the template but never fetched.
- Wire admin/index.php: read ?page=, compute $totalPages/$offset, pass
$perPage=25 + $offset to getThesesList(); include pagination.php partial
below the table with filter-preserving $baseParams.
- Add result-count line (<p class="admin-list-meta">) showing "X–Y sur Z TFE"
when multiple pages exist.
- Add .admin-body .pagination-wrap / .pagination-btn / .pagination-info styles
to admin.css (scoped to .admin-body to avoid colliding with public pages).
0 offset, 8px blur, rgba(0,0,0,0.35) — no directional shadow, just a
soft glow that improves legibility of white text against the lighter
portions of the gradient header.
The file had accumulated severe corruption in its lower half (garbled
selector text, variable names spliced into property values, orphaned
declarations, broken nesting) alongside hardcoded hex colours throughout.
Rewrote the entire file cleanly:
- Every colour is now a var() referencing a token defined in variables.css:
--accent-primary/secondary/foreground, --accent-blue/green/yellow/red,
--bg-secondary/tertiary, --border-primary, --text-primary/secondary/tertiary,
--error, --warning, --success, --accent-muted.
- Zero raw hex values remain in admin.css.
- Removed the corrupted/dead CSS from the bottom half and reconstructed
all selectors from what the templates actually use (audited via grep).
- Fixed structural issues: broken border shorthand, nested rules that
were not valid CSS, orphaned declaration blocks.
- New/restored rules: .admin-maintenance-bar (was corrupted),
.status-access variants (was corrupted), .admin-section-title--danger,
.admin-danger-zone, .admin-account-status (all reconstructed cleanly).
- .admin-btn--warning and .admin-btn--danger now use var(--accent-yellow)
and var(--accent-red) instead of hardcoded dark hex values.
- .admin-btn-remove hover now uses var(--error) instead of #e55.
- .admin-btn-unpublish now uses var(--bg-secondary)/var(--text-tertiary)
instead of hardcoded grey hex values.
- select option background colours removed (browser chrome, not styleable
cross-platform).
Templates: replace 4 inline var(--admin-text-muted) with var(--text-secondary)
in index.php, thanks.php, import.php.
Replace the two undefined variables that had crept in:
- var(--admin-border) → #555 (in .log-output border)
- var(--admin-text-muted) → #969696 (inline style on log toolbar label,
in both system.php and system-fragment.php)
Revert the incorrect intermediate attempt that mapped dark-UI hex values
to light-theme tokens (--bg-primary: #fff, --border-primary: #ddd, etc.)
and also revert the .admin-body override block that was added to
variables.css — variables.css is shared and must not have per-component
overrides.
All remaining var() calls in system.css now reference tokens that exist
in variables.css:
--accent-primary, --accent-green, --error, --warning, --success,
--text-tertiary
The dark surface colours (#1a1a1a, #242424, #0d0d0d, #555, #969696, etc.)
stay as literal hex values, consistent with how admin.css handles them.
Add system-fragment.php — a thin authenticated endpoint that returns only
the tab-panel HTML (toolbar + meta + log/nginx-config output) for a given
?tab=&n= combination. No page shell, no status section, no DB queries.
system.php changes:
- Tab <a> elements gain data-tab= attributes used by JS to identify the
target without parsing hrefs.
- Tab panel content wrapped in <div id=sys-tab-panel data-tab= data-n=>
which JS uses as both the swap target and its own state store.
- JS rewritten: tab clicks and lines-select changes call loadPanel()
which fetch()es system-fragment.php, swaps innerHTML, updates active
tab ARIA attributes, and pushes state via history.pushState.
- Browser back/forward handled via popstate listener.
- bindPanelControls() re-wires the lines-select and copy-to-clipboard
button after every innerHTML swap (event delegation not feasible here
because log-output is replaced wholesale).
- fetch() failure falls back to window.location.href (full page load).
- Tabs without JS still work: <a href> links go to system.php?tab=…
as before.
system-fragment.php:
- Requires AdminAuth::isAuthenticated(); returns 403 on failure.
- Validates tab and n params against the same whitelist as system.php.
- All helper functions namespaced with frag_ prefix to avoid redeclaration
if PHP ever includes both files in the same process.
- Renders identical HTML to the corresponding section in system.php.
system.css:
- #sys-tab-panel gets min-height:8rem and position:relative to prevent
layout jump during fetch.
- .sys-panel-loading: opacity 0.4 + pointer-events:none + subtle
diagonal-stripe ::after overlay with shimmer animation.
Status (services, PHP env, disk) is now always visible above the log/config
tab bar rather than being one of the tab targets:
- Status section rendered unconditionally above <nav class="sys-tabs">.
- Services grid, PHP info grid and disk bar grouped inside a collapsible
<section> with a header row containing the cache-freshness badge and a
toggle button (▲ Réduire / ▼ Développer).
- Collapse state persisted in localStorage so the preference survives
page reloads (e.g. when switching log tabs).
- Tab bar now only contains the three log tabs + nginx config; the 'Statut'
tab is removed. Legacy ?tab=status URLs fall through to nginx_access.
- PHP/disk sub-sections laid out in a 2-col grid inside the status panel;
responsive single-col below 700px.
- system.css: new .sys-status-section / .sys-status-header /
.sys-status-toggle / .sys-status-meta rules added.
- aria-current="page" added to active tab links.
- todo/03-system-cache.md: all items marked done; notes added explaining
why log caching was deliberately omitted.
Add a TTL-based cache for the expensive checks on the admin system page,
eliminating repeated systemctl subprocess calls (~4×~100ms), curl self-pings
(~200-500ms), disk_*_space() and PHP ini reads on every page load.
Changes:
- storage/migrations/007_system_cache.sql: new migration creating the
system_cache table (key TEXT PK, value TEXT, updated_at INTEGER)
- storage/schema.sql: system_cache table added before pages table
- Applied migration to live storage/posterg.db
- src/SystemCache.php: new class with get/set/isStale/ageSeconds/invalidate;
uses SQLite INSERT … ON CONFLICT upsert; no external dependencies
- src/Database.php: added getDatabasePath(): string accessor
- public/admin/system.php:
- Bootstrap SystemCache at request start using the existing DB PDO handle
- system_status: cached with 2-min TTL (systemctl + curl checks)
- php_info: cached with 1-hour TTL (PHP ini values are runtime-constant)
- disk_info: cached with 5-min TTL (total/free/used/pct tuple)
- Logs section: unchanged — always reads live log tail per active tab
- ?refresh=1 GET param invalidates all three cache keys before rendering
- Status panel heading shows cache badge: '⚡ Cache — il y a Xs' (hit)
or '⟳ Actualisé' (miss/fresh), styled via new .sys-cache-badge rules
- public/assets/css/system.css: .sys-cache-badge / --hit / --miss styles
Remove five presentational classes from admin forms and replace with
structural CSS selectors scoped to .admin-form:
- .admin-form-row → .admin-form > div:not(.admin-submit-wrap)
Grid layout (260px label col + 1fr input col) applied directly to div
children of the form; submit-wrap div excluded via :not().
- .admin-label → .admin-form > div:not(.admin-submit-wrap) > label
Scoped to the direct label child of each form row div; does not bleed
into nested checkbox labels inside .admin-checkbox-list.
- .admin-input / .admin-select / .admin-textarea
→ .admin-form input:not([type=checkbox|radio|file|hidden|submit])
→ .admin-form select
→ .admin-form textarea
Also extended to .admin-inline-form input/select (tags page) so the
tags table inputs retain identical base styling and focus colour.
Templates updated: add.php, edit.php, login.php, account.php,
pages-edit.php, import.php, tags.php,
templates/partials/form/jury-fieldset.php — all class= attributes for
the five removed classes stripped.
import.php: added 'admin-form' class alongside 'admin-import-area' so
its single file-input row gets the grid row treatment; submit div was
already using admin-submit-wrap so it is correctly excluded.
No visual change — selectors target the same elements as before.
- admin.css: replace .admin-alert / .admin-alert--error / .admin-alert--success
selectors with [role="alert"][data-type="error"] and [role="status"][data-type="success"]
- All 10 admin templates updated: <div class="admin-alert admin-alert--{type}">
becomes <p role="alert|status" data-type="error|success"> (or <div> for the
import.php multi-item list that contains a <ul>)
- flash-messages.php partial updated to match
- WCAG benefit: role="alert" is an ARIA live region — errors are announced
immediately by screen readers without focus movement (fixes WCAG 3.3.1, 4.1.2)
- role="status" (polite live region) used for success messages — announced
without interrupting the user
- Removes two BEM modifier classes; CSS now targets element semantics directly
Replace four presentational class names in admin.css with structural selectors
that target native HTML elements already present in every admin template:
.admin-main → .admin-body main
.admin-page-title → .admin-body main > h1
.admin-table → .admin-body table
.admin-fieldset → .admin-body fieldset
.admin-fieldset-legend → .admin-body legend
Also migrate the .admin-main > section / h2 / dl / dt / dd block to
.admin-body main > section so the thanks-page section styles survive.
Add .admin-body main > table { margin-top: 1.5rem } to absorb the inline
style="margin-top:1.5rem" that was on tags.php's <table class="admin-table">.
All 10 affected admin templates updated (add, edit, account, index, import,
pages, pages-edit, tags, system, thanks) — class attributes removed where
the element alone is now the selector. Zero visual changes.
- admin.css: remove .admin-hint and .admin-field-hint class rules; add
.admin-body form small with the same font-size/color/margin properties
plus display:block so it stacks below sibling inputs; stub comment left
where .admin-field-hint was to document the change
- add.php: 5× <p class="admin-hint"> → <small>
- edit.php: 3× <p class="admin-hint"> → <small>
- import.php: <div class="admin-hint"> → <small> (block hint below CSV input)
- pages-edit.php: class="admin-hint" removed from already-correct <small>
- account.php: <p class="admin-field-hint"> → <small>
Hint text is now styled purely via the semantic element selector; no class
required on any hint element in admin templates.
- Create public/assets/css/system.css with all 280 lines of CSS that were
inline in system.php: tab bar, status cards, PHP info grid, disk bar,
log viewer, nginx config viewer, and syntax-highlight classes.
- Disk bar dynamic values (width %, colour) moved from PHP-interpolated CSS
rules to CSS custom properties (--disk-pct, --disk-color) set on the
element via an inline style attribute; static .disk-bar rule in system.css
consumes them via var().
- system.php JS block (tab-select auto-nav + copy-to-clipboard) moved to
$extraJsInline heredoc; footer.php emits it before </body> — keeps it
out of the document <head> and removes the bare <script> after </main>.
- system.php now sets $extraCss = ['/assets/css/system.css'] so head.php
emits a proper <link> in <head>, consistent with all other admin pages.
- No behaviour change; system.php is now zero inline CSS/JS.
Replace 6 CSS class selectors across tfe.css, main.css, and search.css with
semantic element-based selectors, removing the corresponding classes from the
HTML templates entirely.
tfe.css:
- .tfe-meta-list → article dl / article dl > div / article dl dt / article dl dd
- .tfe-media-block → aside figure (+ img, video, embed children)
- .tfe-file-caption → aside figcaption
main.css:
- .card__media → .home-body figure (+ img/video children and hover/motion rules)
- .card__caption → .home-body li > a > p
search.css:
- .repertoire-col > h2 → .repertoire-index section > h2
Template changes:
- tfe.php: removed class= from <dl>, <figure>, and <figcaption>
- index.php: removed class= from <figure> and <p class=card__caption>;
stripped orphaned card__media from the gradient <div> (only --gradient needed)
No visual change — selectors match the same elements as before since the
semantic HTML was already in place from prior refactoring work.
- login.php: removed style= on .admin-form-row and .admin-label (already covered
by .admin-login-box scoped rules); extracted submit-wrap spacing and full-width
button to .admin-login-box .admin-submit-wrap and .admin-login-box .admin-btn
- account.php: style="margin-top:3rem" on danger-zone heading moved to
.admin-section-title--danger modifier; <span style="color:..."> replaced with
<small> element styled via .admin-danger-zone__description small
- add.php / edit.php / pages-edit.php: all style="align-items:start" removed from
.admin-form-row (redundant — already the CSS default at line 116 of admin.css);
banner preview inline styles extracted to .admin-banner-preview / .admin-banner-preview img;
add-jury button margin extracted to .admin-add-jury-btn; cancel links use .admin-cancel-link
Zero inline style= attributes remain in login, account, add, edit, pages-edit.
- admin/index.php: replace <div class="admin-stats"> with <dl>; inner
<div class="admin-stat__number"> → <dd>, <div class="admin-stat__label"> → <dt>;
use CSS order to keep number visually first; add scope="col" to all 9 <th> cells
- admin/thanks.php: replace all four <div class="admin-thesis-info"> wrappers
with <section> elements; remove the class entirely; add scope="col" to
the files table <th> cells
- admin/tags.php: add scope="col" to all 3 <th> cells
- admin/pages.php: add scope="col" to all 4 <th> cells
- admin.css: rename .admin-thesis-info selectors to .admin-main > section
(element + context selector — no class needed); add display:flex +
flex-direction:column to .admin-stat so CSS order property works correctly
Addresses TODO items: section X (admin-stats dl, th scope), XI (tags th scope),
XII (admin-thesis-info → section), XIII (pages.php th scope)
CSS: .site-search → header form[role="search"],
.site-search__icon → header form[role="search"] svg,
.site-search__input → header form[role="search"] input,
.site-search__input::placeholder → header form[role="search"] input::placeholder
HTML: Removed class="site-search", class="site-search__icon", and
class="site-search__input" from header.php and search-bar.php.
The form already uses role="search" and contains a single svg + input,
so the semantic selectors are unambiguous.
templates/admin/head.php:
- admin-nav__logo now href="/" with target="_blank" rel="noopener noreferrer"
- Left arrow prefix (← via ←, aria-hidden) signals leaving admin
- sr-only suffix "(site public, nouvel onglet)" for screen readers
public/admin/login.php:
- Same treatment on the standalone login nav (was a bare <span>)
public/assets/css/admin.css:
- admin-nav__list: flex:1 removed; margin-left:auto added
→ entire link list now right-justified inside the nav bar,
mirroring the layout of the public site header
- .admin-nav__logout { margin-left:auto } removed (no longer needed;
logout is just the last item in a right-aligned list)