Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3f788af
feat(streamlet): add content data for the streamlet page
choffmann Aug 21, 2026
23dc9d1
refactor(streamlet): derive changeset status label in the consumer
choffmann Aug 21, 2026
232e137
feat(streamlet): add /streamlet route with hero section
choffmann Aug 21, 2026
ad225ca
fix(streamlet): hide decorative hero icon from assistive tech
choffmann Aug 21, 2026
23d775b
feat(streamlet): add problem section with tour diagram
choffmann Aug 21, 2026
9d5bb1a
fix(streamlet): anchor tour diagram fill level to its reference lines
choffmann Aug 21, 2026
345151b
refactor(streamlet): derive tour diagram polyline from the stop data
choffmann Aug 21, 2026
e3d2738
feat(streamlet): add problem-fit checklist section
choffmann Aug 21, 2026
9661361
fix(streamlet): hide decorative criteria icons from assistive tech
choffmann Aug 21, 2026
7f57c78
feat(streamlet): add api section with request example
choffmann Aug 21, 2026
e5ffba7
fix(streamlet): make the request example scrollable by keyboard
choffmann Aug 21, 2026
e943062
feat(streamlet): add solver section with solomon benchmarks
choffmann Aug 21, 2026
e0e56dc
fix(streamlet): keep the benchmark list a valid description list
choffmann Aug 21, 2026
f8a8fab
feat(streamlet): add tile pipeline section
choffmann Aug 21, 2026
f86033c
fix(streamlet): give the changeset cards an accessible name
choffmann Aug 21, 2026
893b040
feat(streamlet): add operations section with configuration table
choffmann Aug 21, 2026
8d55cac
fix(streamlet): name the configuration table for assistive tech
choffmann Aug 21, 2026
1411f7d
feat(streamlet): add status section with call to action
choffmann Aug 21, 2026
f8da6b0
fix(streamlet): keep the release link readable on hover
choffmann Aug 22, 2026
43ab187
feat(navigation): link the streamlet page and drop the github item
choffmann Aug 22, 2026
4df2c11
style(architecture): constrain the streamlet link paragraph width
choffmann Aug 22, 2026
cf55fdf
refactor(streamlet): drop content that duplicates the repository docs
choffmann Aug 22, 2026
ed85204
fix(streamlet): clear the fixed header in the hero
choffmann Aug 22, 2026
3cf63aa
fix(streamlet): correct factual and logical errors in the page text
choffmann Aug 22, 2026
5e4c401
refactor(streamlet): use one term per concept in the page text
choffmann Aug 22, 2026
fee78d8
refactor(streamlet): address the reader directly, as the rest of the …
choffmann Aug 22, 2026
ee2aeaa
refactor(streamlet): cut repeated claims and tighten wording
choffmann Aug 22, 2026
271b64e
fix(streamlet): correct page claims the implementation does not support
choffmann Aug 22, 2026
665286f
feat(i18n): translate the streamlet page
choffmann Aug 22, 2026
c23c933
feat(i18n): translate the streamlet link in the architecture section
choffmann Aug 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/check-i18n-parity.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'

const NAMESPACES = ['common', 'home', 'project', 'contact', 'releases', 'legal']
const NAMESPACES = ['common', 'home', 'project', 'streamlet', 'contact', 'releases', 'legal']
const LANGUAGES = ['de', 'en']
const REFERENCE = 'de'

Expand Down
124 changes: 124 additions & 0 deletions src/data/streamlet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
Boxes,
Clock,
Container,
Fuel,
GitBranch,
Layers,
Repeat,
Scale,
ServerCog,
Warehouse,
} from 'lucide-react'
import type { ComponentType } from 'react'

type Icon = ComponentType<{ className?: string }>

export const streamletLinks = {
repo: 'https://github.com/green-ecolution/streamlet',
readme: 'https://github.com/green-ecolution/streamlet#readme',
issues: 'https://github.com/green-ecolution/streamlet/issues',
} as const

export type FitCriterionId =
'tankCapacity' | 'timeWindows' | 'midTourRefill' | 'multipleStations' | 'depotReturn'

interface FitCriterion {
id: FitCriterionId
icon: Icon
}

export const fitCriteria: FitCriterion[] = [
{ id: 'tankCapacity', icon: Container },
{ id: 'timeWindows', icon: Clock },
{ id: 'midTourRefill', icon: Fuel },
{ id: 'multipleStations', icon: Repeat },
{ id: 'depotReturn', icon: Warehouse },
]

export type SolveFlowStepId = 'send' | 'solve' | 'receive'

interface SolveFlowStep {
id: SolveFlowStepId
step: string
}

export const solveFlowSteps: SolveFlowStep[] = [
{ id: 'send', step: '01' },
{ id: 'solve', step: '02' },
{ id: 'receive', step: '03' },
]

export type ApiEndpointId = 'solve' | 'health' | 'openapi'

interface ApiEndpoint {
id: ApiEndpointId
method: string
path: string
}

export const apiEndpoints: ApiEndpoint[] = [
{ id: 'solve', method: 'POST', path: '/v1/solve' },
{ id: 'health', method: 'GET', path: '/health' },
{ id: 'openapi', method: 'GET', path: '/api-docs/openapi.json' },
]

export type SolverStageId = 'construction' | 'localSearch'

interface SolverStage {
id: SolverStageId
icon: Icon
}

export const solverStages: SolverStage[] = [
{ id: 'construction', icon: Boxes },
{ id: 'localSearch', icon: GitBranch },
]

export const solverMoveEvaluationIcon: Icon = Layers

interface Benchmark {
instance: string
gap: number
}

export const solverBenchmarks: Benchmark[] = [
{ instance: 'c101', gap: 3.1 },
{ instance: 'r101', gap: 1.9 },
{ instance: 'rc101', gap: 1.5 },
]

export type TileChangesetId = 'construction' | 'allowedPaths'

export interface TileChangeset {
id: TileChangesetId
name: string
status: 'available' | 'inProgress'
}

export const tileChangesets: TileChangeset[] = [
{ id: 'construction', name: 'construction', status: 'available' },
{ id: 'allowedPaths', name: 'allowed-paths', status: 'inProgress' },
]

export type OperationsFactId = 'stateless' | 'swappableEngine' | 'separateCrate' | 'license'

interface OperationsFact {
id: OperationsFactId
icon: Icon
}

export const operationsFacts: OperationsFact[] = [
{ id: 'stateless', icon: ServerCog },
{ id: 'swappableEngine', icon: Repeat },
{ id: 'separateCrate', icon: Layers },
{ id: 'license', icon: Scale },
]

export type StreamletLimitationId = 'geometry' | 'depots' | 'missingFeatures'

export const streamletLimitations: StreamletLimitationId[] = [
'geometry',
'depots',
'missingFeatures',
]
10 changes: 9 additions & 1 deletion src/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import i18next from 'i18next'
import { initReactI18next } from 'react-i18next'
import { DEFAULT_LANGUAGE, type Language } from './languages'

export const NAMESPACES = ['common', 'home', 'project', 'contact', 'releases', 'legal'] as const
export const NAMESPACES = [
'common',
'home',
'project',
'streamlet',
'contact',
'releases',
'legal',
] as const

type Catalog = Record<string, Record<string, unknown>>

Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"ariaLabel": "Hauptnavigation",
"heading": "Hauptnavigation",
"project": "Das Projekt",
"streamlet": "Streamlet",
"releases": "Releases",
"contact": "Kontakt",
"github": "GitHub",
"tryNow": "Jetzt ausprobieren",
"imprint": "Impressum",
"privacy": "Datenschutz"
Expand Down Expand Up @@ -72,6 +72,7 @@
"breadcrumb": {
"home": "Startseite",
"project": "Projekt",
"streamlet": "Streamlet",
"contact": "Kontakt",
"imprint": "Impressum",
"privacy": "Datenschutz"
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/de/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import home from './home.json'
import legal from './legal.json'
import project from './project.json'
import releases from './releases.json'
import streamlet from './streamlet.json'

export default { common, home, project, contact, releases, legal }
export default { common, home, project, streamlet, contact, releases, legal }
1 change: 1 addition & 0 deletions src/i18n/locales/de/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"sectionLabel": "System-Architektur",
"title": "Der Weg der Daten",
"description": "So gelangen die Messwerte vom Baum bis zur optimierten Bewässerungsroute durch das Green Ecolution System.",
"streamletRich": "Die Routenoptimierung im Backend übernimmt <streamlet>Streamlet</streamlet>, ein eigenes Open-Source-Projekt, das sich auch außerhalb von Green Ecolution einsetzen lässt.",
"steps": {
"sensor": {
"label": "Sensor",
Expand Down
155 changes: 155 additions & 0 deletions src/i18n/locales/de/streamlet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
{
"meta": {
"title": "Streamlet | Tourenplanung mit Nachfüllstationen | Green Ecolution",
"description": "Streamlet ist ein eigenständiger Open-Source-Dienst für Tourenplanung mit Zeitfenstern und mehreren Nachfüllstationen. Zustandslose HTTP-API, deterministischer Solver, AGPL-3.0."
},
"hero": {
"sectionLabel": "Open-Source-Projekt",
"title": "Streamlet",
"lead": "Tourenplanung für Fahrzeuge, deren Tankfüllung nicht für die ganze Tour reicht. Streamlet legt die Reihenfolge der Halte fest und plant das Nachfüllen unterwegs gleich mit. Es läuft als eigenständiger Dienst hinter einer HTTP-API.",
"origin": "Entstanden ist Streamlet für Green Ecolution, weil die geprüften Solver dieses Problem nicht abbilden konnten. Heute liegt es in einem eigenen Repository und hat keine Abhängigkeit zu Green Ecolution. Wer dasselbe Planungsproblem hat, kann es unverändert übernehmen.",
"repo": "Repository",
"repoAriaLabel": "Streamlet-Repository auf GitHub öffnen",
"docs": "Dokumentation",
"docsAriaLabel": "Dokumentation von Streamlet öffnen"
},
"problem": {
"sectionLabel": "Das Problem",
"title": "Reihenfolge und Nachfüllen hängen voneinander ab",
"coupling": "Sobald ein Fahrzeug unterwegs nachfüllen muss, hängt die beste Reihenfolge der Halte davon ab, wann und wo nachgefüllt wird. Beides zugleich zu entscheiden, ist der Kern des Problems.",
"workarounds": "Wer das dem Solver nicht mitgeben kann, hat zwei Möglichkeiten, und beide kosten Lösungsqualität: die Tour ohne das Nachfüllen planen und ein unzulässiges Ergebnis bekommen, oder die Tour von Hand in Teilstücke schneiden und damit die Optimierung über das Ganze aufgeben.",
"approach": "Streamlet plant die Nachfüllstationen im selben Optimierungslauf und rechnet sie nicht hinterher hinein. Bei Green Ecolution lief die Routenoptimierung vorher auf Vroom, das mehrere Nachfüllstationen mitten in der Tour nicht abbilden konnte.",
"diagram": {
"ariaLabel": "Diagramm einer Tour: Der Füllstand des Fahrzeugs sinkt an jedem Halt und wird an einer Nachfüllstation mitten in der Tour wieder aufgefüllt.",
"depot": "Depot",
"depotMarker": "D",
"refillStation": "Nachfüllstation",
"refillMarker": "R",
"tankFull": "Tank voll",
"tankEmpty": "leer",
"refilled": "nachgefüllt"
}
},
"fit": {
"sectionLabel": "Einsatzfeld",
"title": "Passt Streamlet zu deinem Problem?",
"description": "Streamlet ist nicht auf eine Branche zugeschnitten, entscheidend ist die Struktur der Tour. Je mehr dieser Merkmale auf deine Touren zutreffen, desto eher passt es.",
"criteria": {
"tankCapacity": "Fahrzeuge mit begrenzter Tankkapazität",
"timeWindows": "Halte mit Zeitfenstern",
"midTourRefill": "Nachfüllen oder Umladen mitten in der Tour",
"multipleStations": "Mehrere mögliche Nachfüllstationen",
"depotReturn": "Rückkehr zum Depot am Schichtende"
},
"cargo": {
"title": "Unabhängig vom Transportgut",
"description": "Streamlet kennt Tankkapazität und Bedarf, nicht Bewässerung. Was du transportierst, bleibt offen. Wasser ist der erste Anwendungsfall, die API setzt ihn an keiner Stelle voraus."
}
},
"api": {
"sectionLabel": "Schnittstelle",
"title": "Ein Aufruf, eine Lösung",
"description": "Streamlet arbeitet synchron. Du schickst das Problem, du bekommst die Routen zurück, ohne Job-Handling und ohne Polling dazwischen.",
"flow": {
"send": {
"label": "Problem senden",
"description": "Ein JSON-Body an POST /v1/solve beschreibt die Fahrzeuge mit Tankkapazität und Schichtzeitfenster, die Depots, die Halte mit Bedarf und Zeitfenster sowie die Nachfüllstationen."
},
"solve": {
"label": "Kosten holen und lösen",
"description": "Streamlet fragt die Reisezeit- und Distanzmatrix bei der Routing-Engine ab und löst das Problem im Solver."
},
"receive": {
"label": "Routen erhalten",
"description": "Die Antwort enthält für jede Route die Reihenfolge der Halte, Distanz, Fahrt- und Wartezeit sowie die Geometrie. Dazu kommt die Liste der Halte, die unbedient bleiben."
}
},
"endpoints": {
"solve": "Problem lösen und Routen zurückgeben",
"health": "Liveness-Prüfung",
"openapi": "OpenAPI-Schema der API"
},
"validation": "Streamlet prüft das Problem schon beim Einlesen. Eine Startladung über der Tankkapazität, ein Zeitfenster, das vor seinem Beginn endet, oder ein Problem ohne Fahrzeug wird mit 422 abgelehnt, statt später im Solver aufzulaufen. Fehlerantworten geben keine internen Details preis, weder Engine-URLs noch fremde Response-Bodies."
},
"solver": {
"sectionLabel": "Solver",
"title": "Nachvollziehbar statt zufällig",
"description": "Der Solver arbeitet in zwei Phasen und ohne Zufall. Solange das Zeitbudget nicht greift, ergibt dieselbe Eingabe dieselbe Ausgabe, das macht Ergebnisse reproduzierbar und Regressionen prüfbar.",
"stages": {
"construction": {
"label": "Konstruktion",
"description": "Cheapest Insertion baut die erste Lösung. Reicht der Tank für den nächsten Halt nicht, fügt der Solver davor die günstigste Nachfüllstation ein."
},
"localSearch": {
"label": "Lokale Suche",
"description": "Eine VND-artige Suche verbessert die Lösung. Zuerst laufen die günstigen Züge innerhalb einer Route, danach die teureren zwischen den Routen. Findet keiner davon mehr etwas, setzt sie alle Nachfüllbesuche neu und verwirft die, die sich nicht mehr lohnen."
}
},
"moveEvaluation": {
"label": "Load- und Duration-Segmente",
"description": "Kapazität und Zeitfenster prüft der Solver über Load- und Duration-Segmente nach Vidal et al. (2014). Sie lassen sich in konstanter Zeit zusammenführen, bewertet wird jeder Zug an der betroffenen Route."
},
"benchmarks": {
"title": "Gemessen an den Solomon-Instanzen",
"description": "Die Regressionstests lösen bekannte Instanzen des VRPTW, also Tourenplanung mit Zeitfenstern. Sie prüfen, ob die Lösung Kapazität und Zeitfenster einhält und ob sie nah genug am Optimum liegt. Nachfüllstationen enthalten diese Instanzen nicht, gemessen wird also die Grundqualität des Solvers.",
"gap": "{{value}}\u00a0%",
"footnote": "Abstand zur besten bekannten Lösung, Schwelle 5\u00a0%"
}
},
"tiles": {
"sectionLabel": "Kartendaten",
"title": "Das Straßennetz an deine Flotte anpassen",
"fleetMismatch": "Eine gute Route nützt nichts, wenn das Straßennetz nicht zur Flotte passt. Ein Betriebsfahrzeug darf über Wirtschafts- und Dienstwege fahren, die für den allgemeinen Verkehr gesperrt sind. Eine Standardkarte weiß das nicht und lässt Ziele unerreichbar aussehen, die es nicht sind.",
"patcher": "Die Routing-Engine rechnet auf Tiles, die aus einem OSM-Extrakt entstehen. Zum Streamlet-Repository gehört ein CLI, das dieses Extrakt vorher über OSC-Changesets verändert, also über Änderungsdateien im OSM-Format. Damit bildest du ab, was deine Fahrzeuge tatsächlich befahren dürfen.",
"status": {
"available": "Verfügbar",
"inProgress": "In Entwicklung"
},
"changesets": {
"construction": "Markiert Straßen, die wegen Baustellen gesperrt sind, als access=no. Routen führen dann nicht mehr durch die Sperrung.",
"allowedPaths": "Öffnet Wege für Kraftfahrzeuge, wenn ein Zielpunkt von keiner befahrbaren Straße aus erreichbar ist. Die Zielpunkte soll der Patcher aus der API von Green Ecolution lesen."
},
"disclaimer": "Der Patcher verändert nur dein Extrakt für deine Routing-Instanz. Er beeinflusst, was die Engine für deine Fahrzeuge als befahrbar ansieht. Eine Befahrungserlaubnis erteilt er damit nicht. Wer ihn einbindet, braucht dafür eine eigene Tile-Pipeline."
},
"operations": {
"sectionLabel": "Betrieb",
"title": "Einbauen und betreiben",
"description": "Streamlet ist ein einzelner Dienst neben deiner Anwendung. Jeder Stand von main wird als Container-Image veröffentlicht, konfiguriert wird es über Umgebungsvariablen. Dazu brauchst du eine erreichbare Routing-Engine.",
"facts": {
"stateless": {
"label": "Zustandslos",
"description": "Keine Datenbank, keine Job-Queue. Beim Herunterfahren laufen offene Anfragen aus, statt abzubrechen."
},
"swappableEngine": {
"label": "Engine austauschbar",
"description": "Die Routing-Engine hängt hinter dem Port-Trait Router. Implementiert ist Valhalla. Eine andere Engine lässt sich dahinter setzen, ohne den Solver zu ändern."
},
"separateCrate": {
"label": "Solver als separate Crate",
"description": "Der Solver liegt in der Crate streamlet-core, ohne tokio, axum und HTTP-Code. Dazu kommen nur Serialisierung und Fehlertypen."
},
"license": {
"label": "AGPL-3.0",
"description": "Der Quellcode steht unter der GNU Affero General Public License, Version 3. Wer Streamlet verändert und als Netzwerkdienst betreibt, muss den geänderten Quellcode dessen Nutzern anbieten."
}
}
},
"status": {
"sectionLabel": "Stand",
"title": "Was heute geht und was nicht",
"description": "Streamlet steht am Anfang und läuft bei Green Ecolution in der Einsatzplanung. Diese Punkte stehen hier, damit niemand sie erst beim Einbauen entdeckt.",
"limitations": {
"geometry": "Die Geometrie kommt als kodierte Polyline zurück. Die Option geojson verhält sich derzeit wie polyline.",
"depots": "Die API nimmt mehrere Depots entgegen, jede Route kehrt aber zum ersten zurück.",
"missingFeatures": "Es gibt keinen GPX-Endpoint und keine eingebettete Routing-Engine."
},
"cta": {
"title": "Mitmachen oder selbst einsetzen",
"descriptionRich": "Fragen, Fehlerberichte und Erfahrungen aus anderen Anwendungsfällen sind im Repository willkommen. Wie Streamlet bei Green Ecolution eingebunden ist, beschreibt <release>Release 0.4.0</release>.",
"repo": "Zum Repository",
"repoAriaLabel": "Streamlet-Repository auf GitHub öffnen",
"issues": "Issues",
"issuesAriaLabel": "Issues von Streamlet auf GitHub öffnen"
}
}
}
3 changes: 2 additions & 1 deletion src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"ariaLabel": "Main navigation",
"heading": "Main navigation",
"project": "The Project",
"streamlet": "Streamlet",
"releases": "Releases",
"contact": "Contact",
"github": "GitHub",
"tryNow": "Try it now",
"imprint": "Imprint",
"privacy": "Privacy Policy"
Expand Down Expand Up @@ -72,6 +72,7 @@
"breadcrumb": {
"home": "Home",
"project": "Project",
"streamlet": "Streamlet",
"contact": "Contact",
"imprint": "Imprint",
"privacy": "Privacy Policy"
Expand Down
3 changes: 2 additions & 1 deletion src/i18n/locales/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import home from './home.json'
import legal from './legal.json'
import project from './project.json'
import releases from './releases.json'
import streamlet from './streamlet.json'

export default { common, home, project, contact, releases, legal }
export default { common, home, project, streamlet, contact, releases, legal }
1 change: 1 addition & 0 deletions src/i18n/locales/en/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"sectionLabel": "System Architecture",
"title": "The path of the data",
"description": "This is how the readings travel from the tree to the optimized irrigation route through the Green Ecolution system.",
"streamletRich": "Route optimisation in the backend is handled by <streamlet>Streamlet</streamlet>, a separate open source project that can also be used outside Green Ecolution.",
"steps": {
"sensor": {
"label": "Sensor",
Expand Down
Loading