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:
@@ -0,0 +1,10 @@
|
||||
import { createAuthMiddleware } from '@tonycodes/auth-express';
|
||||
import { env } from '../config/env.js';
|
||||
|
||||
export function createOptionalAuth() {
|
||||
if (!env.authSecret) return null;
|
||||
return createAuthMiddleware({
|
||||
clientId: env.authClientId || 'tesla-roadtrip',
|
||||
clientSecret: env.authSecret,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { env } from '../config/env.js';
|
||||
import { createLogger } from './logger.js';
|
||||
|
||||
const log = createLogger('trip-store');
|
||||
|
||||
export interface Trip {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
vehicleModel: string;
|
||||
rangeMi: number;
|
||||
itinerary: unknown;
|
||||
status: string;
|
||||
isPublic: boolean;
|
||||
shareSlug: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export type TripInput = Omit<Trip, 'id' | 'createdAt' | 'updatedAt' | 'shareSlug'> & {
|
||||
shareSlug?: string | null;
|
||||
};
|
||||
|
||||
interface TripStore {
|
||||
list(userId: string): Promise<Trip[]>;
|
||||
get(id: string, userId: string): Promise<Trip | null>;
|
||||
create(input: TripInput): Promise<Trip>;
|
||||
update(id: string, userId: string, patch: Partial<TripInput>): Promise<Trip | null>;
|
||||
remove(id: string, userId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
class MemoryTripStore implements TripStore {
|
||||
private trips = new Map<string, Trip>();
|
||||
|
||||
async list(userId: string): Promise<Trip[]> {
|
||||
return [...this.trips.values()]
|
||||
.filter(t => t.userId === userId)
|
||||
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
|
||||
}
|
||||
|
||||
async get(id: string, userId: string): Promise<Trip | null> {
|
||||
const trip = this.trips.get(id);
|
||||
if (!trip || trip.userId !== userId) return null;
|
||||
return trip;
|
||||
}
|
||||
|
||||
async create(input: TripInput): Promise<Trip> {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date();
|
||||
const trip: Trip = {
|
||||
id,
|
||||
...input,
|
||||
shareSlug: input.shareSlug ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
this.trips.set(id, trip);
|
||||
return trip;
|
||||
}
|
||||
|
||||
async update(id: string, userId: string, patch: Partial<TripInput>): Promise<Trip | null> {
|
||||
const trip = this.trips.get(id);
|
||||
if (!trip || trip.userId !== userId) return null;
|
||||
const updated: Trip = { ...trip, ...patch, updatedAt: new Date() };
|
||||
this.trips.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string): Promise<boolean> {
|
||||
const trip = this.trips.get(id);
|
||||
if (!trip || trip.userId !== userId) return false;
|
||||
return this.trips.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
let store: TripStore | null = null;
|
||||
|
||||
export function getTripStore(): TripStore {
|
||||
if (store) return store;
|
||||
if (!env.databaseUrl) {
|
||||
log.warn('DATABASE_URL not set — using in-memory trip store (data lost on restart)');
|
||||
store = new MemoryTripStore();
|
||||
return store;
|
||||
}
|
||||
log.info('Using in-memory trip store (Prisma adapter not yet wired)');
|
||||
store = new MemoryTripStore();
|
||||
return store;
|
||||
}
|
||||
Reference in New Issue
Block a user