feat: wire build/test infra, trips API, and enriched journey stops
- Add tsconfig.json (server) + client/tsconfig.{json,app.json,node.json}
so typecheck and tsc -b actually work.
- Fix npm test to run Playwright (was running vitest on Playwright specs);
typecheck now covers both server and client.
- Mount routes before app.listen, add error handler, mount optional
@tonycodes/auth-express middleware when AUTH_SECRET is set.
- Add /api/trips (GET/POST/PATCH/DELETE) backed by an in-memory store
that gracefully degrades when DATABASE_URL is unset.
- Add prisma/seed.ts skeleton and server/types/express.d.ts for req.auth.
- Rewrite Grok prompt for combo-aware planning: charge+eat,
stay+destination-charging, eat+viewpoint, etc., with amenities,
cuisine, priceLevel, duration, day titles and trip highlights.
- Extend Stop schema + normalization to preserve all enrichment fields.
- New StopCard component renders combo pill, description, meta row
(charge / stop / battery / cuisine / £-level) and amenity icons;
map popups show the same enriched detail; timeline gains day titles
and a HIGHLIGHTS sidebar.
- Fix server TS errors (vehicle accepted as string | {name,rangeKm},
JSON parse results typed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -70,4 +70,20 @@ router.post('/chat', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
router.get('/grok/status', async (_req, res) => {
|
||||
try {
|
||||
const status = await grok.getStatus();
|
||||
res.json(status);
|
||||
} catch (err) {
|
||||
res.json({
|
||||
provider: 'fallback',
|
||||
label: 'Fallback',
|
||||
detail: 'Basic mode',
|
||||
isLocal: false,
|
||||
model: 'unknown',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Router, type Request, type Response } from 'express';
|
||||
import { z } from 'zod';
|
||||
import { createLogger } from '../lib/logger.js';
|
||||
import { getTripStore } from '../lib/tripStore.js';
|
||||
|
||||
const log = createLogger('trips-api');
|
||||
const router = Router();
|
||||
|
||||
const TripBodySchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
vehicleModel: z.string().min(1).max(120),
|
||||
rangeMi: z.number().int().nonnegative(),
|
||||
itinerary: z.unknown().transform((v) => v ?? null),
|
||||
status: z.string().max(40).optional(),
|
||||
isPublic: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const TripPatchSchema = TripBodySchema.partial();
|
||||
|
||||
const ANONYMOUS_USER_ID = 'anonymous';
|
||||
|
||||
function resolveUserId(req: Request): string {
|
||||
return req.auth?.userId ?? ANONYMOUS_USER_ID;
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const userId = resolveUserId(req);
|
||||
const trips = await getTripStore().list(userId);
|
||||
res.json({ trips });
|
||||
});
|
||||
|
||||
router.get('/:id', async (req, res) => {
|
||||
const userId = resolveUserId(req);
|
||||
const trip = await getTripStore().get(String(req.params.id), userId);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
res.json(trip);
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const parsed = TripBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: 'Invalid trip', issues: parsed.error.format() });
|
||||
}
|
||||
const userId = resolveUserId(req);
|
||||
const trip = await getTripStore().create({
|
||||
...parsed.data,
|
||||
userId,
|
||||
status: parsed.data.status ?? 'planning',
|
||||
isPublic: parsed.data.isPublic ?? false,
|
||||
});
|
||||
log.info({ tripId: trip.id, userId }, 'Trip created');
|
||||
res.status(201).json(trip);
|
||||
});
|
||||
|
||||
router.patch('/:id', async (req, res) => {
|
||||
const parsed = TripPatchSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: 'Invalid trip patch', issues: parsed.error.format() });
|
||||
}
|
||||
const userId = resolveUserId(req);
|
||||
const trip = await getTripStore().update(String(req.params.id), userId, parsed.data);
|
||||
if (!trip) return res.status(404).json({ error: 'Trip not found' });
|
||||
res.json(trip);
|
||||
});
|
||||
|
||||
router.delete('/:id', async (req: Request, res: Response) => {
|
||||
const userId = resolveUserId(req);
|
||||
const removed = await getTripStore().remove(String(req.params.id), userId);
|
||||
if (!removed) return res.status(404).json({ error: 'Trip not found' });
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user