From ab355e051228fbb9e7e6d326192ea5799dd3ea03 Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Fri, 22 May 2026 18:34:01 +0200 Subject: [PATCH] some candies --- .../backend/config/config.example.php | 2 + .../backend/public/api/upload.php | 18 ++++- .../backend/public/assets/css/app.css | 71 +++++++++++++++---- .../crash_reporter/backend/src/Database.php | 29 ++++++++ 4 files changed, 107 insertions(+), 13 deletions(-) diff --git a/examples/crash_reporter/backend/config/config.example.php b/examples/crash_reporter/backend/config/config.example.php index c69eba2..6dd193d 100644 --- a/examples/crash_reporter/backend/config/config.example.php +++ b/examples/crash_reporter/backend/config/config.example.php @@ -27,4 +27,6 @@ return [ ], ], 'session_name' => 'ac_crash_sess', + // When true, upload API may include a short error hint (do not enable on public prod long-term) + 'debug' => false, ]; diff --git a/examples/crash_reporter/backend/public/api/upload.php b/examples/crash_reporter/backend/public/api/upload.php index 763c481..4c1633c 100644 --- a/examples/crash_reporter/backend/public/api/upload.php +++ b/examples/crash_reporter/backend/public/api/upload.php @@ -27,6 +27,22 @@ if ((int) ($data['schema_version'] ?? 0) !== 1) { try { ReportRepository::insert($data); json_out(['ok' => true, 'report_id' => $data['report_id'] ?? null]); +} catch (PDOException $e) { + $msg = $e->getMessage(); + if ($e->getCode() === '23000' || stripos($msg, 'UNIQUE') !== false) { + json_out(['ok' => false, 'error' => 'duplicate report_id'], 409); + } + error_log('crash upload PDO: ' . $msg); + $out = ['ok' => false, 'error' => 'store failed']; + if (cfg('debug')) { + $out['hint'] = $msg; + } + json_out($out, 500); } catch (Throwable $e) { - json_out(['ok' => false, 'error' => 'store failed'], 500); + error_log('crash upload: ' . $e->getMessage()); + $out = ['ok' => false, 'error' => 'store failed']; + if (cfg('debug')) { + $out['hint'] = $e->getMessage(); + } + json_out($out, 500); } diff --git a/examples/crash_reporter/backend/public/assets/css/app.css b/examples/crash_reporter/backend/public/assets/css/app.css index 229ab3a..202610a 100644 --- a/examples/crash_reporter/backend/public/assets/css/app.css +++ b/examples/crash_reporter/backend/public/assets/css/app.css @@ -24,13 +24,12 @@ a:hover { text-decoration: underline; } width: 56px; background: var(--surface); border-right: 1px solid var(--border); - transition: width .2s ease; + transition: width .22s ease; overflow: hidden; position: relative; flex-shrink: 0; } -.nav-pane.open, -.nav-pane:hover { width: 72px; } +.nav-pane.open { width: 200px; } .nav-handle { position: absolute; top: 8px; @@ -38,6 +37,7 @@ a:hover { text-decoration: underline; } transform: translateX(-50%); width: 40px; height: 40px; + transition: left .22s ease, transform .22s ease; padding: 0; border: none; border-radius: 10px; @@ -55,6 +55,10 @@ a:hover { text-decoration: underline; } color: var(--text); outline: none; } +.nav-pane.open .nav-handle { + left: 8px; + transform: none; +} .nav-list { list-style: none; margin: 56px 0 0; @@ -72,7 +76,14 @@ a:hover { text-decoration: underline; } border-radius: 10px; color: var(--muted); text-decoration: none; - transition: background .15s ease, color .15s ease; + transition: background .15s ease, color .15s ease, width .22s ease, padding .22s ease; +} +.nav-pane.open .nav-link { + width: 100%; + margin: 0; + padding: 0 12px; + justify-content: flex-start; + gap: 10px; } .nav-link.active, .nav-link:hover { @@ -81,15 +92,31 @@ a:hover { text-decoration: underline; } text-decoration: none; } .nav-label { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); + flex: 0 1 auto; + min-width: 0; + font-size: 14px; + font-weight: 500; + line-height: 1.2; white-space: nowrap; - border: 0; + overflow: hidden; + opacity: 0; + max-width: 0; + transition: opacity .18s ease, max-width .22s ease; +} +.nav-pane.open .nav-label { + opacity: 1; + max-width: 9rem; + transition-delay: .06s; +} +@media (prefers-reduced-motion: reduce) { + .nav-pane, + .nav-link, + .nav-label { + transition: none; + } + .nav-pane.open .nav-label { + transition-delay: 0s; + } } .nav-icon { display: block; @@ -219,6 +246,12 @@ a:hover { text-decoration: underline; } align-items: center; gap: 6px; color: var(--muted); + padding: 0 8px; + box-sizing: border-box; +} +.nav-pane.open .nav-user { + align-items: stretch; + padding: 0 10px; } .nav-user-name { display: flex; @@ -227,6 +260,20 @@ a:hover { text-decoration: underline; } width: 40px; height: 40px; color: var(--muted); + gap: 10px; +} +.nav-pane.open .nav-user-name { + width: 100%; + padding: 0 12px; + justify-content: flex-start; + box-sizing: border-box; +} +.nav-pane.open .nav-link--logout { + width: 100%; + margin: 0; + padding: 0 12px; + justify-content: flex-start; + gap: 10px; } .nav-link--logout { color: var(--muted); } .nav-link--logout:hover { color: var(--danger); background: rgba(248, 113, 113, .12); } diff --git a/examples/crash_reporter/backend/src/Database.php b/examples/crash_reporter/backend/src/Database.php index 0a3d349..9fffc89 100644 --- a/examples/crash_reporter/backend/src/Database.php +++ b/examples/crash_reporter/backend/src/Database.php @@ -14,6 +14,7 @@ declare(strict_types=1); final class Database { private static ?PDO $pdo = null; + private static bool $schemaChecked = false; public static function pdo(): PDO { if (self::$pdo !== null) { @@ -45,6 +46,34 @@ final class Database { PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); self::$pdo->exec('PRAGMA foreign_keys = ON'); + self::ensureSchema(); return self::$pdo; } + + /** Idempotent: applies sql/schema.sqlite.sql if core tables are missing. */ + public static function ensureSchema(): void { + if (self::$schemaChecked) { + return; + } + self::$schemaChecked = true; + $pdo = self::$pdo; + if ($pdo === null) { + return; + } + $need = static function (string $table) use ($pdo): bool { + $stmt = $pdo->prepare( + "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1" + ); + $stmt->execute([$table]); + return $stmt->fetchColumn() === false; + }; + if (!$need('users') && !$need('reports')) { + return; + } + $schemaFile = __DIR__ . '/../sql/schema.sqlite.sql'; + if (!is_readable($schemaFile)) { + throw new RuntimeException('schema file missing: ' . $schemaFile); + } + $pdo->exec((string) file_get_contents($schemaFile)); + } }