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.
All admin action files (account, tag, page, edit, visibility, maintenance,
publish, formulaire) now call App::flash('error'|'success', ...) instead of
writing to raw per-page session keys ($_SESSION['error'], 'admin_error',
'edit_error', 'admin_success', 'edit_success', 'form_error').
All admin display pages (add, edit, account, tags, pages, index) now include
templates/partials/flash-messages.php instead of manually reading and
unsetting the legacy session keys and inlining their own alert HTML.
App::consumeFlash() already drained all legacy key variants as a safety net,
so the partial works correctly whether called from pages that were already
migrated or any remaining stragglers. No behaviour change for end users.
Add templates/partials/status-badge.php — a single reusable partial that
renders the <span class="status-badge …"> element for three badge types:
'publish' — Publié / En attente derived from a boolean is_published value
'access' — access-type label (Libre / Interne / Interdit) with slug-based
CSS modifier class and appropriate symbol (○ ◑ ●)
'ok' — generic green/yellow boolean badge with caller-supplied labels
(used for 'Active'/'Non configurée' and 'Présent'/'Absent' in
account.php)
All three variants emit aria-label with a context prefix and wrap the
decorative symbol in aria-hidden="true" — behaviour identical to the
inline code they replace.
Callers set $badgeType + $badgeValue (+ optional $badgeOkLabel /
$badgeWarnLabel / $badgeContext) before the include; the partial unsets
all working variables after rendering so they do not bleed into the
including scope.
Files changed:
templates/partials/status-badge.php — new partial
public/admin/index.php — table status column now uses partial
(removes 15 lines of inline if/else/php)
public/admin/account.php — two credential status rows now use partial
(removes 8 lines of inline if/else)
- 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/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)
admin/index.php — status badges (WCAG 1.4.1 Use of Colour):
- Published badge: prefix ● symbol (aria-hidden) + aria-label="Statut : Publié"
- Pending badge: prefix ◌ symbol (aria-hidden) + aria-label="Statut : En attente"
- Access badges (Libre/Interne/Interdit): prefix ○/◑/● symbol per type (aria-hidden)
+ aria-label="Accès : [type]"; symbol chosen from a PHP map keyed on the slug
Each badge now communicates its state through shape AND colour, not colour alone.
admin/index.php — ✕ Réinitialiser link (WCAG 2.5.3 / 1.1.1):
- ✕ wrapped in <span aria-hidden="true"> so the decorative symbol is skipped by
screen readers; accessible name remains "Réinitialiser"
admin/add.php + admin/edit.php — jury remove buttons (WCAG 2.5.3):
- All four ✕ remove buttons (2 static template rows + 2 JS-generated innerHTML strings)
given aria-label="Supprimer ce lecteur"; the bare ✕ Unicode character has no
speech equivalent so the aria-label replaces rather than supplements the label
admin/index.php showed "TFE total / Publiés / En attente" by running
array_filter() over the already-filtered $theses array returned by
getThesesList(). When any search or year filter was active the three
numbers reflected only the matching subset, making the stats misleading
(e.g. searching for a single student would show "1 total, 0 publiés").
Add Database::getThesesStats(): array — a single SQL aggregation query:
SELECT COUNT(*), SUM(is_published), SUM(NOT is_published) FROM theses
This runs against the raw theses table with no filters, so the counters
always display the true whole-database figures regardless of what filter
the admin has active. admin/index.php now calls getThesesStats() and
reads $stats['total'], $stats['published'], $stats['pending'] instead
of the array_filter expressions.
SQLite performance (Database::__construct):
- PRAGMA journal_mode = WAL: eliminates full-DB read locks on write, safe
for concurrent PHP-FPM workers
- PRAGMA synchronous = NORMAL: durable on commit without full fsync per write
- PRAGMA cache_size = -8000: ~8 MB page cache per connection
Accessibility foundation (WCAG 2.1 AA):
- common.css: add .sr-only utility, .skip-link (hidden until focused),
global :focus-visible (2px purple outline, 2px offset),
prefers-reduced-motion guard; remove bare outline:none from
.site-search__input
- admin.css: same :focus-visible, skip-link, and motion guard scoped to
admin purple; remove outline:none from .admin-input/.admin-select/
.admin-textarea and .admin-filters select (both had :focus border rules
already, so focus is still visually communicated)
- search.css: remove outline:none from .search-filter-select (already has
:focus border-color rule)
- All 5 public pages (index, search, tfe, apropos, licence): add
<a href="#main-content" class="skip-link"> as first child of <body>;
add id="main-content" to <main>
- templates/admin/head.php: same skip link; aria-label="Navigation admin"
on <nav>; id="main-content" on all 10 admin <main> elements
All 4 test suites pass (unit, integration, security, rate-limit).
- Flat purple-gradient nav bar with POSTERG/RÉPERTOIRE/À PROPOS links
- Full-width search bar with icon, bottom-border only, below nav
- Home: white bg, media card grid (thumbnail + author/title label below)
- Répertoire: 4-column index (Années/Catégories/Étudiantes/Mots-clés)
- TFE: 2-column layout (large text left, media right)
- À Propos: 2-column, large monospace text, new apropos.php page
- Admin: dark theme (#1a1a1a), purple gradient nav, bottom-border inputs
- New shared partials: templates/nav.php, templates/search-bar.php
- Rewrote all CSS: common, main, search, tfe, apropos, admin
- Database: extract private buildSearchConditions(array $params): array shared by
searchTheses() and countSearchResults(), eliminating ~80 lines of duplication;
add array type hints to both public methods
- Database: add getThesesList(array $filters) and getAllYears() so admin/index.php
no longer builds raw SQL inline
- admin/index.php: replace inline PDO query block with $db->getThesesList() /
$db->getAllYears(); drop the now-unused $pdo local
- config/bootstrap.php: remove dead include_template() helper and the
vendor/autoload.php Composer stub (no vendor/ directory exists)
- apps/: delete entire directory (leftover artefact, no code references it)
- tests/Integration/SearchTest.php: fix three searchTheses() calls from bare
strings to proper array params to match the method signature (prevented TypeError)
- Created /templates for main site (header.php, footer.php)
- Created /templates/admin for admin section (head.php, footer.php)
- Removed /public/includes and /public/admin/inc
- Updated all references in code and docs
- Tests passing ✅
Cleaner separation: /public only contains web-accessible files (PHP entry points + assets)
- lib/AdminAuth.php: new class with requireLogin(), login(), logout(),
isAuthenticated(); starts session with hardened cookie params
(HttpOnly, SameSite=Strict, Secure, Path=/admin) — also resolves
item #8 (session cookie hardening)
- requireLogin() auto-authenticates from nginx Basic Auth credentials
($_SERVER['PHP_AUTH_PW']) so the user only sees one browser prompt;
falls back to /admin/login.php if the proxy is absent/misconfigured
- config/admin_credentials.php: gitignored credential store; define
ADMIN_PASSWORD_HASH with a bcrypt hash to enable PHP auth
- config/admin_credentials.example.php: template for the above
- config/bootstrap.php: auto-loads admin_credentials.php if present
- .gitignore: exclude config/admin_credentials.php
- public/admin/login.php: fallback login form (shown only when nginx
Basic Auth is bypassed / proxy absent)
- public/admin/logout.php: session destruction + redirect to login
- All 7 admin PHP files: replace session_start() with
AdminAuth::requireLogin() (defence-in-depth behind nginx Basic Auth)
- public/admin/inc/head.php: Déconnexion button when ADMIN_PASSWORD_HASH
is defined
- nginx/PHP_AUTH_LAYER.md: documents dual-auth architecture, UX flow,
and setup instructions
- docs/TODO.SECURITY.md: items #2 and #8 moved to Resolved; priority
order updated (all CRITICAL done)