P1 complete: web front, test suite, CI — acceptance ALL GREEN

Fan-out integration + fixes found by the test/acceptance pass:
- FIX ring-buffer: chunks >= capacity skipped bytes now count into the
  monotonic offset (invariant: stream byte k lives at k % capacity) —
  window order was corrupted on unaligned big chunks
- FIX auth: non-numeric cookie expiry no longer bypasses expiration
- FIX protocol: safe-integer validation on ack.bytes / hello.protocol
- FIX @fastify/websocket v11: websocket route must be registered in an
  encapsulated context after plugin load (handler got REST signature)
- FIX flow-control deadlock found by e2e acceptance: client only ACKs
  on data receipt, so pausing with an unACKed residue in (LOW,
  ACK_EVERY] stalled both sides at 0.9 MB. ACK_EVERY now 64 KiB (<=
  LOW invariant, tested) + trailing debounced ACK in the web client
- Web: Vue 3 + Vite + Pinia + Tailwind 4 + vue-i18n (EN/FR) + xterm 6
  (fit + webgl fallback), multiplexed ws-client with reconnect/backoff
  and resync epochs
- Tests: 100 vitest (protocol fuzz, ring edges, auth, pty-manager flow
  control with mocked pty, REST e2e) ; CI Node 22/24 + pack-smoke
- scripts/acceptance-p1.mjs: real daemon + real WS client — boot,
  login, attach, stdin, 10 MB flood w/ ACK (13.7 MB/1.9s, RSS bounded),
  brutal disconnect + replay resync, kill broadcast, SIGTERM drain

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:29:58 +02:00
parent 3a1396036e
commit 8bc48448c2
42 changed files with 4624 additions and 32 deletions

View File

@@ -0,0 +1,64 @@
<template>
<div class="flex items-center justify-center px-4">
<form
class="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-zinc-800 bg-zinc-900/60 p-6"
@submit.prevent="onSubmit"
>
<div class="flex items-center justify-between">
<h1 class="text-lg font-semibold text-zinc-100">{{ t('common.appName') }}</h1>
<LanguageSwitcher />
</div>
<p class="text-sm text-zinc-400">{{ t('login.title') }}</p>
<label class="flex flex-col gap-1 text-xs text-zinc-400">
{{ t('login.tokenLabel') }}
<input
v-model="token"
type="password"
class="input font-mono"
:placeholder="t('login.tokenPlaceholder')"
autocomplete="current-password"
autofocus
required
/>
</label>
<p v-if="error" class="text-sm text-red-400">{{ error }}</p>
<button type="submit" class="btn-primary" :disabled="submitting || token.trim() === ''">
{{ submitting ? t('login.submitting') : t('login.submit') }}
</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../stores/auth';
import { ApiError } from '../lib/api';
import LanguageSwitcher from '../components/LanguageSwitcher.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const auth = useAuthStore();
const token = ref('');
const error = ref<string | null>(null);
const submitting = ref(false);
async function onSubmit(): Promise<void> {
submitting.value = true;
error.value = null;
try {
await auth.login(token.value.trim());
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/';
await router.replace(redirect);
} catch (err) {
if (err instanceof ApiError && err.status === 401) error.value = t('login.invalidToken');
else if (err instanceof ApiError && err.status === 429) error.value = t('login.rateLimited');
else error.value = t('login.genericError', { status: err instanceof ApiError ? err.status : '?' });
} finally {
submitting.value = false;
}
}
</script>