9 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
8e9db971d3 feat: add inspection_expiry/has_etc/tire_type fields, icons in schedule view, and WebSocket real-time sync
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/6d0f25ae-6db4-4937-ae2b-6674456a5ca1
2026-03-22 03:58:07 +00:00
h
09872737b7 Merge pull request #3 from pdf114514/copilot/add-popup-for-edit-delete-reservations
Add timeline view, right-click context menu on reservations, fix trust proxy
2026-03-21 03:52:02 +09:00
copilot-swe-agent[bot]
cc3ad148fc Add timeline view, right-click context menu, and fix Express trust proxy
- backend/server.js: Add app.set('trust proxy', 1) to fix express-rate-limit
  ValidationError when app runs behind nginx reverse proxy
- ScheduleView.jsx: Add right-click context menu on reservation blocks with
  Edit and Delete options; closes on click-outside or Escape
- ScheduleView.module.css: Add context menu styles
- TimelineView.jsx: New Gantt-style monthly timeline view showing all
  reservations sorted by date, with month navigation and right-click menu
- TimelineView.module.css: Styles for the timeline view
- App.jsx: Add 'タイムライン' tab to navigation

Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/d03ca12c-21ce-45a0-881f-919d6635e7fb
2026-03-20 18:50:51 +00:00
copilot-swe-agent[bot]
1eb96877ff Initial plan 2026-03-20 18:38:28 +00:00
h
76dc94dd78 Merge pull request #2 from pdf114514/copilot/disable-drag-drop-for-touch
Disable touch drag & drop, warn on car delete with reservations, support configurable backend URL
2026-03-21 03:21:50 +09:00
copilot-swe-agent[bot]
c3dd0cfa69 Disable touch drag & drop, warn on car delete with reservations, configure backend URL
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/cd194ca1-b339-4f2f-b717-31a0ba193964
2026-03-20 18:19:53 +00:00
copilot-swe-agent[bot]
40371b43d1 Initial plan 2026-03-20 18:13:46 +00:00
h
19953dff55 Merge pull request #1 from pdf114514/copilot/create-car-rental-schedule-system
Add car rental schedule management system (React+Vite + Express+SQLite)
2026-03-21 03:07:15 +09:00
copilot-swe-agent[bot]
50d3803610 Implement car reservation schedule management system
Co-authored-by: pdf114514 <57948770+pdf114514@users.noreply.github.com>
Agent-Logs-Url: https://github.com/pdf114514/CarReservation/sessions/1d8c6b05-0e8d-4484-a2d8-8d427dfad9cb
2026-03-20 18:03:33 +00:00
7 changed files with 24 additions and 107 deletions

View File

@@ -75,7 +75,7 @@ if (carCount.cnt === 0) {
} }
// --- WebSocket Server --- // --- WebSocket Server ---
const wss = new WebSocketServer({ server, path: '/api/ws' }); const wss = new WebSocketServer({ server, path: '/ws' });
function broadcast(message) { function broadcast(message) {
const data = JSON.stringify(message); const data = JSON.stringify(message);

View File

@@ -275,8 +275,8 @@ export default function CarManagement({ reloadKey = 0 }) {
: car.inspection_expiry : car.inspection_expiry
: '-'} : '-'}
</td> </td>
<td>{car.has_etc ? <span className={styles.badgeEtc}>ETC</span> : 'なし'}</td> <td>{car.has_etc ? '🛣️ あり' : 'なし'}</td>
<td>{car.tire_type === 'スタットレス' ? <span className={styles.badgeStudless}>スタッレス</span> : 'ノーマル'}</td> <td>{car.tire_type === 'スタットレス' ? '❄️ スタッレス' : 'ノーマル'}</td>
<td className={styles.actions}> <td className={styles.actions}>
<button className={styles.btnEdit} onClick={() => startEdit(car)}> <button className={styles.btnEdit} onClick={() => startEdit(car)}>
編集 編集

View File

@@ -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-x: auto; overflow: hidden;
} }
.table { .table {
@@ -218,25 +218,3 @@
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;
}

View File

@@ -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 = 64; // px per car row const ROW_HEIGHT = 52; // 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,15 +443,13 @@ 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.carLabelContent}> <span className={styles.carName}>{car.name}</span>
<span className={styles.carName}>{car.name}</span> <span className={styles.carIcons}>
<span className={styles.carBadges}> {car.has_etc ? <span title="ETC あり">🛣</span> : null}
{car.has_etc ? <span className={styles.badgeEtc}>ETC</span> : null} {car.tire_type === 'スタットレス' ? <span title="スタットレスタイヤ"></span> : null}
{car.tire_type === 'スタッドレス' ? <span className={styles.badgeStudless}>スタッドレス</span> : null} {isInspectionExpirySoon(car.inspection_expiry) ? (
{isInspectionExpirySoon(car.inspection_expiry) ? ( <span title={`車検満了日: ${car.inspection_expiry}(まもなく期限切れ)`}></span>
<span className={styles.badgeWarn} title={`車検満了日: ${car.inspection_expiry}(まもなく期限切れ)`}>車検</span> ) : null}
) : null}
</span>
</span> </span>
</div> </div>

View File

@@ -175,14 +175,6 @@
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;
@@ -190,46 +182,15 @@
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex: 1;
min-width: 0;
} }
.carBadges { .carIcons {
display: flex; display: flex;
gap: 4px; gap: 2px;
flex-wrap: nowrap; font-size: 14px;
} 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 */

View File

@@ -5,9 +5,8 @@ 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 /api/ws to the backend, and in production nginx does the same // proxies /ws to the backend, and in production nginx does the same.
// via the existing location /api/ block (no extra nginx config needed). return `${proto}//${loc.host}/ws`;
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
@@ -72,30 +71,11 @@ 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);
const ws = wsRef.current; wsRef.current?.close();
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]);

View File

@@ -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 /api/ws to the * Custom Vite plugin that tunnels WebSocket upgrade requests at /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 /api/ws path before * hooks directly onto `httpServer.upgrade` and handles the /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 !== '/api/ws') return; if (req.url !== '/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;