diff --git a/backend/atlas_portal/routes/account_overview.py b/backend/atlas_portal/routes/account_overview.py
index b89389d..74d3419 100644
--- a/backend/atlas_portal/routes/account_overview.py
+++ b/backend/atlas_portal/routes/account_overview.py
@@ -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:
diff --git a/backend/atlas_portal/routes/auth_config.py b/backend/atlas_portal/routes/auth_config.py
index ec02f90..337e4e7 100644
--- a/backend/atlas_portal/routes/auth_config.py
+++ b/backend/atlas_portal/routes/auth_config.py
@@ -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,
diff --git a/backend/atlas_portal/routes/lab.py b/backend/atlas_portal/routes/lab.py
index b03dd5c..40d0a23 100644
--- a/backend/atlas_portal/routes/lab.py
+++ b/backend/atlas_portal/routes/lab.py
@@ -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,
}
diff --git a/backend/atlas_portal/settings.py b/backend/atlas_portal/settings.py
index d43eeda..ce5a6e4 100644
--- a/backend/atlas_portal/settings.py
+++ b/backend/atlas_portal/settings.py
@@ -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")
diff --git a/backend/tests/test_account_overview.py b/backend/tests/test_account_overview.py
index bd52e43..1afaa3e 100644
--- a/backend/tests/test_account_overview.py
+++ b/backend/tests/test_account_overview.py
@@ -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}
diff --git a/backend/tests/test_auth_config.py b/backend/tests/test_auth_config.py
index 6ebd375..582b253 100644
--- a/backend/tests/test_auth_config.py
+++ b/backend/tests/test_auth_config.py
@@ -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:
diff --git a/backend/tests/test_lab_routes.py b/backend/tests/test_lab_routes.py
index 21ba1cf..912e73f 100644
--- a/backend/tests/test_lab_routes.py
+++ b/backend/tests/test_lab_routes.py
@@ -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
diff --git a/frontend/index.html b/frontend/index.html
index c76838c..529f12a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,12 +3,49 @@
- bstein.dev | Titan Lab
+ Brad Stein | SDET, DevOps Automation & Platform Engineering
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/public/og-bstein-dev.svg b/frontend/public/og-bstein-dev.svg
new file mode 100644
index 0000000..cb873e7
--- /dev/null
+++ b/frontend/public/og-bstein-dev.svg
@@ -0,0 +1,26 @@
+
+ Brad Stein, DevOps Automation Engineer and Senior SDET
+ A dark bstein.dev social card with professional engineering positioning and Titan Lab platform proof.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ BSTEIN.DEV
+ Brad Stein
+ DevOps Automation Engineer / Senior SDET
+ CI/CD · Kubernetes · Python · Linux · Observability
+ Titan Lab is the live platform proof behind the work.
+
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 11b99a2..aeea30d 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,15 +1,17 @@
-
+
+
+
diff --git a/frontend/src/auth.js b/frontend/src/auth.js
index 84a5978..c9d2ce8 100644
--- a/frontend/src/auth.js
+++ b/frontend/src/auth.js
@@ -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()) {
diff --git a/frontend/src/components/HeroSection.vue b/frontend/src/components/HeroSection.vue
index 16f8a28..18203a4 100644
--- a/frontend/src/components/HeroSection.vue
+++ b/frontend/src/components/HeroSection.vue
@@ -2,12 +2,12 @@
Titan Lab
- atlas · oceanus · nextcloud-ready
+ atlas · member services · nextcloud-ready
{{ title }}
{{ subtitle }}
@@ -23,9 +23,9 @@
Flux + Longhorn + Traefik
-
Oceanus
-
validator
-
Scraped via titan-24 into Grafana
+
Member services
+
identity + apps
+
Request access · onboarding · approved services
Ingress
diff --git a/frontend/src/components/MermaidCard.vue b/frontend/src/components/MermaidCard.vue
index 88921c5..9517d08 100644
--- a/frontend/src/components/MermaidCard.vue
+++ b/frontend/src/components/MermaidCard.vue
@@ -26,7 +26,6 @@
+
+
diff --git a/frontend/src/components/ServiceGrid.vue b/frontend/src/components/ServiceGrid.vue
index 95b6d7c..3e65a27 100644
--- a/frontend/src/components/ServiceGrid.vue
+++ b/frontend/src/components/ServiceGrid.vue
@@ -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"
>
{{ svc.icon || "🛰️" }}
@@ -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) {
diff --git a/frontend/src/components/StatsGrid.vue b/frontend/src/components/StatsGrid.vue
index 5cd9d55..613e846 100644
--- a/frontend/src/components/StatsGrid.vue
+++ b/frontend/src/components/StatsGrid.vue
@@ -21,9 +21,9 @@
Jetson pair for AI, GPU mini-pc titan-22 for Jellyfin.
-
Specialty nodes
+
Dedicated hosts
{{ specialty.length }}
-
Oceanus (validator), Tethys bridge, and Bastion.
+
Database node and jumphost kept outside the Atlas cluster.
Storage fabric
diff --git a/frontend/src/components/TopBar.vue b/frontend/src/components/TopBar.vue
index 0aa06af..a88e904 100644
--- a/frontend/src/components/TopBar.vue
+++ b/frontend/src/components/TopBar.vue
@@ -1,64 +1,192 @@
-
-
-
+
Skip to content
+
+
+
Brad Stein
-
Software Development Engineer
+
{{ content.shortRole }}
-
-
- Home
- About
+
-
-
- Apps
- Account
- Logout
+
+
+
+
+ {{ link.label }}
+
+ Contact
+
+
+
+ Login
+ Register
+
+
+
+ {{ memberAction?.label || "Open Services" }}
+
+ Account
+ Sign Out
+
+
Login
- Request Access
- Reset Password
+ Register
-
+
diff --git a/frontend/src/data/homepageContent.js b/frontend/src/data/homepageContent.js
new file mode 100644
index 0000000..d6232ec
--- /dev/null
+++ b/frontend/src/data/homepageContent.js
@@ -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}`;
+}
diff --git a/frontend/src/data/sample.js b/frontend/src/data/sample.js
index 0f81a54..8b96a71 100644
--- a/frontend/src/data/sample.js
+++ b/frontend/src/data/sample.js
@@ -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 rpi5 4c/8g"]
- titan0b["titan-0b rpi5 4c/8g"]
- titan0c["titan-0c 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 8TB astreae + 12TB asteria"]
- titan13["titan-13 8TB astreae + 12TB asteria"]
- titan14["titan-14 8TB astreae + 12TB asteria"]
- titan15["titan-15 8TB astreae + 12TB asteria"]
- titan16["titan-16 offline"]:::down
- titan17["titan-17"]
- titan18["titan-18"]
- titan19["titan-19"]
- end
-
- subgraph Accel["Accelerators + heavy nodes"]
- titan20["titan-20 Jetson Xavier 6c/16g"]
- titan21["titan-21 Jetson Xavier 6c/16g"]
- titan22["titan-22 10c/32g GPU streaming"]
- titan24["titan-24 (tethys) 12c/64g bridge + metrics"]
- end
-
- longhorn["Longhorn astreae: 4x8TB asteria: 4x12TB"]
- traefik["Traefik ingress"]
- keycloak["Keycloak SSO"]
- services["Services 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 Oceanus + Tethys included"]
+ Services["member services identity / cloud / media / CI / registry / AI"]
end
- subgraph Dedicated["Dedicated hosts (outside Atlas)"]
- titanDb["titan-db Postgres for HA control plane rpi5 4c/8g"]
- theia["titan-jh (theia) bastion rpi5 4c/8g"]
- oceanus["titan-23 (oceanus) SUI validator 24c/256g 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 home / metrics / selected apps"]
+ Traefik --> Auth["auth gate 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 source of truth"] --> CI["Jenkins build + test"]
+ CI --> Image["Harbor image registry"]
+ Image --> GitOps["Flux reconcile desired state"]
+ GitOps --> Atlas["Atlas running workloads"]
- keycloak[sso.bstein.dev] --> gitea
- keycloak --> jenkins
- keycloak --> harbor
- keycloak --> flux
+ CI --> Gates["quality gates tests / lint / reports"]
+ Atlas --> Observe["Prometheus + Grafana 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;
`;
}
diff --git a/frontend/src/member/useMemberAccessState.js b/frontend/src/member/useMemberAccessState.js
new file mode 100644
index 0000000..78d108f
--- /dev/null
+++ b/frontend/src/member/useMemberAccessState.js
@@ -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,
+ };
+}
diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue
index d38c6fe..ec067d0 100644
--- a/frontend/src/views/AboutView.vue
+++ b/frontend/src/views/AboutView.vue
@@ -7,13 +7,13 @@
@@ -21,22 +21,21 @@
About Me
+
{{ paragraph }}
- Senior Backend & DevOps engineer focused on making systems reliable. I build Python-driven tools on Linux, Kubernetes, and CI/CD
- to keep distributed systems healthy.
-
-
- 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 Force’s PTES program.
-
-
- My projects and dashboards run on my local k3s : see
- scm.bstein.dev and
- metrics.bstein.dev .
+ Public platform work is visible in the
+ Titan IaC repo ,
+ scm.bstein.dev and
+ metrics.bstein.dev .
-
@@ -51,7 +50,13 @@
{{ item.title }}
-
{{ item.company }} · {{ item.dates }}
+
+
+ {{ item.company }}
+
+
{{ item.company }}
+
· {{ item.dates }}
+
@@ -60,33 +65,49 @@
-
- Full role history and exact dates are on LinkedIn.
-
+ Web version aligned to Stein.Brad.Resume.2026.06.pdf . Phone and street address are
+ intentionally omitted from the public site.
Titan Lab
- atlas + oceanus
+ atlas platform
- The Titan Lab is my 26-node (and growing) homelab with a production mindset: security, monitoring, and repeatable changes. The core is
- Atlas , a GitOps-managed k3s cluster where services are reconciled by
- Flux .
+ Titan Lab is the live platform behind bstein.dev. The core is Atlas , a GitOps-managed
+ k3s cluster where services are reconciled by Flux and operated
+ with the same habits I use professionally: source of truth, quality gates, observability, identity, storage,
+ recovery paths, and documented handoff.
- Atlas 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 VaultWarden
- but now the cluster hosts everything from email to ai. In my freetime, I'm always working on Atlas 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.
- Oceanus 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 Oceanus is in my lab and is dedicated
- hardware to extend their infrastructure and make the SUI project more resilient.
+ Oceanus and Tethys 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. Veles ,
+ the MTG AI playtester, is deployed at
+ veles.bstein.dev .
+
+
+
+
+
+
+
Education
+ UT Austin
+
+
+
+ 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.
+
+
+ Participated in the Emerging Scholars honors calculus program and later served as an assistant for it.
@@ -94,113 +115,94 @@
diff --git a/frontend/src/views/AccountView.vue b/frontend/src/views/AccountView.vue
index faf578f..270c998 100644
--- a/frontend/src/views/AccountView.vue
+++ b/frontend/src/views/AccountView.vue
@@ -16,7 +16,7 @@
class="pill mono"
:href="auth.accountPasswordUrl"
target="_blank"
- rel="noreferrer"
+ rel="noopener noreferrer"
>
Change password
@@ -53,7 +53,7 @@
@@ -223,7 +223,7 @@
Username
@@ -267,7 +267,7 @@
@@ -330,7 +330,7 @@
diff --git a/frontend/src/views/AppsView.vue b/frontend/src/views/AppsView.vue
index 1437242..d96fcff 100644
--- a/frontend/src/views/AppsView.vue
+++ b/frontend/src/views/AppsView.vue
@@ -27,7 +27,7 @@
class="tile"
:href="app.url"
:target="app.target"
- rel="noreferrer"
+ rel="noopener noreferrer"
>
{{ app.name }}
{{ app.description }}
@@ -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.",
},
],
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
index 5d7ceb2..d918688 100644
--- a/frontend/src/views/HomeView.vue
+++ b/frontend/src/views/HomeView.vue
@@ -1,89 +1,296 @@
-
-
-
-
Titan Lab
-
Overview
-
- Titan Lab is a 26-node homelab with a production mindset. Atlas is its Kubernetes cluster that runs user and dev
- services. Oceanus is a dedicated SUI validator host. Underlying components such as Theia, the bastion, and Tethys, the link between
- Atlas and Oceanus underpin the lab. Membership grants the following services below.
+
+
+
+
{{ content.eyebrow }}
+
{{ content.professionalName }}
+
{{ content.role }}
+
{{ content.summary }}
+
{{ content.experienceLine }}
+
{{ content.availability.label }}
+
+
+
+
+ {{ tech }}
+
+
+
+
+
+
Live platform
+
Titan Lab Status
+
+
+ Last checked: {{ lastCheckedLabel }}
+
+ Live status is temporarily unavailable. The platform overview remains available below.
-
-
Atlas: flux-managed k3s
-
Oceanus: SUI validator
-
-
-
{{ error }}
-
- {{ loading ? "Loading..." : labStatus?.connected ? "Live data connected" : "Live data unavailable" }}
-
-
+
+
+
+
+
+
{{ content.capabilitySection.kicker }}
+
{{ content.capabilitySection.heading }}
+
{{ content.capabilitySection.description }}
-
-
-
-
-
Atlas
-
k3s cluster
+
+
+ {{ capability.icon }}
+ {{ capability.title }}
+ {{ capability.description }}
+
+
+
+
+
+
+
{{ content.serviceSection.kicker }}
+
{{ content.serviceSection.heading }}
+
{{ content.serviceSection.description }}
+
+
+
+ {{ service.title }}
+ Build path: {{ service.investigation }}
+ Outcome: {{ service.outcome }}
+
+
+
+
+
+
+
{{ content.workSection.kicker }}
+
{{ content.workSection.heading }}
+
{{ content.workSection.description }}
+
+
+
+
+
{{ item.icon }}
+
+
{{ item.kind }}
+
{{ item.title }}
-
-
-
-
Oceanus
-
dedicated host
+
{{ item.description }}
+
+
+ {{ point }}
-
Loading Oceanus Availability...
+
+
+
+
+
+
+
+
+
+
+
Engagement
+
Ways to Work Together
+
+ Fixed-scope and contract pricing is based on the problem, access requirements, urgency, and expected
+ deliverables.
+
+
+
+
+ {{ item.title }}
+ {{ item.description }}
+ {{ item.duration }}
+
+
+
+
+
+
+
Process
+
A Practical Engagement Process
+
+
+
+ {{ index + 1 }}
+
+
{{ step.title }}
+
{{ step.description }}
+
+
+
+
+ Do not email passwords, private keys, tokens, customer data, or other secrets. Secure access can be arranged after
+ scope is confirmed.
+
+
+
+
+
+
+
Member platform
+
{{ content.memberAccessCopy.heading }}
+
{{ content.memberAccessCopy.description }}
+
{{ memberPanelCopy }}
+
+
+
+
+
+ Checking session...
+
+
+
+
+ {{ memberAction?.label || "Open Services" }}
+
+ Account
+ Sign Out
+ {{ memberState.error }}
+
+
+
+
+ Sign In
+ Register
+
+
+ Trouble signing in? Reset password
+
+
+
+
+
-
-
-
-
-
Service Grid
-
ai + comms + storage + streaming + development
+
-
+
diff --git a/frontend/src/views/OnboardingView.vue b/frontend/src/views/OnboardingView.vue
index a5f117c..f0f1bd8 100644
--- a/frontend/src/views/OnboardingView.vue
+++ b/frontend/src/views/OnboardingView.vue
@@ -204,7 +204,7 @@
:href="link.href"
:title="link.href"
target="_blank"
- rel="noreferrer"
+ rel="noopener noreferrer"
>
{{ link.text }}
diff --git a/testing/frontend/e2e/home.spec.js b/testing/frontend/e2e/home.spec.js
index 2074700..b71e9d2 100644
--- a/testing/frontend/e2e/home.spec.js
+++ b/testing/frontend/e2e/home.spec.js
@@ -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);
+});
diff --git a/testing/frontend/unit/auth.spec.js b/testing/frontend/unit/auth.spec.js
index e6af415..0771691 100644
--- a/testing/frontend/unit/auth.spec.js
+++ b/testing/frontend/unit/auth.spec.js
@@ -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 () => {
diff --git a/testing/frontend/unit/components.spec.js b/testing/frontend/unit/components.spec.js
index 9f95548..4dcdc7d 100644
--- a/testing/frontend/unit/components.spec.js
+++ b/testing/frontend/unit/components.spec.js
@@ -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" },
],
},
diff --git a/testing/frontend/unit/home.spec.js b/testing/frontend/unit/home.spec.js
index cd660e6..31f25dc 100644
--- a/testing/frontend/unit/home.spec.js
+++ b/testing/frontend/unit/home.spec.js
@@ -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");
});
});
diff --git a/testing/frontend/unit/sample.spec.js b/testing/frontend/unit/sample.spec.js
index 621f318..6e9e2a2 100644
--- a/testing/frontend/unit/sample.spec.js
+++ b/testing/frontend/unit/sample.spec.js
@@ -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.",
});
});
});
diff --git a/testing/frontend/unit/static-views.spec.js b/testing/frontend/unit/static-views.spec.js
index 5b059bd..e73e1ba 100644
--- a/testing/frontend/unit/static-views.spec.js
+++ b/testing/frontend/unit/static-views.spec.js
@@ -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: " ",
+ template: " ",
},
- 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();
});