Add database migration. Add accessibility and mood settings. Add mood to tick format.

This commit is contained in:
Greg Sarjeant 2025-06-23 08:17:38 -04:00
parent d6673c7ed2
commit efe9688289
9 changed files with 220 additions and 21 deletions

View File

@ -66,6 +66,8 @@ function confirm_setup(): void {
validate_storage_subdirs();
validate_tables();
validate_table_contents();
migrate_db();
migrate_tick_files();
}
// Make sure the storage/ directory exists and is writable
@ -114,6 +116,38 @@ function validate_storage_subdirs(): void {
}
}
function migrate_tick_files() {
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(TICKS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($files as $file) {
if ($file->isFile() && str_ends_with($file->getFilename(), '.txt')) {
migrate_tick_file($file->getPathname());
}
}
}
function migrate_tick_file($filepath) {
$lines = file($filepath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$modified = false;
foreach ($lines as &$line) {
$fields = explode('|', $line);
if (count($fields) === 2) {
// Convert id|text to id|emoji|text
$line = $fields[0] . '||' . $fields[1];
$modified = true;
}
}
if ($modified) {
file_put_contents($filepath, implode("\n", $lines) . "\n");
// TODO: log properly
//echo "Migrated: " . basename($filepath) . "\n";
}
}
function get_db(): PDO {
try {
// SQLite will just create this if it doesn't exist.
@ -132,6 +166,93 @@ function get_db(): PDO {
return $db;
}
// The database version will just be an int
// stored as PRAGMA user_version. It will
// correspond to the most recent migration file applied to the db.
function get_db_version(): int {
$db = get_db();
return $db->query("PRAGMA user_version")->fetchColumn() ?? 0;
}
function migration_number_from_file(string $filename): int {
$basename = basename($filename, '.sql');
$parts = explode('_', $basename);
return (int) $parts[0];
}
function set_db_version(int $newVersion): void {
$currentVersion = get_db_version();
if ($newVersion <= $currentVersion){
throw new SetupException(
"New version ($newVersion) must be greater than current version ($currentVersion)",
'db_migration'
);
}
$db = get_db();
$db->exec("PRAGMA user_version = $newVersion");
}
function get_pending_migrations(): array {
$currentVersion = get_db_version();
$files = glob(DATA_DIR . '/migrations/*.sql');
$pending = [];
foreach ($files as $file) {
$version = migration_number_from_file($file);
if ($version > $currentVersion) {
$pending[$version] = $file;
}
}
ksort($pending);
return $pending;
}
function migrate_db(): void {
$migrations = get_pending_migrations();
if (empty($migrations)) {
# TODO: log
return;
}
$db = get_db();
$db->beginTransaction();
try {
foreach ($migrations as $version => $file) {
$filename = basename($file);
// TODO: log properly
$sql = file_get_contents($file);
if ($sql === false) {
throw new Exception("Could not read migration file: $file");
}
// Execute the migration SQL
$db->exec($sql);
}
// Update db version
$db->commit();
set_db_version($version);
//TODO: log properly
//echo "All migrations completed successfully.\n";
} catch (Exception $e) {
$db->rollBack();
throw new SetupException(
"Migration failed: $filename",
'db_migration',
0,
$e
);
}
}
function create_tables(): void {
$db = get_db();

View File

@ -72,6 +72,16 @@ select {
box-sizing: border-box;
}
input[type="checkbox"] {
width: auto;
height: 2rem;
aspect-ratio: 1;
margin: 0;
cursor: pointer;
justify-self: start;
align-self: center;
}
/* A bit of custom styling for the file input */
input[type="file"] {
border-style: dashed;
@ -429,6 +439,13 @@ time {
- Once the width exceeds that (e.g. desktops), it will convert to horizontal alignment
*/
@media (min-width: 600px) {
input[type="checkbox"] {
height: 100%;
/*grid-column: 2;*/
justify-self: start;
align-self: center;
}
label {
text-align: right;
padding-top: 10px;

View File

@ -52,22 +52,24 @@ class AdminController extends Controller {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$errors = [];
// UserModel profile
$username = trim($_POST['username'] ?? '');
$displayName = trim($_POST['display_name'] ?? '');
$about = trim($_POST['about'] ?? '');
$website = trim($_POST['website'] ?? '');
// User profile
$username = trim($_POST['username'] ?? '');
$displayName = trim($_POST['display_name'] ?? '');
$about = trim($_POST['about'] ?? '');
$website = trim($_POST['website'] ?? '');
// Site settings
$siteTitle = trim($_POST['site_title']) ?? '';
$siteDescription = trim($_POST['site_description']) ?? '';
$baseUrl = trim($_POST['base_url'] ?? '');
$basePath = trim($_POST['base_path'] ?? '/');
$itemsPerPage = (int) ($_POST['items_per_page'] ?? 25);
$siteTitle = trim($_POST['site_title']) ?? '';
$siteDescription = trim($_POST['site_description']) ?? '';
$baseUrl = trim($_POST['base_url'] ?? '');
$basePath = trim($_POST['base_path'] ?? '/');
$itemsPerPage = (int) ($_POST['items_per_page'] ?? 25);
$strictAccessibility = isset($_POST['strict_accessibility']);
$showTickMood = isset($_POST['strict_accessibility']);
// Password
$password = $_POST['password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$password = $_POST['password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
// Validate user profile
if (!$username) {
@ -112,6 +114,8 @@ class AdminController extends Controller {
$config->baseUrl = $baseUrl;
$config->basePath = $basePath;
$config->itemsPerPage = $itemsPerPage;
$config->strictAccessibility = $strictAccessibility;
$config->showTickMood = $showTickMood;
// Save site settings and reload config from database
// TODO - raise and handle exception on failure

View File

@ -8,6 +8,8 @@ class ConfigModel {
public int $itemsPerPage = 25;
public string $timezone = 'relative';
public ?int $cssId = null;
public bool $strictAccessibility = true;
public bool $showTickMood = true;
// load config from sqlite database
public static function load(): self {
@ -17,7 +19,16 @@ class ConfigModel {
$c->basePath = ($c->basePath === '') ? $init['base_path'] : $c->basePath;
global $db;
$stmt = $db->query("SELECT site_title, site_description, base_url, base_path, items_per_page, css_id FROM settings WHERE id=1");
$stmt = $db->query("SELECT site_title,
site_description,
base_url,
base_path,
items_per_page,
css_id,
strict_accessibility,
show_tick_mood
FROM settings WHERE id=1");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
@ -26,7 +37,8 @@ class ConfigModel {
$c->baseUrl = $row['base_url'];
$c->basePath = $row['base_path'];
$c->itemsPerPage = (int) $row['items_per_page'];
$c->cssId = (int) $row['css_id'];
$c->strictAccessibility = (bool) $row['strict_accessibility'];
$c->showTickMood = (bool) $row['show_tick_mood'];
}
return $c;
@ -49,11 +61,39 @@ class ConfigModel {
$settingsCount = (int) $db->query("SELECT COUNT(*) FROM settings")->fetchColumn();
if ($settingsCount === 0){
$stmt = $db->prepare("INSERT INTO settings (id, site_title, site_description, base_url, base_path, items_per_page, css_id) VALUES (1, ?, ?, ?, ?, ?, ?)");
$stmt = $db->prepare("INSERT INTO settings (
id,
site_title,
site_description,
base_url,
base_path,
items_per_page,
css_id,
strict_accessibility,
show_tick_mood
)
VALUES (1, ?, ?, ?, ?, ?, ?)");
} else {
$stmt = $db->prepare("UPDATE settings SET site_title=?, site_description=?, base_url=?, base_path=?, items_per_page=?, css_id=? WHERE id=1");
$stmt = $db->prepare("UPDATE settings SET
site_title=?,
site_description=?,
base_url=?,
base_path=?,
items_per_page=?,
css_id=?,
strict_accessibility=?,
show_tick_mood=?
WHERE id=1");
}
$stmt->execute([$this->siteTitle, $this->siteDescription, $this->baseUrl, $this->basePath, $this->itemsPerPage, $this->cssId]);
$stmt->execute([$this->siteTitle,
$this->siteDescription,
$this->baseUrl,
$this->basePath,
$this->itemsPerPage,
$this->cssId,
$this->strictAccessibility,
$this->showTickMood
]);
return self::load();
}

View File

@ -34,9 +34,7 @@ class TickModel {
// Ticks are pipe-delimited: timestamp|text
// But just in case a tick contains a pipe, only split on the first one that occurs
$tickParts = explode('|', $line, 2);
$time = $tickParts[0];
$tick = $tickParts[1];
list($time, $emoji, $tick) = explode('|', $line, 3);
// Build the timestamp from the date and time
// Ticks are always stored in UTC
@ -90,10 +88,13 @@ class TickModel {
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (str_starts_with($line, $timestamp)) {
$tick = explode('|', $line)[1];
echo $line;
exit;
list($time, $emoji, $tick) = explode('|', $line, 3);
return [
'tickTime' => $tickTime,
'emoji' => $emoji,
'tick' => $tick,
'config' => ConfigModel::load(),
];

View File

@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN strict_accessibility BOOLEAN DEFAULT TRUE;

View File

@ -0,0 +1,2 @@
ALTER TABLE settings
ADD COLUMN show_tick_mood BOOLEAN DEFAULT TRUE;

BIN
storage/db/tkr.sqlite.bak Executable file

Binary file not shown.

View File

@ -57,6 +57,18 @@
name="items_per_page"
value="<?= $config->itemsPerPage ?>" min="1" max="50"
required>
<label>Strict accessibility</label>
<input type="checkbox"
id="strict_accessibility"
name="strict_accessibility"
value="1"
<?php if ($config->strictAccessibility): ?> checked <?php endif; ?>>
<label>Show tick mood</label>
<input type="checkbox"
id="show_tick_mood"
name="show_tick_mood"
value="1"
<?php if ($config->showTickMood): ?> checked <?php endif; ?>>
</div>
</fieldset>
<fieldset>