web: parity page renders the three-scenario comparison from live data

App.vue fetches the default config, posts it to /api/comparison, and
renders ComparisonTable; the Vite dev server proxies /api to the Go
server so the browser sees one origin. src/lib/api.ts is the only
fetch code and uses exclusively generated types: no shape is defined
on the frontend. Scaffold example components removed.
Engine fix surfaced by the smoke test: a nil Go slice marshals to
JSON null, violating the generated 'educated: number[]' contract;
RunScenario now returns an empty slice instead.
Verified end to end: curl through the Vite proxy returns the golden
99/70/7. Vitest covers the table rendering; type-check, oxlint,
eslint clean. This completes the milestone 2 parity check.
This commit is contained in:
Justin Visser 2026-06-10 13:47:01 +02:00
parent e9f36f509e
commit c935071bd1
17 changed files with 198 additions and 319 deletions

View file

@ -1,47 +1,40 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
import { onMounted, ref } from 'vue'
import { fetchDefaultConfig, runComparison } from '@/lib/api'
import type { ComparisonResponse } from '@/types/api'
import ComparisonTable from '@/components/ComparisonTable.vue'
const comparison = ref<ComparisonResponse | null>(null)
const error = ref<string | null>(null)
onMounted(async () => {
try {
const config = await fetchDefaultConfig()
comparison.value = await runComparison(config)
} catch (cause) {
error.value = cause instanceof Error ? cause.message : String(cause)
}
})
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
<main>
<TheWelcome />
<h1>spreadlab</h1>
<p>
How a non-consensual deepfake spreads through a simulated school year
group, and what an education program changes. Illustrative, not
validated.
</p>
<p v-if="error" role="alert">Could not reach the spreadlab API: {{ error }}</p>
<ComparisonTable v-else-if="comparison" :comparison="comparison" />
<p v-else>Running scenarios&hellip;</p>
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
main {
max-width: 48rem;
margin: 0 auto;
padding: 2rem 1rem;
}
</style>