API change

This commit is contained in:
Nik Afiq 2025-11-11 22:48:57 +09:00
parent c2d5d54592
commit 062c659cde
7 changed files with 47 additions and 21 deletions

View File

@ -18,7 +18,10 @@ func NewRouter(svc service.EpisodeService) *gin.Engine {
c.JSON(http.StatusOK, gin.H{"status": "ok"}) c.JSON(http.StatusOK, gin.H{"status": "ok"})
}) })
r.GET("/time", func(c *gin.Context) { api := r.Group("/api")
v1 := api.Group("/v1")
v1.GET("/time", func(c *gin.Context) {
now := time.Now().UTC().UnixMilli() now := time.Now().UTC().UnixMilli()
c.Header("Cache-Control", "no-store, max-age=0, must-revalidate") c.Header("Cache-Control", "no-store, max-age=0, must-revalidate")
@ -27,8 +30,6 @@ func NewRouter(svc service.EpisodeService) *gin.Engine {
}) })
}) })
v1 := r.Group("/v1")
// GET /v1/current // GET /v1/current
v1.GET("/current", func(c *gin.Context) { v1.GET("/current", func(c *gin.Context) {
cur, err := svc.GetCurrent(c.Request.Context()) cur, err := svc.GetCurrent(c.Request.Context())

View File

@ -2,24 +2,23 @@ server {
listen 80; listen 80;
server_name _; server_name _;
# Static files for the SPA
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
# (Optional) redirect bare root to /watch-party/ # Redirect bare root to /watch-party/
# Helps avoid loading "/" by mistake (your log shows that happened once)
location = / { return 302 /watch-party/; } location = / { return 302 /watch-party/; }
# Redirect no-trailing-slash to trailing slash # Ensure trailing slash
location = /watch-party { return 302 /watch-party/; } location = /watch-party { return 302 /watch-party/; }
# Serve the SPA under /watch-party/ using alias # Serve SPA under /watch-party/
# /watch-party/... -> /usr/share/nginx/html/...
location /watch-party/ { location /watch-party/ {
alias /usr/share/nginx/html/; alias /usr/share/nginx/html/;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
# Proxy API unchanged (your React app should request /api/current) # Backend API (preserve /api/v1/… path)
location /api/ { location /api/ {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
@ -27,6 +26,6 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection ""; proxy_set_header Connection "";
proxy_pass ${BACKEND_ORIGIN}/; proxy_pass ${BACKEND_ORIGIN}; # no trailing slash!
} }
} }

View File

@ -0,0 +1,7 @@
export const API_ENDPOINT = {
v1: {
CURRENT: `/api/v1/current`,
SHOWS: `/api/v1/shows`,
TIME: `/api/v1/time`
},
} as const;

View File

@ -1,9 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useServerClock } from "../hooks/useServerClock"; import { useServerClock } from "../hooks/useServerClock";
import { API_ENDPOINT } from "../api/endpoint";
// ===== Config & fallbacks ===== // ===== Config & fallbacks =====
const TIMEZONE = "Asia/Tokyo"; // JST (UTC+09) const TIMEZONE = "Asia/Tokyo"; // JST (UTC+09)
const API_URL = "/api/v1/current"; const API_URL_CURRENT = API_ENDPOINT.v1.CURRENT
const FALLBACK_START_HOUR = 19; const FALLBACK_START_HOUR = 19;
const FALLBACK_START_MINUTE = 25; const FALLBACK_START_MINUTE = 25;
const FALLBACK_END_SECONDS = 300; const FALLBACK_END_SECONDS = 300;
@ -74,7 +75,7 @@ function jstToUtcMs(y: number, m: number, d: number, hh: number, mm: number, ss
} }
async function loadSchedule(signal?: AbortSignal) { async function loadSchedule(signal?: AbortSignal) {
const res = await fetch(API_URL, { cache: "no-store", signal }); const res = await fetch(API_URL_CURRENT, { cache: "no-store", signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as ApiSchedule; return (await res.json()) as ApiSchedule;
} }
@ -97,7 +98,6 @@ export default function Timer() {
const [untilStart, setUntilStart] = useState(0); const [untilStart, setUntilStart] = useState(0);
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
const { nowMs, ready, error: timeError } = useServerClock({ const { nowMs, ready, error: timeError } = useServerClock({
endpoint: `${import.meta.env.BASE_URL}api/time`, // works under /watch-party/ in prod, /api in dev
refreshMs: TIME_SYNC_INTERVAL, refreshMs: TIME_SYNC_INTERVAL,
}); });

View File

@ -1,13 +1,14 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { API_ENDPOINT } from "../api/endpoint";
/** Uses /api/time => { now: <ms UTC> } and returns a server-correct "nowMs()" */ /** Uses /api/time => { now: <ms UTC> } and returns a server-correct "nowMs()" */
const TIME_URL_ENDPOINT = API_ENDPOINT.v1.TIME;
export function useServerClock(opts?: { export function useServerClock(opts?: {
endpoint?: string;
refreshMs?: number; refreshMs?: number;
enabled?: boolean; enabled?: boolean;
}) { }) {
const { const {
endpoint = "/api/time",
refreshMs = 60_000, refreshMs = 60_000,
enabled = true, enabled = true,
} = opts || {}; } = opts || {};
@ -20,7 +21,7 @@ export function useServerClock(opts?: {
try { try {
setError(null); setError(null);
const t0 = Date.now(); const t0 = Date.now();
const res = await fetch(endpoint, { cache: "no-store" }); const res = await fetch(TIME_URL_ENDPOINT, { cache: "no-store" });
const t1 = Date.now(); const t1 = Date.now();
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json(); const data = await res.json();
@ -31,7 +32,7 @@ export function useServerClock(opts?: {
} catch (e: any) { } catch (e: any) {
setError(e?.message || "time sync failed"); setError(e?.message || "time sync failed");
} }
}, [endpoint]); }, []);
useEffect(() => { useEffect(() => {
if (!enabled) return; if (!enabled) return;

View File

@ -1,4 +1,5 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { API_ENDPOINT } from "../api/endpoint";
type Show = { type Show = {
id: number; id: number;
@ -10,8 +11,8 @@ type Show = {
date_created: string; date_created: string;
}; };
const GET_URL = "/api/v1/shows"; const GET_URL = API_ENDPOINT.v1.SHOWS;
const POST_URL = "/api/v1/current"; const POST_URL = API_ENDPOINT.v1.CURRENT;
const HHMM = /^(\d{1,2}):([0-5]\d)$/; const HHMM = /^(\d{1,2}):([0-5]\d)$/;

View File

@ -1,11 +1,19 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
// Use PUBLIC_BASE_PATH="/watch-party/" for prod container builds.
// In dev, keep "/" so local paths are simple.
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const base = mode === "development" ? "/" : process.env.PUBLIC_BASE_PATH || "/"; const base =
mode === "development"
? "/"
: process.env.PUBLIC_BASE_PATH || "/watch-party/";
return { return {
base, base,
plugins: [react()], plugins: [react()],
// DEV: proxy /api/* directly to Go backend
server: { server: {
port: 5173, port: 5173,
open: true, open: true,
@ -13,7 +21,16 @@ export default defineConfig(({ mode }) => {
"/api": { "/api": {
target: "http://localhost:8082", target: "http://localhost:8082",
changeOrigin: true, changeOrigin: true,
rewrite: p => p.replace(/^\/api/, ""), },
},
},
// For `vite preview`, keep the same proxy
preview: {
proxy: {
"/api": {
target: "http://localhost:8082",
changeOrigin: true,
}, },
}, },
}, },