bstein-dev-home/frontend/src/components/PlatformSection.vue
2026-06-29 13:23:07 -03:00

919 lines
23 KiB
Vue

<template>
<section id="platform" class="platform-section" aria-labelledby="platform-title">
<div class="section-kicker mono">Live technical platform</div>
<div class="section-heading">
<div>
<h2 id="platform-title">Titan Lab: Live Platform</h2>
<p>
Titan Lab is the live infrastructure behind bstein.dev. It demonstrates the same disciplines I apply
professionally: automation, testing, GitOps delivery, identity, observability, service operations, and
infrastructure troubleshooting.
</p>
</div>
<div class="section-actions">
<a class="section-link mono" href="https://metrics.bstein.dev" target="_blank" rel="noopener noreferrer">
Public metrics
</a>
<a class="section-link mono" href="https://scm.bstein.dev/bstein/titan-iac" target="_blank" rel="noopener noreferrer">
IaC source
</a>
</div>
</div>
<div class="platform-dashboard glass">
<div class="dashboard-copy">
<p class="eyebrow">Titan Lab</p>
<h3>Atlas services and production-minded operations</h3>
<p class="lede">
Atlas runs the member platform, service catalog, identity, delivery automation, observability, source control,
container delivery, storage, and onboarding workflows that support bstein.dev.
</p>
<div class="bullets">
<div :class="['pill', 'mono', atlasPillClass]">Atlas: flux-managed k3s</div>
<div class="pill mono">Public metrics</div>
<div class="pill mono">Member platform</div>
</div>
<div class="live-status" aria-live="polite">
<div v-if="error" class="status">{{ error }}</div>
<div v-else class="status mono">
{{ loading ? "Loading..." : labStatus?.connected ? "Live data connected" : "Live data unavailable" }}
</div>
<p v-if="error || (!loading && !labStatus?.connected)" class="fallback-copy">
Live status is temporarily unavailable. The platform overview remains available below.
</p>
</div>
</div>
<div class="availability dashboard-panel">
<div class="availability-grid">
<div class="availability-panel">
<div class="panel-title availability-title">
<div>
<h4>Atlas availability</h4>
<p class="panel-subtitle mono">k3s cluster</p>
</div>
</div>
<iframe
title="Atlas public availability panel"
src="https://metrics.bstein.dev/d-solo/atlas-overview/atlas-overview?from=now-24h&to=now&refresh=1m&theme=dark&panelId=27&__feature.dashboardSceneSolo"
width="100%"
height="180"
frameborder="0"
loading="lazy"
></iframe>
</div>
<div class="availability-panel">
<div class="panel-title availability-title">
<div>
<h4>Dedicated host availability</h4>
<p :class="['panel-subtitle', 'mono', dedicatedHostsPillClass]">{{ dedicatedHostsLabel }}</p>
</div>
</div>
<div class="host-status-list">
<div v-for="host in dedicatedHostItems" :key="host.label" class="host-status-row">
<span class="host-dot" :class="host.state" aria-hidden="true"></span>
<div>
<div class="host-label">{{ host.label }}</div>
<div class="host-value mono">{{ host.value }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<MetricRow class="dashboard-metrics" :items="metricItems" />
</div>
</section>
<section class="platform-subsection platform-services-section" aria-labelledby="platform-services-title">
<div class="section-heading platform-subheading">
<div>
<p class="section-kicker mono">Live service surface</p>
<h2 id="platform-services-title">Service Overview</h2>
<p>
A compact surface of public and member-facing services that shows how Titan Lab is delivered, routed,
observed, and maintained as a running platform.
</p>
</div>
</div>
<div class="platform-card card">
<div class="section-head">
<h3>Selected Services</h3>
<span class="pill mono">identity + ci/cd + registry + observability + product services</span>
</div>
<ServiceGrid :services="featuredServices" />
</div>
</section>
<section class="platform-subsection platform-architecture-section" aria-labelledby="platform-architecture-title">
<div class="section-heading platform-subheading">
<div>
<p class="section-kicker mono">Architecture</p>
<h2 id="platform-architecture-title">How It All Fits Together</h2>
<p>
A high-level operating map for public edge traffic, Kubernetes services, delivery flow, and non-cluster host
responsibilities. Internal addresses, credentials, and member-only details stay private.
</p>
</div>
</div>
<div class="architecture-board card" aria-label="Titan Lab operating map">
<div class="board-head">
<div>
<h3>Atlas operating map</h3>
<p>Public edge, Kubernetes services, delivery machinery, and the two dedicated hosts that sit outside the cluster.</p>
</div>
<span class="pill mono">public abstraction</span>
</div>
<div class="architecture-map">
<div class="map-column edge-column">
<div class="map-node edge-node">
<span class="map-label mono">public edge</span>
<strong>DNS / TLS / Traefik</strong>
<p>Routes public pages, safe dashboards, and auth-gated member services.</p>
</div>
<div class="map-node access-node">
<span class="map-label mono">access</span>
<strong>Keycloak + OAuth gates</strong>
<p>OIDC, SSO, account flows, request access, verification, and service boundaries.</p>
</div>
</div>
<div class="map-arrow" aria-hidden="true">-&gt;</div>
<div class="map-node cluster-node">
<span class="map-label mono">atlas k3s</span>
<strong>Flux-managed service platform</strong>
<div class="node-grid" aria-label="Atlas cluster layers and operating systems">
<span>
<b>Control plane</b>
HA k3s backed by the dedicated database node.
</span>
<span>
<b>Workloads</b>
Member apps, source control, CI, registry, media, and AI services.
</span>
<span>
<b>State + storage</b>
Longhorn volumes, storage pools, and PVC-backed workloads.
</span>
<span>
<b>Secrets + identity</b>
Vault, Keycloak, OAuth gates, and scoped configuration.
</span>
<span>
<b>Observability</b>
Prometheus, Grafana, OpenSearch, and Fluent Bit logs.
</span>
<span>
<b>Backups + restore</b>
Backup policies, restore checks, freshness reports, and recovery paths.
</span>
<span class="delivery-layer">
<b>Delivery</b>
<span class="delivery-chain mono">Gitea -> Jenkins -> Harbor -> Flux</span>
</span>
</div>
</div>
<div class="map-arrow" aria-hidden="true">&lt;-&gt;</div>
<div class="map-column ops-column">
<div class="map-node ops-node">
<span class="map-label mono">operations</span>
<strong>Operator tools + recovery</strong>
<p>Remote control, backup status, node rebuild paths, restore checks, and operational reports.</p>
</div>
<div class="map-node dedicated-node">
<span class="map-label mono">outside cluster</span>
<strong>Dedicated hosts</strong>
<p>Database node plus jumphost / KVM landing host for operations and controlled access.</p>
</div>
</div>
</div>
</div>
<details class="source-diagrams">
<summary>
<span>
<strong>Source diagrams</strong>
<small>Mermaid views for full-screen inspection.</small>
</span>
<span class="pill mono">Open diagrams</span>
</summary>
<div class="platform-diagrams" aria-label="Titan Lab architecture diagrams">
<MermaidCard
class="architecture-primary"
title="Atlas layout"
description="Control plane, workers, accelerators, and edge assets."
:diagram="hardwareDiagram"
card-id="hardware-home"
/>
<MermaidCard
title="Build + deploy flow"
description="Gitea to Jenkins to Harbor to Flux."
:diagram="pipelineDiagram"
card-id="pipeline-home"
/>
<MermaidCard
title="Ingress flow"
description="DNS to Traefik to workloads backed by Longhorn."
:diagram="networkDiagram"
card-id="network-home"
/>
</div>
</details>
</section>
</template>
<script setup>
import { computed } from "vue";
import MetricRow from "./MetricRow.vue";
import ServiceGrid from "./ServiceGrid.vue";
import MermaidCard from "./MermaidCard.vue";
import { buildHardwareDiagram, buildNetworkDiagram, buildPipelineDiagram, fallbackServices } from "../data/sample.js";
const props = defineProps({
labData: Object,
labStatus: Object,
serviceData: Object,
networkData: Object,
metricsData: Object,
loading: Boolean,
error: String,
});
const atlasPillClass = computed(() => (props.labStatus?.atlas?.up ? "pill-ok" : "pill-bad"));
const dedicatedHosts = computed(() => props.labStatus?.dedicated_hosts || null);
const dedicatedHostItems = computed(() => {
const hosts = dedicatedHosts.value?.hosts?.length
? dedicatedHosts.value.hosts
: [
{ label: "Database node", known: false, up: false },
{ label: "Jumphost", known: false, up: false },
];
return hosts.map((host) => {
const state = hostState(host);
return {
label: host.label || "Dedicated host",
state,
value: statusLabelFrom(state),
};
});
});
const dedicatedHostsPillClass = computed(() => {
if (!dedicatedHosts.value?.known) return "";
return dedicatedHosts.value.up ? "pill-ok" : "pill-bad";
});
const dedicatedHostsLabel = computed(() => {
if (!dedicatedHosts.value?.known) return "status pending";
const total = Number.isFinite(dedicatedHosts.value.total) ? dedicatedHosts.value.total : 2;
const upCount = Number.isFinite(dedicatedHosts.value.up_count) ? dedicatedHosts.value.up_count : 0;
return `${upCount}/${total} responding`;
});
const metricItems = computed(() => {
const items = props.metricsData?.items?.length
? props.metricsData.items
: [
{
label: "Lab nodes",
value: "26",
note: "Atlas includes the main service fleet plus Oceanus and Tethys as cluster members.\nDedicated non-cluster hosts: titan-db and titan-jh.",
},
{
label: "CPU cores",
value: "142",
note: "Mixed arm64 and amd64 capacity for platform, storage, media, and AI workloads.",
},
{
label: "Memory",
value: "552 GB",
note: "Raspberry Pi, NVIDIA Jetson, and AMD64 systems across service and platform tiers.",
},
{
label: "Storage",
value: "80 TB",
note: "Longhorn-backed storage pools for system and user workloads.",
},
];
return items.map((item) => ({
...item,
note: item.note ? item.note.replaceAll("\t", " ") : "",
}));
});
const displayServices = computed(() => {
const services = props.serviceData?.services || fallbackServices().services;
return services
.filter((svc) => svc.status !== "planned")
.map((svc) => sanitizeService({
...svc,
icon: svc.icon || pickIcon(svc.name),
}))
.sort((a, b) => serviceRank(a) - serviceRank(b) || a.name.localeCompare(b.name));
});
const featuredServices = computed(() => displayServices.value.slice(0, 12));
const hardwareDiagram = computed(() => buildHardwareDiagram(props.labData || {}));
const networkDiagram = computed(() => buildNetworkDiagram(props.networkData || {}));
const pipelineDiagram = computed(() => buildPipelineDiagram());
function sanitizeService(service) {
const host = typeof service.host === "string" ? service.host : "";
const link = typeof service.link === "string" ? service.link : "";
const hostLooksInternal = host.includes(".svc.cluster.local") || (host && !host.includes(".") && !host.includes("/"));
if (!hostLooksInternal) return service;
return { ...service, host: link.startsWith("/") ? link : "" };
}
function pickIcon(name) {
const h = name.toLowerCase();
if (h.includes("nextcloud")) return "NC";
if (h.includes("jellyfin")) return "JF";
if (h.includes("matrix")) return "MX";
if (h.includes("element")) return "EL";
if (h.includes("livekit")) return "LK";
if (h.includes("coturn") || h.includes("turn")) return "TN";
if (h.includes("mail")) return "ML";
if (h.includes("vaultwarden")) return "VW";
if (h.includes("vault")) return "VT";
if (h.includes("gitea")) return "GT";
if (h.includes("jenkins")) return "JK";
if (h.includes("harbor")) return "HB";
if (h.includes("flux")) return "FX";
if (h.includes("monero")) return "XM";
if (h.includes("sui")) return "SU";
if (h.includes("keycloak")) return "KC";
if (h.includes("translation")) return "TR";
if (h.includes("grafana")) return "GF";
if (h.includes("pegasus")) return "PG";
if (h.includes("veles")) return "VL";
if (h.includes("ai chat")) return "AI";
if (h.includes("ai image") || h.includes("vision")) return "VI";
if (h.includes("ai speech")) return "SP";
return "TL";
}
function serviceRank(service) {
const priority = [
"Keycloak",
"Gitea",
"Jenkins",
"Harbor",
"Flux",
"Grafana",
"OpenSearch",
"Veles",
"Nextcloud",
"Outline",
"Planka",
"VaultWarden",
"Vault",
];
const index = priority.indexOf(service.name);
return index === -1 ? priority.length : index;
}
function hostState(host) {
if (!host || host.known === false) return "unknown";
return host.up ? "ok" : "bad";
}
function statusLabelFrom(state) {
if (state === "ok") return "Responding";
if (state === "bad") return "Needs attention";
return "Unknown";
}
</script>
<style scoped>
.platform-section,
.platform-subsection {
box-sizing: border-box;
scroll-margin-top: 0;
margin-top: 0;
padding: clamp(44px, 6vh, 72px) 0;
}
.platform-subsection {
display: grid;
align-content: start;
}
.section-kicker {
color: var(--accent-cyan);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 8px;
}
.section-heading {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 18px;
}
.section-heading h2 {
margin: 0;
font-size: clamp(30px, 5vw, 44px);
}
.section-heading p {
max-width: 760px;
margin: 10px 0 0;
font-size: 17px;
line-height: 1.65;
}
.section-link {
flex: 0 0 auto;
min-height: 44px;
display: inline-flex;
align-items: center;
padding: 10px 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
color: var(--text-strong);
}
.section-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.platform-subheading {
margin-bottom: 18px;
}
.platform-dashboard {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(430px, 1.1fr);
gap: 14px;
padding: 18px;
margin-bottom: 0;
background:
radial-gradient(circle at 82% 10%, rgba(0, 229, 197, 0.1), transparent 28%),
linear-gradient(135deg, rgba(255, 255, 255, 0.045), rgba(5, 12, 28, 0.72));
}
.dashboard-copy,
.dashboard-panel {
min-width: 0;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
margin: 0 0 6px;
font-size: 13px;
}
h3,
h4 {
margin-top: 0;
}
.platform-dashboard h3 {
font-size: clamp(24px, 3vw, 32px);
margin-bottom: 8px;
}
.lede {
margin: 0 0 12px;
color: var(--text-muted);
max-width: 650px;
line-height: 1.62;
}
.bullets {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 10px 0 12px;
}
.live-status {
margin-top: 18px;
}
.status {
color: var(--text-muted);
}
.fallback-copy {
margin: 8px 0 0;
}
.availability {
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius);
padding: 10px;
background: rgba(255, 255, 255, 0.03);
min-width: 0;
}
.dashboard-metrics {
grid-column: 1 / -1;
margin: 2px 0 0;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.dashboard-metrics :deep(.metric-card) {
min-height: 116px;
padding: 13px;
}
.availability-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.availability-panel {
min-width: 0;
display: grid;
gap: 10px;
align-content: start;
padding: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.025);
}
.availability-panel iframe {
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
}
.availability-title {
align-items: flex-start;
margin-bottom: 0;
}
.availability-title h4 {
margin: 0;
}
.panel-subtitle {
margin: 3px 0 0;
color: var(--text-muted);
font-size: 13px;
}
.panel-subtitle.pill-ok {
color: var(--accent-cyan);
}
.panel-subtitle.pill-bad {
color: var(--accent-rose);
}
.placeholder {
border: 1px dashed rgba(255, 255, 255, 0.2);
border-radius: var(--radius-sm);
padding: 12px;
color: var(--text-muted);
text-align: center;
min-height: 180px;
display: grid;
place-items: center;
}
.host-status-list {
min-height: 180px;
display: grid;
align-content: center;
gap: 10px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
padding: 12px;
background: rgba(255, 255, 255, 0.025);
}
.host-status-row {
display: grid;
grid-template-columns: 12px 1fr;
gap: 10px;
align-items: start;
padding: 10px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.035);
}
.host-dot {
width: 10px;
height: 10px;
border-radius: 999px;
margin-top: 5px;
background: var(--text-muted);
}
.host-dot.ok {
background: var(--accent-cyan);
}
.host-dot.bad {
background: var(--accent-rose);
}
.host-label {
color: var(--text-strong);
font-weight: 800;
}
.host-value {
color: var(--text-muted);
font-size: 13px;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.platform-card {
margin-top: 0;
}
.platform-services-section .platform-card {
overflow: visible;
}
.architecture-board {
padding: 20px;
margin-bottom: 14px;
}
.board-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.board-head h3 {
margin: 0 0 6px;
font-size: clamp(22px, 3vw, 30px);
}
.board-head p {
max-width: 720px;
margin: 0;
color: var(--text-muted);
line-height: 1.55;
}
.architecture-map {
display: grid;
grid-template-columns: minmax(210px, 0.82fr) 28px minmax(450px, 1.65fr) 28px minmax(220px, 0.9fr);
gap: 12px;
align-items: stretch;
}
.map-column {
min-width: 0;
display: grid;
gap: 12px;
align-content: stretch;
}
.map-node {
min-width: 0;
display: grid;
align-content: start;
gap: 8px;
padding: 16px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.035);
}
.map-node strong {
color: var(--text-strong);
font-size: 17px;
}
.map-node p {
margin: 0;
color: var(--text-muted);
line-height: 1.5;
}
.map-label {
width: fit-content;
padding: 5px 8px;
border-radius: 999px;
color: var(--accent-cyan);
background: rgba(0, 229, 197, 0.08);
border: 1px solid rgba(0, 229, 197, 0.18);
font-size: 11px;
text-transform: uppercase;
}
.edge-node {
border-color: rgba(0, 229, 197, 0.16);
}
.access-node {
border-color: rgba(0, 229, 197, 0.12);
}
.cluster-node {
border-color: rgba(120, 180, 255, 0.18);
background:
linear-gradient(135deg, rgba(0, 229, 197, 0.05), rgba(120, 180, 255, 0.08)),
rgba(255, 255, 255, 0.035);
}
.ops-node {
border-color: rgba(127, 124, 255, 0.2);
}
.dedicated-node {
border-color: rgba(255, 220, 120, 0.22);
}
.map-arrow {
display: grid;
place-items: center;
color: var(--accent-cyan);
font-weight: 800;
opacity: 0.8;
align-self: center;
min-height: 100%;
}
.node-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
margin-top: 8px;
}
.node-grid > span {
min-height: 92px;
display: grid;
align-content: start;
gap: 4px;
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(3, 8, 18, 0.55);
color: var(--text-muted);
font-size: 14px;
line-height: 1.42;
}
.node-grid > span b {
display: block;
color: var(--text-strong);
}
.delivery-chain {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-primary);
white-space: nowrap;
}
.delivery-layer {
grid-column: 1 / -1;
min-height: 74px;
}
.source-diagrams {
margin-top: 14px;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, 0.025);
overflow: hidden;
}
.source-diagrams summary {
min-height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
cursor: pointer;
list-style: none;
}
.source-diagrams summary::-webkit-details-marker {
display: none;
}
.source-diagrams small {
display: inline;
margin-left: 4px;
color: var(--text-muted);
font-weight: 400;
}
.platform-diagrams {
padding: 0 14px 14px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
align-items: stretch;
}
.platform-diagrams :deep(.mermaid-card) {
min-height: 260px;
}
.platform-diagrams :deep(.diagram) {
min-height: 150px;
}
.platform-diagrams :deep(.diagram svg) {
min-width: 0;
max-width: 100%;
}
@media (max-width: 980px) {
.section-heading,
.platform-dashboard {
grid-template-columns: 1fr;
}
.section-heading {
display: grid;
}
.platform-section,
.platform-subsection {
padding: 44px 0;
}
.dashboard-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.platform-diagrams {
grid-template-columns: 1fr;
}
.architecture-map {
grid-template-columns: 1fr;
}
.map-arrow {
min-height: 18px;
transform: rotate(90deg);
}
.platform-diagrams :deep(.diagram) {
min-height: 220px;
}
.section-actions {
justify-content: flex-start;
}
}
@media (max-width: 720px) {
.availability-grid {
grid-template-columns: 1fr;
}
.platform-dashboard {
padding: 14px;
}
.dashboard-metrics {
grid-template-columns: 1fr;
}
.board-head,
.source-diagrams summary {
display: grid;
}
.node-grid {
grid-template-columns: 1fr;
}
.section-head {
align-items: flex-start;
flex-direction: column;
}
}
</style>