Refresh professional homepage

This commit is contained in:
codex 2026-06-29 13:23:07 -03:00
parent 8126bd3c96
commit 0437bfe9b3
32 changed files with 4294 additions and 637 deletions

View File

@ -31,6 +31,65 @@ def _tcp_check(host: str, port: int, timeout_sec: float) -> bool:
def register_account_overview(app) -> None:
"""Register the account overview endpoint."""
@app.route("/api/account/member-state", methods=["GET"])
@require_auth
def account_member_state() -> Any:
"""Return non-secret member navigation state for auth-aware public UI."""
ok, resp = require_account_access()
if not ok:
return resp
username = g.keycloak_username
keycloak_email = g.keycloak_email or ""
status = "unknown"
request_code = ""
onboarding_url = "/onboarding"
if settings.PORTAL_DATABASE_URL and username:
try:
with connect() as conn:
row = conn.execute(
"""
SELECT request_code, status
FROM access_requests
WHERE username = %s
ORDER BY created_at DESC
LIMIT 1
""",
(username,),
).fetchone()
if not row and keycloak_email:
row = conn.execute(
"""
SELECT request_code, status
FROM access_requests
WHERE contact_email = %s
ORDER BY created_at DESC
LIMIT 1
""",
(keycloak_email,),
).fetchone()
if row and isinstance(row, dict):
request_code = str(row.get("request_code") or "").strip()
status = str(row.get("status") or "unknown").strip() or "unknown"
except Exception:
status = "unknown"
request_code = ""
if request_code:
onboarding_url = f"{settings.PORTAL_PUBLIC_BASE_URL}/onboarding?code={quote(request_code)}"
return jsonify(
{
"user": {"username": username, "email": keycloak_email, "groups": g.keycloak_groups},
"status": status,
"onboarding_url": onboarding_url,
"dashboard_url": "/apps",
"account_url": "/account",
}
)
@app.route("/api/account/overview", methods=["GET"])
@require_auth
def account_overview() -> Any:

View File

@ -15,9 +15,6 @@ def register(app) -> None:
def auth_config() -> Any:
"""Render the auth configuration payload consumed by the SPA."""
if not settings.KEYCLOAK_ENABLED:
return jsonify({"enabled": False})
issuer = settings.KEYCLOAK_ISSUER
public_origin = request.host_url.rstrip("/")
redirect_uri = quote(f"{public_origin}/", safe="")
@ -36,6 +33,17 @@ def register(app) -> None:
account_url = f"{issuer}/account"
account_password_url = f"{account_url}/#/security/signingin"
if not settings.KEYCLOAK_ENABLED:
return jsonify(
{
"enabled": False,
"login_url": login_url,
"reset_url": reset_url,
"account_url": account_url,
"account_password_url": account_password_url,
}
)
return jsonify(
{
"enabled": True,

View File

@ -41,6 +41,28 @@ def _vm_query(expr: str) -> float | None:
return max(values)
def _vm_query_vector(expr: str) -> list[dict[str, Any]]:
"""Run one instant VictoriaMetrics query and return result samples."""
url = f"{settings.VM_BASE_URL}/api/v1/query?{urlencode({'query': expr})}"
with urlopen(url, timeout=settings.VM_QUERY_TIMEOUT_SEC) as resp:
payload = json.loads(resp.read().decode("utf-8"))
if payload.get("status") != "success":
return []
result = (payload.get("data") or {}).get("result") or []
samples: list[dict[str, Any]] = []
for item in result:
try:
value = float(item["value"][1])
except (KeyError, IndexError, TypeError, ValueError):
continue
metric = item.get("metric") if isinstance(item.get("metric"), dict) else {}
samples.append({"metric": metric, "value": value})
return samples
def _http_ok(url: str, expect_substring: str | None = None) -> bool:
"""Return whether a URL responds successfully and optionally contains text."""
@ -56,12 +78,95 @@ def _http_ok(url: str, expect_substring: str | None = None) -> bool:
return False
_PUBLIC_SERVICE_ALIASES = [
(("cloud.bstein.dev", "nextcloud", "cloud"), "Nextcloud", "https://cloud.bstein.dev"),
(("live.bstein.dev", "element", "matrix", "synapse", "livekit"), "Element / Matrix", "https://live.bstein.dev"),
(("stream.bstein.dev", "jellyfin", "stream"), "Jellyfin", "https://stream.bstein.dev"),
(("notes.bstein.dev", "outline", "notes"), "Outline", "https://notes.bstein.dev"),
(("tasks.bstein.dev", "planka", "tasks"), "Planka", "https://tasks.bstein.dev"),
(("vault.bstein.dev", "vaultwarden"), "VaultWarden", "https://vault.bstein.dev"),
(("scm.bstein.dev", "gitea"), "Gitea", "https://scm.bstein.dev"),
(("ci.bstein.dev", "jenkins"), "Jenkins", "https://ci.bstein.dev"),
(("registry.bstein.dev", "harbor", "registry"), "Harbor", "https://registry.bstein.dev"),
(("metrics.bstein.dev", "grafana", "metrics"), "Grafana", "https://metrics.bstein.dev"),
(("chat.ai.bstein.dev", "ai-chat", "atlasbot", "ollama"), "AI Chat", "https://bstein.dev/ai/chat"),
(("health.bstein.dev", "wger", "health"), "Wger", "https://health.bstein.dev"),
(("budget.bstein.dev", "actual", "budget"), "Actual Budget", "https://budget.bstein.dev"),
(("money.bstein.dev", "firefly", "money"), "Firefly III", "https://money.bstein.dev"),
(("mail.bstein.dev", "mailu", "mail"), "Mailu", "https://mail.bstein.dev"),
]
def _public_active_service() -> dict[str, Any]:
"""Return the busiest mapped public service without leaking raw metrics."""
if not settings.LAB_ACTIVE_SERVICE_QUERY:
return {"known": False}
samples = _vm_query_vector(settings.LAB_ACTIVE_SERVICE_QUERY)
for sample in sorted(samples, key=lambda item: item["value"], reverse=True):
labels = sample.get("metric") or {}
haystack = " ".join(str(value).lower() for value in labels.values())
for aliases, label, url in _PUBLIC_SERVICE_ALIASES:
if any(alias in haystack for alias in aliases):
return {
"known": True,
"label": label,
"url": url,
"window": settings.LAB_ACTIVE_SERVICE_WINDOW,
}
return {"known": False}
def _dedicated_host_status(label: str, health_url: str, up_query: str) -> dict[str, Any]:
"""Check one dedicated host and return only public-safe status fields."""
if health_url:
if _http_ok(health_url):
return {"label": label, "known": True, "up": True, "source": "health-check"}
return {"label": label, "known": True, "up": False, "source": "health-check"}
if up_query:
value = _vm_query(up_query)
if value is not None:
return {"label": label, "known": True, "up": value > 0, "source": "victoria-metrics"}
return {"label": label, "known": False, "up": False, "source": "unknown"}
def _dedicated_hosts_status() -> dict[str, Any]:
"""Return aggregate availability for the non-cluster DB and jumphost."""
hosts = [
_dedicated_host_status(
"Database node",
settings.ATLAS_DB_HOST_HEALTH_URL,
settings.ATLAS_DB_HOST_UP_QUERY,
),
_dedicated_host_status(
"Jumphost",
settings.ATLAS_JUMPHOST_HEALTH_URL,
settings.ATLAS_JUMPHOST_UP_QUERY,
),
]
known_hosts = [host for host in hosts if host["known"]]
up_count = sum(1 for host in known_hosts if host["up"])
return {
"known": bool(known_hosts),
"up": bool(known_hosts) and up_count == len(known_hosts),
"up_count": up_count,
"total": len(hosts),
"known_count": len(known_hosts),
"hosts": hosts,
}
def register(app) -> None:
"""Register the lightweight lab connectivity status endpoint."""
@app.route("/api/lab/status")
def lab_status() -> Any:
"""Return cached Atlas/Oceanus health hints for the home page."""
"""Return cached public-safe lab health hints for the home page."""
now = time.time()
cached = _LAB_STATUS_CACHE.get("value")
@ -76,9 +181,8 @@ def register(app) -> None:
atlas_known = False
atlas_source = "unknown"
oceanus_up = False
oceanus_known = False
oceanus_source = "unknown"
dedicated_hosts = {"known": False, "up": False, "up_count": 0, "total": 2, "known_count": 0, "hosts": []}
active_service = {"known": False}
# Atlas
try:
@ -106,15 +210,21 @@ def register(app) -> None:
except Exception:
pass
# Oceanus (node-exporter direct probe)
try:
t_probe = time.perf_counter()
if _http_ok(settings.OCEANUS_NODE_EXPORTER_URL):
timings_ms["oceanus_node_exporter"] = int((time.perf_counter() - t_probe) * 1000)
dedicated_hosts = _dedicated_hosts_status()
timings_ms["dedicated_hosts"] = int((time.perf_counter() - t_probe) * 1000)
if dedicated_hosts["known"]:
connected = True
except Exception:
pass
try:
t_probe = time.perf_counter()
active_service = _public_active_service()
timings_ms["active_service"] = int((time.perf_counter() - t_probe) * 1000)
if active_service.get("known"):
connected = True
oceanus_known = True
oceanus_up = True
oceanus_source = "node-exporter"
except Exception:
pass
@ -123,7 +233,9 @@ def register(app) -> None:
payload = {
"connected": connected,
"atlas": {"up": atlas_up, "known": atlas_known, "source": atlas_source},
"oceanus": {"up": oceanus_up, "known": oceanus_known, "source": oceanus_source},
"dedicated_hosts": dedicated_hosts,
"active_service": active_service,
"oceanus": {"up": atlas_up, "known": atlas_known, "source": "atlas-member"},
"checked_at": int(now),
"timings_ms": timings_ms,
}

View File

@ -20,6 +20,21 @@ K8S_API_TIMEOUT_SEC = float(os.getenv("K8S_API_TIMEOUT_SEC", "5"))
LAB_STATUS_CACHE_SEC = float(os.getenv("LAB_STATUS_CACHE_SEC", "30"))
GRAFANA_HEALTH_URL = os.getenv("GRAFANA_HEALTH_URL", "http://grafana.monitoring.svc.cluster.local/api/health")
OCEANUS_NODE_EXPORTER_URL = os.getenv("OCEANUS_NODE_EXPORTER_URL", "http://192.168.22.24:9100/metrics")
LAB_ACTIVE_SERVICE_WINDOW = os.getenv("LAB_ACTIVE_SERVICE_WINDOW", "15m").strip() or "15m"
LAB_ACTIVE_SERVICE_QUERY = os.getenv(
"LAB_ACTIVE_SERVICE_QUERY",
f'topk(1, sum by (service) (increase(traefik_service_requests_total{{service!=""}}[{LAB_ACTIVE_SERVICE_WINDOW}])))',
).strip()
ATLAS_DB_HOST_HEALTH_URL = os.getenv("ATLAS_DB_HOST_HEALTH_URL", "").strip()
ATLAS_DB_HOST_UP_QUERY = os.getenv(
"ATLAS_DB_HOST_UP_QUERY",
'up{instance=~".*(titan-db|atlas-db).*"}',
).strip()
ATLAS_JUMPHOST_HEALTH_URL = os.getenv("ATLAS_JUMPHOST_HEALTH_URL", "").strip()
ATLAS_JUMPHOST_UP_QUERY = os.getenv(
"ATLAS_JUMPHOST_UP_QUERY",
'up{instance=~".*(titan-jh|theia).*"}',
).strip()
AI_CHAT_API = os.getenv("AI_CHAT_API", "http://ollama.ai.svc.cluster.local:11434").rstrip("/")
AI_CHAT_MODEL = os.getenv("AI_CHAT_MODEL", "qwen2.5-coder:7b-instruct-q4_0")

View File

@ -17,8 +17,16 @@ class DummyResult:
class DummyConn:
def __init__(self, *, request_code: str = "alice~CODE", step_done: bool = True, fail: bool = False) -> None:
def __init__(
self,
*,
request_code: str = "alice~CODE",
status: str = "awaiting_onboarding",
step_done: bool = True,
fail: bool = False,
) -> None:
self.request_code = request_code
self.status = status
self.step_done = step_done
self.fail = fail
self.executed: list[tuple[str, object | None]] = []
@ -30,7 +38,7 @@ class DummyConn:
if "access_request_onboarding_steps" in query:
return DummyResult({"exists": 1} if self.step_done else None)
if "FROM access_requests" in query:
return DummyResult({"request_code": self.request_code} if self.request_code else None)
return DummyResult({"request_code": self.request_code, "status": self.status} if self.request_code else None)
return DummyResult()
@ -150,6 +158,7 @@ def test_tcp_check_paths(monkeypatch) -> None:
def test_overview_preflight_and_admin_unavailable(monkeypatch) -> None:
client, _conn = make_client(monkeypatch, account_ok=False)
assert client.get("/api/account/overview").status_code == 403
assert client.get("/api/account/member-state").status_code == 403
client, _conn = make_client(monkeypatch, admin=DummyAdmin(ready=False))
data = client.get("/api/account/overview").get_json()
@ -157,6 +166,22 @@ def test_overview_preflight_and_admin_unavailable(monkeypatch) -> None:
assert data["jellyfin"]["sync_detail"] == "keycloak admin not configured"
def test_member_state_returns_navigation_state_without_service_secrets(monkeypatch) -> None:
client, _conn = make_client(monkeypatch, conn=DummyConn(status="awaiting_onboarding"))
data = client.get("/api/account/member-state").get_json()
assert data == {
"user": {"username": "alice", "email": "", "groups": ["dev"]},
"status": "awaiting_onboarding",
"onboarding_url": "https://portal.example.dev/onboarding?code=alice~CODE",
"dashboard_url": "/apps",
"account_url": "/account",
}
assert "mailu" not in data
assert "password" not in str(data).lower()
def test_overview_reads_list_attributes_and_reports_ldap_ok(monkeypatch) -> None:
attrs = list_attrs(full_attrs())
user = {"id": "user-1", "email": "alice@idp.dev", "federationLink": "ldap", "attributes": attrs}

View File

@ -13,7 +13,11 @@ def test_auth_config_disabled_by_default() -> None:
resp = client.get("/api/auth/config")
assert resp.status_code == 200
assert resp.get_json() == {"enabled": False}
data = resp.get_json()
assert data["enabled"] is False
assert data["login_url"].startswith("https://sso.bstein.dev/realms/atlas/protocol/openid-connect/auth")
assert data["reset_url"].startswith("https://sso.bstein.dev/realms/atlas/login-actions/reset-credentials")
assert data["account_url"] == "https://sso.bstein.dev/realms/atlas/account"
def test_auth_config_builds_urls_when_enabled(monkeypatch) -> None:

View File

@ -49,6 +49,38 @@ def test_vm_query_success_and_empty_paths(monkeypatch) -> None:
assert lab._vm_query("up") is None
def test_vm_query_vector_filters_bad_samples(monkeypatch) -> None:
payload = {
"status": "success",
"data": {
"result": [
{"metric": {"service": "nextcloud"}, "value": [0, "12"]},
{"metric": {"service": "broken"}, "value": []},
],
},
}
monkeypatch.setattr(lab, "urlopen", lambda url, timeout: DummyUrlResponse(payload))
assert lab._vm_query_vector("topk(1, up)") == [{"metric": {"service": "nextcloud"}, "value": 12.0}]
def test_public_active_service_uses_external_ai_chat_url(monkeypatch) -> None:
monkeypatch.setattr(lab.settings, "LAB_ACTIVE_SERVICE_QUERY", "topk(1, fake)")
monkeypatch.setattr(
lab,
"_vm_query_vector",
lambda expr: [{"metric": {"service": "atlas-ai-chat"}, "value": 9.0}],
)
assert lab._public_active_service() == {
"known": True,
"label": "AI Chat",
"url": "https://bstein.dev/ai/chat",
"window": lab.settings.LAB_ACTIVE_SERVICE_WINDOW,
}
def test_http_ok_status_substring_and_errors(monkeypatch) -> None:
responses = [
DummyUrlResponse("service ok"),
@ -76,16 +108,22 @@ def test_lab_status_uses_cache_and_probe_fallbacks(monkeypatch) -> None:
client = app.test_client()
lab._LAB_STATUS_CACHE["ts"] = 0.0
lab._LAB_STATUS_CACHE["value"] = None
monkeypatch.setattr(lab.settings, "OCEANUS_NODE_EXPORTER_URL", "https://oceanus.example.dev/metrics")
monkeypatch.setattr(lab.settings, "ATLAS_DB_HOST_HEALTH_URL", "https://db.example.dev/health")
monkeypatch.setattr(lab.settings, "ATLAS_JUMPHOST_HEALTH_URL", "https://jump.example.dev/health")
calls: list[str] = []
def fake_http_ok(url, expect_substring=None):
calls.append(url)
return "grafana" in url or "oceanus" in url
return "grafana" in url or "db" in url or "jump" in url
monkeypatch.setattr(lab, "_http_ok", fake_http_ok)
monkeypatch.setattr(lab, "_vm_query", lambda expr: 1.0)
monkeypatch.setattr(
lab,
"_vm_query_vector",
lambda expr: [{"metric": {"service": "nextcloud-web"}, "value": 8.0}],
)
response = client.get("/api/lab/status")
payload = response.get_json()
@ -93,22 +131,36 @@ def test_lab_status_uses_cache_and_probe_fallbacks(monkeypatch) -> None:
assert response.status_code == 200
assert payload["connected"] is True
assert payload["atlas"]["source"] == "grafana"
assert payload["oceanus"]["source"] == "node-exporter"
assert payload["dedicated_hosts"]["up"] is True
assert payload["dedicated_hosts"]["up_count"] == 2
assert payload["dedicated_hosts"]["hosts"][0]["label"] == "Database node"
assert payload["active_service"] == {
"known": True,
"label": "Nextcloud",
"url": "https://cloud.bstein.dev",
"window": lab.settings.LAB_ACTIVE_SERVICE_WINDOW,
}
assert payload["oceanus"]["source"] == "atlas-member"
second = client.get("/api/lab/status")
assert second.get_json() == payload
lab._LAB_STATUS_CACHE["ts"] = 0.0
lab._LAB_STATUS_CACHE["value"] = None
monkeypatch.setattr(lab.settings, "ATLAS_DB_HOST_HEALTH_URL", "")
monkeypatch.setattr(lab.settings, "ATLAS_JUMPHOST_HEALTH_URL", "")
monkeypatch.setattr(lab, "_http_ok", lambda *a, **k: False)
monkeypatch.setattr(lab, "_vm_query", lambda expr: 0.0)
monkeypatch.setattr(lab, "_vm_query_vector", lambda expr: [])
response = client.get("/api/lab/status")
payload = response.get_json()
assert payload["atlas"]["source"] == "victoria-metrics"
assert payload["atlas"]["up"] is False
assert payload["oceanus"]["known"] is False
assert payload["dedicated_hosts"]["known"] is True
assert payload["dedicated_hosts"]["up"] is False
assert payload["active_service"]["known"] is False
def test_lab_status_handles_probe_exceptions(monkeypatch) -> None:
@ -122,6 +174,7 @@ def test_lab_status_handles_probe_exceptions(monkeypatch) -> None:
monkeypatch.setattr(lab, "_http_ok", boom)
monkeypatch.setattr(lab, "_vm_query", boom)
monkeypatch.setattr(lab, "_vm_query_vector", boom)
response = client.get("/api/lab/status")
payload = response.get_json()
@ -129,4 +182,5 @@ def test_lab_status_handles_probe_exceptions(monkeypatch) -> None:
assert response.status_code == 200
assert payload["connected"] is False
assert payload["atlas"]["known"] is False
assert payload["oceanus"]["known"] is False
assert payload["dedicated_hosts"]["known"] is False
assert payload["active_service"]["known"] is False

View File

@ -3,12 +3,49 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bstein.dev | Titan Lab</title>
<title>Brad Stein | SDET, DevOps Automation & Platform Engineering</title>
<meta
name="description"
content="Live status for the Titan Lab clusters powering bstein.dev."
content="Senior SDET and DevOps Automation Engineer helping teams improve test automation, CI/CD, Docker and Kubernetes reliability, Python tooling, Linux systems, and observability. Available for remote project and contract work."
/>
<link rel="canonical" href="https://bstein.dev/" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://bstein.dev/" />
<meta property="og:title" content="Brad Stein — SDET & DevOps Automation Engineer" />
<meta
property="og:description"
content="Test automation, CI/CD, Kubernetes, Docker, Python tooling, Linux infrastructure, observability, and live platform engineering."
/>
<meta property="og:image" content="https://bstein.dev/og-bstein-dev.svg" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Brad Stein — SDET & DevOps Automation Engineer" />
<meta
name="twitter:description"
content="Test automation, CI/CD, Kubernetes, Docker, Python tooling, Linux infrastructure, observability, and live platform engineering."
/>
<meta name="twitter:image" content="https://bstein.dev/og-bstein-dev.svg" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"name": "Brad Stein",
"jobTitle": "DevOps Automation Engineer / Senior SDET",
"url": "https://bstein.dev/",
"email": "mailto:brad@bstein.dev",
"sameAs": ["https://www.linkedin.com/in/steinbradley/", "https://scm.bstein.dev/bstein"]
},
{
"@type": "WebSite",
"name": "bstein.dev",
"url": "https://bstein.dev/",
"description": "Brad Stein's professional engineering site and the home of the Titan Lab platform."
}
]
}
</script>
</head>
<body>
<div id="app"></div>

View File

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
<title id="title">Brad Stein, DevOps Automation Engineer and Senior SDET</title>
<desc id="desc">A dark bstein.dev social card with professional engineering positioning and Titan Lab platform proof.</desc>
<defs>
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#050914"/>
<stop offset="0.55" stop-color="#0b1228"/>
<stop offset="1" stop-color="#071d22"/>
</linearGradient>
<linearGradient id="accent" x1="0" x2="1" y1="0" y2="0">
<stop offset="0" stop-color="#00e5c5"/>
<stop offset="1" stop-color="#7f7cff"/>
</linearGradient>
</defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<path d="M80 118h1040M80 512h1040" stroke="#ffffff" stroke-opacity=".08" stroke-width="1"/>
<path d="M820 120c90 28 158 94 193 178 32 78 30 158-5 213" fill="none" stroke="url(#accent)" stroke-width="2" stroke-opacity=".45"/>
<circle cx="964" cy="250" r="6" fill="#00e5c5"/>
<circle cx="1016" cy="386" r="6" fill="#7f7cff"/>
<circle cx="880" cy="174" r="6" fill="#00e5c5"/>
<text x="80" y="196" fill="#00e5c5" font-family="JetBrains Mono, monospace" font-size="26" letter-spacing="3">BSTEIN.DEV</text>
<text x="80" y="286" fill="#f7fbff" font-family="Space Grotesk, Inter, sans-serif" font-size="74" font-weight="700">Brad Stein</text>
<text x="80" y="352" fill="#d7e5ff" font-family="Space Grotesk, Inter, sans-serif" font-size="40" font-weight="600">DevOps Automation Engineer / Senior SDET</text>
<text x="80" y="422" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="30">CI/CD · Kubernetes · Python · Linux · Observability</text>
<text x="80" y="482" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="26">Titan Lab is the live platform proof behind the work.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,15 +1,17 @@
<template>
<div class="app-shell">
<TopBar />
<router-view
:lab-data="labData"
:lab-status="labStatus"
:service-data="serviceData"
:network-data="networkData"
:metrics-data="metricsData"
:loading="statusLoading"
:error="statusError"
/>
<main id="main-content" tabindex="-1">
<router-view
:lab-data="labData"
:lab-status="labStatus"
:service-data="serviceData"
:network-data="networkData"
:metrics-data="metricsData"
:loading="statusLoading"
:error="statusError"
/>
</main>
</div>
</template>

View File

@ -7,6 +7,7 @@ export const auth = reactive({
authenticated: false,
username: "",
email: "",
emailVerified: null,
groups: [],
loginUrl: "",
resetUrl: "",
@ -58,6 +59,7 @@ function updateFromToken() {
auth.token = keycloak?.token || "";
auth.username = parsed.preferred_username || "";
auth.email = parsed.email || "";
auth.emailVerified = typeof parsed.email_verified === "boolean" ? parsed.email_verified : null;
auth.groups = normalizeGroups(parsed.groups);
}
@ -131,7 +133,10 @@ export async function login(
redirectPath = window.location.pathname + window.location.search + window.location.hash,
loginHint = "",
) {
if (!keycloak) return;
if (!keycloak) {
if (auth.loginUrl) window.location.assign(auth.loginUrl);
return;
}
const redirectUri = new URL(redirectPath, window.location.origin).toString();
const options = { redirectUri };
if (typeof loginHint === "string" && loginHint.trim()) {

View File

@ -2,12 +2,12 @@
<header class="hero card glass">
<div class="eyebrow">
<span class="pill">Titan Lab</span>
<span class="mono accent">atlas · oceanus · nextcloud-ready</span>
<span class="mono accent">atlas · member services · nextcloud-ready</span>
</div>
<h1>{{ title }}</h1>
<p class="lede">{{ subtitle }}</p>
<div class="cta">
<a v-for="link in links" :key="link.label" class="btn" :href="link.href" target="_blank" rel="noreferrer">
<a v-for="link in links" :key="link.label" class="btn" :href="link.href" target="_blank" rel="noopener noreferrer">
{{ link.label }}
</a>
</div>
@ -23,9 +23,9 @@
<small>Flux + Longhorn + Traefik</small>
</div>
<div class="mini-card">
<div class="label">Oceanus</div>
<div class="value">validator</div>
<small>Scraped via titan-24 into Grafana</small>
<div class="label">Member services</div>
<div class="value">identity + apps</div>
<small>Request access · onboarding · approved services</small>
</div>
<div class="mini-card">
<div class="label">Ingress</div>

View File

@ -26,7 +26,6 @@
<script setup>
import { onMounted, onUnmounted, ref, watch } from "vue";
import mermaid from "mermaid";
const props = defineProps({
title: String,
@ -41,11 +40,15 @@ const isOpen = ref(false);
let initialized = false;
let scheduledHandle = null;
let scheduledKind = "";
let mermaidApi = null;
const renderDiagram = async () => {
if (!props.diagram) return;
if (!mermaidApi) {
mermaidApi = (await import("mermaid")).default;
}
if (!initialized) {
mermaid.initialize({
mermaidApi.initialize({
startOnLoad: false,
theme: "dark",
securityLevel: "loose",
@ -60,7 +63,7 @@ const renderDiagram = async () => {
initialized = true;
}
try {
const { svg } = await mermaid.render(`${renderKey.value}-${Date.now()}`, props.diagram);
const { svg } = await mermaidApi.render(`${renderKey.value}-${Date.now()}`, props.diagram);
svgContent.value = svg;
} catch (err) {
svgContent.value = `<pre class="mono" style="color:#ff4f93">Mermaid render error: ${err}</pre>`;
@ -146,7 +149,9 @@ onUnmounted(() => {
<style scoped>
.mermaid-card {
min-height: 320px;
min-height: 300px;
display: flex;
flex-direction: column;
}
.actions {
@ -165,15 +170,38 @@ onUnmounted(() => {
}
.diagram {
flex: 1;
margin-top: 12px;
padding: 10px;
border-radius: var(--radius-sm);
border: 1px dashed rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.02);
overflow-x: auto;
min-height: 170px;
display: grid;
place-items: center;
overflow: auto;
overscroll-behavior: contain;
cursor: zoom-in;
}
.diagram:empty::before {
content: "Rendering diagram...";
color: var(--text-muted);
font-size: 13px;
}
.diagram :deep(svg) {
width: 100%;
max-width: 100%;
height: auto;
display: block;
}
.diagram :deep(.label),
.modal-body :deep(.label) {
font-family: "Space Grotesk", "Inter", system-ui, sans-serif;
}
.overlay {
position: fixed;
inset: 0;
@ -227,7 +255,8 @@ onUnmounted(() => {
}
.modal-body :deep(svg) {
width: 100%;
width: max(100%, 920px);
max-width: none;
height: auto;
}
</style>

View File

@ -8,7 +8,7 @@
<div class="metrics-body">
<iframe
:src="metrics.dashboard"
title="Atlas + Oceanus metrics"
title="Atlas platform metrics"
loading="lazy"
allowfullscreen
></iframe>
@ -16,8 +16,8 @@
<div class="label">Highlights</div>
<ul>
<li>Atlas scraping for cluster + service SLOs.</li>
<li>Tethys (titan-24) pushes Oceanus validator stats.</li>
<li>Grafana ready for embedding into Nextcloud widgets.</li>
<li>Public dashboards summarize health and operations without exposing member-only details.</li>
<li>Grafana panels can be embedded into member tools where safe.</li>
</ul>
</div>
</div>

View File

@ -0,0 +1,918 @@
<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>

View File

@ -10,7 +10,7 @@
:href="!isInternal(svc.link) ? svc.link || '#' : undefined"
:title="svc.link"
:target="isInternal(svc.link) ? undefined : '_blank'"
rel="noreferrer"
rel="noopener noreferrer"
>
<div class="service-top">
<div class="icon">{{ svc.icon || "🛰️" }}</div>
@ -48,7 +48,7 @@ const isInternal = (link) => typeof link === "string" && link.startsWith("/");
.service-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
gap: 10px;
}
.service {
@ -56,6 +56,8 @@ const isInternal = (link) => typeof link === "string" && link.startsWith("/");
color: inherit;
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
border-color: rgba(255, 255, 255, 0.08);
min-height: 108px;
padding: 12px;
}
.service:hover {
@ -78,27 +80,28 @@ const isInternal = (link) => typeof link === "string" && link.startsWith("/");
.service-top {
display: flex;
align-items: center;
gap: 10px;
gap: 9px;
}
.icon {
width: 36px;
height: 36px;
border-radius: 10px;
width: 30px;
height: 30px;
border-radius: 9px;
display: grid;
place-items: center;
background: rgba(255, 255, 255, 0.06);
font-size: 18px;
font-size: 16px;
}
.name {
font-weight: 700;
color: var(--text-strong);
line-height: 1.15;
}
.category {
color: var(--text-muted);
font-size: 13px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
@ -106,12 +109,18 @@ const isInternal = (link) => typeof link === "string" && link.startsWith("/");
.summary {
color: var(--text-muted);
margin: 8px 0 0;
min-height: 22px;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.link {
margin-top: 6px;
color: var(--accent-cyan);
font-size: 13px;
font-size: 12px;
overflow-wrap: anywhere;
}
@media (max-width: 1100px) {

View File

@ -21,9 +21,9 @@
<p>Jetson pair for AI, GPU mini-pc titan-22 for Jellyfin.</p>
</div>
<div class="card stat">
<div class="label">Specialty nodes</div>
<div class="label">Dedicated hosts</div>
<div class="value">{{ specialty.length }}</div>
<p>Oceanus (validator), Tethys bridge, and Bastion.</p>
<p>Database node and jumphost kept outside the Atlas cluster.</p>
</div>
<div class="card stat">
<div class="label">Storage fabric</div>

View File

@ -1,64 +1,192 @@
<template>
<header class="topbar">
<div class="profile" @click="goAbout">
<div class="avatar">
<img src="@/assets/profile-avatar.jpg" alt="Brad Stein" />
<a class="skip-link" href="#main-content">Skip to content</a>
<RouterLink class="profile" to="/" @click="closeMenu">
<div class="avatar" aria-hidden="true">
<img src="@/assets/profile-avatar.jpg" alt="" />
</div>
<div>
<div class="name">Brad Stein</div>
<div class="role">Software Development Engineer</div>
<div class="role">{{ content.shortRole }}</div>
</div>
</div>
<nav class="links">
<RouterLink to="/" class="nav-link">Home</RouterLink>
<RouterLink to="/about" class="nav-link">About</RouterLink>
</RouterLink>
<template v-if="auth.enabled">
<template v-if="auth.authenticated">
<RouterLink to="/apps" class="nav-link">Apps</RouterLink>
<RouterLink to="/account" class="nav-link">Account</RouterLink>
<button class="nav-link button" type="button" @click="doLogout">Logout</button>
<button
ref="menuButton"
class="menu-toggle"
type="button"
aria-controls="site-navigation"
:aria-expanded="menuOpen ? 'true' : 'false'"
aria-label="Toggle navigation"
@click="toggleMenu"
>
<span></span>
<span></span>
<span></span>
</button>
<nav
id="site-navigation"
ref="navEl"
class="links"
:class="{ open: menuOpen }"
aria-label="Primary navigation"
@keydown="onNavKeydown"
>
<a v-for="link in sectionLinks" :key="link.href" class="nav-link" :href="link.href" @click="closeMenu">
{{ link.label }}
</a>
<a class="nav-link professional-action" href="/#contact" @click="closeMenu">Contact</a>
<div class="utility">
<template v-if="!auth.ready">
<button class="nav-link button" type="button" @click="doLogin">Login</button>
<RouterLink class="nav-link" to="/request-access" @click="closeMenu">Register</RouterLink>
</template>
<template v-else-if="auth.authenticated">
<RouterLink class="nav-link strong" :to="memberAction?.to || '/apps'" @click="closeMenu">
{{ memberAction?.label || "Open Services" }}
</RouterLink>
<RouterLink class="nav-link" to="/account" @click="closeMenu">Account</RouterLink>
<button class="nav-link button" type="button" @click="doLogout">Sign Out</button>
</template>
<template v-else>
<button class="nav-link button" type="button" @click="doLogin">Login</button>
<RouterLink to="/request-access" class="nav-link">Request Access</RouterLink>
<a v-if="auth.resetUrl" :href="auth.resetUrl" class="nav-link" target="_blank" rel="noreferrer">Reset Password</a>
<RouterLink class="nav-link" to="/request-access" @click="closeMenu">Register</RouterLink>
</template>
</template>
</div>
</nav>
</header>
</template>
<script setup>
import { useRouter, RouterLink } from "vue-router";
import { nextTick, onUnmounted, ref, watch } from "vue";
import { RouterLink } from "vue-router";
import { auth, login, logout } from "@/auth";
import { homepageContent } from "@/data/homepageContent";
import { useMemberAccessState } from "@/member/useMemberAccessState";
const router = useRouter();
const goAbout = () => router.push("/about");
const { memberAction } = useMemberAccessState();
const content = homepageContent;
const doLogin = () => login();
const doLogout = () => logout();
const sectionLinks = [
{ label: "Work", href: "/#work" },
{ label: "Capabilities", href: "/#capabilities" },
{ label: "Platform", href: "/#platform" },
{ label: "Process", href: "/#process" },
];
const menuOpen = ref(false);
const navEl = ref(null);
const menuButton = ref(null);
const focusableSelector = "a[href], button:not([disabled]), [tabindex]:not([tabindex='-1'])";
function closeMenu() {
menuOpen.value = false;
}
async function toggleMenu() {
menuOpen.value = !menuOpen.value;
if (menuOpen.value) {
await nextTick();
const first = navEl.value?.querySelector(focusableSelector);
first?.focus();
}
}
function onWindowKeydown(event) {
if (event.key !== "Escape" || !menuOpen.value) return;
closeMenu();
menuButton.value?.focus();
}
function onNavKeydown(event) {
if (event.key === "Escape") {
closeMenu();
menuButton.value?.focus();
return;
}
if (event.key !== "Tab" || !menuOpen.value) return;
const items = Array.from(navEl.value?.querySelectorAll(focusableSelector) || []);
if (!items.length) return;
const first = items[0];
const last = items[items.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
const doLogout = () => {
closeMenu();
logout();
};
const doLogin = () => {
closeMenu();
login();
};
watch(menuOpen, (open) => {
if (open) {
window.addEventListener("keydown", onWindowKeydown);
} else {
window.removeEventListener("keydown", onWindowKeydown);
}
});
onUnmounted(() => {
window.removeEventListener("keydown", onWindowKeydown);
});
</script>
<style scoped>
.topbar {
position: sticky;
top: 0;
z-index: 10;
z-index: 30;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 28px;
background: rgba(5, 9, 20, 0.92);
gap: 18px;
padding: 12px 28px;
background: rgba(5, 9, 20, 0.94);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
backdrop-filter: blur(12px);
}
.skip-link {
position: fixed;
top: 8px;
left: 8px;
transform: translateY(-140%);
background: var(--text-strong);
color: var(--bg-deep);
padding: 10px 12px;
border-radius: 8px;
font-weight: 800;
z-index: 60;
}
.skip-link:focus {
transform: translateY(0);
}
.profile {
display: flex;
align-items: center;
gap: 12px;
cursor: pointer;
color: inherit;
text-decoration: none;
min-height: 48px;
border-radius: 10px;
}
.avatar {
@ -84,7 +212,7 @@ const doLogout = () => logout();
.name {
color: var(--text-strong);
font-weight: 700;
font-weight: 800;
}
.role {
@ -95,31 +223,128 @@ const doLogout = () => logout();
.links {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
gap: 8px;
}
.utility {
display: flex;
align-items: center;
gap: 8px;
margin-left: 4px;
padding-left: 12px;
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.nav-link {
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
font-weight: 600;
font-weight: 700;
text-decoration: none;
padding: 8px 10px;
padding: 9px 11px;
border-radius: 10px;
border: 1px solid transparent;
white-space: nowrap;
}
.button {
background: transparent;
cursor: pointer;
font: inherit;
}
.professional-action,
.nav-link.strong {
border-color: rgba(255, 255, 255, 0.14);
border-color: rgba(0, 229, 197, 0.22);
color: var(--accent-cyan);
background: rgba(0, 229, 197, 0.06);
}
.nav-link:hover,
.nav-link:focus-visible,
.profile:focus-visible,
.button:focus-visible,
.menu-toggle:focus-visible {
outline: 2px solid rgba(0, 229, 197, 0.72);
outline-offset: 2px;
border-color: rgba(255, 255, 255, 0.16);
color: var(--accent-cyan);
}
.nav-link:hover {
border-color: rgba(255, 255, 255, 0.14);
color: var(--accent-cyan);
.muted {
color: var(--text-muted);
}
.menu-toggle {
display: none;
width: 44px;
height: 44px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
color: var(--text-strong);
padding: 10px;
cursor: pointer;
}
.menu-toggle span {
display: block;
height: 2px;
background: currentColor;
border-radius: 999px;
margin: 5px 0;
}
@media (max-width: 1120px) {
.topbar {
padding: 12px 18px;
}
.menu-toggle {
display: block;
}
.links {
position: fixed;
top: 73px;
right: 14px;
left: 14px;
display: none;
align-items: stretch;
flex-direction: column;
gap: 8px;
padding: 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-sm);
background: rgba(5, 9, 20, 0.98);
box-shadow: var(--shadow-strong);
}
.links.open {
display: flex;
}
.utility {
margin-left: 0;
padding-left: 0;
padding-top: 10px;
border-left: 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
align-items: stretch;
flex-direction: column;
}
.nav-link {
justify-content: flex-start;
white-space: normal;
}
}
@media (max-width: 520px) {
.role {
max-width: 190px;
}
}
</style>

View File

@ -0,0 +1,803 @@
const focusMode = (import.meta.env?.VITE_HOMEPAGE_FOCUS || "devops").trim().toLowerCase();
const focusProfiles = {
devops: {
eyebrow: "AVAILABLE FOR REMOTE DEVOPS AUTOMATION AND PLATFORM WORK",
role: "DevOps Automation Engineer / Senior SDET",
shortRole: "DevOps Automation / Platform",
summary:
"I build and repair the machinery that gets code safely into production: CI/CD, Docker, Kubernetes, Linux infrastructure, Python automation, observability, and test gates.",
experienceLine:
"Engineering experience across Boeing, IBM, deployment automation, infrastructure tooling, system testing, and production-minded platform operations.",
techStack: [
"Jenkins",
"Docker",
"Kubernetes",
"Linux",
"Terraform",
"Flux",
"Grafana",
"Prometheus",
"Python",
"Bash",
"Pytest",
"Playwright",
],
capabilityOrder: [
"CI/CD & Release Flow",
"Docker & Kubernetes",
"Python & Operational Automation",
"Observability & Reliability",
"Platform Tooling & GitOps",
"Test Automation & SDET",
],
serviceOrder: [
"Platform bring-up and GitOps operating model",
"AWS and dedicated-host Kubernetes buildouts",
"Cluster recovery, bootstrap, and outage drills",
"CI/CD quality gates and release machinery",
"Storage, backup, and restore operations",
"Operational tools for real infrastructure",
],
capabilitySection: {
kicker: "Capabilities",
heading: "Where I Add Platform Value",
description:
"Delivery automation, infrastructure operations, and platform troubleshooting across the full path from commit to running service.",
},
serviceSection: {
kicker: "Platform problem solving",
heading: "From 0 to 100",
description:
"Beyond ticket-sized fixes, I build the operating machinery around delivery: cloud or dedicated-host Kubernetes, source-of-truth repositories, CI/CD gates, GitOps, recovery automation, backups, observability, and operator tools.",
},
workSection: {
kicker: "Projects & Experience",
heading: "Platform Showcase",
description:
"Inspectable repos, live services, and professional work samples that show how I approach real infrastructure.",
},
selectedWorkOrder: ["Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing", "Veles"],
engagementOrder: [
"Fixed-Scope Technical Rescue",
"Short-Term Remote Contract",
"Ongoing Engineering Support",
"Senior Remote Engineering Role",
],
contactSection: {
heading: "Have a Platform, Pipeline, Deployment, or Automation Problem?",
description:
"Send the problem, technical stack, expected outcome, and timeline. I will respond with whether it is a fit, what information is needed, and the most practical next step.",
},
veles: {
description:
"Homegrown AI playtester for Magic: The Gathering deck construction, deployed for the community on Atlas with backend orchestration and disposable simulation workers.",
points: ["community AI service", "Kubernetes deployment", "worker orchestration", "job cleanup"],
},
about: {
role: "DevOps Automation Engineer / Senior SDET",
focusLine: "Platform Engineering · CI/CD · Reliability",
summary: [
"DevOps Automation Engineer and Senior SDET with experience across CI/CD pipelines, Kubernetes, Docker, Linux infrastructure, Python automation, observability, release gates, and system testing.",
"I build deployment workflows, infrastructure utilities, dashboards, operator tools, and test automation that make complex delivery environments easier to run and safer to change.",
"My projects and dashboards run on Atlas, the live bstein.dev platform.",
],
skillGroups: [
{
label: "Platform / Cloud",
items: ["AWS", "GCP", "Kubernetes", "Helm", "Docker Compose", "Harbor", "Artifactory", "IronBank", "Keycloak", "Vault"],
},
{
label: "CI/CD & GitOps",
items: ["Jenkins", "Flux CD", "Argo CD", "GitHub Actions", "Drone", "Terraform", "Ansible"],
},
{
label: "Observability / Data",
items: ["Grafana", "Prometheus", "OpenSearch", "Zabbix", "LogDNA", "ELK", "PostgreSQL", "MySQL", "Redis", "Kafka"],
},
{
label: "Automation",
items: ["Python", "Bash", "Go", "Groovy", "SQL", "Rust"],
},
{
label: "Testing",
items: ["Pytest", "Selenium", "Playwright", "Jest", "API", "E2E", "System", "Chaos", "Load", "Performance", "Contract"],
},
{
label: "Web & AI",
items: ["Vue", "React", "Django", "Flask", "Tailwind", "JQuery", "Codex", "Cursor", "Claude Code"],
},
],
},
},
sdet: {
eyebrow: "AVAILABLE FOR REMOTE SDET AND QA AUTOMATION WORK",
role: "Senior SDET / DevOps Automation Engineer",
shortRole: "SDET / QA Automation",
summary:
"I diagnose, automate, and stabilize the systems between code and production: test automation, CI/CD, Docker and Kubernetes, Linux infrastructure, Python tooling, and observability.",
experienceLine:
"Engineering experience across Boeing, IBM, system and end-to-end testing, deployment automation, infrastructure tooling, and production-minded platform operations.",
techStack: [
"Python",
"Pytest",
"Selenium",
"Playwright",
"API testing",
"Jenkins",
"Docker",
"Kubernetes",
"Linux",
"Grafana",
"Prometheus",
"Terraform",
],
capabilityOrder: [
"Test Automation & SDET",
"CI/CD & Release Flow",
"Docker & Kubernetes",
"Python & Operational Automation",
"Observability & Reliability",
"Platform Tooling & GitOps",
],
serviceOrder: [
"CI/CD quality gates and release machinery",
"Platform bring-up and GitOps operating model",
"AWS and dedicated-host Kubernetes buildouts",
"Cluster recovery, bootstrap, and outage drills",
"Operational tools for real infrastructure",
"Storage, backup, and restore operations",
],
capabilitySection: {
kicker: "Capabilities",
heading: "Where I Add Quality Value",
description:
"Test engineering, delivery automation, and release confidence across the full path from code to operating system.",
},
serviceSection: {
kicker: "Delivery confidence",
heading: "Quality Systems I Can Take From Zero to Reliable",
description:
"I build the checks, diagnostics, release gates, and automation that help teams know whether a system is actually ready to ship.",
},
workSection: {
kicker: "Projects & Experience",
heading: "Testing, Automation, and Platform Work",
description:
"Engineering work across system testing, cloud validation, delivery gates, and live platform operations.",
},
selectedWorkOrder: ["Boeing", "IBM", "Titan Lab + titan-iac", "Atlas operator tools", "Veles"],
engagementOrder: [
"Short-Term Remote Contract",
"Fixed-Scope Technical Rescue",
"Ongoing Engineering Support",
"Senior Remote Engineering Role",
],
contactSection: {
heading: "Have a Test, Release, or Automation Problem?",
description:
"Send the problem, technical stack, expected outcome, and timeline. I will respond with whether it is a fit, what information is needed, and the most practical next step.",
},
veles: {
description:
"Homegrown AI playtester with simulation flows, backend service boundaries, and useful surfaces for API, worker, regression, and result-quality testing.",
points: ["simulation flows", "API coverage", "regression surfaces", "result checks"],
},
about: {
role: "Senior SDET / DevOps Automation Engineer",
focusLine: "Test Automation · Release Quality · CI/CD",
summary: [
"Senior SDET and DevOps Automation Engineer with experience across Python automation, system and end-to-end testing, chaos testing, CI/CD pipelines, Kubernetes, Docker, Linux infrastructure, and observability.",
"I build test frameworks, API and browser automation, release gates, diagnostics, dashboards, and delivery workflows that help teams understand whether a system is ready to ship.",
"My projects and dashboards run on Atlas, the live bstein.dev platform.",
],
skillGroups: [
{
label: "Testing",
items: ["Selenium", "Pytest", "Playwright", "Jest", "SonarQube", "Postman", "API", "E2E", "System", "Chaos", "Load", "Performance", "Stress", "Contract"],
},
{
label: "Automation",
items: ["Python", "Go", "Bash", "Groovy", "SQL", "JavaScript", "TypeScript"],
},
{
label: "CI/CD & GitOps",
items: ["Jenkins", "Drone", "Flux CD", "Argo CD", "GitHub Actions", "Terraform", "Ansible"],
},
{
label: "Platform / Cloud",
items: ["AWS", "GCP", "Kubernetes", "Helm", "Docker Compose", "Harbor", "Artifactory", "IronBank", "Keycloak", "Vault"],
},
{
label: "Observability / Data",
items: ["Grafana", "Prometheus", "OpenSearch", "Zabbix", "LogDNA", "ELK", "PostgreSQL", "MySQL", "Redis", "Kafka"],
},
{
label: "Web & AI",
items: ["Vue", "React", "Django", "Flask", "Tailwind", "JQuery", "Codex", "Cursor", "Claude Code"],
},
],
},
},
dev: {
eyebrow: "AVAILABLE FOR REMOTE PRODUCT, PLATFORM, AND AI SERVICE WORK",
role: "Full-Stack Platform Engineer / Senior SDET",
shortRole: "Product / Platform Engineering",
summary:
"I build useful software on real infrastructure: Vue frontends, Python services, AI integrations, CI/CD, Kubernetes, observability, and testable delivery paths.",
experienceLine:
"Engineering experience across live product delivery, Boeing, IBM, AI services, infrastructure tooling, and production-minded platform operations.",
techStack: [
"Vue",
"Python",
"AI integration",
"API design",
"Docker",
"Kubernetes",
"Jenkins",
"Flux",
"Grafana",
"PostgreSQL",
"Pytest",
"Playwright",
],
capabilityOrder: [
"Python & Operational Automation",
"Docker & Kubernetes",
"Platform Tooling & GitOps",
"CI/CD & Release Flow",
"Test Automation & SDET",
"Observability & Reliability",
],
serviceOrder: [
"Operational tools for real infrastructure",
"Platform bring-up and GitOps operating model",
"CI/CD quality gates and release machinery",
"AWS and dedicated-host Kubernetes buildouts",
"Cluster recovery, bootstrap, and outage drills",
"Storage, backup, and restore operations",
],
capabilitySection: {
kicker: "Capabilities",
heading: "Where I Add Product and Platform Value",
description:
"Application engineering, AI service integration, infrastructure automation, and release quality across the full path from idea to running service.",
},
serviceSection: {
kicker: "Product-to-platform delivery",
heading: "From 0 to 100",
description:
"I can help turn a useful idea into a running service: application workflow, API boundaries, background jobs, CI/CD, Kubernetes deployment, observability, and test coverage.",
},
workSection: {
kicker: "Projects & Experience",
heading: "Product and Platform Showcase",
description:
"Inspectable repos, live services, and professional work samples that show product engineering backed by real operations.",
},
selectedWorkOrder: ["Veles", "Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing"],
engagementOrder: [
"Short-Term Remote Contract",
"Fixed-Scope Technical Rescue",
"Ongoing Engineering Support",
"Senior Remote Engineering Role",
],
contactSection: {
heading: "Have a Product, AI Service, Platform, or Automation Problem?",
description:
"Send the idea or problem, technical stack, expected outcome, and timeline. I will respond with whether it is a fit, what information is needed, and the most practical next step.",
},
veles: {
description:
"Homegrown AI service for Magic: The Gathering deck construction, with product workflow, AI integration, backend orchestration, content seeding, and disposable simulation jobs.",
points: ["AI integration", "product workflow", "service boundaries", "simulation workers"],
},
about: {
role: "Full-Stack Platform Engineer / Senior SDET",
focusLine: "AI Services · Vue · Python · Kubernetes",
summary: [
"Full-stack platform engineer with experience across Vue frontends, Python services, AI integration, CI/CD, Kubernetes, Docker, Linux infrastructure, observability, and testable delivery paths.",
"I build product workflows, APIs, dashboards, background jobs, deployment automation, and test surfaces that turn useful ideas into systems people can actually run.",
"My projects and dashboards run on Atlas, the live bstein.dev platform.",
],
skillGroups: [
{
label: "Product / Web",
items: ["Vue", "Quasar", "React", "Tailwind", "JavaScript", "TypeScript", "JQuery", "FusionCharts"],
},
{
label: "Backend / AI",
items: ["Python", "Django", "Flask", "API design", "AI integration", "workers", "PostgreSQL", "Redis"],
},
{
label: "Platform",
items: ["Kubernetes", "Docker Compose", "Helm", "Flux CD", "Jenkins", "Harbor", "Keycloak", "Vault"],
},
{
label: "Testing",
items: ["Pytest", "Playwright", "Selenium", "Jest", "API", "E2E", "Regression", "Contract"],
},
{
label: "Observability",
items: ["Grafana", "Prometheus", "OpenSearch", "Zabbix", "ELK", "LogDNA"],
},
{
label: "Tools",
items: ["GitHub Actions", "Terraform", "Ansible", "Go", "Bash", "Codex", "Cursor", "Claude Code"],
},
],
},
},
};
export const homepageFocus = focusProfiles[focusMode] ? focusMode : "devops";
const capabilities = [
{
icon: "TA",
title: "Test Automation & SDET",
description:
"System, end-to-end, API, browser, and release-gate testing using Python, Pytest, Selenium, Playwright, and related tooling. Includes test architecture, coverage strategy, flaky-test diagnosis, and CI integration.",
},
{
icon: "CI",
title: "CI/CD & Release Flow",
description:
"Jenkins pipelines, build and test stages, promotion workflows, GitOps delivery, artifact handling, deployment gates, and failure diagnosis.",
},
{
icon: "K8",
title: "Docker & Kubernetes",
description:
"Docker and Docker Compose troubleshooting, Kubernetes workloads, Helm deployments, Flux or Argo CD workflows, service routing, configuration, storage, and deployment debugging.",
},
{
icon: "PY",
title: "Python & Operational Automation",
description:
"Python and Bash tools that replace repetitive work, connect APIs, validate systems, manage configuration, and make operational processes more reliable.",
},
{
icon: "OB",
title: "Observability & Reliability",
description:
"Metrics, dashboards, logs, alerting, and root-cause investigation using Grafana, Prometheus, Zabbix, OpenSearch, ELK, and related systems.",
},
{
icon: "GT",
title: "Platform Tooling & GitOps",
description:
"Internal tools, infrastructure utilities, configuration-drift prevention, environment management, GitOps workflows, and production-minded platform operations.",
},
];
const services = [
{
title: "Platform bring-up and GitOps operating model",
investigation:
"Turn ad hoc services into a source-controlled platform: Kubernetes layout, Kustomize or Flux structure, environments, ingress, identity, storage, secrets, and validation workflow.",
outcome:
"A platform that can be changed through reviewable commits, reconciled by automation, and recovered without mystery manual edits.",
},
{
title: "Cluster recovery, bootstrap, and outage drills",
investigation:
"Encode startup and shutdown order, node reachability, Flux health, workload convergence, ingress checks, service checklists, critical endpoints, and stability soaks.",
outcome:
"Repeatable recovery runs with status reports and gaps converted into automation instead of tribal knowledge.",
},
{
title: "AWS and dedicated-host Kubernetes buildouts",
investigation:
"Design and stand up Kubernetes where the work actually lives: AWS VPCs, subnets, IAM, security groups, EKS or k3s on EC2, plus dedicated-host clusters with ingress, storage, identity, observability, and Terraform-backed repeatability.",
outcome:
"A documented cluster path from empty account or bare host to running services, with the foundations needed for CI/CD, GitOps, monitoring, and future handoff.",
},
{
title: "CI/CD quality gates and release machinery",
investigation:
"Build Jenkins pipelines, container/image promotion, coverage floors, lint and security gates, artifact publishing, branch-aware metrics, and failure reporting.",
outcome:
"A delivery path that tells teams what failed, blocks unsafe changes, and produces evidence for every release.",
},
{
title: "Storage, backup, and restore operations",
investigation:
"Map stateful workloads, PVCs, Longhorn volumes, policy backups, namespace restore flows, auth boundaries, and backup freshness metrics.",
outcome:
"Recoverable data paths, visible backup health, safer restore testing, and fewer surprise gaps around stateful services.",
},
{
title: "Operational tools for real infrastructure",
investigation:
"Build CLIs, web UIs, diagnostics, logs, Prometheus metrics, remote-control surfaces, and admin workflows around the parts operators actually touch.",
outcome:
"A platform that is easier to operate under pressure because the important actions and evidence are available in one place.",
},
];
function orderedBy(items, order) {
const rank = new Map(order.map((title, index) => [title, index]));
return [...items].sort((a, b) => (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99));
}
const selectedFocus = focusProfiles[homepageFocus];
const workDetailsByFocus = {
devops: {
titan: {
kind: "Live platform",
description:
"Public IaC for bstein.dev: k3s, Flux, CI/CD, identity, ingress, Longhorn, observability, hosted services, recovery paths, and member onboarding.",
points: [
"IaC repo",
"Kubernetes",
"GitOps",
"CI/CD",
"identity",
"registry",
"storage",
"observability",
"backups",
"recovery",
],
},
operators: {
kind: "Public tooling",
description:
"Public tools for real Atlas operations: remote control, job reporting, backups, recovery image creation, sentinels, and UPS orchestration.",
points: ["remote control", "job reporting", "backup status", "recovery images", "sentinels", "UPS orchestration"],
},
ibm: {
description:
"IBM Cloud automation across infrastructure validation, GPU environments, Terraform workflows, API health, and operational monitoring.",
points: ["Terraform", "cloud validation", "RHEL/SUSE", "GPU environments", "Zabbix", "API health", "autoscaling"],
},
boeing: {
description:
"DevSecOps-adjacent automation, Flux-controlled environments, Jenkins promotion gates, Docker Compose drift protection, and infrastructure diagnosis.",
points: ["Flux envs", "Jenkins gates", "Docker Compose", "drift protection", "infra diagnosis", "Lanterna", "Python tools"],
},
},
sdet: {
titan: {
kind: "Live platform",
description:
"A running member platform with authentication, onboarding, dashboards, service boundaries, metrics, health checks, and failure modes that can be tested end to end.",
points: ["auth flows", "onboarding", "service checks", "dashboard smoke", "metrics", "failure fallback", "release gates"],
},
operators: {
kind: "Automation tooling",
description:
"Operational tools with repeatable checks, job status, failure reporting, and validation surfaces useful for system-level testing.",
points: ["state checks", "job reports", "failure diagnosis", "repeatable jobs", "validation flows", "safe actions"],
},
ibm: {
description:
"IBM Cloud test automation across system validation, GPU environments, API workflows, Go and Terraform tests, and operational health checks.",
points: ["Python E2E", "Go tests", "Terraform tests", "GPU validation", "API workflows", "data-center validation", "Zabbix"],
},
boeing: {
description:
"System and end-to-end testing, Python and Selenium automation, microservice and web UI validation, release gates, and failure diagnostics.",
points: ["Python", "Selenium", "system E2E", "microservices", "web UI", "quality gates", "test architecture"],
},
},
dev: {
titan: {
kind: "Live platform",
description:
"A productized platform surface with member accounts, service catalog, OAuth, dashboards, hosted applications, AI workloads, and delivery automation.",
points: ["Vue frontend", "member system", "service catalog", "OAuth", "AI workloads", "CI/CD", "observability"],
},
operators: {
kind: "Operator products",
description:
"Small internal products for real operations: admin workflows, job surfaces, status reporting, remote actions, and automation APIs.",
points: ["admin workflows", "job surfaces", "status reports", "remote actions", "automation APIs", "operator UX"],
},
ibm: {
description:
"IBM Cloud engineering across Python, Go, Terraform, API workflows, platform health dashboards, feature demos, and knowledge transfer.",
points: ["Python", "Go", "Terraform", "API workflows", "health dashboards", "feature demos", "knowledge transfer"],
},
boeing: {
description:
"Engineering automation around system workflows, Python tools, release visualization, environment state, and complex integration surfaces.",
points: ["Python tools", "Lanterna", "workflow visibility", "system integration", "environment state", "automation"],
},
},
};
const workDetails = workDetailsByFocus[homepageFocus] || workDetailsByFocus.devops;
export const homepageContent = {
professionalName: "Brad Stein",
role: selectedFocus.role,
shortRole: selectedFocus.shortRole,
focus: homepageFocus,
eyebrow: selectedFocus.eyebrow,
title: "Brad Stein | SDET, DevOps Automation & Platform Engineering",
description:
"Senior SDET and DevOps Automation Engineer helping teams improve test automation, CI/CD, Docker and Kubernetes reliability, Python tooling, Linux systems, and observability. Available for remote project and contract work.",
summary: selectedFocus.summary,
experienceLine: selectedFocus.experienceLine,
availability: {
visible: true,
label: "Available for remote project, contract, and engineering opportunities",
timeZone: "Remote, US-friendly time zones",
updatedAt: "2026-06-19",
},
primaryContact: {
email: "brad@bstein.dev",
subject: "Project, contract, or engineering inquiry from bstein.dev",
},
resumeUrl: "/about",
resumePdfUrl: "",
linkedInUrl: "https://www.linkedin.com/in/steinbradley/",
sourceUrl: "https://scm.bstein.dev/bstein",
platformMetricsUrl: "https://metrics.bstein.dev",
techStack: selectedFocus.techStack,
credibility: [
"Professional experience: Boeing",
"Professional experience: IBM",
"Python automation",
"Kubernetes and CI/CD",
"Remote availability",
],
capabilitySection: selectedFocus.capabilitySection,
capabilities: orderedBy(capabilities, selectedFocus.capabilityOrder),
serviceSection: selectedFocus.serviceSection,
services: orderedBy(services, selectedFocus.serviceOrder),
workSection: selectedFocus.workSection,
contactSection: selectedFocus.contactSection,
about: selectedFocus.about,
selectedWork: [
{
icon: "IaC",
kind: workDetails.titan.kind,
title: "Titan Lab + titan-iac",
description: workDetails.titan.description,
points: workDetails.titan.points,
links: [
{
label: "IaC repo",
href: "https://scm.bstein.dev/bstein/titan-iac",
},
],
},
{
icon: "OPS",
kind: workDetails.operators.kind,
title: "Atlas operator tools",
description: workDetails.operators.description,
points: workDetails.operators.points,
links: [
{ label: "lesavka", href: "https://scm.bstein.dev/bstein/lesavka" },
{ label: "ariadne", href: "https://scm.bstein.dev/bstein/ariadne" },
{ label: "soteria", href: "https://scm.bstein.dev/bstein/soteria" },
{ label: "metis", href: "https://scm.bstein.dev/bstein/metis" },
{ label: "ananke", href: "https://scm.bstein.dev/bstein/ananke" },
],
},
{
icon: "IBM",
kind: "Professional experience",
title: "IBM",
description: workDetails.ibm.description,
points: workDetails.ibm.points,
},
{
icon: "BA",
kind: "Professional experience",
title: "Boeing",
description: workDetails.boeing.description,
points: workDetails.boeing.points,
},
{
icon: "AI",
kind: "Community AI service",
title: "Veles",
description: selectedFocus.veles.description,
points: selectedFocus.veles.points,
weight: "minor",
links: [
{
label: "Live app",
href: "https://veles.bstein.dev/",
},
{
label: "Content seed",
href: "https://scm.bstein.dev/bstein/veles-content",
},
],
},
].sort((a, b) => {
const rank = new Map(selectedFocus.selectedWorkOrder.map((title, index) => [title, index]));
return (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99);
}),
engagementTypes: [
{
title: "Fixed-Scope Technical Rescue",
duration: "Several hours to several days.",
description:
"A bounded diagnosis or repair for a failing pipeline, deployment, automation workflow, test suite, or infrastructure problem.",
},
{
title: "Short-Term Remote Contract",
duration: "Several weeks to several months.",
description:
"Focused engineering capacity for test automation, CI/CD, infrastructure tooling, platform quality, release flow, or technical backlog reduction.",
},
{
title: "Ongoing Engineering Support",
duration: "Recurring support based on the operating need.",
description:
"Recurring help with automation, quality engineering, operational tooling, release systems, and platform reliability.",
},
{
title: "Senior Remote Engineering Role",
duration: "Full-time remote roles where the fit is strong.",
description:
homepageFocus === "devops"
? "I am open to well-aligned remote DevOps Automation, platform engineering, CI/CD, infrastructure automation, Kubernetes, Linux, or systems reliability roles. SDET and test automation depth comes with it."
: "I am open to well-aligned remote SDET, QA Automation, systems testing, release quality, CI/CD, or DevOps Automation roles. Platform engineering depth comes with it.",
},
].sort((a, b) => {
const rank = new Map(selectedFocus.engagementOrder.map((title, index) => [title, index]));
return (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99);
}),
processSteps: [
{
title: "Share the Problem",
description:
"Send a concise description, relevant stack, desired outcome, timeline, and any safe logs, screenshots, or links.",
},
{
title: "Confirm Fit and Scope",
description:
"Review the problem, identify missing information, and determine whether it fits a fixed-scope diagnostic, project, or contract engagement.",
},
{
title: "Diagnose and Implement",
description:
"Work proceeds through a written scope, bounded investigation, implementation, testing, and documented findings.",
},
{
title: "Handoff",
description:
"Receive the fix or deliverable, root-cause notes, operational guidance, and a concise handoff appropriate to the project.",
},
],
memberAccessCopy: {
heading: "Titan Lab Member Access",
description:
"Titan Lab member accounts are for family, friends, and the lucky few with an invite: register for access or sign in here.",
authenticated:
"Use the existing member tools for dashboards, account management, onboarding, and approved service access.",
restricted:
"If access is pending or restricted, use the existing request-access status and onboarding flow. No platform permissions are bypassed here.",
},
memberServices: [
{
name: "Keycloak",
icon: "🔐",
href: "https://sso.bstein.dev",
description: "Use one account to sign in across bstein.dev.",
},
{
name: "Nextcloud",
icon: "☁️",
href: "https://cloud.bstein.dev",
description: "Keep files, photos, office docs, and personal cloud data in sync.",
},
{
name: "Mailu",
icon: "✉️",
href: "https://mail.bstein.dev",
description: "Use your bstein.dev mailbox and webmail.",
},
{
name: "Element",
icon: "💬",
href: "https://live.bstein.dev",
description: "Chat in private rooms and join voice or video calls.",
},
{
name: "Vaultwarden",
icon: "🔑",
href: "https://vault.bstein.dev",
description: "Store passwords, secure notes, and shared secrets privately.",
},
{
name: "Outline",
icon: "📝",
href: "https://notes.bstein.dev",
description: "Read and write shared notes, guides, and docs.",
},
{
name: "Planka",
icon: "🧭",
href: "https://tasks.bstein.dev",
description: "Track shared projects and personal task boards.",
},
{
name: "Jellyfin",
icon: "🎬",
href: "https://stream.bstein.dev",
description: "Stream the family media library from your devices.",
},
{
name: "Pegasus",
icon: "📤",
href: "https://pegasus.bstein.dev",
description: "Upload media into the shared streaming library.",
},
{
name: "Wger",
icon: "🏋️",
href: "https://health.bstein.dev",
description: "Track workouts, routines, and nutrition.",
},
{
name: "Actual Budget",
icon: "💸",
href: "https://budget.bstein.dev",
description: "Plan budgets with private, local-first finance tracking.",
},
{
name: "Firefly III",
icon: "💵",
href: "https://money.bstein.dev",
description: "Review accounts, spending, and personal finance history.",
},
{
name: "Gitea",
icon: "🍵",
href: "https://scm.bstein.dev",
description: "Host Git repos and collaborate on code projects.",
},
{
name: "Jenkins",
icon: "🏗️",
href: "https://ci.bstein.dev",
description: "Run builds and automation for lab projects.",
},
{
name: "Harbor",
icon: "⚓",
href: "https://registry.bstein.dev",
description: "Publish and pull container images for projects.",
},
{
name: "Grafana",
icon: "📈",
href: "https://metrics.bstein.dev",
description: "Check dashboards for lab and service health.",
},
{
name: "AI Chat",
icon: "🤖",
href: "https://bstein.dev/ai/chat",
description: "Ask the Titan Lab AI assistant for help.",
},
{
name: "Veles",
icon: "🃏",
href: "https://veles.bstein.dev/",
description: "Test Magic: The Gathering deck ideas with AI playtesting.",
},
{
name: "Monero",
icon: "🪙",
href: "https://bstein.dev/monero",
description: "Use the private RPC endpoint through the Monero guide.",
},
],
};
export function contactMailto() {
const email = homepageContent.primaryContact.email;
const subject = encodeURIComponent(homepageContent.primaryContact.subject);
return `mailto:${email}?subject=${subject}`;
}

View File

@ -1,5 +1,5 @@
/**
* Return the static Atlas and Oceanus hardware inventory used as fallback data.
* Return the static Atlas hardware inventory used as fallback data.
*
* WHY: the home page needs stable content when live cluster data cannot be
* fetched during startup or testing.
@ -33,13 +33,13 @@ export function fallbackHardware() {
{ name: "titan-20", role: "jetson ai workload", hardware: "jetson xavier", status: "ready" },
{ name: "titan-21", role: "jetson ai workload", hardware: "jetson xavier", status: "ready" },
{ name: "titan-22", role: "gpu mini-pc (jellyfin)", hardware: "mini pc", status: "ready" },
{ name: "titan-23", alias: "oceanus", role: "worker (upcoming AI workload staging)", hardware: "epyc-24c", status: "ready" },
{ name: "titan-24", alias: "tethys", role: "worker (utility + metrics)", hardware: "ryzen-3900x", status: "ready" },
],
},
],
specialty: [
{ name: "titan-db", alias: "atlas-db", role: "control-plane database (postgres)", hardware: "rpi5", status: "active" },
{ name: "titan-23", alias: "oceanus", role: "SUI validator (baremetal)", status: "active", hardware: "epyc-24c" },
{ name: "titan-24", alias: "tethys", role: "bridge node + scraper for oceanus metrics", status: "active", hardware: "ryzen-3900x" },
{ name: "titan-jh", alias: "theia", role: "bastion / KVM landing / lesavka", status: "active", hardware: "rpi5" },
],
};
@ -221,27 +221,27 @@ export function fallbackServices() {
icon: "🪙",
category: "crypto",
summary: "Private monero node for monero wallets.",
link: "/monero",
link: "https://bstein.dev/monero",
host: "monerod.crypto.svc.cluster.local:18081",
},
{
name: "Oceanus",
icon: "🌊",
category: "crypto",
summary: "Dedicated SUI Validator - Planned.",
link: "#",
host: "oceanus",
status: "planned",
},
{
name: "AI Chat",
icon: "🤖",
category: "ai",
summary: "Customized LLM for the titan home lab.",
link: "/ai/chat",
link: "https://bstein.dev/ai/chat",
host: "chat.ai.bstein.dev",
status: "live",
},
{
name: "Veles",
icon: "🃏",
category: "ai",
summary: "Magic: The Gathering AI playtester for deck construction.",
link: "https://veles.bstein.dev/",
host: "veles.bstein.dev",
status: "live",
},
{
name: "AI Vision",
icon: "👁️",
@ -306,7 +306,7 @@ export function fallbackNetwork() {
{
name: "metrics",
path: "Atlas scraping -> Prometheus -> Grafana -> metrics.bstein.dev",
notes: "titan-24 scrapes oceanus (titan-23).",
notes: "Cluster and service telemetry feed public dashboards where safe.",
},
],
ingress_gateway: "Traefik with oauth2-proxy and Keycloak; Longhorn backs stateful ingress targets.",
@ -322,7 +322,7 @@ export function fallbackNetwork() {
export function fallbackMetrics() {
return {
dashboard: "https://metrics.bstein.dev",
description: "Atlas + Oceanus metrics.",
description: "Atlas platform metrics.",
};
}
@ -337,67 +337,38 @@ export function fallbackMetrics() {
*/
export function buildHardwareDiagram(_data) {
return `
flowchart TB
subgraph TitanLab["Titan Lab (25 nodes)"]
subgraph Atlas["Atlas (k3s cluster)"]
subgraph CP["Control plane (rpi5)"]
titan0a["titan-0a<br/>rpi5 4c/8g"]
titan0b["titan-0b<br/>rpi5 4c/8g"]
titan0c["titan-0c<br/>rpi5 4c/8g"]
end
flowchart LR
Users["Members + public visitors"] --> Edge["DNS / TLS / Traefik"]
subgraph Pi5["Workers (rpi5)"]
titan04["titan-04"]
titan05["titan-05"]
titan06["titan-06"]
titan07["titan-07"]
titan08["titan-08"]
titan09["titan-09"]
titan10["titan-10"]
titan11["titan-11"]
end
subgraph Storage["Storage workers (rpi4 + disks)"]
titan12["titan-12<br/>8TB astreae + 12TB asteria"]
titan13["titan-13<br/>8TB astreae + 12TB asteria"]
titan14["titan-14<br/>8TB astreae + 12TB asteria"]
titan15["titan-15<br/>8TB astreae + 12TB asteria"]
titan16["titan-16<br/>offline"]:::down
titan17["titan-17"]
titan18["titan-18"]
titan19["titan-19"]
end
subgraph Accel["Accelerators + heavy nodes"]
titan20["titan-20<br/>Jetson Xavier 6c/16g"]
titan21["titan-21<br/>Jetson Xavier 6c/16g"]
titan22["titan-22<br/>10c/32g<br/>GPU streaming"]
titan24["titan-24 (tethys)<br/>12c/64g<br/>bridge + metrics"]
end
longhorn["Longhorn<br/>astreae: 4x8TB<br/>asteria: 4x12TB"]
traefik["Traefik ingress"]
keycloak["Keycloak SSO"]
services["Services<br/>cloud / stream / ci / registry / cd / secret"]
keycloak --> traefik
traefik --> services
services --> longhorn
longhorn --> Storage
subgraph TitanLab["Titan Lab"]
subgraph Atlas["Atlas k3s cluster"]
Control["HA control plane"]
Workers["service workers"]
Storage["Longhorn storage pool"]
Heavy["AI / media / utility nodes<br/>Oceanus + Tethys included"]
Services["member services<br/>identity / cloud / media / CI / registry / AI"]
end
subgraph Dedicated["Dedicated hosts (outside Atlas)"]
titanDb["titan-db<br/>Postgres for HA control plane<br/>rpi5 4c/8g"]
theia["titan-jh (theia)<br/>bastion<br/>rpi5 4c/8g"]
oceanus["titan-23 (oceanus)<br/>SUI validator<br/>24c/256g<br/>2.5GbE"]
subgraph Dedicated["Dedicated non-cluster hosts"]
DB["database node"]
Jump["jumphost / KVM landing"]
end
titanDb -->|DB| titan0a
theia -->|ssh| titan0a
oceanus -->|metrics| titan24
end
classDef down fill:#311023,stroke:#ff4f93,color:#fff,stroke-width:2px;
Edge --> Services
Control --> Workers
Workers --> Services
Services --> Storage
Heavy --> Services
DB --> Control
Jump --> Control
classDef cluster fill:#0d1a2b,stroke:#4fb3ff,color:#eef6ff;
classDef edge fill:#082421,stroke:#00e5c5,color:#effffc;
classDef dedicated fill:#21182b,stroke:#b58cff,color:#f8f2ff;
class TitanLab,Atlas,Control,Workers,Storage,Heavy,Services cluster;
class Users,Edge edge;
class Dedicated,DB,Jump dedicated;
`;
}
@ -409,25 +380,26 @@ flowchart TB
*/
export function buildNetworkDiagram() {
return `
sequenceDiagram
participant U as User
participant DNS as DNS (*.bstein.dev)
participant T as Traefik (Atlas)
participant A as auth.bstein.dev (oauth2-proxy)
participant K as sso.bstein.dev (Keycloak)
participant S as Service (cloud/stream/ci/registry/cd/secret)
participant L as Longhorn PVC
flowchart LR
Browser["browser"] --> DNS["DNS + TLS"]
DNS --> Traefik["Traefik ingress"]
U->>DNS: resolve host
DNS-->>U: Traefik VIP
U->>T: HTTPS request
T->>A: forwardAuth
A->>K: OIDC login/refresh
K-->>A: token
A-->>T: allow
T->>S: route to service
S-->>L: persistent storage operations
S-->>U: response / stream / artifact
Traefik --> Public["public routes<br/>home / metrics / selected apps"]
Traefik --> Auth["auth gate<br/>oauth2-proxy"]
Auth --> Keycloak["Keycloak SSO"]
Keycloak --> Auth
Auth --> Member["member services"]
Member --> Storage["Longhorn PVCs"]
Member --> Logs["logs / metrics"]
Logs --> Grafana["Grafana dashboards"]
classDef public fill:#082421,stroke:#00e5c5,color:#effffc;
classDef protected fill:#22172b,stroke:#b58cff,color:#f8f2ff;
classDef data fill:#1e2430,stroke:#7aa2ff,color:#eef6ff;
class Browser,DNS,Traefik,Public public;
class Auth,Keycloak,Member protected;
class Storage,Logs,Grafana data;
`;
}
@ -440,16 +412,26 @@ sequenceDiagram
export function buildPipelineDiagram() {
return `
flowchart LR
dev[Developer] -->|push| gitea[scm.bstein.dev]
gitea -->|webhook| jenkins[ci.bstein.dev]
jenkins -->|build + push| harbor[registry.bstein.dev]
harbor -->|image update| flux[cd.bstein.dev]
flux -->|reconcile| atlas[Atlas]
atlas -->|deploy| svc[cloud / stream / secret / other apps]
Source["scm.bstein.dev<br/>source of truth"] --> CI["Jenkins<br/>build + test"]
CI --> Image["Harbor<br/>image registry"]
Image --> GitOps["Flux<br/>reconcile desired state"]
GitOps --> Atlas["Atlas<br/>running workloads"]
keycloak[sso.bstein.dev] --> gitea
keycloak --> jenkins
keycloak --> harbor
keycloak --> flux
CI --> Gates["quality gates<br/>tests / lint / reports"]
Atlas --> Observe["Prometheus + Grafana<br/>health and evidence"]
Observe --> Feedback["fix / tune / automate"]
Feedback --> Source
Keycloak["Keycloak SSO"] --> Source
Keycloak --> CI
Keycloak --> Image
Keycloak --> GitOps
classDef source fill:#082421,stroke:#00e5c5,color:#effffc;
classDef delivery fill:#0d1a2b,stroke:#4fb3ff,color:#eef6ff;
classDef guard fill:#22172b,stroke:#b58cff,color:#f8f2ff;
class Source,Feedback source;
class CI,Image,GitOps,Atlas,Observe delivery;
class Gates,Keycloak guard;
`;
}

View File

@ -0,0 +1,135 @@
import { computed, reactive, watch } from "vue";
import { auth, authFetch } from "../auth";
const state = reactive({
loading: false,
loaded: false,
status: "",
onboardingUrl: "/onboarding",
dashboardUrl: "/apps",
accountUrl: "/account",
accessAllowed: true,
error: "",
});
let watcherStarted = false;
let requestToken = 0;
function normalizeInternalUrl(value, fallback) {
if (typeof value !== "string" || !value.trim()) return fallback;
try {
const url = new URL(value, window.location.origin);
if (url.origin === window.location.origin) return `${url.pathname}${url.search}${url.hash}`;
return value;
} catch {
return value.startsWith("/") ? value : fallback;
}
}
function resetState() {
state.loading = false;
state.loaded = false;
state.status = "";
state.onboardingUrl = "/onboarding";
state.dashboardUrl = "/apps";
state.accountUrl = "/account";
state.accessAllowed = true;
state.error = "";
}
async function refreshMemberState() {
if (!auth.ready || !auth.authenticated) {
resetState();
return;
}
const token = (requestToken += 1);
state.loading = true;
state.error = "";
try {
const resp = await authFetch("/api/account/member-state", {
headers: { Accept: "application/json" },
cache: "no-store",
});
const data = await resp.json().catch(() => ({}));
if (token !== requestToken) return;
if (resp.status === 403) {
state.loaded = true;
state.status = "restricted";
state.accessAllowed = false;
state.onboardingUrl = "/request-access";
state.dashboardUrl = "/apps";
state.accountUrl = "/account";
return;
}
if (!resp.ok) throw new Error(data.error || `status ${resp.status}`);
state.loaded = true;
state.accessAllowed = true;
state.status = data.status || "unknown";
state.onboardingUrl = normalizeInternalUrl(data.onboarding_url, "/onboarding");
state.dashboardUrl = normalizeInternalUrl(data.dashboard_url, "/apps");
state.accountUrl = normalizeInternalUrl(data.account_url, "/account");
} catch (err) {
if (token !== requestToken) return;
state.loaded = true;
state.status = "unknown";
state.accessAllowed = true;
state.error = err?.message || "Member state unavailable";
} finally {
if (token === requestToken) state.loading = false;
}
}
function startWatcher() {
if (watcherStarted) return;
watcherStarted = true;
watch(
() => [auth.ready, auth.authenticated, auth.token],
() => {
refreshMemberState();
},
{ immediate: true },
);
}
function memberActionForState() {
if (!auth.enabled) return null;
if (!auth.ready) {
return { kind: "loading", label: "Checking Session", to: "", secondary: "" };
}
if (!auth.authenticated) {
return { kind: "signed-out", label: "Sign In", to: "", secondary: "/request-access" };
}
if (auth.emailVerified === false) {
return {
kind: "verify",
label: "Continue Verification",
to: normalizeInternalUrl(auth.accountUrl, "/request-access"),
secondary: state.accountUrl,
};
}
if (state.loading && !state.loaded) {
return { kind: "loading", label: "Checking Account", to: "", secondary: state.accountUrl };
}
if (["pending_email_verification", "pending"].includes(state.status)) {
return { kind: "pending", label: "Check Access Status", to: "/request-access", secondary: state.accountUrl };
}
if (["accounts_building", "awaiting_onboarding"].includes(state.status)) {
return { kind: "onboarding", label: "Continue Onboarding", to: state.onboardingUrl, secondary: state.accountUrl };
}
if (state.status === "restricted" || !state.accessAllowed) {
return { kind: "restricted", label: "Account Status", to: "/request-access", secondary: state.accountUrl };
}
return { kind: "ready", label: "Open Services", to: state.dashboardUrl, secondary: state.accountUrl };
}
export function useMemberAccessState() {
startWatcher();
return {
auth,
memberState: state,
memberAction: computed(memberActionForState),
refreshMemberState,
};
}

View File

@ -7,13 +7,13 @@
</div>
<div class="contact">
<div class="name">Brad Stein</div>
<div class="role">Senior Software Development Engineer </div>
<div class="role">SDET/ DevOps / Platform Tooling</div>
<div class="role">{{ about.role }}</div>
<div class="role">{{ about.focusLine }}</div>
<div class="meta">US Citizen · Remote</div>
<div class="links">
<a href="https://www.linkedin.com/in/steinbradley/" target="_blank" rel="noreferrer">LinkedIn</a>
<a href="https://scm.bstein.dev/bstein" target="_blank" rel="noreferrer">Gitea</a>
<a href="https://metrics.bstein.dev" target="_blank" rel="noreferrer">Metrics</a>
<a href="https://www.linkedin.com/in/steinbradley/" target="_blank" rel="noopener noreferrer">LinkedIn</a>
<a href="https://scm.bstein.dev/bstein" target="_blank" rel="noopener noreferrer">Gitea</a>
<a href="https://metrics.bstein.dev" target="_blank" rel="noopener noreferrer">Metrics</a>
<a href="mailto:brad@bstein.dev">brad@bstein.dev</a>
</div>
</div>
@ -21,22 +21,21 @@
<div class="right">
<h1>About Me</h1>
<div class="copy">
<p v-for="paragraph in about.summary.slice(0, 2)" :key="paragraph">{{ paragraph }}</p>
<p>
Senior Backend &amp; DevOps engineer focused on making systems reliable. I build Python-driven tools on Linux, Kubernetes, and CI/CD
to keep distributed systems healthy.
</p>
<p>
I like simplifying environments and release pipelines so teams can ship and operate confidently. Recent work: platform tooling for a
Flux-managed Kubernetes microservice stack on the U.S. Space Forces PTES program.
</p>
<p>
My projects and dashboards run on my local <span class="mono">k3s</span>: see
<a href="https://scm.bstein.dev/bstein" target="_blank" rel="noreferrer">scm.bstein.dev</a> and
<a href="https://metrics.bstein.dev" target="_blank" rel="noreferrer">metrics.bstein.dev</a>.
Public platform work is visible in the
<a href="https://scm.bstein.dev/bstein/titan-iac" target="_blank" rel="noopener noreferrer">Titan IaC repo</a>,
<a href="https://scm.bstein.dev/bstein" target="_blank" rel="noopener noreferrer">scm.bstein.dev</a> and
<a href="https://metrics.bstein.dev" target="_blank" rel="noopener noreferrer">metrics.bstein.dev</a>.
</p>
</div>
<div class="highlights">
<div v-for="skill in skills" :key="skill" class="pill mono">{{ skill }}</div>
<div class="skill-groups" aria-label="Résumé skill summary">
<div v-for="group in about.skillGroups" :key="group.label" class="skill-group">
<h2>{{ group.label }}</h2>
<div class="highlights">
<div v-for="skill in group.items" :key="skill" class="pill mono">{{ skill }}</div>
</div>
</div>
</div>
</div>
</section>
@ -51,7 +50,13 @@
<div class="dot"></div>
<div>
<div class="entry-title">{{ item.title }}</div>
<div class="entry-sub">{{ item.company }} · {{ item.dates }}</div>
<div class="entry-sub">
<a v-if="item.companyUrl" :href="item.companyUrl" target="_blank" rel="noopener noreferrer">
{{ item.company }}
</a>
<span v-else>{{ item.company }}</span>
<span> · {{ item.dates }}</span>
</div>
<ul>
<li v-for="point in item.points" :key="point">{{ point }}</li>
</ul>
@ -60,33 +65,49 @@
</div>
<div class="divider"></div>
<p class="note">
<a href="https://www.linkedin.com/in/steinbradley/" target="_blank" rel="noreferrer">
Full role history and exact dates are on LinkedIn.
</a>
Web version aligned to <span class="mono">Stein.Brad.Resume.2026.06.pdf</span>. Phone and street address are
intentionally omitted from the public site.
</p>
</section>
<section class="card">
<div class="section-head">
<h2>Titan Lab</h2>
<span class="pill mono">atlas + oceanus</span>
<span class="pill mono">atlas platform</span>
</div>
<div class="copy">
<p>
The Titan Lab is my 26-node (and growing) homelab with a production mindset: security, monitoring, and repeatable changes. The core is
<span class="mono">Atlas</span>, a GitOps-managed <span class="mono">k3s</span> cluster where services are reconciled by
<span class="mono">Flux</span>.
Titan Lab is the live platform behind bstein.dev. The core is <span class="mono">Atlas</span>, a GitOps-managed
<span class="mono">k3s</span> cluster where services are reconciled by <span class="mono">Flux</span> and operated
with the same habits I use professionally: source of truth, quality gates, observability, identity, storage,
recovery paths, and documented handoff.
</p>
<p>
<span class="mono">Atlas</span> is my attempt to fully replace all online services that I need or use with self-hosted versions. I care
deeply about security both for the lab and the cluster and personally. My first hosted services was <span class="mono">VaultWarden</span>
but now the cluster hosts everything from email to ai. In my freetime, I'm always working on <span class="mono">Atlas</span> with small
services or organization improvements to make it better and cleaner and cooler.
The platform runs identity, source control, CI/CD, container delivery, observability, storage, member services,
and experimental AI workloads. It is also where I build automation around bootstrap, recovery, service health,
and operator workflows instead of leaving them as manual notes.
</p>
<p>
<span class="mono">Oceanus</span> is an intentionally separated host for validator workloads while still feeding data back into the same
observability stack. SUI is a crypto currency I follow and believe in and so <span class="mono">Oceanus</span> is in my lab and is dedicated
hardware to extend their infrastructure and make the SUI project more resilient.
<span class="mono">Oceanus</span> and <span class="mono">Tethys</span> are now part of Atlas capacity instead of separate public status surfaces.
The dedicated non-cluster hosts are the control-plane database node and the jumphost. <span class="mono">Veles</span>,
the MTG AI playtester, is deployed at
<a href="https://veles.bstein.dev/" target="_blank" rel="noopener noreferrer">veles.bstein.dev</a>.
</p>
</div>
</section>
<section class="card">
<div class="section-head">
<h2>Education</h2>
<span class="pill mono">UT Austin</span>
</div>
<div class="copy">
<p>
Studied Mechanical Engineering and Applied Math at the University of Texas at Austin, including Java, C++,
probability, statistics, complex analysis, numerical methods, number theory, and differential equations.
</p>
<p>
Participated in the Emerging Scholars honors calculus program and later served as an assistant for it.
</p>
</div>
</section>
@ -94,113 +115,94 @@
</template>
<script setup>
const skills = [
"Python",
"Linux",
"Kubernetes (k3s/k8s)",
"Containers (Docker/OCI)",
"GitOps (Flux)",
"CI/CD (Jenkins)",
"Release gating",
"Test automation",
"Keycloak / OIDC",
"Grafana + VictoriaMetrics",
"Traefik ingress",
"Longhorn storage",
"Go",
"Rust",
"Bash",
"Terraform",
"SQL",
];
import { homepageContent } from "../data/homepageContent";
const about = homepageContent.about;
const timeline = [
{
id: "actalent-boeing-ptes",
title: "Senior Software Development Engineer in Test",
company: "Boeing (Actalent contract)",
dates: "Sep 2023 Oct 2025",
points: [
"Built internal tools and infrastructure around a Kubernetes microservice platform (PTES).",
"Created Lanterna, a visualization tool mapping service and environment relationships from a Flux-controlled monorepo.",
"Designed Jenkins promotion workflows and test-gating pipelines to move builds safely through environments.",
"Built TaskWatcher for drift protection and orchestration of non-Kubernetes systems, including capture of transient crypto artifacts.",
],
},
{
id: "titan-lab-architect",
title: "Titan Lab Architect",
company: "Titan Lab (personal platform)",
title: "Kubernetes Cluster Development",
company: "Titan Lab",
companyUrl: "https://scm.bstein.dev/bstein/titan-iac",
dates: "Apr 2020 Present",
points: [
"Operate a mixed arm64/amd64 environment with GitOps (Gitea -> Jenkins -> Harbor -> Flux).",
"Centralized identity with Keycloak and front services via Traefik ingress.",
"Run tiered Longhorn storage: astreae (system) and asteria (user).",
"Build observability with Grafana + VictoriaMetrics and OpenSearch dashboards around real service health and log tracking.",
"Audio, video, and text communication on a Matrix-LiveKit-Coturn-Element stack with mobile compatibility and AI chat integration.",
"Video streaming of home movies via Jellyfin and instant uploading/publishing with Pegasus.",
"Host crypto projects with a XRM node and a SUI validator.",
"Has a knowledge base about the cluster itself for AI bot awareness.",
"Vault based secret management - no critical information in Kubernetes secrets.",
"Created and operate a local k3s cluster with metrics for home and self-hosted services.",
"Run personal SCM, CI/CD, password management, mail, cloud storage, movie streaming, video conferencing, AI chatbot, and cluster automation services.",
"Build automation around bootstrap, recovery, service health, operator workflows, identity, storage, and dashboards.",
],
},
{
id: "actalent-boeing-ptes",
title: "Software Development Engineer in Test",
company: "Boeing",
companyUrl: "https://www.boeing.com/",
dates: "Oct 2023 Oct 2025",
points: [
"Consulted through Actalent on a complex communications program at the intersection of test engineering, DevSecOps, and infrastructure automation.",
"Created system and end-to-end tests using Python and Selenium to validate microservice and web UI interactions.",
"Built TaskWatcher tooling for key capture/decryption and Docker Compose drift protection on dedicated hosts.",
"Created Lanterna to visualize a Flux-driven monorepo with Mermaid charts rerun after merged changes.",
"Created promotion-pipeline quality gates for a system test suite and microservice release flow.",
],
},
{
id: "tradehat-frontend",
title: "Frontend Engineer",
company: "TradeHat",
companyUrl: "https://www.tradehat.com/",
dates: "Mar 2020 Mar 2021",
points: [
"Worked as one of four engineers building the startup product from scratch.",
"Selected Vue.js and Quasar as the frontend foundation for multi-architecture support.",
"Helped build the alpha deployment infrastructure using redundant ThinkPads hosted at each engineer's home in Dallas.",
"Built reporting and dashboard pages in Vue with Tailwind CSS and FusionCharts.",
],
},
{
id: "ibm-sdet",
title: "Software Development Engineer in Test",
company: "IBM",
dates: "Mar 2020 May 2023",
companyUrl: "https://www.ibm.com/cloud",
dates: "Nov 2018 May 2023",
points: [
"Authored and planned system and end-to-end integration tests using Go, Terraform, Python, and Bash.",
"Monitored critical API endpoints with Zabbix and worked incident-style issues end to end.",
"Supported platform testing for the Madrid Data Center rollout.",
],
},
{
id: "softlayer-ibmcloud-sdet",
title: "Software Development Engineer in Test",
company: "SoftLayer / IBM Cloud (TekSystems Contract)",
dates: "Nov 2018 Mar 2020",
points: [
"Built automated end-to-end tests in Go and Python across REST and SOAP APIs.",
"Worked across frontend and backend efforts (Vue, WordPress, Docker, MySQL/Postgres).",
"Used Jenkins + Splunk/Kibana to diagnose failures and produce coverage-focused test reports.",
"Authored and planned automated end-to-end and system tests for IBM Cloud systems in Python, Go, and Terraform.",
"Served as a consultant before accepting a full-time role in 2020; documented contributions and hosted feature demos and knowledge transfers.",
"Automated platform validation for a remote data center, including RHEL and SUSE functionality, licensing, repository availability, and performance.",
"Automated A100 and V100 GPU resource tests for CUDA functionality and data security in accelerated-computing environments.",
"Created a Zabbix health dashboard for Cloud APIs and planned key feature testing in Python.",
"Created Terraform and Go system tests for microservice interactions and platform scalability/autoscaling.",
],
},
{
id: "unifocus-survey-programmer",
title: "Survey Programmer",
title: "Lead Survey Programmer",
company: "UniFocus",
companyUrl: "https://www.unifocus.com/",
dates: "Aug 2014 Nov 2018",
points: [
"Wrote SQL-driven survey sites and reporting logic; customized sites via CSS and JavaScript.",
"Built Python and VBA automation tools to reduce manual work for teams and business partners.",
"Transformed a configuration role into an automation role using Python and SQL for online surveys and reports.",
"Served as team lead, recruiting staff and mentoring junior engineers.",
"Converted Python automation scripts into a CLI tool and built a VBA-driven Excel workbook for business contacts.",
"Automated responsive design updates for web surveys with Python and added JavaScript/CSS mobile support.",
"Created a Python XML parser to restore survey data that could otherwise be lost.",
"Won UniFocus Excellence Award (2015) and Innovation Award (2016).",
],
},
{
id: "magic-aire-engineering-assistant",
title: "Engineering Assistant",
company: "United Electric Company - Magic Aire",
dates: "Apr 2011 Aug 2014",
points: [
"Wrote and maintained VBA tools for internal use, including interpolators and data-entry utilities.",
"Supported mechanical engineering work in SolidWorks (drawing modernization and component standardization).",
],
},
];
</script>
<style scoped>
.page {
max-width: 1000px;
max-width: 1260px;
margin: 0 auto;
padding: 32px 22px 72px;
}
.about {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 18px;
grid-template-columns: minmax(220px, 0.48fr) minmax(0, 2fr);
gap: 30px;
}
.portrait {
@ -224,8 +226,10 @@ const timeline = [
.left {
display: flex;
align-items: center;
flex-direction: column;
align-items: flex-start;
gap: 14px;
padding-top: 8px;
}
.contact .name {
@ -235,6 +239,8 @@ const timeline = [
.contact .role {
color: var(--text-muted);
max-width: 220px;
line-height: 1.35;
}
.contact .meta {
@ -270,6 +276,27 @@ const timeline = [
margin: 0 0 8px;
}
.skill-groups {
display: grid;
gap: 0;
margin-top: 18px;
}
.skill-group {
display: grid;
grid-template-columns: minmax(130px, 160px) minmax(0, 1fr);
gap: 16px;
align-items: start;
padding: 11px 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.skill-group h2 {
margin: 5px 0 0;
color: var(--text-strong);
font-size: 15px;
}
.section-head {
display: flex;
align-items: center;
@ -292,9 +319,15 @@ const timeline = [
.highlights {
display: flex;
gap: 8px;
gap: 6px;
flex-wrap: wrap;
margin-top: 12px;
margin-top: 0;
}
.highlights .pill {
min-height: 30px;
padding: 5px 9px;
font-size: 12px;
}
.note {
@ -343,6 +376,15 @@ const timeline = [
margin-bottom: 4px;
}
.entry-sub a {
color: var(--accent-cyan);
text-decoration: none;
}
.entry-sub a:hover {
color: var(--accent-rose);
}
ul {
margin: 0;
padding-left: 16px;
@ -353,8 +395,14 @@ ul {
.about {
grid-template-columns: 1fr;
}
.left {
justify-content: flex-start;
}
.skill-group {
grid-template-columns: 1fr;
gap: 8px;
}
}
</style>

View File

@ -16,7 +16,7 @@
class="pill mono"
:href="auth.accountPasswordUrl"
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
Change password
</a>
@ -53,7 +53,7 @@
<div class="kv">
<div class="row">
<span class="k mono">URL</span>
<a class="v mono link" href="https://money.bstein.dev" target="_blank" rel="noreferrer">
<a class="v mono link" href="https://money.bstein.dev" target="_blank" rel="noopener noreferrer">
money.bstein.dev
</a>
</div>
@ -223,7 +223,7 @@
<div class="kv">
<div class="row">
<span class="k mono">URL</span>
<a class="v mono link" href="https://vault.bstein.dev" target="_blank" rel="noreferrer">vault.bstein.dev</a>
<a class="v mono link" href="https://vault.bstein.dev" target="_blank" rel="noopener noreferrer">vault.bstein.dev</a>
</div>
<div class="row">
<span class="k mono">Username</span>
@ -267,7 +267,7 @@
<div class="kv">
<div class="row">
<span class="k mono">URL</span>
<a class="v mono link" href="https://health.bstein.dev" target="_blank" rel="noreferrer">
<a class="v mono link" href="https://health.bstein.dev" target="_blank" rel="noopener noreferrer">
health.bstein.dev
</a>
</div>
@ -330,7 +330,7 @@
<div class="kv">
<div class="row">
<span class="k mono">URL</span>
<a class="v mono link" href="https://stream.bstein.dev" target="_blank" rel="noreferrer">
<a class="v mono link" href="https://stream.bstein.dev" target="_blank" rel="noopener noreferrer">
stream.bstein.dev
</a>
</div>

View File

@ -27,7 +27,7 @@
class="tile"
:href="app.url"
:target="app.target"
rel="noreferrer"
rel="noopener noreferrer"
>
<div class="tile-title">{{ app.name }}</div>
<div class="tile-desc">{{ app.description }}</div>
@ -154,8 +154,8 @@ const sections = [
},
{
name: "AI Chat",
url: "/ai/chat",
target: "_self",
url: "https://bstein.dev/ai/chat",
target: "_blank",
description: "Chat with Atlas AI (GPU-accelerated).",
},
],
@ -194,8 +194,8 @@ const sections = [
apps: [
{
name: "Monero Node",
url: "/monero",
target: "_self",
url: "https://bstein.dev/monero",
target: "_blank",
description: "Faster sync using the Atlas Monero node.",
},
],

File diff suppressed because it is too large Load Diff

View File

@ -204,7 +204,7 @@
:href="link.href"
:title="link.href"
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
{{ link.text }}
</a>

View File

@ -3,9 +3,9 @@ import { expect, test } from "../../../frontend/node_modules/@playwright/test/in
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const originalFetch = window.fetch.bind(window);
const jsonResponse = (body) =>
const jsonResponse = (body, status = 200) =>
new Response(JSON.stringify(body), {
status: 200,
status,
headers: { "content-type": "application/json" },
});
@ -13,14 +13,28 @@ test.beforeEach(async ({ page }) => {
const requestUrl = typeof resource === "string" ? resource : resource?.url || "";
const url = new URL(requestUrl, window.location.origin);
if (url.pathname === "/api/auth/config") {
return Promise.resolve(jsonResponse({ enabled: false }));
return Promise.resolve(jsonResponse({ enabled: true, reset_url: "https://sso.example.dev/reset" }));
}
if (url.pathname === "/api/lab/status") {
if (window.__LAB_STATUS_FAIL__) {
return Promise.resolve(jsonResponse({ error: "status 503" }, 503));
}
return Promise.resolve(
jsonResponse({
connected: true,
atlas: { up: true },
oceanus: { up: false },
atlas: { up: true, known: true },
active_service: { known: true, label: "Nextcloud", url: "https://cloud.bstein.dev", window: "15m" },
dedicated_hosts: {
known: true,
up: true,
up_count: 2,
total: 2,
hosts: [
{ label: "Database node", known: true, up: true },
{ label: "Jumphost", known: true, up: true },
],
},
checked_at: 1000,
}),
);
}
@ -32,7 +46,7 @@ test.beforeEach(async ({ page }) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ enabled: false }),
body: JSON.stringify({ enabled: true, reset_url: "https://sso.example.dev/reset" }),
});
});
await page.route("**/api/lab/status", async (route) => {
@ -41,24 +55,68 @@ test.beforeEach(async ({ page }) => {
contentType: "application/json",
body: JSON.stringify({
connected: true,
atlas: { up: true },
oceanus: { up: false },
atlas: { up: true, known: true },
active_service: { known: true, label: "Nextcloud", url: "https://cloud.bstein.dev", window: "15m" },
dedicated_hosts: {
known: true,
up: true,
up_count: 2,
total: 2,
hosts: [
{ label: "Database node", known: true, up: true },
{ label: "Jumphost", known: true, up: true },
],
},
checked_at: 1000,
}),
});
});
});
test("shows the overview and opens the diagram overlay", async ({ page }) => {
test("shows Brad's professional homepage and opens the platform diagram overlay", async ({ page }) => {
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Overview" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Brad Stein" })).toBeVisible();
await expect(page.getByRole("heading", { name: "DevOps Automation Engineer / Senior SDET" })).toBeVisible();
await expect(page.getByRole("link", { name: "Discuss a Project" })).toBeVisible();
await expect(page.getByRole("link", { name: "View Résumé" })).toBeVisible();
await expect(page.getByRole("link", { name: "Explore the Live Platform" })).toBeVisible();
await expect(page.getByRole("button", { name: "Login" })).toBeVisible();
await expect(page.getByRole("link", { name: "Register" })).toBeVisible();
await expect(page.getByRole("heading", { name: "From 0 to 100" })).toBeVisible();
await expect(page.getByRole("link", { name: "Nextcloud", exact: true })).toBeVisible();
await expect(page.getByText("2 of 2 responding")).toBeVisible();
await page.getByRole("link", { name: "Explore the Live Platform" }).click();
await expect(page.getByRole("heading", { name: "Titan Lab: Live Platform" })).toBeVisible();
await expect(page.getByRole("link", { name: "IaC source" })).toBeVisible();
await expect(page.getByText("Live data connected")).toBeVisible();
await expect(page.locator(".service-grid .service").filter({ hasText: "Nextcloud" }).first()).toBeVisible();
const firstCard = page.locator(".mermaid-card").first();
await page.locator("details.source-diagrams summary").click();
const firstCard = page.locator("details.source-diagrams .mermaid-card").first();
await expect(firstCard).toBeVisible();
await firstCard.locator(".diagram").click();
await expect(page.locator(".overlay")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator(".overlay")).toHaveCount(0);
});
test("keeps the homepage usable when public status fails", async ({ page }) => {
await page.addInitScript(() => {
window.__LAB_STATUS_FAIL__ = true;
});
await page.route("**/api/lab/status", async (route) => {
await route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({ error: "status 503" }),
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Brad Stein" })).toBeVisible();
await expect(page.getByText("Live status is temporarily unavailable").first()).toBeVisible();
await expect(page.getByText("status 503")).toHaveCount(0);
});

View File

@ -92,6 +92,7 @@ describe("auth helpers", () => {
tokenParsed: {
preferred_username: "bob",
email: "bob@example.dev",
email_verified: true,
groups: ["/ops"],
},
init: vi.fn(async () => true),
@ -168,6 +169,7 @@ describe("auth helpers", () => {
expect(authModule.auth.authenticated).toBe(false);
expect(authModule.auth.username).toBe("");
expect(authModule.auth.email).toBe("");
expect(authModule.auth.emailVerified).toBeNull();
expect(authModule.auth.groups).toEqual([]);
});
@ -179,6 +181,7 @@ describe("auth helpers", () => {
tokenParsed: {
preferred_username: "alice",
email: "alice@example.dev",
email_verified: true,
groups: ["/dev", "/admin"],
},
init: vi.fn(async () => {
@ -239,6 +242,7 @@ describe("auth helpers", () => {
expect(authModule.auth.authenticated).toBe(true);
expect(authModule.auth.username).toBe("alice");
expect(authModule.auth.email).toBe("alice@example.dev");
expect(authModule.auth.emailVerified).toBe(true);
expect(authModule.auth.groups).toEqual(["dev", "admin"]);
expect(authModule.auth.token).toBe("mock-token");
});
@ -251,6 +255,7 @@ describe("auth helpers", () => {
tokenParsed: {
preferred_username: "carol",
email: "carol@example.dev",
email_verified: false,
groups: ["/ops"],
},
init: vi.fn(async () => true),
@ -315,12 +320,19 @@ describe("auth helpers", () => {
await authModule.authFetch("/api/healthz");
expect(authModule.auth.username).toBe("carol");
expect(authModule.auth.emailVerified).toBe(false);
expect(authModule.auth.groups).toEqual(["ops"]);
expect(client.updateToken).toHaveBeenCalled();
});
it("leaves auth alone when login/logout are called before initialization", async () => {
const authModule = await loadAuth();
const assign = vi.fn();
Object.defineProperty(window, "location", {
configurable: true,
value: { ...window.location, assign },
});
authModule.auth.loginUrl = "https://sso.example.dev/login";
const fetchMock = vi.fn(async () => new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchMock);
@ -333,6 +345,7 @@ describe("auth helpers", () => {
const headers = new Headers(options.headers);
expect(headers.get("X-Test")).toBe("1");
expect(headers.get("Authorization")).toBeNull();
expect(assign).toHaveBeenCalledWith("https://sso.example.dev/login");
});
it("recovers when the auth config endpoint fails", async () => {

View File

@ -11,7 +11,7 @@ describe("shared dashboard components", () => {
const wrapper = shallowMount(MetricRow, {
props: {
items: [
{ label: "Nodes", value: "26", note: "atlas + oceanus" },
{ label: "Nodes", value: "26", note: "atlas platform" },
{ label: "Storage", value: "80 TB", note: "Longhorn" },
],
},

View File

@ -1,80 +1,160 @@
import { describe, expect, it } from "@jest/globals";
import { shallowMount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
import { RouterLinkStub, flushPromises, shallowMount } from "@vue/test-utils";
import PlatformSection from "../../../frontend/src/components/PlatformSection.vue";
import { auth } from "../../../frontend/src/auth.js";
import HomeView from "../../../frontend/src/views/HomeView.vue";
function resetAuth() {
auth.ready = true;
auth.enabled = true;
auth.authenticated = false;
auth.username = "";
auth.email = "";
auth.emailVerified = null;
auth.groups = [];
auth.resetUrl = "https://sso.example.dev/reset";
}
describe("HomeView", () => {
it("adds fallback icons and builds diagrams for the overview page", () => {
beforeEach(() => {
global.fetch = jest.fn(async (resource) => {
const url = typeof resource === "string" ? resource : resource?.url || "";
if (url.includes("/api/account/member-state")) {
return new Response(
JSON.stringify({
status: "ready",
onboarding_url: "/onboarding?code=ada~CODE",
dashboard_url: "/apps",
account_url: "/account",
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
}
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
});
});
afterEach(() => {
resetAuth();
jest.restoreAllMocks();
});
it("renders the professional homepage while logged out", () => {
resetAuth();
const wrapper = shallowMount(HomeView, {
global: {
stubs: {
MetricRow: true,
ServiceGrid: true,
MermaidCard: true,
RouterLink: RouterLinkStub,
PlatformSection: true,
},
},
props: {
serviceData: {
services: [
{ name: "Nextcloud", summary: "Storage", link: "https://cloud.example.dev" },
{ name: "Jellyfin", summary: "Media", link: "https://stream.example.dev" },
{ name: "Matrix (Synapse)", summary: "Chat", link: "https://chat.example.dev" },
{ name: "Element", summary: "Rooms", link: "https://rooms.example.dev" },
{ name: "LiveKit", summary: "Calls", link: "https://calls.example.dev" },
{ name: "Coturn", summary: "TURN", link: "https://turn.example.dev" },
{ name: "Mailu", summary: "Mail", link: "https://mail.example.dev" },
{ name: "Vaultwarden", summary: "Passwords", link: "https://vault.example.dev" },
{ name: "Vault", summary: "Secrets", link: "https://secret.example.dev" },
{ name: "Gitea", summary: "Git", link: "https://scm.example.dev" },
{ name: "Jenkins", summary: "CI", link: "https://ci.example.dev" },
{ name: "Harbor", summary: "Registry", link: "https://registry.example.dev" },
{ name: "Flux", summary: "GitOps", link: "https://cd.example.dev" },
{ name: "Monero", summary: "Node", link: "https://monero.example.dev" },
{ name: "SUI Validator", summary: "Crypto", link: "https://sui.example.dev" },
{ name: "Keycloak", summary: "SSO", link: "https://sso.example.dev" },
{ name: "AI Translation", summary: "Translate", link: "https://translate.example.dev" },
{ name: "Grafana", summary: "Metrics", link: "https://metrics.example.dev" },
{ name: "Pegasus", summary: "Uploads", link: "https://pegasus.example.dev" },
{ name: "AI Chat", summary: "Chat", link: "https://chat.example.dev" },
{ name: "AI Vision", summary: "Vision", link: "https://vision.example.dev" },
{ name: "AI Speech", summary: "Speech", link: "https://speech.example.dev" },
{ name: "Mystery", summary: "Default", link: "https://default.example.dev" },
],
labStatus: {
connected: true,
atlas: { up: true, known: true },
active_service: { known: true, label: "Nextcloud", url: "https://cloud.example.dev", window: "15m" },
dedicated_hosts: {
known: true,
up: true,
up_count: 2,
total: 2,
hosts: [
{ label: "Database node", known: true, up: true },
{ label: "Jumphost", known: true, up: true },
],
},
checked_at: 1000,
},
loading: false,
error: "",
},
});
const icons = wrapper.vm.displayServices.map((service) => service.icon);
expect(icons).toContain("☁️");
expect(icons).toContain("🎞️");
expect(icons).toContain("🗨️");
expect(icons).toContain("🧩");
expect(icons).toContain("🎥");
expect(icons).toContain("📞");
expect(icons).toContain("📮");
expect(icons).toContain("🔒");
expect(icons).toContain("🔑");
expect(icons).toContain("📚");
expect(icons).toContain("🧰");
expect(icons).toContain("📦");
expect(icons).toContain("🔄");
expect(icons).toContain("⛏️");
expect(icons).toContain("💠");
expect(icons).toContain("🛡️");
expect(icons).toContain("🌐");
expect(icons).toContain("📈");
expect(icons).toContain("🚀");
expect(icons).toContain("💬");
expect(icons).toContain("🖼️");
expect(icons).toContain("🎙️");
expect(icons).toContain("🛰️");
expect(wrapper.vm.hardwareDiagram).toContain("Titan Lab");
expect(wrapper.vm.networkDiagram).toContain("oauth2-proxy");
expect(wrapper.vm.pipelineDiagram).toContain("flux[cd.bstein.dev]");
expect(wrapper.text()).toContain("Brad Stein");
expect(wrapper.text()).toContain("DevOps Automation Engineer / Senior SDET");
expect(wrapper.text()).toContain("Discuss a Project");
expect(wrapper.text()).toContain("View Résumé");
expect(wrapper.text()).toContain("Explore the Live Platform");
expect(wrapper.text()).toContain("From 0 to 100");
expect(wrapper.text()).toContain("Platform Showcase");
expect(wrapper.text()).toContain("Atlas operator tools");
expect(wrapper.text()).toContain("Veles");
expect(wrapper.text()).not.toContain("TaskWatcher");
expect(wrapper.text()).toContain("family, friends, and the lucky few");
expect(wrapper.text()).toContain("Register");
expect(wrapper.text()).toContain("Request Lab Access");
expect(wrapper.find("[aria-label='Keycloak: Use one account to sign in across bstein.dev.']").exists()).toBe(true);
expect(wrapper.find("[title='Nextcloud: Keep files, photos, office docs, and personal cloud data in sync.']").exists()).toBe(true);
expect(wrapper.find("a[href='https://bstein.dev/ai/chat']").exists()).toBe(true);
expect(wrapper.find("a[href='https://bstein.dev/monero']").exists()).toBe(true);
expect(wrapper.find("a[href='https://chat.ai.bstein.dev']").exists()).toBe(false);
expect(wrapper.find("a[href='https://monero.bstein.dev:443']").exists()).toBe(false);
expect(wrapper.find("a[href='/ai/chat']").exists()).toBe(false);
expect(wrapper.find("a[href='/monero']").exists()).toBe(false);
expect(wrapper.text()).not.toContain("Existing registration, verification, onboarding");
expect(wrapper.text()).not.toContain("Looking for hosted services or an existing Titan Lab account");
expect(wrapper.find("a[href^='mailto:brad@bstein.dev']").exists()).toBe(true);
expect(wrapper.findComponent(PlatformSection).exists()).toBe(true);
expect(wrapper.text()).toContain("Atlas platform");
expect(wrapper.text()).toContain("Most active service");
expect(wrapper.text()).toContain("Nextcloud");
expect(wrapper.text()).toContain("Dedicated hosts");
expect(wrapper.text()).toContain("2 of 2 responding");
expect(wrapper.text()).toContain("Responding");
});
it("renders loading, error, and healthy status states", () => {
it("shows a calm fallback when public status is unavailable", () => {
resetAuth();
const wrapper = shallowMount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,
PlatformSection: true,
},
},
props: {
labStatus: null,
loading: false,
error: "Live data unavailable",
},
});
expect(wrapper.text()).toContain("Live status is temporarily unavailable");
expect(wrapper.text()).not.toContain("status 500");
});
it("shows authenticated member actions without exposing service secrets", async () => {
resetAuth();
auth.authenticated = true;
auth.username = "ada";
auth.email = "ada@example.dev";
const wrapper = shallowMount(HomeView, {
global: {
stubs: {
RouterLink: RouterLinkStub,
PlatformSection: true,
},
},
props: {
labStatus: { connected: true, atlas: { up: true, known: true } },
loading: false,
error: "",
},
});
await flushPromises();
expect(wrapper.text()).toContain("Open Services");
expect(wrapper.text()).toContain("Account");
expect(wrapper.text()).not.toContain("app_password");
expect(wrapper.text()).not.toContain("mailu_app_password");
});
});
describe("PlatformSection", () => {
it("preserves platform service, status, metric, and diagram behavior", () => {
const wrapper = shallowMount(PlatformSection, {
global: {
stubs: {
MetricRow: true,
@ -83,21 +163,49 @@ describe("HomeView", () => {
},
},
props: {
labStatus: { connected: true, atlas: { up: true }, oceanus: { up: true } },
serviceData: undefined,
labStatus: {
connected: true,
atlas: { up: true, known: true },
dedicated_hosts: {
known: true,
up: false,
up_count: 1,
total: 2,
hosts: [
{ label: "Database node", known: true, up: true },
{ label: "Jumphost", known: true, up: false },
],
},
},
serviceData: {
services: [
{ name: "Nextcloud", summary: "Storage", link: "https://cloud.example.dev" },
{ name: "Monero", summary: "Node", link: "/monero", host: "monerod.crypto.svc.cluster.local:18081" },
{ name: "Mystery", summary: "Default", link: "https://default.example.dev" },
],
},
loading: true,
error: "",
},
});
expect(wrapper.vm.atlasPillClass).toBe("pill-ok");
expect(wrapper.vm.oceanusPillClass).toBe("pill-ok");
expect(wrapper.vm.dedicatedHostsPillClass).toBe("pill-bad");
expect(wrapper.vm.dedicatedHostsLabel).toBe("1/2 responding");
expect(wrapper.vm.dedicatedHostItems.map((host) => host.value)).toEqual(["Responding", "Needs attention"]);
expect(wrapper.get(".status").text()).toBe("Loading...");
expect(wrapper.vm.displayServices.length).toBeGreaterThan(0);
expect(wrapper.vm.displayServices.map((service) => service.icon)).toEqual(["NC", "XM", "TL"]);
expect(wrapper.vm.displayServices.find((service) => service.name === "Monero").host).toBe("/monero");
expect(wrapper.vm.hardwareDiagram).toContain("Titan Lab");
expect(wrapper.vm.hardwareDiagram).toContain("Dedicated non-cluster hosts");
expect(wrapper.vm.networkDiagram).toContain("oauth2-proxy");
expect(wrapper.vm.pipelineDiagram).toContain("Flux");
expect(wrapper.text()).toContain("IaC source");
expect(wrapper.text()).toContain("Titan Lab: Live Platform");
});
it("shows the error state when lab data fetches fail", () => {
const wrapper = shallowMount(HomeView, {
it("shows public status errors without raw API detail", () => {
const wrapper = shallowMount(PlatformSection, {
global: {
stubs: {
MetricRow: true,
@ -106,39 +214,17 @@ describe("HomeView", () => {
},
},
props: {
labStatus: { connected: false, atlas: { up: false }, oceanus: { up: false } },
labStatus: { connected: false, atlas: { up: false, known: false } },
serviceData: { services: [] },
metricsData: { items: [{ label: "Custom", value: "1", note: "" }] },
loading: false,
error: "unable to load status",
error: "Live data unavailable",
},
});
expect(wrapper.vm.atlasPillClass).toBe("pill-bad");
expect(wrapper.vm.oceanusPillClass).toBe("pill-bad");
expect(wrapper.get(".status").text()).toBe("unable to load status");
});
it("shows live data unavailable and respects custom metric items", () => {
const wrapper = shallowMount(HomeView, {
global: {
stubs: {
MetricRow: true,
ServiceGrid: true,
MermaidCard: true,
},
},
props: {
labStatus: { connected: false, atlas: { up: false }, oceanus: { up: false } },
serviceData: { services: [] },
metricsData: {
items: [{ label: "Custom", value: "1", note: "" }],
},
loading: false,
error: "",
},
});
expect(wrapper.get(".status").text()).toBe("Live data unavailable");
expect(wrapper.vm.metricItems[0].note).toBe("");
expect(wrapper.text()).toContain("The platform overview remains available below");
});
});

View File

@ -16,20 +16,26 @@ describe("sample data builders", () => {
const services = fallbackServices();
expect(hardware.clusters[0].name).toBe("atlas");
expect(hardware.specialty.map((node) => node.alias)).toContain("oceanus");
expect(hardware.clusters[0].nodes.map((node) => node.alias)).toContain("oceanus");
expect(hardware.specialty.map((node) => node.alias)).toEqual(["atlas-db", "theia"]);
expect(services.services.some((service) => service.name === "Keycloak")).toBe(true);
expect(services.services.some((service) => service.name === "Oceanus")).toBe(false);
expect(services.services.some((service) => service.name === "Veles")).toBe(true);
expect(services.services.some((service) => service.name === "AI Chat")).toBe(true);
expect(services.services.find((service) => service.name === "AI Chat").link).toBe("https://bstein.dev/ai/chat");
expect(services.services.find((service) => service.name === "Monero").link).toBe("https://bstein.dev/monero");
});
it("builds the rendered diagrams and network summary", () => {
expect(buildHardwareDiagram({})).toContain("Titan Lab");
expect(buildHardwareDiagram({})).toContain("titan-0a");
expect(buildHardwareDiagram({})).toContain("Atlas k3s cluster");
expect(buildHardwareDiagram({})).toContain("Dedicated non-cluster hosts");
expect(buildNetworkDiagram()).toContain("oauth2-proxy");
expect(buildPipelineDiagram()).toContain("flux[cd.bstein.dev]");
expect(buildPipelineDiagram()).toContain("Flux");
expect(fallbackNetwork().ingress_gateway).toContain("Traefik");
expect(fallbackMetrics()).toEqual({
dashboard: "https://metrics.bstein.dev",
description: "Atlas + Oceanus metrics.",
description: "Atlas platform metrics.",
});
});
});

View File

@ -13,8 +13,6 @@ import AiPlanView from "../../../frontend/src/views/AiPlanView.vue";
import AppsView from "../../../frontend/src/views/AppsView.vue";
import MoneroView from "../../../frontend/src/views/MoneroView.vue";
const mockRouterPush = jest.fn();
jest.mock("axios", () => ({
__esModule: true,
default: {
@ -26,17 +24,17 @@ jest.mock("vue-router", () => ({
RouterLink: {
name: "RouterLink",
props: ["to"],
template: "<a><slot /></a>",
template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>",
},
useRouter: () => ({ push: mockRouterPush }),
}), { virtual: true });
describe("static shell views and components", () => {
afterEach(() => {
jest.restoreAllMocks();
jest.useRealTimers();
mockRouterPush.mockClear();
document.body.style.overflow = "";
auth.enabled = false;
auth.ready = false;
auth.authenticated = false;
auth.resetUrl = "";
});
@ -45,12 +43,27 @@ describe("static shell views and components", () => {
const about = shallowMount(AboutView);
expect(about.text()).toContain("About Me");
expect(about.text()).toContain("Titan Lab");
expect(about.vm.skills).toContain("Kubernetes (k3s/k8s)");
expect(about.text()).toContain("DevOps Automation Engineer / Senior SDET");
expect(about.text()).toContain("Kubernetes");
expect(about.text()).toContain("IBM Cloud");
expect(about.text()).toContain("TradeHat");
expect(about.find("a[href='https://www.tradehat.com/']").exists()).toBe(true);
expect(about.find("a[href='https://www.unifocus.com/']").exists()).toBe(true);
expect(about.find("a[href='https://scm.bstein.dev/bstein/titan-iac']").exists()).toBe(true);
const apps = shallowMount(AppsView);
expect(apps.text()).toContain("Apps");
expect(apps.text()).toContain("Nextcloud");
expect(apps.vm.sections.map((section) => section.title)).toContain("Security");
const appLinks = apps.vm.sections.flatMap((section) => section.groups.flatMap((group) => group.apps));
expect(appLinks.find((app) => app.name === "AI Chat")).toMatchObject({
url: "https://bstein.dev/ai/chat",
target: "_blank",
});
expect(appLinks.find((app) => app.name === "Monero Node")).toMatchObject({
url: "https://bstein.dev/monero",
target: "_blank",
});
const aiPlan = shallowMount(AiPlanView);
expect(aiPlan.text()).toContain("Roadmap");
@ -92,25 +105,41 @@ describe("static shell views and components", () => {
});
it("drives top bar navigation and auth state rendering", async () => {
global.fetch = jest.fn(async (resource) => {
const url = typeof resource === "string" ? resource : resource?.url || "";
if (url.includes("/api/account/member-state")) {
return new Response(JSON.stringify({ status: "ready", dashboard_url: "/apps", account_url: "/account" }), {
status: 200,
headers: { "content-type": "application/json" },
});
}
return new Response("{}", { status: 200, headers: { "content-type": "application/json" } });
});
auth.enabled = true;
auth.ready = true;
auth.authenticated = false;
auth.resetUrl = "https://sso.example.dev/reset";
const login = jest.spyOn(await import("../../../frontend/src/auth.js"), "login").mockImplementation(() => {});
const logout = jest.spyOn(await import("../../../frontend/src/auth.js"), "logout").mockImplementation(() => {});
const authModule = await import("../../../frontend/src/auth.js");
const logout = jest.spyOn(authModule, "logout").mockImplementation(() => {});
jest.spyOn(authModule, "login").mockImplementation(() => {});
const wrapper = mount(TopBar);
expect(wrapper.text()).toContain("Contact");
expect(wrapper.text()).toContain("Login");
expect(wrapper.text()).toContain("Request Access");
await wrapper.find(".profile").trigger("click");
expect(mockRouterPush).toHaveBeenCalledWith("/about");
expect(wrapper.text()).toContain("Register");
expect(wrapper.text()).not.toContain("Reset Password");
expect(wrapper.find("a[href='/#contact']").exists()).toBe(true);
await wrapper.find("button").trigger("click");
expect(login).toHaveBeenCalled();
await wrapper.find("button.menu-toggle").trigger("click");
expect(wrapper.find("nav").classes()).toContain("open");
await wrapper.find("a[href='/#contact']").trigger("click");
expect(wrapper.find("nav").classes()).not.toContain("open");
auth.authenticated = true;
await wrapper.vm.$nextTick();
await flushPromises();
expect(wrapper.text()).toContain("Account");
await wrapper.find("button").trigger("click");
expect(wrapper.text()).toContain("Open Services");
await wrapper.findAll("button.button").at(-1).trigger("click");
expect(logout).toHaveBeenCalled();
});