mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 12:29:40 +02:00
📝 docs(landing): add blueprints section for architecture decisions
Add new blueprints content collection and pages for documenting architecture decisions, technology strategies and long-term planning. - Add blueprints collection schema with status tracking (draft/proposal/accepted/implemented/superseded) - Create index and detail pages for blueprints at /blueprints - Add first blueprint: Mana Cluster & Federation Architecture - Add ADR-002 in docs/decisions for internal reference - Add nav.blueprints translation for all 5 languages - Add blueprints link to main navigation
This commit is contained in:
parent
d605366460
commit
d2f00c1d77
7 changed files with 1856 additions and 0 deletions
|
|
@ -22,6 +22,7 @@ const navLinks = [
|
|||
{ href: getLocalizedRoute('/pricing', lang), label: t('nav.pricing') },
|
||||
{ href: getLocalizedRoute('/clients', lang), label: t('nav.references') },
|
||||
{ href: getLocalizedRoute('/devlog', lang), label: t('nav.devlog') },
|
||||
{ href: getLocalizedRoute('/blueprints', lang), label: t('nav.blueprints') },
|
||||
{ href: getLocalizedRoute('/privacy', lang), label: t('nav.privacy') },
|
||||
];
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -157,6 +157,33 @@ const devlogCollection = defineCollection({
|
|||
}),
|
||||
});
|
||||
|
||||
const blueprintsCollection = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
date: z.date(),
|
||||
author: z.string().default('Till Schneider'),
|
||||
category: z.enum([
|
||||
'architecture',
|
||||
'infrastructure',
|
||||
'database',
|
||||
'security',
|
||||
'federation',
|
||||
'licensing',
|
||||
'business-model',
|
||||
]),
|
||||
status: z
|
||||
.enum(['draft', 'proposal', 'accepted', 'implemented', 'superseded'])
|
||||
.default('proposal'),
|
||||
tags: z.array(z.string()).optional(),
|
||||
featured: z.boolean().default(false),
|
||||
readTime: z.number().optional(),
|
||||
relatedBlueprints: z.array(z.string()).optional(),
|
||||
decisionDate: z.date().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
apps: appsCollection,
|
||||
branchen: targetGroupsCollection,
|
||||
|
|
@ -166,4 +193,5 @@ export const collections = {
|
|||
mission: missionCollection,
|
||||
context: contextCollection,
|
||||
devlog: devlogCollection,
|
||||
blueprints: blueprintsCollection,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export const ui = {
|
|||
'nav.references': 'Referenzen',
|
||||
'nav.privacy': 'Datenschutz',
|
||||
'nav.devlog': 'Devlog',
|
||||
'nav.blueprints': 'Blueprints',
|
||||
|
||||
// Buttons
|
||||
'button.startFree': 'Kostenlos testen',
|
||||
|
|
@ -115,6 +116,7 @@ export const ui = {
|
|||
'nav.references': 'References',
|
||||
'nav.privacy': 'Privacy',
|
||||
'nav.devlog': 'Devlog',
|
||||
'nav.blueprints': 'Blueprints',
|
||||
|
||||
// Buttons
|
||||
'button.startFree': 'Start for free',
|
||||
|
|
@ -194,6 +196,7 @@ export const ui = {
|
|||
'nav.references': 'Referenze',
|
||||
'nav.privacy': 'Privacy',
|
||||
'nav.devlog': 'Devlog',
|
||||
'nav.blueprints': 'Blueprints',
|
||||
|
||||
// Buttons
|
||||
'button.startFree': 'Prova gratuita',
|
||||
|
|
@ -276,6 +279,7 @@ export const ui = {
|
|||
'nav.references': 'Références',
|
||||
'nav.privacy': 'Confidentialité',
|
||||
'nav.devlog': 'Devlog',
|
||||
'nav.blueprints': 'Blueprints',
|
||||
|
||||
// Buttons
|
||||
'button.startFree': 'Essai gratuit',
|
||||
|
|
@ -360,6 +364,7 @@ export const ui = {
|
|||
'nav.references': 'Referencias',
|
||||
'nav.privacy': 'Privacidad',
|
||||
'nav.devlog': 'Devlog',
|
||||
'nav.blueprints': 'Blueprints',
|
||||
|
||||
// Buttons
|
||||
'button.startFree': 'Prueba gratuita',
|
||||
|
|
|
|||
344
apps/manacore/apps/landing/src/pages/blueprints/[slug].astro
Normal file
344
apps/manacore/apps/landing/src/pages/blueprints/[slug].astro
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Navbar from '../../components/navigation/Navbar.astro';
|
||||
import Footer from '../../components/navigation/Footer.astro';
|
||||
import Section from '../../components/content/Section.astro';
|
||||
import Container from '../../components/layout/Container.astro';
|
||||
import Heading from '../../components/typography/Heading.astro';
|
||||
import Text from '../../components/typography/Text.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blueprints = await getCollection('blueprints');
|
||||
return blueprints.map((blueprint) => ({
|
||||
params: { slug: blueprint.slug },
|
||||
props: { blueprint },
|
||||
}));
|
||||
}
|
||||
|
||||
const { blueprint } = Astro.props;
|
||||
const { Content } = await blueprint.render();
|
||||
|
||||
const categoryColors: Record<string, { bg: string; text: string; border: string; icon: string }> = {
|
||||
architecture: {
|
||||
bg: 'from-blue-500/10 to-cyan-500/10',
|
||||
text: 'text-blue-500',
|
||||
border: 'border-blue-500/30',
|
||||
icon: 'mdi:sitemap',
|
||||
},
|
||||
infrastructure: {
|
||||
bg: 'from-emerald-500/10 to-teal-500/10',
|
||||
text: 'text-emerald-500',
|
||||
border: 'border-emerald-500/30',
|
||||
icon: 'mdi:server-network',
|
||||
},
|
||||
database: {
|
||||
bg: 'from-amber-500/10 to-orange-500/10',
|
||||
text: 'text-amber-500',
|
||||
border: 'border-amber-500/30',
|
||||
icon: 'mdi:database',
|
||||
},
|
||||
security: {
|
||||
bg: 'from-red-500/10 to-rose-500/10',
|
||||
text: 'text-red-500',
|
||||
border: 'border-red-500/30',
|
||||
icon: 'mdi:shield-lock',
|
||||
},
|
||||
federation: {
|
||||
bg: 'from-purple-500/10 to-violet-500/10',
|
||||
text: 'text-purple-500',
|
||||
border: 'border-purple-500/30',
|
||||
icon: 'mdi:hub',
|
||||
},
|
||||
licensing: {
|
||||
bg: 'from-indigo-500/10 to-blue-500/10',
|
||||
text: 'text-indigo-500',
|
||||
border: 'border-indigo-500/30',
|
||||
icon: 'mdi:license',
|
||||
},
|
||||
'business-model': {
|
||||
bg: 'from-pink-500/10 to-fuchsia-500/10',
|
||||
text: 'text-pink-500',
|
||||
border: 'border-pink-500/30',
|
||||
icon: 'mdi:chart-timeline-variant',
|
||||
},
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
architecture: 'Architektur',
|
||||
infrastructure: 'Infrastruktur',
|
||||
database: 'Datenbank',
|
||||
security: 'Sicherheit',
|
||||
federation: 'Federation',
|
||||
licensing: 'Lizenzierung',
|
||||
'business-model': 'Geschäftsmodell',
|
||||
};
|
||||
|
||||
const statusColors: Record<string, { bg: string; text: string; icon: string }> = {
|
||||
draft: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-500', icon: 'mdi:pencil-outline' },
|
||||
proposal: {
|
||||
bg: 'bg-yellow-100 dark:bg-yellow-900/30',
|
||||
text: 'text-yellow-600 dark:text-yellow-400',
|
||||
icon: 'mdi:lightbulb-outline',
|
||||
},
|
||||
accepted: {
|
||||
bg: 'bg-green-100 dark:bg-green-900/30',
|
||||
text: 'text-green-600 dark:text-green-400',
|
||||
icon: 'mdi:check-circle-outline',
|
||||
},
|
||||
implemented: {
|
||||
bg: 'bg-blue-100 dark:bg-blue-900/30',
|
||||
text: 'text-blue-600 dark:text-blue-400',
|
||||
icon: 'mdi:rocket-launch-outline',
|
||||
},
|
||||
superseded: {
|
||||
bg: 'bg-gray-100 dark:bg-gray-800',
|
||||
text: 'text-gray-400',
|
||||
icon: 'mdi:archive-outline',
|
||||
},
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
draft: 'Entwurf',
|
||||
proposal: 'Vorschlag',
|
||||
accepted: 'Akzeptiert',
|
||||
implemented: 'Implementiert',
|
||||
superseded: 'Ersetzt',
|
||||
};
|
||||
|
||||
const colors = categoryColors[blueprint.data.category] || categoryColors.architecture;
|
||||
const status = statusColors[blueprint.data.status] || statusColors.draft;
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
weekday: 'long',
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}).format(date);
|
||||
};
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={`${blueprint.data.title} - ManaCore Blueprints`}
|
||||
description={blueprint.data.description}
|
||||
>
|
||||
<div
|
||||
class="bg-gradient-to-b from-blue-50/30 via-white to-blue-50/30 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900 min-h-screen"
|
||||
>
|
||||
<Navbar />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="relative pt-24 pb-12">
|
||||
<div class="absolute inset-0">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-blue-50/50 to-transparent dark:from-gray-900 dark:to-transparent"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 right-0 w-96 h-96 bg-purple-500/10 dark:bg-purple-500/5 rounded-full blur-3xl"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 w-96 h-96 bg-blue-500/10 dark:bg-blue-500/5 rounded-full blur-3xl"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Container class="relative z-10">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Back link -->
|
||||
<a
|
||||
href="/blueprints"
|
||||
class="inline-flex items-center gap-2 text-gray-500 hover:text-purple-500 transition-colors mb-8"
|
||||
>
|
||||
<Icon name="mdi:arrow-left" class="w-4 h-4" />
|
||||
<Text size="sm">Zurück zu Blueprints</Text>
|
||||
</a>
|
||||
|
||||
<!-- Meta info -->
|
||||
<div class="flex flex-wrap items-center gap-4 mb-6">
|
||||
<!-- Status Badge -->
|
||||
<span
|
||||
class={`inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-sm font-medium ${status.bg} ${status.text}`}
|
||||
>
|
||||
<Icon name={status.icon} class="w-4 h-4" />
|
||||
{statusLabels[blueprint.data.status]}
|
||||
</span>
|
||||
|
||||
<!-- Category Badge -->
|
||||
<span
|
||||
class={`inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full text-sm font-medium bg-gradient-to-r ${colors.bg} ${colors.text} border ${colors.border}`}
|
||||
>
|
||||
<Icon name={colors.icon} class="w-4 h-4" />
|
||||
{categoryLabels[blueprint.data.category]}
|
||||
</span>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||
<Icon name="mdi:calendar" class="w-5 h-5" />
|
||||
<Text size="base">{formatDate(blueprint.data.date)}</Text>
|
||||
</div>
|
||||
|
||||
<!-- Read Time -->
|
||||
{
|
||||
blueprint.data.readTime && (
|
||||
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||
<Icon name="mdi:clock-outline" class="w-5 h-5" />
|
||||
<Text size="base">{blueprint.data.readTime} min Lesezeit</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Decision Date if different from date -->
|
||||
{
|
||||
blueprint.data.decisionDate && (
|
||||
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400 mb-6">
|
||||
<Icon name="mdi:gavel" class="w-5 h-5" />
|
||||
<Text size="sm">
|
||||
Entscheidung getroffen am: {formatDate(blueprint.data.decisionDate)}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Title -->
|
||||
<Heading as="h1" size="1" class="mb-6">
|
||||
{blueprint.data.title}
|
||||
</Heading>
|
||||
|
||||
<!-- Description -->
|
||||
<Text size="xl" class="text-gray-600 dark:text-gray-400 mb-8">
|
||||
{blueprint.data.description}
|
||||
</Text>
|
||||
|
||||
<!-- Author -->
|
||||
<div class="flex items-center gap-3 pb-8 border-b border-gray-200 dark:border-gray-700">
|
||||
<div
|
||||
class="w-10 h-10 bg-gradient-to-br from-purple-500 to-blue-600 rounded-full flex items-center justify-center text-white font-bold"
|
||||
>
|
||||
{blueprint.data.author.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<Text weight="semibold">{blueprint.data.author}</Text>
|
||||
<Text size="sm" class="text-gray-500">Autor</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<Section spacing="large" class="relative">
|
||||
<Container class="relative z-10">
|
||||
<article
|
||||
class="max-w-4xl mx-auto prose prose-lg dark:prose-invert prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-600 dark:prose-p:text-gray-300 prose-a:text-purple-500 prose-a:no-underline hover:prose-a:underline prose-strong:text-gray-900 dark:prose-strong:text-white prose-code:text-purple-500 prose-code:bg-gray-100 dark:prose-code:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-700 prose-table:border-collapse prose-th:bg-gray-100 dark:prose-th:bg-gray-800 prose-th:border prose-th:border-gray-300 dark:prose-th:border-gray-600 prose-th:px-4 prose-th:py-2 prose-td:border prose-td:border-gray-300 dark:prose-td:border-gray-600 prose-td:px-4 prose-td:py-2 prose-hr:border-gray-200 dark:prose-hr:border-gray-700"
|
||||
>
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<!-- Tags -->
|
||||
{
|
||||
blueprint.data.tags && blueprint.data.tags.length > 0 && (
|
||||
<div class="max-w-4xl mx-auto mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<Text size="sm" weight="semibold" class="mb-4 text-gray-500">
|
||||
Tags
|
||||
</Text>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{blueprint.data.tags.map((tag: string) => (
|
||||
<span class="px-3 py-1.5 text-sm rounded-lg bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-700">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Related Blueprints -->
|
||||
{
|
||||
blueprint.data.relatedBlueprints && blueprint.data.relatedBlueprints.length > 0 && (
|
||||
<div class="max-w-4xl mx-auto mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<Text size="sm" weight="semibold" class="mb-4 text-gray-500">
|
||||
Verwandte Blueprints
|
||||
</Text>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{blueprint.data.relatedBlueprints.map((related: string) => (
|
||||
<a
|
||||
href={`/blueprints/${related}`}
|
||||
class="px-3 py-1.5 text-sm rounded-lg bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400 border border-purple-200 dark:border-purple-700 hover:bg-purple-100 dark:hover:bg-purple-900/40 transition-colors"
|
||||
>
|
||||
{related}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Navigation -->
|
||||
<div class="max-w-4xl mx-auto mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<a
|
||||
href="/blueprints"
|
||||
class="inline-flex items-center gap-2 px-6 py-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-purple-500 transition-colors"
|
||||
>
|
||||
<Icon name="mdi:arrow-left" class="w-5 h-5" />
|
||||
<Text weight="semibold">Alle Blueprints</Text>
|
||||
</a>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Additional prose customizations */
|
||||
:global(.prose h2) {
|
||||
margin-top: 2.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(156, 163, 175, 0.2);
|
||||
}
|
||||
|
||||
:global(.prose h3) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
:global(.prose h4) {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
:global(.prose table) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(.prose ul) {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
:global(.prose li::marker) {
|
||||
color: #a855f7;
|
||||
}
|
||||
|
||||
/* Code blocks with better scrolling */
|
||||
:global(.prose pre) {
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
:global(.prose pre code) {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* ASCII art boxes styling */
|
||||
:global(.prose pre code) {
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', monospace;
|
||||
font-size: 0.85em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
271
apps/manacore/apps/landing/src/pages/blueprints/index.astro
Normal file
271
apps/manacore/apps/landing/src/pages/blueprints/index.astro
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
---
|
||||
import Layout from '../../layouts/Layout.astro';
|
||||
import Navbar from '../../components/navigation/Navbar.astro';
|
||||
import Footer from '../../components/navigation/Footer.astro';
|
||||
import Section from '../../components/content/Section.astro';
|
||||
import Container from '../../components/layout/Container.astro';
|
||||
import Heading from '../../components/typography/Heading.astro';
|
||||
import Text from '../../components/typography/Text.astro';
|
||||
import HeroSection from '../../components/content/HeroSection.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
const blueprints = await getCollection('blueprints');
|
||||
const sortedBlueprints = blueprints.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
|
||||
const categoryColors: Record<string, { bg: string; text: string; border: string; icon: string }> = {
|
||||
architecture: {
|
||||
bg: 'from-blue-500/10 to-cyan-500/10',
|
||||
text: 'text-blue-500',
|
||||
border: 'border-blue-500/30',
|
||||
icon: 'mdi:sitemap',
|
||||
},
|
||||
infrastructure: {
|
||||
bg: 'from-emerald-500/10 to-teal-500/10',
|
||||
text: 'text-emerald-500',
|
||||
border: 'border-emerald-500/30',
|
||||
icon: 'mdi:server-network',
|
||||
},
|
||||
database: {
|
||||
bg: 'from-amber-500/10 to-orange-500/10',
|
||||
text: 'text-amber-500',
|
||||
border: 'border-amber-500/30',
|
||||
icon: 'mdi:database',
|
||||
},
|
||||
security: {
|
||||
bg: 'from-red-500/10 to-rose-500/10',
|
||||
text: 'text-red-500',
|
||||
border: 'border-red-500/30',
|
||||
icon: 'mdi:shield-lock',
|
||||
},
|
||||
federation: {
|
||||
bg: 'from-purple-500/10 to-violet-500/10',
|
||||
text: 'text-purple-500',
|
||||
border: 'border-purple-500/30',
|
||||
icon: 'mdi:hub',
|
||||
},
|
||||
licensing: {
|
||||
bg: 'from-indigo-500/10 to-blue-500/10',
|
||||
text: 'text-indigo-500',
|
||||
border: 'border-indigo-500/30',
|
||||
icon: 'mdi:license',
|
||||
},
|
||||
'business-model': {
|
||||
bg: 'from-pink-500/10 to-fuchsia-500/10',
|
||||
text: 'text-pink-500',
|
||||
border: 'border-pink-500/30',
|
||||
icon: 'mdi:chart-timeline-variant',
|
||||
},
|
||||
};
|
||||
|
||||
const categoryLabels: Record<string, string> = {
|
||||
architecture: 'Architektur',
|
||||
infrastructure: 'Infrastruktur',
|
||||
database: 'Datenbank',
|
||||
security: 'Sicherheit',
|
||||
federation: 'Federation',
|
||||
licensing: 'Lizenzierung',
|
||||
'business-model': 'Geschäftsmodell',
|
||||
};
|
||||
|
||||
const statusColors: Record<string, { bg: string; text: string }> = {
|
||||
draft: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-500' },
|
||||
proposal: {
|
||||
bg: 'bg-yellow-100 dark:bg-yellow-900/30',
|
||||
text: 'text-yellow-600 dark:text-yellow-400',
|
||||
},
|
||||
accepted: { bg: 'bg-green-100 dark:bg-green-900/30', text: 'text-green-600 dark:text-green-400' },
|
||||
implemented: { bg: 'bg-blue-100 dark:bg-blue-900/30', text: 'text-blue-600 dark:text-blue-400' },
|
||||
superseded: { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-400 line-through' },
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
draft: 'Entwurf',
|
||||
proposal: 'Vorschlag',
|
||||
accepted: 'Akzeptiert',
|
||||
implemented: 'Implementiert',
|
||||
superseded: 'Ersetzt',
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
}).format(date);
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title="Blueprints - ManaCore Architektur-Entscheidungen">
|
||||
<div
|
||||
class="bg-gradient-to-b from-blue-50/30 via-white to-blue-50/30 dark:from-gray-900 dark:via-gray-900 dark:to-gray-900"
|
||||
>
|
||||
<Navbar />
|
||||
|
||||
<!-- Hero Section -->
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 -bottom-32">
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-blue-50/50 to-transparent dark:from-gray-900 dark:to-transparent"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 right-0 w-96 h-96 bg-purple-500/10 dark:bg-purple-500/5 rounded-full blur-3xl"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 w-96 h-96 bg-blue-500/10 dark:bg-blue-500/5 rounded-full blur-3xl"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HeroSection
|
||||
title="Blueprints"
|
||||
subtitle="Architektur-Entscheidungen, Technologie-Strategien und langfristige Planung. Transparente Dokumentation unserer technischen Vision."
|
||||
background="none"
|
||||
minHeight="small"
|
||||
spacing="small"
|
||||
containerClass="py-16 relative z-10"
|
||||
centered={true}
|
||||
debug={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Blueprints Grid -->
|
||||
<Section spacing="xlarge" class="relative">
|
||||
<div class="absolute inset-0 -top-32 -bottom-32"></div>
|
||||
<div
|
||||
class="absolute top-0 left-1/4 w-96 h-96 bg-purple-500/15 dark:bg-purple-500/5 rounded-full blur-3xl"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-0 right-1/4 w-96 h-96 bg-blue-500/15 dark:bg-blue-500/5 rounded-full blur-3xl"
|
||||
>
|
||||
</div>
|
||||
|
||||
<Container class="relative z-10">
|
||||
<!-- Legend -->
|
||||
<div class="max-w-4xl mx-auto mb-12">
|
||||
<div
|
||||
class="bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm rounded-xl p-6 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<Text size="sm" weight="semibold" class="mb-4 text-gray-500">Status-Legende</Text>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{
|
||||
Object.entries(statusLabels).map(([key, label]) => (
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class={`px-2 py-0.5 rounded text-xs font-medium ${statusColors[key].bg} ${statusColors[key].text}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-4xl mx-auto space-y-8">
|
||||
{
|
||||
sortedBlueprints.map((blueprint) => {
|
||||
const colors = categoryColors[blueprint.data.category] || categoryColors.architecture;
|
||||
const status = statusColors[blueprint.data.status] || statusColors.draft;
|
||||
return (
|
||||
<article class="group relative">
|
||||
<div
|
||||
class={`absolute -inset-0.5 bg-gradient-to-r ${colors.bg} rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300 blur`}
|
||||
/>
|
||||
<a
|
||||
href={`/blueprints/${blueprint.slug}`}
|
||||
class="block relative bg-white/90 backdrop-blur-sm dark:bg-gray-800/90 rounded-2xl p-8 border border-gray-200 dark:border-gray-700 group-hover:border-purple-500/50 transition-all duration-300"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||
<span
|
||||
class={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${status.bg} ${status.text}`}
|
||||
>
|
||||
{statusLabels[blueprint.data.status]}
|
||||
</span>
|
||||
|
||||
<span
|
||||
class={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r ${colors.bg} ${colors.text} border ${colors.border}`}
|
||||
>
|
||||
<Icon name={colors.icon} class="w-3.5 h-3.5" />
|
||||
{categoryLabels[blueprint.data.category]}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-400">
|
||||
<Icon name="mdi:calendar" class="w-4 h-4" />
|
||||
<Text size="sm">{formatDate(blueprint.data.date)}</Text>
|
||||
</div>
|
||||
|
||||
{blueprint.data.readTime && (
|
||||
<div class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
||||
<Icon name="mdi:clock-outline" class="w-4 h-4" />
|
||||
<Text size="sm">{blueprint.data.readTime} min</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Heading
|
||||
as="h2"
|
||||
size="4"
|
||||
class="mb-3 group-hover:text-purple-500 transition-colors"
|
||||
>
|
||||
{blueprint.data.title}
|
||||
</Heading>
|
||||
|
||||
<Text size="base" class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
{blueprint.data.description}
|
||||
</Text>
|
||||
|
||||
{blueprint.data.tags && blueprint.data.tags.length > 0 && (
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{blueprint.data.tags.slice(0, 6).map((tag: string) => (
|
||||
<span class="px-2 py-1 text-xs rounded-md bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
{blueprint.data.tags.length > 6 && (
|
||||
<span class="px-2 py-1 text-xs rounded-md bg-gray-100 dark:bg-gray-700 text-gray-500">
|
||||
+{blueprint.data.tags.length - 6} mehr
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="mt-6 flex items-center text-purple-500 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Text size="sm" weight="semibold">
|
||||
Blueprint lesen
|
||||
</Text>
|
||||
<Icon
|
||||
name="mdi:arrow-right"
|
||||
class="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
sortedBlueprints.length === 0 && (
|
||||
<div class="text-center py-16">
|
||||
<div class="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-purple-500/10 to-blue-500/10 rounded-2xl mb-4">
|
||||
<Icon name="mdi:file-document-outline" class="w-8 h-8 text-purple-500" />
|
||||
</div>
|
||||
<Heading as="h2" size="4" class="mb-2">
|
||||
Noch keine Blueprints
|
||||
</Heading>
|
||||
<Text class="text-gray-500">Hier erscheinen bald Architektur-Dokumentationen.</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Container>
|
||||
</Section>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</Layout>
|
||||
158
docs/decisions/002-cluster-federation-architecture.md
Normal file
158
docs/decisions/002-cluster-federation-architecture.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# ADR-002: Mana Cluster & Federation Architecture
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-01-31
|
||||
**Author:** Till Schneider
|
||||
**Category:** Architecture, Infrastructure
|
||||
|
||||
## Context
|
||||
|
||||
Die gesamte Mana-Infrastruktur läuft aktuell auf einem einzigen Mac Mini M4. Dies stellt ein erhebliches Risiko dar:
|
||||
|
||||
- **Single Point of Failure:** Fällt der Mac Mini aus, sind alle Services offline
|
||||
- **Keine Daten-Redundanz:** Hardware-Defekt kann zu Datenverlust führen
|
||||
- **Keine Skalierungsmöglichkeit:** Ressourcen auf ein Gerät beschränkt
|
||||
- **Kein B2B-Support:** Keine Möglichkeit für Kunden, eigene Instanzen zu betreiben
|
||||
|
||||
## Decision
|
||||
|
||||
Wir implementieren ein selbstheilendes, dezentrales Multi-Node-Cluster mit folgenden Technologie-Entscheidungen:
|
||||
|
||||
### 1. Orchestrierung: K3s (Kubernetes)
|
||||
|
||||
**Gewählt:** K3s
|
||||
**Abgelehnt:** Nomad (HashiCorp), Docker Swarm
|
||||
|
||||
**Begründung:**
|
||||
- Apache 2.0 Lizenz (echtes Open Source)
|
||||
- CNCF-Governance (neutral, community-driven)
|
||||
- Industriestandard mit riesigem Ecosystem
|
||||
- Native HA-Unterstützung
|
||||
- ARM64 + x86_64 Support
|
||||
|
||||
**Kritischer Faktor:** HashiCorp wechselte im August 2023 zu BSL (Business Source License). Dies ist keine Open-Source-Lizenz und birgt langfristige Risiken für kommerzielle Nutzung.
|
||||
|
||||
### 2. Datenbank: YugabyteDB
|
||||
|
||||
**Gewählt:** YugabyteDB
|
||||
**Abgelehnt:** CockroachDB, TiDB
|
||||
|
||||
**Begründung:**
|
||||
- Apache 2.0 Lizenz (keine Feature-Gating wie bei CockroachDB)
|
||||
- 99% PostgreSQL-Kompatibilität (Drizzle ORM funktioniert ohne Änderungen)
|
||||
- Geo-Distribution ohne Enterprise-Lizenz
|
||||
- Trigger und Stored Procedures unterstützt
|
||||
|
||||
**Migration:** Bestehende PostgreSQL-Datenbanken können via pg_dump/ysqlsh migriert werden.
|
||||
|
||||
### 3. Mesh Networking: Headscale
|
||||
|
||||
**Gewählt:** Headscale (self-hosted Tailscale)
|
||||
**Abgelehnt:** ZeroTier (BSL), natives WireGuard
|
||||
|
||||
**Begründung:**
|
||||
- BSD-Lizenz
|
||||
- WireGuard-basiert (modern, schnell, sicher)
|
||||
- Zero-Config für neue Nodes
|
||||
- Exzellentes NAT Traversal
|
||||
|
||||
### 4. Distributed Computing: Ray
|
||||
|
||||
**Gewählt:** Ray
|
||||
**Begründung:**
|
||||
- Apache 2.0 Lizenz
|
||||
- Python-native (passt zu ML-Workloads)
|
||||
- Einfache Task-Distribution
|
||||
- Integration mit Ollama möglich
|
||||
|
||||
### 5. Federated Learning: Flower
|
||||
|
||||
**Gewählt:** Flower
|
||||
**Begründung:**
|
||||
- Apache 2.0 Lizenz
|
||||
- Einfache Integration
|
||||
- Privacy-by-Design
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **High Availability:** Automatisches Failover bei Node-Ausfall
|
||||
2. **Keine Vendor Lock-in:** Alle Kernkomponenten echtes Open Source
|
||||
3. **Skalierbarkeit:** Von 2 Nodes bis zu global verteilten Clustern
|
||||
4. **B2B-Ready:** Kunden können isolierte oder föderierte Instanzen betreiben
|
||||
5. **Heterogene Hardware:** Mac, Linux, Windows, Raspberry Pi im selben Cluster
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Komplexität:** Kubernetes hat steilere Lernkurve als Docker Compose
|
||||
2. **Migration:** Bestehende docker-compose.yml muss zu K8s Manifests konvertiert werden
|
||||
3. **Ressourcen:** K3s benötigt ~500MB RAM pro Node (mehr als Nomad)
|
||||
|
||||
### Neutral
|
||||
|
||||
1. **LISTEN/NOTIFY:** Nicht von YugabyteDB unterstützt, muss auf Redis Pub/Sub migriert werden (falls verwendet)
|
||||
|
||||
## B2B Deployment Tiers
|
||||
|
||||
| Tier | Name | Beschreibung | Datenisolation |
|
||||
|------|------|--------------|----------------|
|
||||
| 1 | Isolated | Komplett eigenes Cluster, keine Verbindung zu Mana | 100% |
|
||||
| 2 | Federated Compute | Eigene DB, kann anonymisierte Compute-Tasks ausführen | 100% |
|
||||
| 3 | Federated Learning | Wie Tier 2, plus lokales ML-Training mit Gradienten-Sharing | 100% |
|
||||
| 4 | Full Federation | Maximum Sharing mit Differential Privacy und ZK-Proofs | 100% |
|
||||
|
||||
## Open-Source Stack
|
||||
|
||||
```
|
||||
Orchestration: K3s (Apache 2.0)
|
||||
Mesh Networking: Headscale (BSD)
|
||||
Database: YugabyteDB (Apache 2.0)
|
||||
Object Storage: MinIO (AGPL 3.0)
|
||||
LLM Runtime: Ollama (MIT)
|
||||
Distributed AI: Ray (Apache 2.0)
|
||||
Federated ML: Flower (Apache 2.0)
|
||||
Secrets: Infisical (MIT)
|
||||
IaC: OpenTofu (MPL 2.0)
|
||||
Monitoring: VictoriaMetrics (Apache 2.0)
|
||||
```
|
||||
|
||||
## Vermiedene Technologien (BSL/SSPL)
|
||||
|
||||
- HashiCorp: Terraform, Vault, Nomad, Consul
|
||||
- CockroachDB
|
||||
- MongoDB
|
||||
- Elasticsearch
|
||||
- ZeroTier
|
||||
|
||||
## Hardware-Empfehlungen
|
||||
|
||||
### Mana Central (Minimal HA)
|
||||
- 2x Mac Mini M4 (16GB RAM, 512GB SSD)
|
||||
- Kosten: ~2.400€
|
||||
|
||||
### Mana Central (Empfohlen)
|
||||
- 2x Mac Mini M4 Pro (24GB RAM)
|
||||
- 1x Raspberry Pi 5 (Tiebreaker)
|
||||
- Kosten: ~4.000€
|
||||
|
||||
## Migrations-Roadmap
|
||||
|
||||
1. **Phase 1 (Woche 1-2):** Zweiter Mac Mini, K3s HA Cluster, Headscale
|
||||
2. **Phase 2 (Woche 3-4):** PostgreSQL → YugabyteDB Migration
|
||||
3. **Phase 3 (Woche 5-6):** Ray Cluster für verteilte AI-Workloads
|
||||
4. **Phase 4 (Woche 7-8):** B2B Deployment Kit (Helm Charts)
|
||||
5. **Phase 5 (Woche 9-12):** Federation Features (Tier 2-4)
|
||||
|
||||
## References
|
||||
|
||||
- [K3s Documentation](https://docs.k3s.io/)
|
||||
- [YugabyteDB Documentation](https://docs.yugabyte.com/)
|
||||
- [Headscale GitHub](https://github.com/juanfont/headscale)
|
||||
- [Ray Documentation](https://docs.ray.io/)
|
||||
- [Flower Documentation](https://flower.dev/docs/)
|
||||
- [HashiCorp BSL Announcement](https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license)
|
||||
|
||||
## Full Documentation
|
||||
|
||||
Siehe auch: `apps/manacore/apps/landing/src/content/blueprints/001-mana-cluster-federation-architecture.md` für die ausführliche Version mit Diagrammen.
|
||||
Loading…
Add table
Add a link
Reference in a new issue