tripoli.cloud/app/src/components/marketing/ArchitecturePage.tsx
Adam Belkacemi 3172b3e256
All checks were successful
CI / build-and-test (push) Successful in 2m16s
CI / deploy (push) Successful in 3m16s
Vitrine publique (accueil, architecture, statut) + recherche plein texte
- Nouvelles pages publiques sans routeur externe (History API minimale,
  useRoute/RouteLink) : accueil, /architecture (état réel du projet,
  honnête sur les limites), /statut (télémétrie live de tripoli-edge
  lue directement sur /healthz et /metrics, aucune donnée simulée).
- Recherche façon Spotlight dans TopBar : nouvel endpoint
  GET /api/search côté tripoli-api, recherche plein texte Postgres
  (tsvector généré + index GIN) sur le nom ET le contenu des fichiers
  texte extrait à l'upload (formats texte/code, borné à 5 Mio) ;
  extraits de résultat via ts_headline.
- AuthLayout : logo cliquable vers l'accueil.

Vérifié : tsc --noEmit propre, npm run build propre, cargo build -p
tripoli-api propre.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 13:03:52 +02:00

157 lines
7.1 KiB
TypeScript

import type { ReactNode } from 'react';
import { Cpu, GitBranch, Network, Radio, Server, ShieldCheck } from 'lucide-react';
import { MarketingHeader } from './MarketingHeader';
import { MarketingFooter } from './MarketingFooter';
const FLOW = [
{
title: 'Caddy (edge partagé)',
detail:
"TLS Let's Encrypt automatique, routage par nom d'hôte. Ce Caddy sert aussi six autres domaines sur ce même serveur — tripoli-edge n'a jamais eu les ports 80/443 pour lui seul.",
},
{
title: 'tripoli-edge — Rust (hyper/tokio)',
detail:
'Reverse-proxy interne (172.17.0.1:9000, passerelle docker0, jamais exposé directement). Expose /healthz et /metrics en clair, sans framework de métriques : cinq compteurs atomiques suffisaient.',
},
{
title: 'tripoli-api — Rust (axum/sqlx)',
detail:
"Authentification par session opaque (Argon2, pas de JWT — révoquer une session, c'est supprimer une ligne). Upload en streaming vers disque, recherche plein texte Postgres.",
},
{
title: 'PostgreSQL + blobs',
detail:
"Conteneur Docker dédié à Tripoli Cloud, séparé de celui d'un autre projet sur ce même serveur. Les fichiers sont nommés par UUID sur disque — le nom d'origine ne vit qu'en base.",
},
];
const LABS: { icon: ReactNode; title: string; detail: string }[] = [
{
icon: <Network size={18} />,
title: 'Tunnel WireGuard',
detail:
"10.100.0.0/24 entre le Mac d'administration et le serveur — canal SSH/Ansible, jamais dans le chemin de trafic applicatif.",
},
{
icon: <Server size={18} />,
title: 'tripoli-netctl',
detail:
'Netns + veth créés à la demande via rtnetlink (Rust). Testé (création, ping traversant, destruction propre) — volontairement jamais branché sur le trafic de prod.',
},
{
icon: <Radio size={18} />,
title: 'bird-lab — BGP + BFD',
detail:
'Deux namespaces en session BGP via une racine en transit (AS 65000/65010/65011). Sessions Established, BFD Up et détection de panne vérifiées, puis démonté proprement.',
},
];
const STATE: [string, string][] = [
['Frontend (app/)', 'Fait — branché sur les vraies données de tripoli-api, aucun mock.'],
['WireGuard', 'Fait — 10.100.0.0/24.'],
['tripoli-edge', 'Fait, en prod — /healthz, /metrics vérifiés.'],
['tripoli-api', 'Fait, en prod — auth, fichiers, notifications, recherche.'],
['tripoli-netctl', 'Fait — labo, jamais branché sur le trafic de prod.'],
['Observabilité', 'Fait — scrape Prometheus/Grafana partagé, alertes versionnées.'],
['bird-lab (BGP/BFD)', 'Fait, bonus — démonté après vérification.'],
['CI/CD', 'Fait — Forgejo Actions, compte deploy sans privilège.'],
['OpenTofu', 'Code écrit, non appliqué — identifiants OVH/Hetzner non fournis.'],
];
export function ArchitecturePage() {
return (
<div className="min-h-screen bg-bg text-ink">
<MarketingHeader />
<main className="mx-auto max-w-[880px] px-6 py-16 sm:px-10 sm:py-24">
<span className="inline-flex items-center gap-2 rounded-full border border-line bg-field px-3.5 py-1.5 text-xs font-semibold tracking-wide text-ink-soft">
<Cpu size={13} />
Un seul serveur, pas un cluster
</span>
<h1 className="m-0 my-5 font-display text-[36px] font-semibold leading-[1.08] tracking-tight sm:text-[48px]">
Ce qui tourne <span className="italic">réellement</span>.
</h1>
<p className="m-0 max-w-[62ch] text-[15.5px] leading-relaxed text-ink-soft">
Un VPS Hetzner CPX22 (2 vCPU, 3,7 Go de RAM), partagé avec six autres projets personnels.
Ce qui suit est honnête sur ce qui est en place — pas de cluster simulé, pas de haute
disponibilité qui n'existe pas.
</p>
<section className="mt-14">
<h2 className="m-0 mb-6 font-display text-[22px] font-semibold text-ink">
Le chemin d'une requête
</h2>
<div className="flex flex-col gap-3">
{FLOW.map((step, i) => (
<div key={step.title} className="flex gap-4 rounded-2xl border border-line bg-panel p-5 shadow-tc">
<span className="flex h-8 w-8 flex-none items-center justify-center rounded-full bg-field text-[13px] font-bold text-ink">
{i + 1}
</span>
<div>
<p className="m-0 mb-1 text-[14.5px] font-bold text-ink">{step.title}</p>
<p className="m-0 text-[13.5px] leading-relaxed text-ink-soft">{step.detail}</p>
</div>
</div>
))}
</div>
</section>
<section className="mt-14">
<h2 className="m-0 mb-2 font-display text-[22px] font-semibold text-ink">
Hors du chemin de trafic
</h2>
<p className="m-0 mb-6 text-[13.5px] text-ink-soft">
Labos pédagogiques — jamais branchés sur le trafic de production.
</p>
<div className="grid gap-3 sm:grid-cols-3">
{LABS.map((lab) => (
<div key={lab.title} className="rounded-2xl border border-line bg-panel p-5 shadow-tc">
<div className="mb-2 text-ink-soft">{lab.icon}</div>
<p className="m-0 mb-1 text-[13.5px] font-bold text-ink">{lab.title}</p>
<p className="m-0 text-[12.5px] leading-relaxed text-ink-soft">{lab.detail}</p>
</div>
))}
</div>
</section>
<section className="mt-14">
<h2 className="m-0 mb-6 font-display text-[22px] font-semibold text-ink">État réel</h2>
<div className="overflow-hidden rounded-2xl border border-line bg-panel shadow-tc">
{STATE.map(([name, status], i) => (
<div
key={name}
className={`flex items-center justify-between gap-4 px-5 py-3.5 text-[13px] ${
i !== STATE.length - 1 ? 'border-b border-line/70' : ''
}`}
>
<span className="font-semibold text-ink">{name}</span>
<span className="text-right text-ink-soft">{status}</span>
</div>
))}
</div>
</section>
<section className="mt-14 rounded-2xl border border-line bg-field p-6">
<div className="mb-2 flex items-center gap-2 text-ink">
<ShieldCheck size={16} />
<p className="m-0 text-[13.5px] font-bold">Limites assumées</p>
</div>
<p className="m-0 text-[13px] leading-relaxed text-ink-soft">
SSH public encore ouvert (choix explicite, pas encore restreint au VPN). Pas de
virtualisation imbriquée sur ce VPS. Alertes Prometheus qui se déclenchent mais ne sont
pas encore routées par notification. Détail complet dans le README du dépôt.
</p>
</section>
<div className="mt-10 flex items-center gap-2 text-[13px] text-ink-soft">
<GitBranch size={15} />
Code et historique de commits versionnés sur Forgejo.
</div>
</main>
<MarketingFooter />
</div>
);
}