$config */ $config = require __DIR__ . '/../config/database.php'; prepareSqliteDatabase($config['dsn'] ?? ''); $database = new Database($config); return $database; } function set_database(Database $database): void { global $databaseOverride; $databaseOverride = $database; } function reset_database(): void { global $databaseOverride; $databaseOverride = null; } function migration_manager(): MigrationManager { static $migrationManager = null; if ($migrationManager === null) { $migrationManager = new MigrationManager(database(), __DIR__ . '/../database/migrations'); } return $migrationManager; } function ensureSessionStarted(): void { if (session_status() === PHP_SESSION_NONE) { session_start(); } } function prepareSqliteDatabase(string $dsn): void { if (!str_starts_with($dsn, 'sqlite:')) { return; } $path = substr($dsn, 7); if ($path === false || $path === '') { return; } $directory = dirname($path); if (!is_dir($directory)) { mkdir($directory, 0777, true); } if (!is_writable($directory)) { @chmod($directory, 0777); } if (!file_exists($path)) { touch($path); } if (!is_writable($path)) { @chmod($path, 0666); } } function e(?string $value): string { return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function asset(string $path): string { return '/' . ltrim($path, '/'); } function csrf_token(): string { ensureSessionStarted(); if (!isset($_SESSION['_csrf_token']) || !is_string($_SESSION['_csrf_token'])) { $_SESSION['_csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['_csrf_token']; } function csrf_field(): string { return ''; } function verify_csrf_token(?string $token): bool { ensureSessionStarted(); if (!is_string($token) || $token === '') { return false; } $sessionToken = $_SESSION['_csrf_token'] ?? null; return is_string($sessionToken) && hash_equals($sessionToken, $token); } function h(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function format_date(?string $value, string $format = 'M j, Y'): string { if (!is_string($value) || trim($value) === '') { return '—'; } try { $date = new DateTimeImmutable($value); } catch (Throwable) { return $value; } return $date->format($format); } function format_number(int|float|null $value): string { if ($value === null) { return '0'; } return number_format((float) $value); } function money_cents(int|null $value): string { if ($value === null) { return '$0'; } return '$' . number_format($value / 100, 0); } function project_status_label(string $status): string { return match ($status) { 'planned' => 'Planned', 'active' => 'Active', 'at-risk' => 'At risk', 'paused' => 'Paused', 'done' => 'Done', default => ucfirst($status), }; } function task_status_label(string $status): string { return match ($status) { 'backlog', 'todo' => 'Backlog', 'in-progress', 'doing' => 'In progress', 'blocked' => 'Blocked', 'review' => 'In review', 'done' => 'Done', default => ucfirst($status), }; } function status_class(string $status): string { return match ($status) { 'planned', 'backlog', 'todo' => 'is-neutral', 'active', 'in-progress', 'doing' => 'is-blue', 'blocked', 'at-risk' => 'is-red', 'review' => 'is-amber', 'done' => 'is-green', 'paused' => 'is-slate', default => 'is-neutral', }; }