Changes before error encountered
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com> Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/a42d4e36-a3cf-4ff7-b1cb-f076e601b1b8
This commit is contained in:
@@ -75,7 +75,7 @@ if (carCount.cnt === 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- WebSocket Server ---
|
// --- WebSocket Server ---
|
||||||
const wss = new WebSocketServer({ server, path: '/ws' });
|
const wss = new WebSocketServer({ server, path: '/api/ws' });
|
||||||
|
|
||||||
function broadcast(message) {
|
function broadcast(message) {
|
||||||
const data = JSON.stringify(message);
|
const data = JSON.stringify(message);
|
||||||
|
|||||||
@@ -275,8 +275,8 @@ export default function CarManagement({ reloadKey = 0 }) {
|
|||||||
: car.inspection_expiry
|
: car.inspection_expiry
|
||||||
: '-'}
|
: '-'}
|
||||||
</td>
|
</td>
|
||||||
<td>{car.has_etc ? '🛣️ あり' : 'なし'}</td>
|
<td>{car.has_etc ? <span className={styles.badgeEtc}>ETC</span> : 'なし'}</td>
|
||||||
<td>{car.tire_type === 'スタットレス' ? '❄️ スタットレス' : 'ノーマル'}</td>
|
<td>{car.tire_type === 'スタットレス' ? <span className={styles.badgeStudless}>スタッドレス</span> : 'ノーマル'}</td>
|
||||||
<td className={styles.actions}>
|
<td className={styles.actions}>
|
||||||
<button className={styles.btnEdit} onClick={() => startEdit(car)}>
|
<button className={styles.btnEdit} onClick={() => startEdit(car)}>
|
||||||
編集
|
編集
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
background: white;
|
background: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@@ -218,3 +218,25 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badgeEtc {
|
||||||
|
display: inline-block;
|
||||||
|
background: #7c3aed;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeStudless {
|
||||||
|
display: inline-block;
|
||||||
|
background: #0ea5e9;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import ReservationModal from './ReservationModal.jsx';
|
|||||||
import styles from './ScheduleView.module.css';
|
import styles from './ScheduleView.module.css';
|
||||||
|
|
||||||
const CELL_WIDTH = 52; // px per day column
|
const CELL_WIDTH = 52; // px per day column
|
||||||
const ROW_HEIGHT = 52; // px per car row
|
const ROW_HEIGHT = 64; // px per car row
|
||||||
const LABEL_WIDTH = 140; // px for car name column
|
const LABEL_WIDTH = 140; // px for car name column
|
||||||
const HEADER_HEIGHT = 72; // px for the date header row
|
const HEADER_HEIGHT = 72; // px for the date header row
|
||||||
const DAYS_SHOWN = 21; // number of days to show
|
const DAYS_SHOWN = 21; // number of days to show
|
||||||
@@ -443,13 +443,15 @@ export default function ScheduleView({ reloadKey = 0 }) {
|
|||||||
title={car.description || car.name}
|
title={car.description || car.name}
|
||||||
>
|
>
|
||||||
<span className={styles.carDot} style={{ background: color.border }} />
|
<span className={styles.carDot} style={{ background: color.border }} />
|
||||||
<span className={styles.carName}>{car.name}</span>
|
<span className={styles.carLabelContent}>
|
||||||
<span className={styles.carIcons}>
|
<span className={styles.carName}>{car.name}</span>
|
||||||
{car.has_etc ? <span title="ETC あり">🛣️</span> : null}
|
<span className={styles.carBadges}>
|
||||||
{car.tire_type === 'スタットレス' ? <span title="スタットレスタイヤ">❄️</span> : null}
|
{car.has_etc ? <span className={styles.badgeEtc}>ETC</span> : null}
|
||||||
{isInspectionExpirySoon(car.inspection_expiry) ? (
|
{car.tire_type === 'スタッドレス' ? <span className={styles.badgeStudless}>スタッドレス</span> : null}
|
||||||
<span title={`車検満了日: ${car.inspection_expiry}(まもなく期限切れ)`}>⚠️</span>
|
{isInspectionExpirySoon(car.inspection_expiry) ? (
|
||||||
) : null}
|
<span className={styles.badgeWarn} title={`車検満了日: ${car.inspection_expiry}(まもなく期限切れ)`}>車検</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,14 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.carLabelContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.carName {
|
.carName {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -182,15 +190,46 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.carIcons {
|
.carBadges {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 4px;
|
||||||
font-size: 14px;
|
flex-wrap: nowrap;
|
||||||
flex-shrink: 0;
|
}
|
||||||
|
|
||||||
|
.badgeEtc {
|
||||||
|
background: #7c3aed;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeStudless {
|
||||||
|
background: #0ea5e9;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badgeWarn {
|
||||||
|
background: #d97706;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cell area */
|
/* Cell area */
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ const WS_BASE = (() => {
|
|||||||
const loc = window.location;
|
const loc = window.location;
|
||||||
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
// Use the same origin as the page; in development the Vite dev server
|
// Use the same origin as the page; in development the Vite dev server
|
||||||
// proxies /ws to the backend, and in production nginx does the same.
|
// proxies /api/ws to the backend, and in production nginx does the same
|
||||||
return `${proto}//${loc.host}/ws`;
|
// via the existing location /api/ block (no extra nginx config needed).
|
||||||
|
return `${proto}//${loc.host}/api/ws`;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 15000, 30000]; // ms
|
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 15000, 30000]; // ms
|
||||||
@@ -71,11 +72,30 @@ export default function useWebSocket(onMessage) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Reset on each mount so that React StrictMode's simulated unmount/remount
|
||||||
|
// does not leave unmountedRef permanently true and block reconnection.
|
||||||
|
unmountedRef.current = false;
|
||||||
connect();
|
connect();
|
||||||
return () => {
|
return () => {
|
||||||
unmountedRef.current = true;
|
unmountedRef.current = true;
|
||||||
clearTimeout(retryTimerRef.current);
|
clearTimeout(retryTimerRef.current);
|
||||||
wsRef.current?.close();
|
const ws = wsRef.current;
|
||||||
|
if (ws) {
|
||||||
|
// Null out all handlers first to prevent any reconnect attempts.
|
||||||
|
ws.onopen = null;
|
||||||
|
ws.onclose = null;
|
||||||
|
ws.onerror = null;
|
||||||
|
ws.onmessage = null;
|
||||||
|
if (ws.readyState === WebSocket.CONNECTING) {
|
||||||
|
// Closing a CONNECTING socket triggers the browser warning
|
||||||
|
// "WebSocket is closed before the connection is established".
|
||||||
|
// Instead, schedule a close as soon as it opens so the server
|
||||||
|
// isn't left with a permanently idle connection.
|
||||||
|
ws.onopen = () => ws.close();
|
||||||
|
} else if (ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [connect]);
|
}, [connect]);
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import net from 'net';
|
|||||||
const backendOrigin = process.env.BACKEND_URL || 'http://localhost:3001';
|
const backendOrigin = process.env.BACKEND_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Vite plugin that tunnels WebSocket upgrade requests at /ws to the
|
* Custom Vite plugin that tunnels WebSocket upgrade requests at /api/ws to the
|
||||||
* backend via raw TCP. Vite's built-in proxy `ws: true` can silently drop
|
* backend via raw TCP. Vite's built-in proxy `ws: true` can silently drop
|
||||||
* upgrade events that Vite's own HMR handler intercepts first. This plugin
|
* upgrade events that Vite's own HMR handler intercepts first. This plugin
|
||||||
* hooks directly onto `httpServer.upgrade` and handles the /ws path before
|
* hooks directly onto `httpServer.upgrade` and handles the /api/ws path before
|
||||||
* Vite gets a chance to claim it.
|
* Vite gets a chance to claim it.
|
||||||
*/
|
*/
|
||||||
function wsProxyPlugin() {
|
function wsProxyPlugin() {
|
||||||
@@ -16,7 +16,7 @@ function wsProxyPlugin() {
|
|||||||
name: 'ws-proxy',
|
name: 'ws-proxy',
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
server.httpServer?.on('upgrade', (req, socket, head) => {
|
server.httpServer?.on('upgrade', (req, socket, head) => {
|
||||||
if (req.url !== '/ws') return;
|
if (req.url !== '/api/ws') return;
|
||||||
|
|
||||||
const { hostname, port: rawPort } = new URL(backendOrigin);
|
const { hostname, port: rawPort } = new URL(backendOrigin);
|
||||||
const port = parseInt(rawPort) || 3001;
|
const port = parseInt(rawPort) || 3001;
|
||||||
|
|||||||
Reference in New Issue
Block a user