919 lines
23 KiB
Vue
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">-></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"><-></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>
|