Import initial : code en production sur Hetzner
Some checks are pending
CI / tests (push) Waiting to run
CI / deploiement (push) Blocked by required conditions

This commit is contained in:
Adam Belkacemi 2026-09-07 18:21:38 +00:00
commit cf6ee64e31
43 changed files with 14404 additions and 0 deletions

View file

@ -0,0 +1,97 @@
import { z } from 'zod'
import { EMISSION_FACTORS } from '#shared/emissions'
import { thin } from '#shared/polyline'
import { estimateTrainTrip } from '../utils/estimate'
import type { Coord } from '#shared/types'
const BodySchema = z.object({
from: z.object({ lat: z.number(), lon: z.number() }),
to: z.object({ lat: z.number(), lon: z.number() }),
})
interface OrsResponse {
features: Array<{
properties: { summary: { distance: number, duration: number } }
geometry: { coordinates: Coord[] }
}>
}
/** Interroge OpenRouteService pour un profil donne. */
async function route(profile: string, body: unknown, key: string) {
const data = await $fetch<OrsResponse>(
`https://api.openrouteservice.org/v2/directions/${profile}/geojson`,
{
method: 'POST',
headers: {
Authorization: key,
// ORS refuse de repondre si on n'annonce pas accepter ce format precis.
Accept: 'application/geo+json',
},
body,
},
)
const feature = data.features[0]
if (!feature) throw new Error('Itinéraire introuvable')
return {
km: feature.properties.summary.distance / 1000,
min: feature.properties.summary.duration / 60,
// ORS renvoie deja du GeoJSON [lon, lat] : rien a decoder, juste a alleger.
geometry: thin(feature.geometry.coordinates, 800),
}
}
const unavailable = (mode: string, reason: string) => ({ mode, unavailable: true, reason })
export default defineEventHandler(async (event) => {
const parsed = BodySchema.safeParse(await readBody(event))
if (!parsed.success) {
throw createError({ statusCode: 400, statusMessage: 'Départ ou arrivée invalide.' })
}
const { from, to } = parsed.data
const { orsApiKey } = useRuntimeConfig(event)
const body = { coordinates: [[from.lon, from.lat], [to.lon, to.lat]] }
// allSettled et non all : un fournisseur en panne ne doit pas vider la page.
const [car, bike, pt] = await Promise.allSettled([
route('driving-car', body, orsApiKey),
route('cycling-regular', body, orsApiKey),
publicTransport(from, to, new Date()),
])
const options = []
options.push(car.status === 'fulfilled'
? {
mode: 'electric_car',
distanceKm: Math.round(car.value.km * 10) / 10,
durationMin: Math.round(car.value.min),
co2Grams: Math.round(car.value.km * EMISSION_FACTORS.electric_car),
geometry: car.value.geometry,
}
: unavailable('electric_car', 'Aucune route carrossable trouvée.'))
options.push(bike.status === 'fulfilled'
? {
mode: 'bike',
distanceKm: Math.round(bike.value.km * 10) / 10,
durationMin: Math.round(bike.value.min),
co2Grams: 0,
geometry: bike.value.geometry,
}
: unavailable('bike', 'Aucun itinéraire cyclable trouvé.'))
if (pt.status === 'fulfilled') {
options.push(pt.value.train ?? estimateTrainTrip(from, to))
options.push(pt.value.bus ?? unavailable('bus', 'Pas de ligne régulière.'))
}
else {
options.push(unavailable('train', 'Horaires indisponibles.'))
options.push(unavailable('bus', 'Horaires indisponibles.'))
}
return options.sort((a, b) =>
('co2Grams' in a ? a.co2Grams : Infinity) - ('co2Grams' in b ? b.co2Grams : Infinity))
})

10
server/api/geocode.get.ts Normal file
View file

@ -0,0 +1,10 @@
import { z } from 'zod'
import type { Place } from '#shared/types'
const QuerySchema = z.object({ q: z.string().min(3).max(200) })
export default defineEventHandler(async (event): Promise<Place[]> => {
const parsed = QuerySchema.safeParse(getQuery(event))
if (!parsed.success) return []
return searchPlaces(parsed.data.q)
})

13
server/api/health.get.ts Normal file
View file

@ -0,0 +1,13 @@
/**
* Cible du health check de l'ALB.
* Volontairement sans dependance externe : si OpenRouteService tombe,
* la tache reste saine et continue de servir ce qu'elle peut.
*/
export default defineEventHandler((event) => {
setHeader(event, 'Cache-Control', 'no-store')
return {
status: 'ok',
uptimeSeconds: Math.round(process.uptime()),
version: process.env.APP_VERSION ?? 'dev',
}
})