perf+a11y: WAL mode for SQLite, skip links, :focus-visible, .sr-only

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).
This commit is contained in:
Pontoporeia
2026-03-27 13:45:01 +01:00
parent a9877b1d1d
commit 42af4644c5
23 changed files with 128 additions and 45 deletions

31
TODO.md
View File

@@ -347,7 +347,7 @@ Goal: rename the tables and column to the canonical M2M pattern (`tags`, `thesis
### A — SQLite / Query performance ### A — SQLite / Query performance
- [ ] **WAL mode** — set `PRAGMA journal_mode = WAL` and `PRAGMA synchronous = NORMAL` in `Database::__construct()` after `foreign_keys = ON`. - [x] **WAL mode** — set `PRAGMA journal_mode = WAL` and `PRAGMA synchronous = NORMAL` in `Database::__construct()` after `foreign_keys = ON`.
Eliminates full-database read-locks on every write; makes concurrent PHP-FPM workers safe. Eliminates full-database read-locks on every write; makes concurrent PHP-FPM workers safe.
Also add `PRAGMA cache_size = -8000` (≈8 MB page cache) while there. Also add `PRAGMA cache_size = -8000` (≈8 MB page cache) while there.
@@ -1308,27 +1308,14 @@ Current state: **zero ARIA attributes, zero skip links, zero focus-visible style
These are things that must be added once and apply everywhere: These are things that must be added once and apply everywhere:
- [ ] **Add `.sr-only` utility class to `common.css`** — needed for skip links, visually-hidden - [x] **Add `.sr-only` utility class to `common.css`** — needed for skip links, visually-hidden
labels, and screen-reader-only context text referenced throughout this audit: labels, and screen-reader-only context text referenced throughout this audit.
```css
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
```
- [ ] **Add skip-to-content link in all page templates** — as described in 2.4.1 above. This - [x] **Add skip-to-content link in all page templates** — added to all 5 public pages and
one change has the highest impact-per-line-of-code ratio of any item in this audit. admin head template; `id="main-content"` added to every `<main>` in the codebase.
- [ ] **Add global `:focus-visible` rule in `common.css` and `admin.css`** — as described in - [x] **Add global `:focus-visible` rule in `common.css` and `admin.css`** — consistent
2.4.7. Second highest impact item. 2px purple outline with 2px offset; `prefers-reduced-motion` guard also added.
- [ ] **Remove all `outline: none` declarations that have no replacement focus style** — - [x] **Remove all `outline: none` declarations that have no replacement focus style** —
`common.css:125`, `admin.css:121`, `admin.css:323`, `search.css:241`. removed from `common.css`, `admin.css` (×2), and `search.css`.

View File

@@ -18,7 +18,7 @@ if (empty($_SESSION['csrf_token'])) {
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Compte administrateur</h1> <h1 class="admin-page-title">Compte administrateur</h1>
<?php if ($error): ?> <?php if ($error): ?>

View File

@@ -41,7 +41,7 @@ function wasSelected($key, $value) {
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Ajouter un TFE</h1> <h1 class="admin-page-title">Ajouter un TFE</h1>
<?php if ($error): ?> <?php if ($error): ?>

View File

@@ -234,7 +234,7 @@ try {
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Modifier un TFE</h1> <h1 class="admin-page-title">Modifier un TFE</h1>
<?php if ($error): ?> <?php if ($error): ?>

View File

@@ -279,7 +279,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Importer une liste de TFE</h1> <h1 class="admin-page-title">Importer une liste de TFE</h1>
<?php if (!empty($errors)): ?> <?php if (!empty($errors)): ?>

View File

@@ -62,7 +62,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
</script> </script>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Liste des TFE</h1> <h1 class="admin-page-title">Liste des TFE</h1>
<?php if (isset($_SESSION['error'])): ?> <?php if (isset($_SESSION['error'])): ?>

View File

@@ -33,7 +33,7 @@ $pageTitle = "Éditer : " . htmlspecialchars($page['title']);
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Éditer : <?= htmlspecialchars($page['title']) ?></h1> <h1 class="admin-page-title">Éditer : <?= htmlspecialchars($page['title']) ?></h1>
<form action="/admin/actions/page.php" method="post" class="admin-form"> <form action="/admin/actions/page.php" method="post" class="admin-form">

View File

@@ -20,7 +20,7 @@ unset($_SESSION['success']);
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Pages statiques</h1> <h1 class="admin-page-title">Pages statiques</h1>
<?php if ($success): ?> <?php if ($success): ?>

View File

@@ -601,7 +601,7 @@ require_once APP_ROOT . '/templates/admin/head.php';
.nginx-location { color: #79dac8; } .nginx-location { color: #79dac8; }
</style> </style>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Système</h1> <h1 class="admin-page-title">Système</h1>
<p class="sys-refresh-note"> <p class="sys-refresh-note">

View File

@@ -24,7 +24,7 @@ unset($_SESSION['admin_error'], $_SESSION['admin_success']);
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Mots-clés (<?= count($tags) ?>)</h1> <h1 class="admin-page-title">Mots-clés (<?= count($tags) ?>)</h1>
<?php if ($error): ?> <?php if ($error): ?>

View File

@@ -73,7 +73,7 @@ $pageTitle = "Récapitulatif TFE";
?> ?>
<?php require_once APP_ROOT . '/templates/admin/head.php'; ?> <?php require_once APP_ROOT . '/templates/admin/head.php'; ?>
<main class="admin-main"> <main class="admin-main" id="main-content">
<h1 class="admin-page-title">Récapitulatif TFE</h1> <h1 class="admin-page-title">Récapitulatif TFE</h1>
<?php if ($error): ?> <?php if ($error): ?>

View File

@@ -46,11 +46,12 @@ $aboutHtml = $pd->text($rawContent);
<?php endif; ?> <?php endif; ?>
</head> </head>
<body class="apropos-body"> <body class="apropos-body">
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<?php include APP_ROOT . '/templates/nav.php'; ?> <?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?> <?php include APP_ROOT . '/templates/search-bar.php'; ?>
<main class="apropos-main"> <main class="apropos-main" id="main-content">
<div class="apropos-layout"> <div class="apropos-layout">
<!-- LEFT: main text (from DB, Markdown-rendered) --> <!-- LEFT: main text (from DB, Markdown-rendered) -->

View File

@@ -118,7 +118,6 @@ html, body {
font-size: 0.92rem; font-size: 0.92rem;
font-family: inherit; font-family: inherit;
padding: 0.4rem 0; padding: 0.4rem 0;
outline: none;
border-radius: 0; border-radius: 0;
transition: border-color 0.15s; transition: border-color 0.15s;
-webkit-appearance: none; -webkit-appearance: none;
@@ -320,7 +319,6 @@ html, body {
font-size: 0.88rem; font-size: 0.88rem;
font-family: inherit; font-family: inherit;
padding: 0.45rem 0.75rem; padding: 0.45rem 0.75rem;
outline: none;
cursor: pointer; cursor: pointer;
} }
@@ -808,3 +806,42 @@ html, body {
font-size: 0.9rem; font-size: 0.9rem;
line-height: 1.5; line-height: 1.5;
} }
/* ============================================================
ACCESSIBILITY UTILITIES
============================================================ */
/* Consistent keyboard-focus outline for admin interactive elements */
:focus-visible {
outline: 2px solid var(--admin-purple);
outline-offset: 2px;
}
/* Skip-to-admin-content link */
.skip-link {
position: absolute;
top: -999px;
left: 1rem;
z-index: 9999;
padding: 0.5rem 1rem;
background: var(--admin-purple);
color: #fff;
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
border-radius: 0 0 4px 4px;
}
.skip-link:focus {
top: 0;
}
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}

View File

@@ -122,7 +122,6 @@ a:hover {
.site-search__input { .site-search__input {
flex: 1; flex: 1;
border: none; border: none;
outline: none;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--black); color: var(--black);
background: transparent; background: transparent;
@@ -133,3 +132,55 @@ a:hover {
.site-search__input::placeholder { .site-search__input::placeholder {
color: #aaa; color: #aaa;
} }
/* ============================================================
ACCESSIBILITY UTILITIES
============================================================ */
/* Visually-hidden but screen-reader-accessible */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Skip-to-content link (visible only on keyboard focus) */
.skip-link {
position: absolute;
top: -999px;
left: 1rem;
z-index: 9999;
padding: 0.5rem 1rem;
background: var(--purple);
color: var(--white);
font-size: 0.9rem;
font-weight: 600;
text-decoration: none;
border-radius: 0 0 4px 4px;
}
.skip-link:focus {
top: 0;
}
/* Consistent keyboard-focus outline for all interactive elements */
:focus-visible {
outline: 2px solid var(--purple);
outline-offset: 2px;
}
/* Respect user motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}

View File

@@ -238,7 +238,6 @@ html, body {
padding: 0.2rem 0.5rem; padding: 0.2rem 0.5rem;
background: var(--white); background: var(--white);
color: var(--black); color: var(--black);
outline: none;
font-family: inherit; font-family: inherit;
cursor: pointer; cursor: pointer;
} }

View File

@@ -82,6 +82,7 @@ $currentNav = '';
<?php endif; ?> <?php endif; ?>
</head> </head>
<body class="home-body"> <body class="home-body">
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<?php include APP_ROOT . '/templates/nav.php'; ?> <?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?> <?php include APP_ROOT . '/templates/search-bar.php'; ?>
@@ -97,7 +98,7 @@ $currentNav = '';
</div> </div>
<?php endif; ?> <?php endif; ?>
<main class="home-main"> <main class="home-main" id="main-content">
<div class="cards-container"> <div class="cards-container">
<?php foreach ($itemsToLoad as $item): ?> <?php foreach ($itemsToLoad as $item): ?>
<a href="tfe.php?id=<?= (int)$item["id"] ?>" class="card-link"> <a href="tfe.php?id=<?= (int)$item["id"] ?>" class="card-link">

View File

@@ -41,11 +41,12 @@ $html = $pd->text($content);
<?php endif; ?> <?php endif; ?>
</head> </head>
<body class="apropos-body"> <body class="apropos-body">
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<?php include APP_ROOT . '/templates/nav.php'; ?> <?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?> <?php include APP_ROOT . '/templates/search-bar.php'; ?>
<main class="apropos-main"> <main class="apropos-main" id="main-content">
<div class="apropos-layout"> <div class="apropos-layout">
<div class="apropos-left"> <div class="apropos-left">
<div class="apropos-description apropos-page-content"> <div class="apropos-description apropos-page-content">

View File

@@ -83,6 +83,7 @@ $searchBarValue = $_GET['query'] ?? '';
<?php endif; ?> <?php endif; ?>
</head> </head>
<body class="search-body"> <body class="search-body">
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<?php include APP_ROOT . '/templates/nav.php'; ?> <?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?> <?php include APP_ROOT . '/templates/search-bar.php'; ?>
@@ -140,7 +141,7 @@ $searchBarValue = $_GET['query'] ?? '';
<a href="search.php" class="search-reset-link">Réinitialiser</a> <a href="search.php" class="search-reset-link">Réinitialiser</a>
</form> </form>
<main class="search-main"> <main class="search-main" id="main-content">
<div class="search-results-view"> <div class="search-results-view">
<p class="search-results-header"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></p> <p class="search-results-header"><?= $totalItems ?> résultat<?= $totalItems > 1 ? 's' : '' ?></p>
@@ -173,7 +174,7 @@ $searchBarValue = $_GET['query'] ?? '';
<?php else: ?> <?php else: ?>
<!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── --> <!-- ── RÉPERTOIRE INDEX VIEW ─────────────────────────── -->
<main class="search-main"> <main class="search-main" id="main-content">
<div class="repertoire-index"> <div class="repertoire-index">
<!-- ANNÉES --> <!-- ANNÉES -->

View File

@@ -39,11 +39,12 @@ $currentNav = '';
<?php endif; ?> <?php endif; ?>
</head> </head>
<body class="tfe-body"> <body class="tfe-body">
<a href="#main-content" class="skip-link">Aller au contenu principal</a>
<?php include APP_ROOT . '/templates/nav.php'; ?> <?php include APP_ROOT . '/templates/nav.php'; ?>
<?php include APP_ROOT . '/templates/search-bar.php'; ?> <?php include APP_ROOT . '/templates/search-bar.php'; ?>
<main class="tfe-main"> <main class="tfe-main" id="main-content">
<div class="tfe-layout"> <div class="tfe-layout">
<!-- LEFT: info --> <!-- LEFT: info -->

View File

@@ -24,8 +24,11 @@ class Database {
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// Enable foreign key constraints // Enable foreign key constraints + performance pragmas
$this->pdo->exec('PRAGMA foreign_keys = ON'); $this->pdo->exec('PRAGMA foreign_keys = ON');
$this->pdo->exec('PRAGMA journal_mode = WAL');
$this->pdo->exec('PRAGMA synchronous = NORMAL');
$this->pdo->exec('PRAGMA cache_size = -8000');
} catch (PDOException $e) { } catch (PDOException $e) {
error_log("Database connection failed: " . $e->getMessage()); error_log("Database connection failed: " . $e->getMessage());
throw new Exception("Impossible de se connecter à la base de données."); throw new Exception("Impossible de se connecter à la base de données.");

View File

@@ -1 +1 @@
[1774547615,1774547646] [1774615474]

Binary file not shown.

View File

@@ -18,7 +18,8 @@
<?php endif; ?> <?php endif; ?>
</head> </head>
<body class="admin-body"> <body class="admin-body">
<nav class="admin-nav"> <a href="#main-content" class="skip-link">Aller au contenu principal</a>
<nav class="admin-nav" aria-label="Navigation admin">
<a href="/admin/" class="admin-nav__logo">Posterg</a> <a href="/admin/" class="admin-nav__logo">Posterg</a>
<?php <?php
$currentPage = basename($_SERVER['PHP_SELF']); $currentPage = basename($_SERVER['PHP_SELF']);