Fix nettoyage modal: SVG icon files, padding/margin consistency, BBBDMSans font, fix HTMX trigger, nested details cleanup

This commit is contained in:
Pontoporeia
2026-06-21 16:50:44 +02:00
parent 03c9c3566f
commit 71a92d682b
17 changed files with 289 additions and 166 deletions

33
TODO.md
View File

@@ -1,39 +1,23 @@
# TODO # TODO
> Last updated: 2026-06-21 > Last updated: 2026-06-21
> Context: Add SQLite indexes for contenus page language/tag query performance + fix soft-deleted thesis count filtering > Context: nettoyage modal caret icons, padding consistency, font fix
## In Progress
- [x] #contenus-indexes Add index on thesis_languages(language_id) + tags(deleted_at, name); fix count queries to exclude soft-deleted theses `(Database.php, DatabaseMigrations.php, schema.sql, migrations/applied/041_thesis_languages_index.sql)`
## Completed ## Completed
- [x] #cleanup-modal-fixes Fix nettoyage modal: SVG caret icons, margin→padding, BBBDMSans summary `(admin.css, details.css)`
- [x] #structure-formulaire-page Move "Structure du Formulaire" from contenus.php to its own dedicated page with back button `(structure-formulaire.php [new], contenus.php)`
- [x] #contenus-indexes Add index on thesis_languages(language_id) + tags(deleted_at, name); fix count queries to exclude soft-deleted theses `(Database.php, DatabaseMigrations.php, schema.sql, migrations/applied/041_thesis_languages_index.sql)` - [x] #contenus-indexes Add index on thesis_languages(language_id) + tags(deleted_at, name); fix count queries to exclude soft-deleted theses `(Database.php, DatabaseMigrations.php, schema.sql, migrations/applied/041_thesis_languages_index.sql)`
- [x] #peertube-orphans-check Add Peertube orphan video check + relink in admin — listChannelVideos (PeerTubeService), peertube-orphans.php endpoint, UI in nettoyage dialog, relink support on edit page (peertube-relink.php, peertube-browser.php, fichiers-fragment.php, file-upload-filepond.js) ✓ - [x] #peertube-orphans-check Add Peertube orphan video check + relink in admin — listChannelVideos (PeerTubeService), peertube-orphans.php endpoint, UI in nettoyage dialog, relink support on edit page `(peertube-relink.php, peertube-browser.php, fichiers-fragment.php, file-upload-filepond.js)`
## Pending
- [ ] #overtype-analysis Analyse and fix OverType editor reliability on contenus-edit.php
- [x] #tfe-optional-formats Make TFE file optional when format is Site web (1), Performance (4) or Installation (6) — fixed incorrect format IDs [3→1,4,6] + added client-side JS toggle for TFE required/asterisk. Note d'intention remains required. 🎯 `(fichiers-fragment.php, file-upload-filepond.js)` - [x] #tfe-optional-formats Make TFE file optional when format is Site web (1), Performance (4) or Installation (6) — fixed incorrect format IDs [3→1,4,6] + added client-side JS toggle for TFE required/asterisk. Note d'intention remains required. 🎯 `(fichiers-fragment.php, file-upload-filepond.js)`
- [x] #typography-weight-300 Set search placeholder + apropos/charte/licence <p> content to BBBDMSans weight 300 `(search.css, apropos.css)` - [x] #typography-weight-300 Set search placeholder + apropos/charte/licence <p> content to BBBDMSans weight 300 `(search.css, apropos.css)`
- [x] #toc-parts-uppercase Hardcode "PARTIES" uppercase + black bottom border on TOC label `(about.php, charte.php, licence.php, apropos.css)` - [x] #toc-parts-uppercase Hardcode "PARTIES" uppercase + black bottom border on TOC label `(about.php, charte.php, licence.php, apropos.css)`
- [x] #apropos-overflow Prevent #apropos-intro and content-section children from overflowing `(apropos.css)` - [x] #apropos-overflow Prevent #apropos-intro and content-section children from overflowing `(apropos.css)`
- [x] #toc-navigation Fix TOC links not navigating to headings — added `apply_id_to_heading: true` to CommonMark config so IDs land on headings not hidden <a> elements; added scroll-margin-top to headings; unstuck main from flex container so sticky TOC works for full page height `(CharteController.php, LicenceController.php, apropos.css)` - [x] #toc-navigation Fix TOC links not navigating to headings — added `apply_id_to_heading: true` to CommonMark config so IDs land on headings not hidden <a> elements; added scroll-margin-top to headings; unstuck main from flex container so sticky TOC works for full page height `(CharteController.php, LicenceController.php, apropos.css)`
- [x] #apropos-toc-style Fix TOC "Parties" label: Ductus font + lowercase, remove border-left from links, match global link style; rename .apropos-content → section.content, .apropos-section → .content-section, remove .prose wrapper `(apropos.css, about.php, charte.php, licence.php)` - [x] #apropos-toc-style Fix TOC "Parties" label: Ductus font + lowercase, remove border-left from links, match global link style; rename .apropos-content → section.content, .apropos-section → .content-section, remove .prose wrapper `(apropos.css, about.php, charte.php, licence.php)`
- [x] #apropos-toc-confirm Fixed sticky TOC: removed `flex: 1; min-height: 0` on main for apropos-body so the sticky container is full content height; added `max-height` + `overflow-y: auto` to TOC for long lists `(apropos.css)` - [x] #apropos-toc-confirm Fixed sticky TOC: removed `flex: 1; min-height: 0` on main for apropos-body so the sticky container is full content height; added `max-height` + `overflow-y: auto` to TOC for long lists `(apropos.css)`
- [ ] #contact-test-manual Test contact decoupling end-to-end: student submission → admin edit → public TFE display
- [ ] #aria-test-manual Test WCAG changes with VoiceOver and NVDA on full add/edit/partage form flows
- [ ] #nojs-upload-test Test end-to-end: submit partage form with JS disabled, verify files arrive via `$_FILES`
- [ ] #csp-media-iframe-deploy Deploy nginx config fix to server, test PDF iframe on /tfe?id=221
- [x] #fix-finality-types Create standalone script + just command to rename finality types (Approfondi→Approfondie, Enseignement→Didactique, Spécialisé→Spécialisée) `(scripts/fix-finality-types.php, justfile)` - [x] #fix-finality-types Create standalone script + just command to rename finality types (Approfondi→Approfondie, Enseignement→Didactique, Spécialisé→Spécialisée) `(scripts/fix-finality-types.php, justfile)`
- [x] #context-note-synopsis Display contextual note above synopsis (italic) instead of in meta column on TFE page `(tfe.php, tfe.css)` - [x] #context-note-synopsis Display contextual note above synopsis (italic) instead of in meta column on TFE page `(tfe.php, tfe.css)`
## Completed
- [x] #decouple-contacts Decouple contact_visible (public) & contact_interne (private email): backend already decoupled; made contact_public checkbox functional in admin add/edit forms; contact_public now controls TFE page visibility `(FormBootstrap.php, ThesisCreateController.php, ThesisEditController.php, tfe.php, form.php)` - [x] #decouple-contacts Decouple contact_visible (public) & contact_interne (private email): backend already decoupled; made contact_public checkbox functional in admin add/edit forms; contact_public now controls TFE page visibility `(FormBootstrap.php, ThesisCreateController.php, ThesisEditController.php, tfe.php, form.php)`
- [x] #csrf-rotation-race Stop CSRF token rotation in draft.php + remove hx-post from <form> — both broke FilePond uploads and form submission `(admin/actions/draft.php, partage/fragments/draft.php, FormBootstrap.php, pill-search.js)` - [x] #csrf-rotation-race Stop CSRF token rotation in draft.php + remove hx-post from <form> — both broke FilePond uploads and form submission `(admin/actions/draft.php, partage/fragments/draft.php, FormBootstrap.php, pill-search.js)`
- [x] ~~#filepond-csrf-stale~~ (superseded by #csrf-rotation-race) - [x] ~~#filepond-csrf-stale~~ (superseded by #csrf-rotation-race)
- [x] #adminold-return-type Fix adminOld closure return type from `:string` to `:string|array` `(FormBootstrap.php)` - [x] #adminold-return-type Fix adminOld closure return type from `:string` to `:string|array` `(FormBootstrap.php)`
@@ -42,9 +26,7 @@
- [x] #restore-languages Un-soft-delete anglais (id=2) and néerlandais (id=71) in dev DB ✓ - [x] #restore-languages Un-soft-delete anglais (id=2) and néerlandais (id=71) in dev DB ✓
- [x] #php-upload-limits Increase PHP upload_max_filesize to 8G, post_max_size to 8.5G `(.user.ini)` - [x] #php-upload-limits Increase PHP upload_max_filesize to 8G, post_max_size to 8.5G `(.user.ini)`
- [x] #formdata-fieldset-crash Remove leftover debug console.log that called new FormData(fieldset) `(admin/footer.php)` - [x] #formdata-fieldset-crash Remove leftover debug console.log that called new FormData(fieldset) `(admin/footer.php)`
- [x] #csp-media-iframe-fix Fix CSP `frame-ancestors 'none'` blocking PDF iframes — replaced `try_files` redirect with direct `fastcgi_pass` in `location = /media` so `add_header` CSP override survives internal nginx redirect `(nginx/xamxam.conf)` - [x] #csp-media-iframe-fix Fix CSP `frame-ancestors 'none'` blocking PDF iframes — replaced `try_files` redirect with direct `fastcgi_pass` in `location = /media` so `add_header` CSP override survives internal nginx redirect `(nginx/xamxam.conf)`
- [x] #duration-migration Add migration to reintroduce `duration_value` and `duration_unit` columns + update views `(migrations/applied/040_duration_fields.sql)` - [x] #duration-migration Add migration to reintroduce `duration_value` and `duration_unit` columns + update views `(migrations/applied/040_duration_fields.sql)`
- [x] #duration-database Update `createThesis`, `updateThesis`, `getThesisRawFields` in Database `(Database.php)` - [x] #duration-database Update `createThesis`, `updateThesis`, `getThesisRawFields` in Database `(Database.php)`
- [x] #duration-controllers Handle duration in `ThesisCreateController` and `ThesisEditController` `(ThesisCreateController.php, ThesisEditController.php)` - [x] #duration-controllers Handle duration in `ThesisCreateController` and `ThesisEditController` `(ThesisCreateController.php, ThesisEditController.php)`
@@ -68,4 +50,11 @@
- [x] #split-form-css Split `form.css` into `form-base.css` and `form-admin.css` - [x] #split-form-css Split `form.css` into `form-base.css` and `form-admin.css`
- [x] #extra-css-admin Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets `(head.php)` - [x] #extra-css-admin Update `head.php` to support `$extraCssAdmin` for admin-only stylesheets `(head.php)`
## Pending
- [ ] #overtype-analysis Analyse and fix OverType editor reliability on contenus-edit.php
- [ ] #contact-test-manual Test contact decoupling end-to-end: student submission → admin edit → public TFE display
- [ ] #aria-test-manual Test WCAG changes with VoiceOver and NVDA on full add/edit/partage form flows
- [ ] #nojs-upload-test Test end-to-end: submit partage form with JS disabled, verify files arrive via `$_FILES`
- [ ] #csp-media-iframe-deploy Deploy nginx config fix to server, test PDF iframe on /tfe?id=221
## Deferred / Blocked ## Deferred / Blocked

View File

@@ -4,7 +4,7 @@
* *
* GET /admin/actions/cleanup-stats-fragment.php * GET /admin/actions/cleanup-stats-fragment.php
* *
* Returns an HTML fragment ready for HTMX swap into #tmp-cleanup-stats. * Returns an HTML fragment ready for HTMX swap into #tmp-cleanup-stats-wrapper.
*/ */
require_once __DIR__ . '/../../../bootstrap.php'; require_once __DIR__ . '/../../../bootstrap.php';
require_once __DIR__ . '/../../../src/AdminAuth.php'; require_once __DIR__ . '/../../../src/AdminAuth.php';
@@ -16,76 +16,115 @@ require __DIR__ . '/cleanup-stats.php';
$json = ob_get_clean(); $json = ob_get_clean();
$d = json_decode($json, true); $d = json_decode($json, true);
$hasFilePond = ($d['filepond_stale_count'] ?? 0) > 0 || ($d['filepond_active_count'] ?? 0) > 0; $fpStale = $d['filepond_stale_count'] ?? 0;
$hasTrash = ($d['trash_stale_count'] ?? 0) > 0 || ($d['trash_active_count'] ?? 0) > 0; $fpActive = $d['filepond_active_count'] ?? 0;
$trStale = $d['trash_stale_count'] ?? 0;
$trActive = $d['trash_active_count'] ?? 0;
if (!$hasFilePond && !$hasTrash): ?> $totalStale = $fpStale + $trStale;
<p style="margin:0;color:var(--accent-green)">✓ Aucun fichier temporaire.</p> $totalFiles = $fpStale + $fpActive + $trStale + $trActive;
<?php return; endif; ?>
<?php if ($d['filepond_stale_count'] > 0): ?> // Build summary meta: total stale count + total size
<p class="n-heading"> $staleParts = [];
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M230.64,25.36a32,32,0,0,0-45.26,0q-.21.21-.42.45L131.55,88.22,121,77.64a24,24,0,0,0-33.95,0l-76.69,76.7a8,8,0,0,0,0,11.31l80,80a8,8,0,0,0,11.31,0L178.36,169a24,24,0,0,0,0-33.95l-10.58-10.57L230.19,71c.15-.14.31-.28.45-.43A32,32,0,0,0,230.64,25.36ZM96,228.69,79.32,212l22.34-22.35a8,8,0,0,0-11.31-11.31L68,200.68,55.32,188l22.34-22.35a8,8,0,0,0-11.31-11.31L44,176.68,27.31,160,72,115.31,140.69,184ZM219.52,59.1l-68.71,58.81a8,8,0,0,0-.46,11.74L167,146.34a8,8,0,0,1,0,11.31l-15,15L83.32,104l15-15a8,8,0,0,1,11.31,0l16.69,16.69a8,8,0,0,0,11.74-.46L196.9,36.48A16,16,0,0,1,219.52,59.1Z"></path></svg> if ($fpStale > 0) $staleParts[] = $fpStale . ' téléversement';
Téléversements abandonnés <span class="n-meta"><?= $d['filepond_stale_count'] ?> dossier(s) · <?= htmlspecialchars($d['filepond_stale_human']) ?></span> if ($trStale > 0) $staleParts[] = $trStale . ' corbeille';
</p> $staleMeta = implode(' + ', $staleParts);
<table class="n-table">
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead> // Approximate total stale size
<tbody> $staleSize = ($d['filepond_stale_size'] ?? 0) + ($d['trash_stale_size'] ?? 0);
<?php foreach ($d['filepond_stale_files'] as $f): ?> $staleHuman = '';
<tr> if ($staleSize >= 1073741824) { $staleHuman = number_format($staleSize / 1073741824, 1) . ' GB'; }
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td> elseif ($staleSize >= 1048576) { $staleHuman = number_format($staleSize / 1048576, 1) . ' MB'; }
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td> elseif ($staleSize >= 1024) { $staleHuman = number_format($staleSize / 1024, 0) . ' KB'; }
<td style="white-space:nowrap">~<?= (int)$f['age_minutes'] ?> min</td> elseif ($staleSize > 0) { $staleHuman = $staleSize . ' B'; }
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)" $summaryMeta = $staleMeta;
hx-post="/admin/actions/cleanup-tmp.php" if ($staleHuman) $summaryMeta .= ' · ' . $staleHuman;
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","filepond_dir":"<?= htmlspecialchars($f['name']) ?>"}'
hx-target="#tmp-cleanup-stats" if ($totalStale === 0 && $totalFiles === 0): ?>
hx-swap="innerHTML" <div id="tmp-cleanup-stats-wrapper">
hx-indicator="#tmp-cleanup-stats"> <details class="n-section" open>
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <summary>
Supprimer <img src="/assets/icons/paint-brush-household.svg" width="14" height="14" alt="" aria-hidden="true">
</button> Fichiers temporaires
</td> </summary>
</tr> <p style="margin:0;color:var(--accent-green)">✓ Aucun fichier temporaire.</p>
<?php endforeach; ?> </details>
</tbody> </div>
</table> <?php return; endif; ?>
<div id="tmp-cleanup-stats-wrapper">
<details class="n-section" open>
<summary>
<img src="/assets/icons/paint-brush-household.svg" width="14" height="14" alt="" aria-hidden="true">
Fichiers temporaires <span class="n-meta"><?= htmlspecialchars($summaryMeta) ?></span>
</summary>
<?php if ($fpStale > 0): ?>
<p class="n-heading">
<img src="/assets/icons/paint-brush-household.svg" width="14" height="14" alt="" aria-hidden="true">
Téléversements abandonnés
</p>
<table class="n-table">
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
<tbody>
<?php foreach ($d['filepond_stale_files'] as $f): ?>
<tr>
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
<td style="white-space:nowrap">~<?= (int)$f['age_minutes'] ?> min</td>
<td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
hx-post="/admin/actions/cleanup-tmp.php"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","filepond_dir":"<?= htmlspecialchars($f['name']) ?>"}'
hx-target="#tmp-cleanup-stats-wrapper"
hx-swap="outerHTML"
hx-indicator="#tmp-cleanup-stats-wrapper">
<img src="/assets/icons/trash.svg" width="14" height="14" alt="" aria-hidden="true">
Supprimer
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?> <?php endif; ?>
<?php if ($d['trash_stale_count'] > 0): ?> <?php if ($trStale > 0): ?>
<p class="n-heading"> <p class="n-heading">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <img src="/assets/icons/trash.svg" width="14" height="14" alt="" aria-hidden="true">
Corbeille <span class="n-meta"><?= $d['trash_stale_count'] ?> fichier(s) · <?= htmlspecialchars($d['trash_stale_human']) ?></span> Corbeille
</p> </p>
<table class="n-table"> <table class="n-table">
<thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead> <thead><tr><th>Nom</th><th>Taille</th><th>Âge</th><th width="1%"></th></tr></thead>
<tbody> <tbody>
<?php foreach ($d['trash_stale_files'] as $f): ?> <?php foreach ($d['trash_stale_files'] as $f): ?>
<tr> <tr>
<td><strong><?= htmlspecialchars($f['name']) ?></strong></td> <td><strong><?= htmlspecialchars($f['name']) ?></strong></td>
<td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td> <td style="white-space:nowrap"><?= htmlspecialchars($f['human']) ?></td>
<td style="white-space:nowrap">~<?= (int)$f['age_days'] ?> j</td> <td style="white-space:nowrap">~<?= (int)$f['age_days'] ?> j</td>
<td style="white-space:nowrap"> <td style="white-space:nowrap">
<button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)" <button type="button" class="btn btn--sm btn--danger" style="font-size:0.85em;padding:2px var(--space-xs)"
hx-post="/admin/actions/cleanup-tmp.php" hx-post="/admin/actions/cleanup-tmp.php"
hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","trash_file":"<?= htmlspecialchars($f['name']) ?>"}' hx-vals='{"csrf_token":"<?= htmlspecialchars($_SESSION['csrf_token']) ?>","trash_file":"<?= htmlspecialchars($f['name']) ?>"}'
hx-target="#tmp-cleanup-stats" hx-target="#tmp-cleanup-stats-wrapper"
hx-swap="innerHTML" hx-swap="outerHTML"
hx-indicator="#tmp-cleanup-stats"> hx-indicator="#tmp-cleanup-stats-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <img src="/assets/icons/trash.svg" width="14" height="14" alt="" aria-hidden="true">
Supprimer Supprimer
</button> </button>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
<?php endif; ?> <?php endif; ?>
<?php if (($d['filepond_active_count'] ?? 0) > 0 || ($d['trash_active_count'] ?? 0) > 0): ?> <?php if ($fpActive > 0 || $trActive > 0): ?>
<p style="margin:var(--space-sm) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés : <p style="margin:var(--space-sm) 0 0 0;font-size:0.85em;color:var(--text-secondary)">Conservés :
<?php if ($d['filepond_active_count']) echo $d['filepond_active_count'] . ' téléversement(s) actif(s) (' . htmlspecialchars($d['filepond_active_human']) . '), '; ?> <?php if ($fpActive) echo $fpActive . ' téléversement(s) actif(s) (' . htmlspecialchars($d['filepond_active_human']) . '), '; ?>
<?php if ($d['trash_active_count']) echo $d['trash_active_count'] . ' fichier(s) récent(s) (' . htmlspecialchars($d['trash_active_human']) . ')'; ?> <?php if ($trActive) echo $trActive . ' fichier(s) récent(s) (' . htmlspecialchars($d['trash_active_human']) . ')'; ?>
</p> </p>
<?php endif; ?> <?php endif; ?>
</details>
</div>

View File

@@ -19,7 +19,10 @@ $d = json_decode($json, true);
if (!($d['configured'] ?? false)): ?> if (!($d['configured'] ?? false)): ?>
<div id="peertube-orphans-wrapper"> <div id="peertube-orphans-wrapper">
<details id="peertube-orphans-col" class="n-section" open> <details id="peertube-orphans-col" class="n-section" open>
<summary>Vidéos PeerTube orphelines</summary> <summary>
<img src="/assets/icons/video.svg" width="14" height="14" alt="" aria-hidden="true">
Vidéos PeerTube orphelines
</summary>
<div id="peertube-orphans-stats"> <div id="peertube-orphans-stats">
<p style="margin:0;color:var(--color-warning)">⚠️ PeerTube non configuré.</p> <p style="margin:0;color:var(--color-warning)">⚠️ PeerTube non configuré.</p>
</div> </div>
@@ -30,7 +33,10 @@ if (!($d['configured'] ?? false)): ?>
<?php if (!empty($d['error'])): ?> <?php if (!empty($d['error'])): ?>
<div id="peertube-orphans-wrapper"> <div id="peertube-orphans-wrapper">
<details id="peertube-orphans-col" class="n-section" open> <details id="peertube-orphans-col" class="n-section" open>
<summary>Vidéos PeerTube orphelines</summary> <summary>
<img src="/assets/icons/video.svg" width="14" height="14" alt="" aria-hidden="true">
Vidéos PeerTube orphelines
</summary>
<div id="peertube-orphans-stats"> <div id="peertube-orphans-stats">
<p style="margin:0;color:var(--color-error)">✗ <?= htmlspecialchars($d['error']) ?></p> <p style="margin:0;color:var(--color-error)">✗ <?= htmlspecialchars($d['error']) ?></p>
</div> </div>
@@ -40,7 +46,10 @@ if (!($d['configured'] ?? false)): ?>
<div id="peertube-orphans-wrapper"> <div id="peertube-orphans-wrapper">
<details id="peertube-orphans-col" class="n-section" open> <details id="peertube-orphans-col" class="n-section" open>
<summary>Vidéos PeerTube orphelines <span class="n-meta"><?= (int)($d['total_on_channel'] ?? 0) ?> vidéos · <?= (int)($d['total_linked'] ?? 0) ?> liées</span></summary> <summary>
<img src="/assets/icons/video.svg" width="14" height="14" alt="" aria-hidden="true">
Vidéos PeerTube orphelines <span class="n-meta"><?= (int)($d['orphan_count'] ?? 0) ?> vidéos orphelines</span>
</summary>
<div id="peertube-orphans-stats"> <div id="peertube-orphans-stats">
<?php if (($d['orphan_count'] ?? 0) > 0): ?> <?php if (($d['orphan_count'] ?? 0) > 0): ?>
<table class="n-table"> <table class="n-table">
@@ -61,7 +70,7 @@ if (!($d['configured'] ?? false)): ?>
hx-swap="outerHTML" hx-swap="outerHTML"
hx-trigger="click" hx-trigger="click"
hx-indicator="#peertube-orphans-wrapper"> hx-indicator="#peertube-orphans-wrapper">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 256 256" aria-hidden="true"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg> <img src="/assets/icons/trash.svg" width="14" height="14" alt="" aria-hidden="true">
Supprimer Supprimer
</button> </button>
</td> </td>
@@ -77,7 +86,10 @@ if (!($d['configured'] ?? false)): ?>
<?php if (($d['stale_count'] ?? 0) > 0): ?> <?php if (($d['stale_count'] ?? 0) > 0): ?>
<details id="peertube-stale-section" class="n-section" open> <details id="peertube-stale-section" class="n-section" open>
<summary>Références DB obsolètes <span class="n-meta"><?= $d['stale_count'] ?></span></summary> <summary>
<img src="/assets/icons/warning-diamond.svg" width="14" height="14" alt="" aria-hidden="true">
Références DB obsolètes <span class="n-meta"><?= $d['stale_count'] ?></span>
</summary>
<p style="margin:0 0 var(--space-sm) 0;font-size:0.85em;color:var(--text-secondary)">Ces UUID sont référencés en base de données mais n'existent plus sur la chaîne PeerTube. Les TFE liés affichent des liens morts.</p> <p style="margin:0 0 var(--space-sm) 0;font-size:0.85em;color:var(--text-secondary)">Ces UUID sont référencés en base de données mais n'existent plus sur la chaîne PeerTube. Les TFE liés affichent des liens morts.</p>
<table class="n-table"> <table class="n-table">
<thead><tr><th>UUID</th><th>TFE(s)</th></tr></thead> <thead><tr><th>UUID</th><th>TFE(s)</th></tr></thead>

View File

@@ -18,7 +18,6 @@ try {
$allPages = $db->getAllPages(); $allPages = $db->getAllPages();
$pages = array_values(array_filter($allPages, fn($p) => in_array($p['slug'], $allowedPageSlugs, true))); $pages = array_values(array_filter($allPages, fn($p) => in_array($p['slug'], $allowedPageSlugs, true)));
$aproposKeys = $db->getAllAproposContents(); $aproposKeys = $db->getAllAproposContents();
$formHelpBlocks = $db->getAllFormHelpBlocks();
$siteSettings = $db->getAllSettings(); $siteSettings = $db->getAllSettings();
} catch (Exception $e) { } catch (Exception $e) {
error_log("Error loading contenus: " . $e->getMessage()); error_log("Error loading contenus: " . $e->getMessage());

View File

@@ -0,0 +1,25 @@
<?php
require_once __DIR__ . "/../../bootstrap.php";
require_once __DIR__ . '/../../src/AdminAuth.php';
AdminAuth::requireLogin();
require_once __DIR__ . '/../../src/Database.php';
$pageTitle = "Structure du Formulaire";
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
try {
$db = new Database();
$formHelpBlocks = $db->getAllFormHelpBlocks();
} catch (Exception $e) {
error_log("Error loading structure-formulaire: " . $e->getMessage());
die("Erreur lors du chargement de la structure du formulaire.");
}
$isAdmin = true; $bodyClass = 'admin-body';
require_once APP_ROOT . '/templates/head.php';
include APP_ROOT . '/templates/header.php';
include APP_ROOT . '/templates/admin/structure-formulaire.php';
require_once APP_ROOT . '/templates/admin/footer.php';

View File

@@ -1075,7 +1075,6 @@ th.admin-ap-col {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
font-size: 0.85em; font-size: 0.85em;
margin: var(--space-xs) 0 var(--space-md) 0;
} }
.n-table thead th { .n-table thead th {
text-align: left; text-align: left;
@@ -1119,6 +1118,10 @@ th.admin-ap-col {
.n-grid > section, .n-grid > section,
.n-grid > details { .n-grid > details {
margin: 2ch 0; margin: 2ch 0;
border: none;
border-radius: 0;
background: transparent;
overflow: visible;
} }
.n-section:first-child, .n-section:first-child,
.n-grid > section:first-child, .n-grid > section:first-child,
@@ -1135,27 +1138,54 @@ th.admin-ap-col {
} }
/* <details>/<summary> inside cleanup sections */ /* <details>/<summary> inside cleanup sections */
.n-grid > details > summary { .n-grid > details > summary,
.n-section > summary {
cursor: pointer; cursor: pointer;
font-weight: 600; font-weight: 600;
font-size: var(--step--1); font-size: var(--step--1);
color: var(--text-primary); color: var(--text-primary);
padding: var(--space-xs) 0; padding: var(--space-xs);
list-style: none; list-style: none;
font-family: var(--font-body);
display: flex;
align-items: center;
gap: var(--space-2xs);
} }
.n-grid > details > summary::-webkit-details-marker { .n-grid > details > summary::-webkit-details-marker,
.n-section > summary::-webkit-details-marker {
display: none; display: none;
} }
.n-grid > details > summary::before { .n-section[open],
content: '▸ '; .n-grid > details[open] {
padding-bottom: 0;
}
.n-grid > details > summary:hover,
.n-section > summary:hover {
background: transparent;
}
.n-grid > details > summary::before,
.n-section > summary::before {
content: '';
display: inline-block; display: inline-block;
width: 16px;
height: 16px;
flex-shrink: 0;
background: currentColor;
mask-image: url("/assets/icons/caret-right.svg");
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
transition: transform 0.15s; transition: transform 0.15s;
} }
.n-grid > details[open] > summary::before { .n-grid > details[open] > summary::before,
.n-section[open] > summary::before {
transform: rotate(90deg); transform: rotate(90deg);
} }
.n-grid > details > :not(summary) { .n-grid > details > :not(summary),
padding-left: calc(1ch + var(--space-xs)); .n-section > :not(summary) {
padding: 0 var(--space-s) var(--space-xs) var(--space-s);
} }
/* ── Import results log ─────────────────────────────────────────────── */ /* ── Import results log ─────────────────────────────────────────────── */

View File

@@ -24,8 +24,8 @@ details[open] {
} }
details > :not(summary) { details > :not(summary) {
margin-left: var(--space-s); padding-left: var(--space-s);
margin-right: var(--space-s); padding-right: var(--space-s);
} }
summary { summary {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"></path></svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path></svg>

After

Width:  |  Height:  |  Size: 243 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M230.64,25.36a32,32,0,0,0-45.26,0q-.21.21-.42.45L131.55,88.22,121,77.64a24,24,0,0,0-33.95,0l-76.69,76.7a8,8,0,0,0,0,11.31l80,80a8,8,0,0,0,11.31,0L178.36,169a24,24,0,0,0,0-33.95l-10.58-10.57L230.19,71c.15-.14.31-.28.45-.43A32,32,0,0,0,230.64,25.36ZM96,228.69,79.32,212l22.34-22.35a8,8,0,0,0-11.31-11.31L68,200.68,55.32,188l22.34-22.35a8,8,0,0,0-11.31-11.31L44,176.68,27.31,160,72,115.31,140.69,184ZM219.52,59.1l-68.71,58.81a8,8,0,0,0-.46,11.74L167,146.34a8,8,0,0,1,0,11.31l-15,15L83.32,104l15-15a8,8,0,0,1,11.31,0l16.69,16.69a8,8,0,0,0,11.74-.46L196.9,36.48A16,16,0,0,1,219.52,59.1Z"></path></svg>

After

Width:  |  Height:  |  Size: 705 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z"></path></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M164.44,105.34l-48-32A8,8,0,0,0,104,80v64a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM120,129.05V95l25.58,17ZM216,40H40A16,16,0,0,0,24,56V168a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,128H40V56H216V168Zm16,40a8,8,0,0,1-8,8H32a8,8,0,0,1,0-16H224A8,8,0,0,1,232,208Z"></path></svg>

After

Width:  |  Height:  |  Size: 412 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="#000000" viewBox="0 0 256 256"><path d="M128,72a8,8,0,0,1,8,8v56a8,8,0,0,1-16,0V80A8,8,0,0,1,128,72ZM116,172a12,12,0,1,0,12-12A12,12,0,0,0,116,172Zm124-44a15.85,15.85,0,0,1-4.67,11.28l-96.05,96.06a16,16,0,0,1-22.56,0h0l-96-96.06a16,16,0,0,1,0-22.56l96.05-96.06a16,16,0,0,1,22.56,0l96.05,96.06A15.85,15.85,0,0,1,240,128Zm-16,0L128,32,32,128,128,224h0Z"></path></svg>

After

Width:  |  Height:  |  Size: 434 B

View File

@@ -229,58 +229,9 @@
Chaque <strong>bloc d'aide</strong> s'affiche au-dessus de sa section dans le formulaire de soumission. Chaque <strong>bloc d'aide</strong> s'affiche au-dessus de sa section dans le formulaire de soumission.
Le <strong>bouton rond</strong> active/désactive l'affichage. Le <strong>bouton rond</strong> active/désactive l'affichage.
</p> </p>
<p>
<?php <a href="/admin/structure-formulaire.php" class="btn btn--primary btn--sm">Gérer la structure du formulaire (page dédiée)</a>
$blocks = $formHelpBlocks; </p>
$pairs = [
['partage_intro', null, null],
['fieldset_tfe_info', 'Informations du TFE',
['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact visible', 'Synopsis']],
['fieldset_languages', 'Langue(s)',
['Langues du TFE (cases à cocher)', 'Autre(s) langue(s)']],
['fieldset_keywords', 'Mots-clés',
['Mots-clés (max 10), séparés par des virgules']],
['fieldset_academic', 'Cadre académique',
['Année', 'Orientation', 'AP', 'Finalité']],
['fieldset_jury', 'Composition du jury',
['Président·e', 'Promoteur·ice(s)', 'Lecteur·ices']],
['fieldset_files', 'Format(s) + Fichiers',
['Formats (PDF, vidéo, audio, site web…)', 'Couverture', 'Note d\'intention', 'Fichier principal', 'Annexes']],
['fieldset_access', 'Degrés d\'ouverture et licences',
['Généralités', 'Degré (libre/interne/interdit)', 'Licence', 'CC2r']],
['fieldset_email', 'E-mail de confirmation',
['Adresse e-mail']],
];
?>
<div class="fhb-structure">
<?php foreach ($pairs as [$helpKey, $fieldsetName, $inputs]):
$b = $blocks[$helpKey] ?? ['content' => '', 'name' => '', 'enabled' => 0];
$title = $b['name'] ?: ($fieldsetName ?? $helpKey);
?>
<div class="fhb-block-wrapper" data-key="<?= htmlspecialchars($helpKey) ?>">
<div class="fhb-inline"
hx-get="/admin/form-help-inline-fragment.php?key=<?= urlencode($helpKey) ?>"
hx-trigger="load"
hx-swap="outerHTML">
<div class="fhb-inline-name"><?= htmlspecialchars($title) ?></div>
</div>
</div>
<?php if ($fieldsetName !== null): ?>
<div class="fhb-fieldset-card">
<div class="fhb-fieldset-card-legend"><?= htmlspecialchars($fieldsetName) ?></div>
<?php if ($inputs): ?>
<ul class="fhb-fieldset-card-inputs">
<?php foreach ($inputs as $inp): ?>
<li><?= htmlspecialchars($inp) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</fieldset> </fieldset>
</section> </section>
</article> </article>

View File

@@ -41,7 +41,7 @@ document.addEventListener('htmx:afterSwap',()=>{document.querySelectorAll('input
<?php endif; ?> <?php endif; ?>
<?php if ($tmpTotalCount > 0): ?> <?php if ($tmpTotalCount > 0): ?>
<button type="button" class="btn btn--sm btn--secondary" id="tmp-cleanup-btn" <button type="button" class="btn btn--sm btn--secondary" id="tmp-cleanup-btn"
onclick="document.getElementById('tmp-cleanup-dialog').showModal(); htmx.trigger('#tmp-cleanup-stats','loadStats'); htmx.trigger('#peertube-orphans-wrapper','loadPeertube')"> onclick="document.getElementById('tmp-cleanup-dialog').showModal(); htmx.trigger('#tmp-cleanup-stats-wrapper','loadStats'); htmx.trigger('#peertube-orphans-wrapper','loadPeertube')">
Nettoyer (<?= $tmpTotalCount ?>) Nettoyer (<?= $tmpTotalCount ?>)
</button> </button>
<?php endif; ?> <?php endif; ?>

View File

@@ -8,14 +8,19 @@
<div id="tmp-cleanup-result" style="display:none;margin-bottom:var(--space-sm)"></div> <div id="tmp-cleanup-result" style="display:none;margin-bottom:var(--space-sm)"></div>
<div class="n-grid" id="cleanup-grid-parent"> <div class="n-grid" id="cleanup-grid-parent">
<!-- ═══════ FilePond / Trash ═══════ --> <!-- ═══════ FilePond / Trash ═══════ -->
<details id="tmp-cleanup-stats" class="n-section" open> <div id="tmp-cleanup-stats-wrapper"
<summary>Fichiers temporaires</summary> hx-get="/admin/actions/cleanup-stats-fragment.php"
<div hx-get="/admin/actions/cleanup-stats-fragment.php" hx-trigger="loadStats"
hx-trigger="loadStats" hx-swap="outerHTML"
hx-swap="innerHTML"> hx-indicator="#tmp-cleanup-stats-wrapper">
<details class="n-section" open>
<summary>
<img src="/assets/icons/paint-brush-household.svg" width="14" height="14" alt="" aria-hidden="true">
Fichiers temporaires
</summary>
<p style="margin:0;color:var(--text-secondary)">Chargement…</p> <p style="margin:0;color:var(--text-secondary)">Chargement…</p>
</div> </details>
</details> </div>
<!-- ═══════ PeerTube ═══════ --> <!-- ═══════ PeerTube ═══════ -->
<div id="peertube-orphans-wrapper" <div id="peertube-orphans-wrapper"
hx-get="/admin/actions/peertube-orphans-fragment.php" hx-get="/admin/actions/peertube-orphans-fragment.php"

View File

@@ -0,0 +1,67 @@
<main id="main-content" class="admin-main">
<article>
<h1>Structure du Formulaire</h1>
<p style="margin-bottom:var(--space-s)">
<a href="/admin/contenus.php" class="btn btn--sm btn--secondary"> Retour aux Contenus</a>
</p>
<p class="fhb-hint">
Chaque <strong>bloc d'aide</strong> s'affiche au-dessus de sa section dans le formulaire de soumission.
Le <strong>bouton rond</strong> active/désactive l'affichage.
</p>
<?php
$blocks = $formHelpBlocks;
$pairs = [
['partage_intro', null, null],
['fieldset_tfe_info', 'Informations du TFE',
['Titre', 'Sous-titre', 'Auteur·ice(s)', 'Contact visible', 'Synopsis']],
['fieldset_languages', 'Langue(s)',
['Langues du TFE (cases à cocher)', 'Autre(s) langue(s)']],
['fieldset_keywords', 'Mots-clés',
['Mots-clés (max 10), séparés par des virgules']],
['fieldset_academic', 'Cadre académique',
['Année', 'Orientation', 'AP', 'Finalité']],
['fieldset_jury', 'Composition du jury',
['Président·e', 'Promoteur·ice(s)', 'Lecteur·ices']],
['fieldset_files', 'Format(s) + Fichiers',
['Formats (PDF, vidéo, audio, site web…)', 'Couverture', 'Note d\'intention', 'Fichier principal', 'Annexes']],
['fieldset_access', 'Degrés d\'ouverture et licences',
['Généralités', 'Degré (libre/interne/interdit)', 'Licence', 'CC2r']],
['fieldset_email', 'E-mail de confirmation',
['Adresse e-mail']],
];
?>
<div class="fhb-structure">
<?php foreach ($pairs as [$helpKey, $fieldsetName, $inputs]):
$b = $blocks[$helpKey] ?? ['content' => '', 'name' => '', 'enabled' => 0];
$title = $b['name'] ?: ($fieldsetName ?? $helpKey);
?>
<div class="fhb-block-wrapper" data-key="<?= htmlspecialchars($helpKey) ?>">
<div class="fhb-inline"
hx-get="/admin/form-help-inline-fragment.php?key=<?= urlencode($helpKey) ?>"
hx-trigger="load"
hx-swap="outerHTML">
<div class="fhb-inline-name"><?= htmlspecialchars($title) ?></div>
</div>
</div>
<?php if ($fieldsetName !== null): ?>
<div class="fhb-fieldset-card">
<div class="fhb-fieldset-card-legend"><?= htmlspecialchars($fieldsetName) ?></div>
<?php if ($inputs): ?>
<ul class="fhb-fieldset-card-inputs">
<?php foreach ($inputs as $inp): ?>
<li><?= htmlspecialchars($inp) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</article>
</main>