API change
This commit is contained in:
parent
c2d5d54592
commit
062c659cde
@ -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())
|
||||||
|
|||||||
@ -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!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7
frontend/src/api/endpoint.ts
Normal file
7
frontend/src/api/endpoint.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const API_ENDPOINT = {
|
||||||
|
v1: {
|
||||||
|
CURRENT: `/api/v1/current`,
|
||||||
|
SHOWS: `/api/v1/shows`,
|
||||||
|
TIME: `/api/v1/time`
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)$/;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user