false, 'error' => 'unauthorized'], 401); } $raw = file_get_contents('php://input'); $data = json_decode($raw !== false ? $raw : '', true); if (!is_array($data)) { json_out(['ok' => false, 'error' => 'invalid json'], 400); } $user = Auth::user(); $title = trim((string) ($data['title'] ?? '')); $body = trim((string) ($data['body'] ?? '')); $brief = trim((string) ($data['brief'] ?? '')); if ($title === '') { json_out(['ok' => false, 'error' => 'title required'], 400); } if ($body === '' && $brief !== '') { $body = $brief; } $tags = is_array($data['tags'] ?? null) ? $data['tags'] : []; if ($tags === []) { $tags = [['id' => 'open', 'label' => 'open', 'bg' => '#22c55e']]; } $now = (int) round(microtime(true) * 1000); $payload = [ 'schema_version' => 1, 'ingest_kind' => 'ticket', 'ticket_id' => 'web-' . date('Ymd-His') . '-' . bin2hex(random_bytes(4)), 'title' => mb_substr($title, 0, 256), 'brief' => $brief !== '' ? mb_substr($brief, 0, 512) : mb_substr($title, 0, 512), 'body' => $body !== '' ? $body : $title, 'opened_at_epoch_ms' => $now, 'rating' => max(0, min(5, (int) ($data['rating'] ?? 3))), 'owner' => (string) ($user['username'] ?? 'admin'), 'tags' => $tags, 'device' => [ 'manufacturer' => 'web', 'model' => 'console', 'sdk_int' => 0, 'release' => 'n/a', ], 'app' => [ 'package' => 'com.foxx.androidcast', 'version_name' => 'console', 'version_code' => 0, ], ]; try { Database::requireTicketsTable(); $id = TicketRepository::insert($payload); json_out(['ok' => true, 'id' => $id, 'ticket_id' => $payload['ticket_id']]); } catch (InvalidArgumentException $e) { json_out(['ok' => false, 'error' => $e->getMessage()], 400); } catch (Throwable $e) { error_log('ticket_create: ' . $e->getMessage()); json_out(['ok' => false, 'error' => 'create failed'], 500); }