From 409cd4a22f167ae1d93a9db177b7c2e45cc4063d Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Fri, 5 Dec 2025 22:09:40 +0900 Subject: [PATCH] Squashed commit of the following: commit 8acd255dd459488ac1d9346780e05dc099ba74b1 Author: Nik Afiq Date: Fri Dec 5 21:58:10 2025 +0900 Fix optional parameters handling in time parsing functions and enhance API response logging commit 7e9a82a137057c85a909d027c51d71654ec61ac1 Author: Nik Afiq Date: Fri Dec 5 21:53:44 2025 +0900 Handle API schedule loading errors and log them for debugging commit ec993908043c841b18fcb66916c5f9ce66b9e2f1 Author: Nik Afiq Date: Fri Dec 5 21:48:16 2025 +0900 Implement logging utility and integrate API request/response error handling --- .env.example | 3 +- docker-compose.yml | 3 +- frontend/Dockerfile | 5 +++- frontend/src/components/Timer.tsx | 24 ++++++++++++---- frontend/src/hooks/useServerClock.ts | 7 ++++- frontend/src/hooks/useTimeSkew.ts | 6 +++- frontend/src/pages/ShowsPage.tsx | 9 +++++- frontend/src/utils/logger.ts | 42 ++++++++++++++++++++++++++++ 8 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 frontend/src/utils/logger.ts diff --git a/.env.example b/.env.example index a85ed22..612eb06 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,7 @@ WEB_PORT=3000 PUBLIC_BASE_PATH=/watch-party/ BACKEND_ORIGIN=http://api:8082 +FRONTEND_MODE=debug ################# ## Backend env ## @@ -20,4 +21,4 @@ PGHOST=db POSTGRES_PORT=5432 PGSSLMODE=disable GIN_MODE=release -ADDR=:8082 \ No newline at end of file +ADDR=:8082 diff --git a/docker-compose.yml b/docker-compose.yml index f270e51..88fd746 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,7 @@ services: dockerfile: Dockerfile args: PUBLIC_BASE_PATH: ${PUBLIC_BASE_PATH} + FRONTEND_MODE: ${FRONTEND_MODE:-production} image: watchparty-frontend:prod container_name: watchparty-frontend environment: @@ -97,4 +98,4 @@ networks: driver: bridge volumes: - pgdata: \ No newline at end of file + pgdata: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index d583112..5c2b718 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -8,6 +8,9 @@ COPY . . # NEW: base path for Vite (e.g. /watch-party/) ARG PUBLIC_BASE_PATH=/ ENV PUBLIC_BASE_PATH=${PUBLIC_BASE_PATH} +ARG FRONTEND_MODE=production +ENV FRONTEND_MODE=${FRONTEND_MODE} +ENV VITE_APP_MODE=${FRONTEND_MODE} RUN npm run build @@ -20,4 +23,4 @@ COPY ops/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENV BACKEND_ORIGIN=http://localhost:8082 EXPOSE 80 -ENTRYPOINT ["/bin/sh","/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/bin/sh","/entrypoint.sh"] diff --git a/frontend/src/components/Timer.tsx b/frontend/src/components/Timer.tsx index 75d5d70..329db19 100644 --- a/frontend/src/components/Timer.tsx +++ b/frontend/src/components/Timer.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useServerClock } from "../hooks/useServerClock"; import { API_ENDPOINT } from "../api/endpoint"; +import { logApiError, logApiRequest, logApiResponse } from "../utils/logger"; // ===== Config & fallbacks ===== const TIMEZONE = "Asia/Tokyo"; // JST (UTC+09) @@ -30,7 +31,8 @@ function formatHMS(total: number) { return h > 0 ? `${HH}:${MM}:${SS}` : `${MM}:${SS}`; } -function parseStartTime(s: string): { hour: number; minute: number; second: number } | null { +function parseStartTime(s?: string): { hour: number; minute: number; second: number } | null { + if (!s || typeof s !== "string") return null; const m = /^(\d{1,2}):([0-5]\d)(?::([0-5]\d))?$/.exec(s.trim()); if (!m) return null; const hour = Number(m[1]); @@ -40,7 +42,8 @@ function parseStartTime(s: string): { hour: number; minute: number; second: numb return { hour, minute, second }; } -function parseDurationToSeconds(s: string): number { +function parseDurationToSeconds(s?: string): number { + if (!s || typeof s !== "string") return FALLBACK_END_SECONDS; const parts = s.trim().split(":").map(Number); if (parts.some((n) => Number.isNaN(n) || n < 0)) return FALLBACK_END_SECONDS; if (parts.length === 2) { @@ -75,9 +78,18 @@ function jstToUtcMs(y: number, m: number, d: number, hh: number, mm: number, ss } async function loadSchedule(signal?: AbortSignal) { + logApiRequest("loadSchedule", { url: API_URL_CURRENT }); const res = await fetch(API_URL_CURRENT, { cache: "no-store", signal }); + logApiResponse("loadSchedule", res); if (!res.ok) throw new Error(`HTTP ${res.status}`); - return (await res.json()) as ApiSchedule; + const payload = (await res.json()) as ApiSchedule; + logApiResponse("loadSchedule payload", res, { + ep_num: payload?.ep_num, + start_time: payload?.start_time, + playback_length: payload?.playback_length, + season_name: payload?.season_name, + }); + return payload; } export default function Timer() { @@ -119,9 +131,10 @@ export default function Timer() { setLoaded(true); setErrorMsg(null); }) - .catch(() => { + .catch((e) => { setLoaded(true); setErrorMsg("Failed to load schedule; using defaults."); + logApiError("loadSchedule", e); }); return () => ac.abort(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -195,6 +208,7 @@ export default function Timer() { setLoaded(true); // If the new schedule has moved us out of "ended", the ticking effect will handle it. } catch (e) { + logApiError("loadSchedule (ended poll)", e); // keep current UI; try again next tick } }; @@ -303,4 +317,4 @@ function NowJst({ getNowMs }: { getNowMs: () => number }) { {time} ); -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useServerClock.ts b/frontend/src/hooks/useServerClock.ts index 98d119e..c5f15ad 100644 --- a/frontend/src/hooks/useServerClock.ts +++ b/frontend/src/hooks/useServerClock.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { API_ENDPOINT } from "../api/endpoint"; +import { logApiError, logApiRequest, logApiResponse } from "../utils/logger"; /** Uses /api/time => { now: } and returns a server-correct "nowMs()" */ const TIME_URL_ENDPOINT = API_ENDPOINT.v1.TIME; @@ -21,16 +22,20 @@ export function useServerClock(opts?: { try { setError(null); const t0 = Date.now(); + logApiRequest("useServerClock", { url: TIME_URL_ENDPOINT }); const res = await fetch(TIME_URL_ENDPOINT, { cache: "no-store" }); const t1 = Date.now(); + logApiResponse("useServerClock", res, { roundTripMs: t1 - t0 }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (typeof data?.now !== "number") throw new Error("Bad time payload"); const s = data.now; const offset = Math.round(((t0 + t1) / 2) - s); // (+) client ahead, (-) client behind setOffsetMs(offset); + logApiResponse("useServerClock computed", res, { serverNow: s, offset }); } catch (e: any) { setError(e?.message || "time sync failed"); + logApiError("useServerClock", e); } }, []); @@ -50,4 +55,4 @@ export function useServerClock(opts?: { const ready = offsetMs != null; return { nowMs, ready, error, resync: syncOnce, offsetMs }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useTimeSkew.ts b/frontend/src/hooks/useTimeSkew.ts index cb12355..28b772e 100644 --- a/frontend/src/hooks/useTimeSkew.ts +++ b/frontend/src/hooks/useTimeSkew.ts @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from "react"; import { API_ENDPOINT } from "../api/endpoint"; +import { logApiError, logApiRequest, logApiResponse } from "../utils/logger"; /** * Measures client clock skew vs server time {now: }. @@ -29,11 +30,13 @@ export function useTimeSkew(opts?: { const measureOnce = async () => { const t0 = Date.now(); + logApiRequest("useTimeSkew", { url: TIME_URL_ENDPOINT }); const res = await fetch(TIME_URL_ENDPOINT, { cache: "no-store", headers: { Accept: "application/json" }, }); const t1 = Date.now(); + logApiResponse("useTimeSkew", res, { roundTripMs: t1 - t0 }); if (!res.ok) throw new Error(`HTTP ${res.status}`); // Robust parsing: number or string, seconds or ms @@ -71,6 +74,7 @@ export function useTimeSkew(opts?: { setRttMs(minRtt); } catch (e: any) { setError(e?.message || "Time sync failed"); + logApiError("useTimeSkew", e); } }; @@ -84,4 +88,4 @@ export function useTimeSkew(opts?: { }, [intervalMs, samples, enabled]); return { skewMs, rttMs, error, recheck: measure }; -} \ No newline at end of file +} diff --git a/frontend/src/pages/ShowsPage.tsx b/frontend/src/pages/ShowsPage.tsx index f551934..f2996fb 100644 --- a/frontend/src/pages/ShowsPage.tsx +++ b/frontend/src/pages/ShowsPage.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useState, useRef } from "react"; import { useNavigate } from "react-router-dom"; import { API_ENDPOINT } from "../api/endpoint"; +import { logApiError, logApiRequest, logApiResponse } from "../utils/logger"; type Show = { id: number; @@ -73,13 +74,16 @@ export default function ShowsPage() { (async () => { try { setLoading(true); + logApiRequest("fetch shows", { url: GET_URL }); const res = await fetch(GET_URL, { cache: "no-store" }); + logApiResponse("fetch shows", res); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = (await res.json()) as Show[]; data.sort((a, b) => a.id - b.id); // ASC setShows(data); } catch (e: any) { if (!cancelled) setError(e.message || "番組一覧の取得に失敗しました。"); + logApiError("fetch shows", e); } finally { if (!cancelled) setLoading(false); } @@ -115,11 +119,13 @@ export default function ShowsPage() { try { setPosting(true); + logApiRequest("set current episode", { url: POST_URL, payload }); const res = await fetch(POST_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); + logApiResponse("set current episode", res); if (!res.ok) throw new Error(`POST 失敗 (${res.status})`); setRedirectIn(REDIRECT_DELAY_S); const start = Date.now(); @@ -130,6 +136,7 @@ export default function ShowsPage() { }, 250); } catch (e: any) { setError(e.message || "現在のエピソード設定に失敗しました。"); + logApiError("set current episode", e); } finally { setPosting(false); } @@ -202,4 +209,4 @@ export default function ShowsPage() { )} ); -} \ No newline at end of file +} diff --git a/frontend/src/utils/logger.ts b/frontend/src/utils/logger.ts new file mode 100644 index 0000000..41783aa --- /dev/null +++ b/frontend/src/utils/logger.ts @@ -0,0 +1,42 @@ +const envMode = (import.meta.env.VITE_APP_MODE || import.meta.env.MODE || "").toString().toLowerCase(); +const enabled = envMode === "debug"; +const prefix = "[watch-party]"; + +type Level = "debug" | "info" | "warn" | "error"; + +function write(level: Level, ...args: unknown[]) { + if (!enabled) return; + const fn = console[level] || console.log; + fn(prefix, ...args); +} + +export const logger = { + enabled, + mode: envMode, + debug: (...args: unknown[]) => write("debug", ...args), + info: (...args: unknown[]) => write("info", ...args), + warn: (...args: unknown[]) => write("warn", ...args), + error: (...args: unknown[]) => write("error", ...args), +}; + +export function logApiRequest(label: string, details?: Record) { + if (!enabled) return; + logger.debug(`${label}: request`, details || {}); +} + +export function logApiResponse(label: string, res: Response, details?: Record) { + if (!enabled) return; + logger.debug(`${label}: response`, { + status: res.status, + statusText: res.statusText, + ok: res.ok, + redirected: res.redirected, + url: res.url, + ...details, + }); +} + +export function logApiError(label: string, error: unknown) { + if (!enabled) return; + logger.error(`${label}: error`, error); +}