mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-09-25 01:53:03 +02:00
fix: display db timestamps in Brussels time
(heure de dépôt was showing UTC) - feat: add date_depot column (real TFE deposit date) with CSV round-trip + Brussels→UTC sanitization - fix: keep PHP default tz at UTC to preserve token/share-link expiry consistency; convert to Brussels only in db_datetime()
This commit is contained in:
@@ -7,6 +7,7 @@ app/storage/test.db
|
|||||||
*.db
|
*.db
|
||||||
*.db-wal
|
*.db-wal
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
*.db.gz
|
||||||
app/.env
|
app/.env
|
||||||
|
|
||||||
### Logs ###
|
### Logs ###
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ nginx
|
|||||||
src/cache/rate_limit
|
src/cache/rate_limit
|
||||||
*.db-wal
|
*.db-wal
|
||||||
*.db-shm
|
*.db-shm
|
||||||
|
*.db.gz
|
||||||
|
|||||||
@@ -10,6 +10,42 @@ $autoloadPath = file_exists(__DIR__ . '/vendor/autoload.php')
|
|||||||
: __DIR__ . '/../vendor/autoload.php';
|
: __DIR__ . '/../vendor/autoload.php';
|
||||||
require_once $autoloadPath;
|
require_once $autoloadPath;
|
||||||
|
|
||||||
|
// The site serves the ERG (Brussels), but the server + SQLite timestamps run in
|
||||||
|
// UTC (CURRENT_TIMESTAMP). We intentionally do NOT change the PHP default
|
||||||
|
// timezone: code that writes timestamps via date()/time() (e.g. one-time
|
||||||
|
// tokens, share-link expiries) must stay wall-clock consistent with SQLite's
|
||||||
|
// CURRENT_TIMESTAMP (UTC), otherwise expiries drift by the UTC↔Brussels offset.
|
||||||
|
// Conversion to Brussels is handled explicitly at the display layer via
|
||||||
|
// db_datetime() below.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a database-stored datetime for display in Brussels local time.
|
||||||
|
*
|
||||||
|
* SQLite timestamps (submitted_at, created_at, published_at, …) are written
|
||||||
|
* with CURRENT_TIMESTAMP (= UTC). This parses the value as UTC and converts it
|
||||||
|
* to Europe/Brussels (UTC+1 winter / UTC+2 summer) — independent of the PHP
|
||||||
|
* default timezone, so it never affects DB write/read consistency elsewhere.
|
||||||
|
*/
|
||||||
|
function db_datetime(string $raw, string $format = 'd/m/Y à H:i'): string
|
||||||
|
{
|
||||||
|
if ($raw === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$dt = DateTime::createFromFormat('Y-m-d H:i:s', $raw, new DateTimeZone('UTC'));
|
||||||
|
if ($dt === false) {
|
||||||
|
// Non-ISO fallback: parse the naive string as UTC.
|
||||||
|
$ts = strtotime($raw . ' UTC');
|
||||||
|
if ($ts === false) {
|
||||||
|
return $raw;
|
||||||
|
}
|
||||||
|
$dt = (new DateTime('@' . $ts))->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$dt->setTimezone(new DateTimeZone('Europe/Brussels'));
|
||||||
|
return $dt->format($format);
|
||||||
|
}
|
||||||
|
|
||||||
// Define application root
|
// Define application root
|
||||||
define('APP_ROOT', __DIR__);
|
define('APP_ROOT', __DIR__);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Migration 044 — add `date_depot` column to `theses`.
|
||||||
|
*
|
||||||
|
* `submitted_at` was historically used as "when the record entered the system"
|
||||||
|
* (set to CURRENT_TIMESTAMP on create/CSV import). That conflates two concepts:
|
||||||
|
* the *system* submission time and the *student's* real TFE deposit date. When
|
||||||
|
* theses were bulk-imported via CSV, every imported row got stamped with the
|
||||||
|
* same CURRENT_TIMESTAMP, losing the real deposit date.
|
||||||
|
*
|
||||||
|
* This migration:
|
||||||
|
* 1. Adds a nullable `date_depot DATETIME` column to hold the real deposit date.
|
||||||
|
* 2. Backfills `date_depot` from `submitted_at` for theses that have not been
|
||||||
|
* bulk-imported (i.e. where submitted_at differs from the import time) —
|
||||||
|
* this is conservative: it copies the existing value so nothing is lost,
|
||||||
|
* while real per-thesis values must be re-supplied via a CSV import
|
||||||
|
* (or the admin edit form) since the bulk import already flattened them.
|
||||||
|
* 3. Recreates v_theses_full to expose the new column.
|
||||||
|
*
|
||||||
|
* Idempotent: safe to re-run (guards on existing column).
|
||||||
|
*/
|
||||||
|
|
||||||
|
defined('APP_ROOT') || define('APP_ROOT', dirname(__DIR__, 2));
|
||||||
|
defined('STORAGE_ROOT') || define('STORAGE_ROOT', APP_ROOT . '/storage');
|
||||||
|
|
||||||
|
$dbPath = $argv[1] ?? (APP_ROOT . '/storage/xamxam.db');
|
||||||
|
if (!file_exists($dbPath)) {
|
||||||
|
echo "ERROR: database not found at $dbPath\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo = new PDO('sqlite:' . $dbPath);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// 1. Add the column if missing.
|
||||||
|
$cols = array_column($pdo->query("PRAGMA table_info(theses)")->fetchAll(), 'name');
|
||||||
|
if (!in_array('date_depot', $cols, true)) {
|
||||||
|
$pdo->exec("ALTER TABLE theses ADD COLUMN date_depot DATETIME");
|
||||||
|
echo "Added column theses.date_depot\n";
|
||||||
|
} else {
|
||||||
|
echo "Column theses.date_depot already present\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Backfill: for any thesis with a submitted_at but no date_depot, copy it.
|
||||||
|
// (Conservative — preserves the value so display still works; the real
|
||||||
|
// deposit date must be supplied via a later CSV import for bulk-imported rows.)
|
||||||
|
$updated = $pdo->exec(
|
||||||
|
"UPDATE theses SET date_depot = submitted_at WHERE date_depot IS NULL AND submitted_at IS NOT NULL"
|
||||||
|
);
|
||||||
|
echo "Backfilled date_depot from submitted_at for {$updated} row(s)\n";
|
||||||
|
|
||||||
|
// 3. Recreate v_theses_full so it exposes date_depot.
|
||||||
|
$pdo->exec("DROP VIEW IF EXISTS v_theses_full");
|
||||||
|
$pdo->exec(
|
||||||
|
"CREATE VIEW v_theses_full AS
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.identifier,
|
||||||
|
t.title,
|
||||||
|
t.subtitle,
|
||||||
|
t.year,
|
||||||
|
t.is_doctoral,
|
||||||
|
t.objet,
|
||||||
|
o.name as orientation,
|
||||||
|
ap.name as ap_program,
|
||||||
|
ft.name as finality_type,
|
||||||
|
t.synopsis,
|
||||||
|
t.context_note,
|
||||||
|
t.duration_pages,
|
||||||
|
t.duration_minutes,
|
||||||
|
t.has_annexes,
|
||||||
|
at.name as access_type,
|
||||||
|
lt.name as license_type,
|
||||||
|
t.license_id,
|
||||||
|
t.license_custom,
|
||||||
|
t.access_type_id,
|
||||||
|
t.jury_points,
|
||||||
|
t.submitted_at,
|
||||||
|
t.date_depot,
|
||||||
|
t.defense_date,
|
||||||
|
t.published_at,
|
||||||
|
t.is_published,
|
||||||
|
t.baiu_link,
|
||||||
|
t.exemplaire_baiu,
|
||||||
|
t.exemplaire_erg,
|
||||||
|
t.cc2r,
|
||||||
|
t.remarks,
|
||||||
|
t.jury_note_added,
|
||||||
|
t.contact_visible,
|
||||||
|
GROUP_CONCAT(DISTINCT a.name ORDER BY a.name ASC) as authors,
|
||||||
|
GROUP_CONCAT(DISTINCT s.name) as supervisors,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'president' THEN s.name END) as jury_president,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 0 THEN s.name END) as jury_promoteurs,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'promoteur' AND ts.is_ulb = 1 THEN s.name END) as jury_promoteurs_ulb,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 0 THEN s.name END) as jury_lecteurs_internes,
|
||||||
|
GROUP_CONCAT(DISTINCT CASE WHEN ts.role = 'lecteur' AND ts.is_external = 1 THEN s.name END) as jury_lecteurs_externes,
|
||||||
|
GROUP_CONCAT(DISTINCT l.name) as languages,
|
||||||
|
GROUP_CONCAT(DISTINCT fmt.name) as formats,
|
||||||
|
GROUP_CONCAT(DISTINCT tg.name) as keywords,
|
||||||
|
(SELECT a2.email FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_interne,
|
||||||
|
(SELECT a2.show_contact FROM authors a2 JOIN thesis_authors ta2 ON a2.id = ta2.author_id WHERE ta2.thesis_id = t.id ORDER BY ta2.author_order LIMIT 1) as contact_public
|
||||||
|
FROM theses t
|
||||||
|
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||||
|
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||||
|
LEFT JOIN finality_types ft ON t.finality_id = ft.id
|
||||||
|
LEFT JOIN access_types at ON t.access_type_id = at.id
|
||||||
|
LEFT JOIN license_types lt ON t.license_id = lt.id
|
||||||
|
LEFT JOIN thesis_authors ta ON t.id = ta.thesis_id
|
||||||
|
LEFT JOIN authors a ON ta.author_id = a.id
|
||||||
|
LEFT JOIN thesis_supervisors ts ON t.id = ts.thesis_id
|
||||||
|
LEFT JOIN supervisors s ON ts.supervisor_id = s.id
|
||||||
|
LEFT JOIN thesis_languages tl ON t.id = tl.thesis_id
|
||||||
|
LEFT JOIN languages l ON tl.language_id = l.id
|
||||||
|
LEFT JOIN thesis_formats tf ON t.id = tf.thesis_id
|
||||||
|
LEFT JOIN format_types fmt ON tf.format_id = fmt.id
|
||||||
|
LEFT JOIN thesis_tags tt ON t.id = tt.thesis_id
|
||||||
|
LEFT JOIN tags tg ON tt.tag_id = tg.id
|
||||||
|
GROUP BY t.id;"
|
||||||
|
);
|
||||||
|
echo "Recreated v_theses_full with date_depot\n";
|
||||||
|
|
||||||
|
// v_theses_public is SELECT * FROM v_theses_full, so it inherits the new column
|
||||||
|
// automatically; re-assert it in case the old view is still cached.
|
||||||
|
$pdo->exec("DROP VIEW IF EXISTS v_theses_public");
|
||||||
|
$pdo->exec("CREATE VIEW IF NOT EXISTS v_theses_public AS SELECT * FROM v_theses_full WHERE is_published = 1;");
|
||||||
|
echo "Recreated v_theses_public\n";
|
||||||
|
|
||||||
|
echo "Migration 044 complete.\n";
|
||||||
@@ -143,6 +143,51 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
|||||||
return ($raw === 'oui' || $raw === '1' || $raw === 'yes' || $raw === 'true') ? 1 : 0;
|
return ($raw === 'oui' || $raw === '1' || $raw === 'yes' || $raw === 'true') ? 1 : 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper: parse a bulk-import date cell into a UTC 'Y-m-d H:i:s' string.
|
||||||
|
// The cell is authored in Brussels local time (e.g. "12/05/2026",
|
||||||
|
// "12/05/2026 14:30", ISO "2026-05-12"). We interpret it in the
|
||||||
|
// Europe/Brussels timezone and convert to UTC for storage, matching
|
||||||
|
// the rest of the DB (SQLite CURRENT_TIMESTAMP) and the db_datetime()
|
||||||
|
// display helper which converts UTC → Brussels on read.
|
||||||
|
$parseBulkDate = function(string $raw): ?string {
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') return null;
|
||||||
|
|
||||||
|
// Try a set of formats (d/m/Y first — the CSV convention).
|
||||||
|
// Date-only formats (no time component) are anchored to midnight
|
||||||
|
// local time; PHP otherwise fills a missing time with *now*.
|
||||||
|
$formats = [
|
||||||
|
'd/m/Y H:i:s', 'd/m/Y H:i', 'd/m/Y',
|
||||||
|
'Y-m-d H:i:s', 'Y-m-d H:i', 'Y-m-d',
|
||||||
|
'd.m.Y H:i', 'd.m.Y',
|
||||||
|
];
|
||||||
|
foreach ($formats as $fmt) {
|
||||||
|
$dt = DateTime::createFromFormat($fmt, $raw, new DateTimeZone('Europe/Brussels'));
|
||||||
|
if ($dt === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Reject partial parses (e.g. "12/05/20xx" residual garbage).
|
||||||
|
$errors = DateTime::getLastErrors();
|
||||||
|
if ($errors && ($errors['warning_count'] > 0 || $errors['error_count'] > 0)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Anchor date-only values to midnight local time.
|
||||||
|
if (!str_contains($fmt, 'H') && !str_contains($fmt, ':')) {
|
||||||
|
$dt->setTime(0, 0, 0);
|
||||||
|
}
|
||||||
|
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||||
|
return $dt->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO fallback via strtotime (assumes Brussels for naive strings).
|
||||||
|
$ts = strtotime($raw . ' Europe/Brussels');
|
||||||
|
if ($ts !== false) {
|
||||||
|
return gmdate('Y-m-d H:i:s', $ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
// Code → canonical name (legacy short-code CSV format)
|
// Code → canonical name (legacy short-code CSV format)
|
||||||
$orientationCodeMap = [
|
$orientationCodeMap = [
|
||||||
'SC'=>'Sculpture','VI'=>'Vidéographie','CA'=>"Cinéma d'animation",
|
'SC'=>'Sculpture','VI'=>'Vidéographie','CA'=>"Cinéma d'animation",
|
||||||
@@ -305,6 +350,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
|||||||
$contactVisible = $cell($row, 'contact-visible'); // second "contact", key won't match header → positional
|
$contactVisible = $cell($row, 'contact-visible'); // second "contact", key won't match header → positional
|
||||||
$objetRaw = $cell($row, 'objet');
|
$objetRaw = $cell($row, 'objet');
|
||||||
$objet = in_array($objetRaw, ['tfe', 'thèse', 'frart'], true) ? $objetRaw : 'tfe';
|
$objet = in_array($objetRaw, ['tfe', 'thèse', 'frart'], true) ? $objetRaw : 'tfe';
|
||||||
|
$dateDepotRaw = $cell($row, 'dépôt');
|
||||||
|
$dateDepot = parseBulkDate($dateDepotRaw); // Brussels-local → UTC (Y-m-d H:i:s) or null
|
||||||
|
|
||||||
if ($title === '' || $year === 0) {
|
if ($title === '' || $year === 0) {
|
||||||
$missing = [];
|
$missing = [];
|
||||||
@@ -366,8 +413,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
|||||||
cc2r, exemplaire_baiu, exemplaire_erg,
|
cc2r, exemplaire_baiu, exemplaire_erg,
|
||||||
objet, contact_visible,
|
objet, contact_visible,
|
||||||
duration_pages, duration_minutes, has_annexes,
|
duration_pages, duration_minutes, has_annexes,
|
||||||
is_published, status, submitted_at
|
is_published, status, submitted_at, date_depot
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,'active',CURRENT_TIMESTAMP)
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,'active',CURRENT_TIMESTAMP,?)
|
||||||
");
|
");
|
||||||
$s->execute([
|
$s->execute([
|
||||||
!empty($identifier) ? $identifier : null, $title,
|
!empty($identifier) ? $identifier : null, $title,
|
||||||
@@ -389,6 +436,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['csv_file'])) {
|
|||||||
$durationPages,
|
$durationPages,
|
||||||
$durationMinutes,
|
$durationMinutes,
|
||||||
$hasAnnexes,
|
$hasAnnexes,
|
||||||
|
$dateDepot,
|
||||||
]);
|
]);
|
||||||
$thesisId = $importPdo->lastInsertId();
|
$thesisId = $importPdo->lastInsertId();
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ class ExportController
|
|||||||
['Licence personnalisée', 'license_custom', 'licence-perso'],
|
['Licence personnalisée', 'license_custom', 'licence-perso'],
|
||||||
['Contact visible', 'contact_visible', 'contact-visible'],
|
['Contact visible', 'contact_visible', 'contact-visible'],
|
||||||
['Objet', 'objet', 'objet'],
|
['Objet', 'objet', 'objet'],
|
||||||
|
['Date de dépôt', 'date_depot', 'dépôt'],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -336,6 +337,7 @@ class ExportController
|
|||||||
'Licence personnalisée',
|
'Licence personnalisée',
|
||||||
'Contact visible',
|
'Contact visible',
|
||||||
'Objet',
|
'Objet',
|
||||||
|
'Date de dépôt',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -461,6 +463,7 @@ class ExportController
|
|||||||
$t['license_custom'] ?? '',
|
$t['license_custom'] ?? '',
|
||||||
$t['contact_visible'] ?? '',
|
$t['contact_visible'] ?? '',
|
||||||
$t['objet'] ?? '',
|
$t['objet'] ?? '',
|
||||||
|
!empty($t['date_depot']) ? db_datetime((string)$t['date_depot'], 'd/m/Y H:i') : '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2722,7 +2722,8 @@ class Database
|
|||||||
t.duration_pages,
|
t.duration_pages,
|
||||||
t.duration_minutes,
|
t.duration_minutes,
|
||||||
t.has_annexes,
|
t.has_annexes,
|
||||||
t.objet
|
t.objet,
|
||||||
|
t.date_depot
|
||||||
FROM theses t
|
FROM theses t
|
||||||
LEFT JOIN orientations o ON t.orientation_id = o.id
|
LEFT JOIN orientations o ON t.orientation_id = o.id
|
||||||
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
LEFT JOIN ap_programs ap ON t.ap_program_id = ap.id
|
||||||
|
|||||||
Binary file not shown.
@@ -99,6 +99,7 @@ CREATE TABLE IF NOT EXISTS theses (
|
|||||||
jury_points DECIMAL(4,2),
|
jury_points DECIMAL(4,2),
|
||||||
jury_note_added BOOLEAN DEFAULT 0,
|
jury_note_added BOOLEAN DEFAULT 0,
|
||||||
submitted_at DATETIME,
|
submitted_at DATETIME,
|
||||||
|
date_depot DATETIME, -- real TFE deposit date (bulk-import source, Brussels-local parse → UTC storage)
|
||||||
defense_date DATETIME,
|
defense_date DATETIME,
|
||||||
published_at DATETIME,
|
published_at DATETIME,
|
||||||
is_published BOOLEAN DEFAULT 0,
|
is_published BOOLEAN DEFAULT 0,
|
||||||
@@ -447,6 +448,7 @@ SELECT
|
|||||||
t.access_type_id,
|
t.access_type_id,
|
||||||
t.jury_points,
|
t.jury_points,
|
||||||
t.submitted_at,
|
t.submitted_at,
|
||||||
|
t.date_depot,
|
||||||
t.defense_date,
|
t.defense_date,
|
||||||
t.published_at,
|
t.published_at,
|
||||||
t.is_published,
|
t.is_published,
|
||||||
|
|||||||
@@ -178,18 +178,21 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Dates et publication</legend>
|
<legend>Dates et publication</legend>
|
||||||
<dl class="recap-dl">
|
<dl class="recap-dl">
|
||||||
|
<?php if ($thesis['date_depot']): ?>
|
||||||
|
<dt>Date de dépôt</dt><dd><?= htmlspecialchars(db_datetime($thesis['date_depot'], 'd/m/Y à H:i')) ?></dd>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($thesis['submitted_at']): ?>
|
<?php if ($thesis['submitted_at']): ?>
|
||||||
<dt>Soumis le</dt><dd><?= date('d/m/Y à H:i', strtotime($thesis['submitted_at'])) ?></dd>
|
<dt>Soumis le</dt><dd><?= htmlspecialchars(db_datetime($thesis['submitted_at'])) ?></dd>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($thesis['defense_date']): ?>
|
<?php if ($thesis['defense_date']): ?>
|
||||||
<dt>Date de défense</dt><dd><?= date('d/m/Y', strtotime($thesis['defense_date'])) ?></dd>
|
<dt>Date de défense</dt><dd><?= htmlspecialchars(db_datetime($thesis['defense_date'], 'd/m/Y')) ?></dd>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($thesis['published_at']): ?>
|
<?php if ($thesis['published_at']): ?>
|
||||||
<dt>Publié le</dt><dd><?= date('d/m/Y à H:i', strtotime($thesis['published_at'])) ?></dd>
|
<dt>Publié le</dt><dd><?= htmlspecialchars(db_datetime($thesis['published_at'])) ?></dd>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<dt>Publié</dt><dd><?= !empty($thesis['is_published']) ? 'Oui' : 'Non' ?></dd>
|
<dt>Publié</dt><dd><?= !empty($thesis['is_published']) ? 'Oui' : 'Non' ?></dd>
|
||||||
<?php if ($thesis['jury_note_added']): ?>
|
<?php if ($thesis['jury_note_added']): ?>
|
||||||
<dt>Note du jury ajoutée</dt><dd><?= date('d/m/Y', strtotime($thesis['jury_note_added'])) ?></dd>
|
<dt>Note du jury ajoutée</dt><dd><?= htmlspecialchars(db_datetime($thesis['jury_note_added'], 'd/m/Y')) ?></dd>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</dl>
|
</dl>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -273,7 +276,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="recap-files-type"><?= htmlspecialchars($fileType) ?></td>
|
<td class="recap-files-type"><?= htmlspecialchars($fileType) ?></td>
|
||||||
<td class="recap-files-size"><?= !empty($f['file_size']) && $f['file_size'] > 0 ? formatFileSize($f['file_size']) : '–' ?></td>
|
<td class="recap-files-size"><?= !empty($f['file_size']) && $f['file_size'] > 0 ? formatFileSize($f['file_size']) : '–' ?></td>
|
||||||
<td class="recap-files-date"><?= !empty($f['uploaded_at']) ? date('d/m/Y H:i', strtotime($f['uploaded_at'])) : '–' ?></td>
|
<td class="recap-files-date"><?= !empty($f['uploaded_at']) ? htmlspecialchars(db_datetime($f['uploaded_at'], 'd/m/Y H:i')) : '–' ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ deploy-backup-cron:
|
|||||||
@echo "📋 Installing backup cron jobs…"
|
@echo "📋 Installing backup cron jobs…"
|
||||||
rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron
|
rsync -v deploy/xamxam-backup.cron xamxam:/tmp/xamxam-backup.cron
|
||||||
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron"
|
ssh -t xamxam "sudo install -o root -g root -m 644 /tmp/xamxam-backup.cron /etc/cron.d/xamxam-backup && rm -f /tmp/xamxam-backup.cron"
|
||||||
ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:www-data /var/backups/xamxam && sudo chmod 755 /var/backups/xamxam"
|
ssh -t xamxam "sudo mkdir -p /var/backups/xamxam && sudo chown www-data:xamxam /var/backups/xamxam && sudo chmod 775 /var/backups/xamxam"
|
||||||
ssh -t xamxam "sudo touch /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-backup-\$(date +%Y-%m-%d).log"
|
ssh -t xamxam "sudo touch /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chown www-data:www-data /var/log/xamxam-backup-\$(date +%Y-%m-%d).log && sudo chmod 644 /var/log/xamxam-backup-\$(date +%Y-%m-%d).log"
|
||||||
@echo "✅ Cron jobs installed."
|
@echo "✅ Cron jobs installed."
|
||||||
@echo " Cron file: /etc/cron.d/xamxam-backup"
|
@echo " Cron file: /etc/cron.d/xamxam-backup"
|
||||||
|
|||||||
@@ -97,6 +97,14 @@ chown www-data:xamxam /var/log/xamxam
|
|||||||
chmod 2775 /var/log/xamxam
|
chmod 2775 /var/log/xamxam
|
||||||
ok "Log dir: /var/log/xamxam owned by www-data:xamxam (2775)"
|
ok "Log dir: /var/log/xamxam owned by www-data:xamxam (2775)"
|
||||||
|
|
||||||
|
# Backups dir must be writable by both www-data (cron) and the deploy user
|
||||||
|
# (xamxam group) so scripts/migrate.sh can write a pre-deploy snapshot before
|
||||||
|
# running migrations.
|
||||||
|
mkdir -p /var/backups/xamxam
|
||||||
|
chown www-data:xamxam /var/backups/xamxam
|
||||||
|
chmod 2775 /var/backups/xamxam
|
||||||
|
ok "Backup dir: /var/backups/xamxam owned by www-data:xamxam (2775)"
|
||||||
|
|
||||||
# ── Step 2: Nginx config ──────────────────────────────────────────────────────
|
# ── Step 2: Nginx config ──────────────────────────────────────────────────────
|
||||||
printf "\n📋 Step 2: Deploying nginx configuration...\n"
|
printf "\n📋 Step 2: Deploying nginx configuration...\n"
|
||||||
echo "--------------------------------------------"
|
echo "--------------------------------------------"
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
# Safe to run on existing databases — schema uses IF NOT EXISTS / INSERT OR IGNORE.
|
# Safe to run on existing databases — schema uses IF NOT EXISTS / INSERT OR IGNORE.
|
||||||
# Usage:
|
# Usage:
|
||||||
# scripts/migrate.sh # xamxam.db (default)
|
# scripts/migrate.sh # xamxam.db (default)
|
||||||
|
#
|
||||||
|
# Before any schema/migration is applied, a WAL-safe snapshot of the database is
|
||||||
|
# written (via sqlite3 .backup) so a deploy is always recoverable if a migration
|
||||||
|
# goes wrong. This augments the hourly cron backups with a pre-deploy checkpoint.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -13,15 +17,50 @@ if [ -d "$REPO_ROOT/app/storage" ]; then
|
|||||||
SCHEMA="$REPO_ROOT/app/storage/schema.sql"
|
SCHEMA="$REPO_ROOT/app/storage/schema.sql"
|
||||||
PROD_DB="$REPO_ROOT/app/storage/xamxam.db"
|
PROD_DB="$REPO_ROOT/app/storage/xamxam.db"
|
||||||
MIGRATIONS="$REPO_ROOT/app/migrations/run.php"
|
MIGRATIONS="$REPO_ROOT/app/migrations/run.php"
|
||||||
|
# Local dev DB is disposable — no pre-migration backup needed here.
|
||||||
|
BACKUP_DIR=""
|
||||||
elif [ -f "$REPO_ROOT/storage/schema.sql" ]; then
|
elif [ -f "$REPO_ROOT/storage/schema.sql" ]; then
|
||||||
SCHEMA="$REPO_ROOT/storage/schema.sql"
|
SCHEMA="$REPO_ROOT/storage/schema.sql"
|
||||||
PROD_DB="$REPO_ROOT/storage/xamxam.db"
|
PROD_DB="$REPO_ROOT/storage/xamxam.db"
|
||||||
MIGRATIONS="$REPO_ROOT/migrations/run.php"
|
MIGRATIONS="$REPO_ROOT/migrations/run.php"
|
||||||
|
BACKUP_DIR="/var/backups/xamxam"
|
||||||
else
|
else
|
||||||
echo "ERROR: cannot find storage/schema.sql" >&2
|
echo "ERROR: cannot find storage/schema.sql" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Pre-migration backup (production remote only) ───────────────────────────
|
||||||
|
backup_before_migrate() {
|
||||||
|
[ -n "$BACKUP_DIR" ] || return 0
|
||||||
|
[ -f "$PROD_DB" ] || return 0
|
||||||
|
command -v sqlite3 >/dev/null 2>&1 || { echo " [backup] sqlite3 not found — skipping"; return 0; }
|
||||||
|
command -v gzip >/dev/null 2>&1 || { echo " [backup] gzip not found — skipping"; return 0; }
|
||||||
|
|
||||||
|
if ! mkdir -p "$BACKUP_DIR" 2>/dev/null; then
|
||||||
|
echo " [backup] WARNING: cannot write to $BACKUP_DIR — skipping pre-deploy backup" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local snapshot
|
||||||
|
snapshot="/tmp/xamxam-predeploy-$$.db"
|
||||||
|
if sqlite3 "$PROD_DB" ".backup '$snapshot'" 2>/dev/null; then
|
||||||
|
local out
|
||||||
|
out="$BACKUP_DIR/db-$(date +%Y-%m-%dT%H-%M-%S).db.gz"
|
||||||
|
gzip -c "$snapshot" > "$out"
|
||||||
|
rm -f "$snapshot"
|
||||||
|
echo " [backup] pre-deploy snapshot: $out"
|
||||||
|
else
|
||||||
|
rm -f "$snapshot"
|
||||||
|
echo " [backup] WARNING: sqlite3 .backup failed — continuing without a pre-deploy snapshot" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$BACKUP_DIR" ]; then
|
||||||
|
echo "──────────────────────────────────────────────"
|
||||||
|
echo " [backup] taking pre-migration snapshot…"
|
||||||
|
backup_before_migrate
|
||||||
|
fi
|
||||||
|
|
||||||
echo "──────────────────────────────────────────────"
|
echo "──────────────────────────────────────────────"
|
||||||
echo " [schema] applying schema…"
|
echo " [schema] applying schema…"
|
||||||
if command -v syntaqlite &>/dev/null; then
|
if command -v syntaqlite &>/dev/null; then
|
||||||
|
|||||||
Reference in New Issue
Block a user