Switch homepage focus to SDET

This commit is contained in:
codex 2026-08-01 03:24:51 -03:00
parent 824a2187ea
commit b29cdfde20
14 changed files with 278 additions and 80 deletions

View File

@ -32,7 +32,7 @@
{ {
"@type": "Person", "@type": "Person",
"name": "Brad Stein", "name": "Brad Stein",
"jobTitle": "DevOps Automation Engineer / Senior SDET", "jobTitle": "Senior SDET / DevOps Automation Engineer",
"url": "https://bstein.dev/", "url": "https://bstein.dev/",
"email": "mailto:brad@bstein.dev", "email": "mailto:brad@bstein.dev",
"sameAs": ["https://www.linkedin.com/in/steinbradley/", "https://scm.bstein.dev/bstein"] "sameAs": ["https://www.linkedin.com/in/steinbradley/", "https://scm.bstein.dev/bstein"]

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc"> <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630" role="img" aria-labelledby="title desc">
<title id="title">Brad Stein, DevOps Automation Engineer and Senior SDET</title> <title id="title">Brad Stein, Senior SDET and DevOps Automation Engineer</title>
<desc id="desc">A dark bstein.dev social card with professional engineering positioning and Titan Lab platform proof.</desc> <desc id="desc">A dark bstein.dev social card with professional engineering positioning and Titan Lab platform proof.</desc>
<defs> <defs>
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1"> <linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
@ -20,7 +20,7 @@
<circle cx="880" cy="174" r="6" fill="#00e5c5"/> <circle cx="880" cy="174" r="6" fill="#00e5c5"/>
<text x="80" y="196" fill="#00e5c5" font-family="JetBrains Mono, monospace" font-size="26" letter-spacing="3">BSTEIN.DEV</text> <text x="80" y="196" fill="#00e5c5" font-family="JetBrains Mono, monospace" font-size="26" letter-spacing="3">BSTEIN.DEV</text>
<text x="80" y="286" fill="#f7fbff" font-family="Space Grotesk, Inter, sans-serif" font-size="74" font-weight="700">Brad Stein</text> <text x="80" y="286" fill="#f7fbff" font-family="Space Grotesk, Inter, sans-serif" font-size="74" font-weight="700">Brad Stein</text>
<text x="80" y="352" fill="#d7e5ff" font-family="Space Grotesk, Inter, sans-serif" font-size="40" font-weight="600">DevOps Automation Engineer / Senior SDET</text> <text x="80" y="352" fill="#d7e5ff" font-family="Space Grotesk, Inter, sans-serif" font-size="40" font-weight="600">Senior SDET / DevOps Automation Engineer</text>
<text x="80" y="422" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="30">CI/CD · Kubernetes · Python · Linux · Observability</text> <text x="80" y="422" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="30">CI/CD · Kubernetes · Python · Linux · Observability</text>
<text x="80" y="482" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="26">Titan Lab is the live platform proof behind the work.</text> <text x="80" y="482" fill="#9fb2d0" font-family="Space Grotesk, Inter, sans-serif" font-size="26">Titan Lab is the live platform proof behind the work.</text>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -350,7 +350,7 @@ function pickIcon(name) {
if (h.includes("translation")) return "TR"; if (h.includes("translation")) return "TR";
if (h.includes("grafana")) return "GF"; if (h.includes("grafana")) return "GF";
if (h.includes("pegasus")) return "PG"; if (h.includes("pegasus")) return "PG";
if (h.includes("veles")) return "VL"; if (h.includes("cassandra")) return "CA";
if (h.includes("ai chat")) return "AI"; if (h.includes("ai chat")) return "AI";
if (h.includes("ai image") || h.includes("vision")) return "VI"; if (h.includes("ai image") || h.includes("vision")) return "VI";
if (h.includes("ai speech")) return "SP"; if (h.includes("ai speech")) return "SP";
@ -366,7 +366,7 @@ function serviceRank(service) {
"Flux", "Flux",
"Grafana", "Grafana",
"OpenSearch", "OpenSearch",
"Veles", "Cassandra",
"Nextcloud", "Nextcloud",
"Outline", "Outline",
"Planka", "Planka",

View File

@ -1,7 +1,7 @@
<template> <template>
<header class="topbar"> <header class="topbar">
<a class="skip-link" href="#main-content">Skip to content</a> <a class="skip-link" href="#main-content">Skip to content</a>
<RouterLink class="profile" to="/" @click="closeMenu"> <RouterLink class="profile" :to="homePath" @click="closeMenu">
<div class="avatar" aria-hidden="true"> <div class="avatar" aria-hidden="true">
<img src="@/assets/profile-avatar.jpg" alt="" /> <img src="@/assets/profile-avatar.jpg" alt="" />
</div> </div>
@ -36,7 +36,20 @@
<a v-for="link in sectionLinks" :key="link.href" class="nav-link" :href="link.href" @click="closeMenu"> <a v-for="link in sectionLinks" :key="link.href" class="nav-link" :href="link.href" @click="closeMenu">
{{ link.label }} {{ link.label }}
</a> </a>
<a class="nav-link professional-action" href="/#contact" @click="closeMenu">Contact</a> <a class="nav-link professional-action" :href="`${homePath}#contact`" @click="closeMenu">Contact</a>
<div class="mode-switch" aria-label="Homepage mode">
<RouterLink
v-for="mode in homepageModes"
:key="mode.id"
class="nav-link mode-link"
:class="{ active: mode.id === currentMode }"
:to="mode.path"
@click="closeMenu"
>
{{ mode.label }}
</RouterLink>
</div>
<div class="utility"> <div class="utility">
<template v-if="!auth.ready"> <template v-if="!auth.ready">
@ -62,21 +75,29 @@
</template> </template>
<script setup> <script setup>
import { nextTick, onUnmounted, ref, watch } from "vue"; import { computed, nextTick, onUnmounted, ref, watch } from "vue";
import { RouterLink } from "vue-router"; import { RouterLink, useRoute } from "vue-router";
import { auth, login, logout } from "@/auth"; import { auth, login, logout } from "@/auth";
import { homepageContent } from "@/data/homepageContent"; import {
homepageContentForMode,
homepageModeFromRoute,
homepageModes,
homepagePathForMode,
} from "@/data/homepageContent";
import { useMemberAccessState } from "@/member/useMemberAccessState"; import { useMemberAccessState } from "@/member/useMemberAccessState";
const { memberAction } = useMemberAccessState(); const { memberAction } = useMemberAccessState();
const content = homepageContent; const route = useRoute();
const currentMode = computed(() => homepageModeFromRoute(route));
const content = computed(() => homepageContentForMode(currentMode.value));
const homePath = computed(() => homepagePathForMode(currentMode.value));
const sectionLinks = [ const sectionLinks = computed(() => [
{ label: "Work", href: "/#work" }, { label: "Work", href: `${homePath.value}#work` },
{ label: "Capabilities", href: "/#capabilities" }, { label: "Capabilities", href: `${homePath.value}#capabilities` },
{ label: "Platform", href: "/#platform" }, { label: "Platform", href: `${homePath.value}#platform` },
{ label: "Process", href: "/#process" }, { label: "Process", href: `${homePath.value}#process` },
]; ]);
const menuOpen = ref(false); const menuOpen = ref(false);
const navEl = ref(null); const navEl = ref(null);
@ -239,6 +260,15 @@ onUnmounted(() => {
border-left: 1px solid rgba(255, 255, 255, 0.08); border-left: 1px solid rgba(255, 255, 255, 0.08);
} }
.mode-switch {
display: flex;
align-items: center;
gap: 4px;
margin-left: 4px;
padding-left: 10px;
border-left: 1px solid rgba(255, 255, 255, 0.08);
}
.nav-link { .nav-link {
min-height: 44px; min-height: 44px;
display: inline-flex; display: inline-flex;
@ -253,6 +283,18 @@ onUnmounted(() => {
white-space: nowrap; white-space: nowrap;
} }
.mode-link {
min-height: 36px;
padding: 7px 9px;
color: var(--text-muted);
}
.mode-link.active {
color: var(--accent-cyan);
border-color: rgba(0, 229, 197, 0.28);
background: rgba(0, 229, 197, 0.07);
}
.button { .button {
background: transparent; background: transparent;
cursor: pointer; cursor: pointer;
@ -340,6 +382,16 @@ onUnmounted(() => {
flex-direction: column; flex-direction: column;
} }
.mode-switch {
margin-left: 0;
padding-left: 0;
padding-top: 10px;
border-left: 0;
border-top: 1px solid rgba(255, 255, 255, 0.08);
align-items: stretch;
flex-direction: column;
}
.nav-link { .nav-link {
justify-content: flex-start; justify-content: flex-start;
white-space: normal; white-space: normal;

View File

@ -1,4 +1,4 @@
const focusMode = (import.meta.env?.VITE_HOMEPAGE_FOCUS || "devops").trim().toLowerCase(); const focusMode = (import.meta.env?.VITE_HOMEPAGE_FOCUS || "sdet").trim().toLowerCase();
const focusProfiles = { const focusProfiles = {
devops: { devops: {
@ -6,21 +6,27 @@ const focusProfiles = {
role: "DevOps Automation Engineer / Senior SDET", role: "DevOps Automation Engineer / Senior SDET",
shortRole: "DevOps Automation / Platform", shortRole: "DevOps Automation / Platform",
summary: 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.", "I build custom software, tooling, and infrastructure that make development and test workflows easier, safer, and more repeatable: platform tools, CI/CD, Docker, Kubernetes, Linux systems, Python automation, observability, and release gates.",
experienceLine: experienceLine:
"Engineering experience across Boeing, IBM, deployment automation, infrastructure tooling, system testing, and production-minded platform operations.", "Engineering experience across Boeing, IBM, deployment automation, infrastructure tooling, system testing, and production-minded platform operations.",
techStack: [ techStack: [
"Jenkins",
"Docker",
"Kubernetes", "Kubernetes",
"Docker",
"Linux", "Linux",
"Terraform", "Terraform",
"Ansible",
"Flux", "Flux",
"Helm",
"Jenkins",
"Harbor",
"Keycloak",
"Vault",
"Grafana", "Grafana",
"Prometheus", "Prometheus",
"Python", "Python",
"Bash", "Bash",
"Pytest", "Pytest",
"Selenium",
"Playwright", "Playwright",
], ],
capabilityOrder: [ capabilityOrder: [
@ -57,7 +63,7 @@ const focusProfiles = {
description: description:
"Inspectable repos, live services, and professional work samples that show how I approach real infrastructure.", "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"], selectedWorkOrder: ["Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing", "Cassandra"],
engagementOrder: [ engagementOrder: [
"Fixed-Scope Technical Rescue", "Fixed-Scope Technical Rescue",
"Short-Term Remote Contract", "Short-Term Remote Contract",
@ -69,7 +75,7 @@ const focusProfiles = {
description: 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.", "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: { cassandra: {
description: description:
"Homegrown AI playtester for Magic: The Gathering deck construction, deployed for the community on Atlas with backend orchestration and disposable simulation workers.", "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"], points: ["community AI service", "Kubernetes deployment", "worker orchestration", "job cleanup"],
@ -123,14 +129,15 @@ const focusProfiles = {
"Pytest", "Pytest",
"Selenium", "Selenium",
"Playwright", "Playwright",
"API testing", "API Testing",
"E2E Testing",
"CI/CD",
"Jenkins", "Jenkins",
"Docker", "Docker",
"Kubernetes", "Kubernetes",
"Linux", "Linux",
"Grafana", "Grafana",
"Prometheus", "Prometheus",
"Terraform",
], ],
capabilityOrder: [ capabilityOrder: [
"Test Automation & SDET", "Test Automation & SDET",
@ -166,7 +173,7 @@ const focusProfiles = {
description: description:
"Engineering work across system testing, cloud validation, delivery gates, and live platform operations.", "Engineering work across system testing, cloud validation, delivery gates, and live platform operations.",
}, },
selectedWorkOrder: ["Boeing", "IBM", "Titan Lab + titan-iac", "Atlas operator tools", "Veles"], selectedWorkOrder: ["Boeing", "IBM", "Titan Lab + titan-iac", "Atlas operator tools", "Cassandra"],
engagementOrder: [ engagementOrder: [
"Short-Term Remote Contract", "Short-Term Remote Contract",
"Fixed-Scope Technical Rescue", "Fixed-Scope Technical Rescue",
@ -178,7 +185,7 @@ const focusProfiles = {
description: 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.", "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: { cassandra: {
description: description:
"Homegrown AI playtester with simulation flows, backend service boundaries, and useful surfaces for API, worker, regression, and result-quality testing.", "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"], points: ["simulation flows", "API coverage", "regression surfaces", "result checks"],
@ -219,7 +226,7 @@ const focusProfiles = {
], ],
}, },
}, },
dev: { developer: {
eyebrow: "AVAILABLE FOR REMOTE PRODUCT, PLATFORM, AND AI SERVICE WORK", eyebrow: "AVAILABLE FOR REMOTE PRODUCT, PLATFORM, AND AI SERVICE WORK",
role: "Full-Stack Platform Engineer / Senior SDET", role: "Full-Stack Platform Engineer / Senior SDET",
shortRole: "Product / Platform Engineering", shortRole: "Product / Platform Engineering",
@ -230,15 +237,17 @@ const focusProfiles = {
techStack: [ techStack: [
"Vue", "Vue",
"Python", "Python",
"AI integration", "Flask",
"API design", "API Design",
"PostgreSQL",
"Redis",
"Workers",
"AI Integration",
"Docker", "Docker",
"Kubernetes", "Kubernetes",
"Jenkins", "CI/CD",
"Flux",
"Grafana",
"PostgreSQL",
"Pytest", "Pytest",
"Selenium",
"Playwright", "Playwright",
], ],
capabilityOrder: [ capabilityOrder: [
@ -275,7 +284,7 @@ const focusProfiles = {
description: description:
"Inspectable repos, live services, and professional work samples that show product engineering backed by real operations.", "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"], selectedWorkOrder: ["Cassandra", "Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing"],
engagementOrder: [ engagementOrder: [
"Short-Term Remote Contract", "Short-Term Remote Contract",
"Fixed-Scope Technical Rescue", "Fixed-Scope Technical Rescue",
@ -287,7 +296,7 @@ const focusProfiles = {
description: 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.", "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: { cassandra: {
description: description:
"Homegrown AI service for Magic: The Gathering deck construction, with product workflow, AI integration, backend orchestration, content seeding, and disposable simulation jobs.", "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"], points: ["AI integration", "product workflow", "service boundaries", "simulation workers"],
@ -315,7 +324,7 @@ const focusProfiles = {
}, },
{ {
label: "Testing", label: "Testing",
items: ["Pytest", "Playwright", "Selenium", "Jest", "API", "E2E", "Regression", "Contract"], items: ["Pytest", "Selenium", "Playwright", "Jest", "API", "E2E", "Regression", "Contract"],
}, },
{ {
label: "Observability", label: "Observability",
@ -330,7 +339,40 @@ const focusProfiles = {
}, },
}; };
export const homepageFocus = focusProfiles[focusMode] ? focusMode : "devops"; export const homepageModes = [
{ id: "sdet", label: "SDET", path: "/" },
{ id: "devops", label: "DevOps", path: "/devops" },
{ id: "developer", label: "Developer", path: "/developer" },
];
const focusAliases = {
dev: "developer",
development: "developer",
qa: "sdet",
test: "sdet",
testing: "sdet",
};
export function normalizeHomepageFocus(mode = "") {
const normalized = String(mode || "").trim().toLowerCase();
const aliased = focusAliases[normalized] || normalized;
return focusProfiles[aliased] ? aliased : "sdet";
}
export const homepageFocus = normalizeHomepageFocus(focusMode);
export function homepagePathForMode(mode = homepageFocus) {
return homepageModes.find((item) => item.id === normalizeHomepageFocus(mode))?.path || "/";
}
export function homepageAboutPathForMode(mode = homepageFocus) {
const focus = normalizeHomepageFocus(mode);
return focus === "sdet" ? "/about" : `/${focus}/about`;
}
export function homepageModeFromRoute(route) {
return normalizeHomepageFocus(route?.params?.focus || route?.meta?.homepageFocus || route?.query?.focus || homepageFocus);
}
const capabilities = [ const capabilities = [
{ {
@ -421,8 +463,6 @@ function orderedBy(items, order) {
return [...items].sort((a, b) => (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99)); return [...items].sort((a, b) => (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99));
} }
const selectedFocus = focusProfiles[homepageFocus];
const workDetailsByFocus = { const workDetailsByFocus = {
devops: { devops: {
titan: { titan: {
@ -483,7 +523,7 @@ const workDetailsByFocus = {
points: ["Python", "Selenium", "system E2E", "microservices", "web UI", "quality gates", "test architecture"], points: ["Python", "Selenium", "system E2E", "microservices", "web UI", "quality gates", "test architecture"],
}, },
}, },
dev: { developer: {
titan: { titan: {
kind: "Live platform", kind: "Live platform",
description: description:
@ -509,13 +549,16 @@ const workDetailsByFocus = {
}, },
}; };
const workDetails = workDetailsByFocus[homepageFocus] || workDetailsByFocus.devops; export function homepageContentForMode(mode = homepageFocus) {
const focus = normalizeHomepageFocus(mode);
const selectedFocus = focusProfiles[focus];
const workDetails = workDetailsByFocus[focus] || workDetailsByFocus.devops;
export const homepageContent = { return {
professionalName: "Brad Stein", professionalName: "Brad Stein",
role: selectedFocus.role, role: selectedFocus.role,
shortRole: selectedFocus.shortRole, shortRole: selectedFocus.shortRole,
focus: homepageFocus, focus,
eyebrow: selectedFocus.eyebrow, eyebrow: selectedFocus.eyebrow,
title: "Brad Stein | SDET, DevOps Automation & Platform Engineering", title: "Brad Stein | SDET, DevOps Automation & Platform Engineering",
description: description:
@ -532,7 +575,7 @@ export const homepageContent = {
email: "brad@bstein.dev", email: "brad@bstein.dev",
subject: "Project, contract, or engineering inquiry from bstein.dev", subject: "Project, contract, or engineering inquiry from bstein.dev",
}, },
resumeUrl: "/about", resumeUrl: homepageAboutPathForMode(focus),
resumePdfUrl: "", resumePdfUrl: "",
linkedInUrl: "https://www.linkedin.com/in/steinbradley/", linkedInUrl: "https://www.linkedin.com/in/steinbradley/",
sourceUrl: "https://scm.bstein.dev/bstein", sourceUrl: "https://scm.bstein.dev/bstein",
@ -597,18 +640,18 @@ export const homepageContent = {
{ {
icon: "AI", icon: "AI",
kind: "Community AI service", kind: "Community AI service",
title: "Veles", title: "Cassandra",
description: selectedFocus.veles.description, description: selectedFocus.cassandra.description,
points: selectedFocus.veles.points, points: selectedFocus.cassandra.points,
weight: "minor", weight: "minor",
links: [ links: [
{ {
label: "Live app", label: "Live app",
href: "https://veles.bstein.dev/", href: "https://cassandra.bstein.dev/",
}, },
{ {
label: "Content seed", label: "Content seed",
href: "https://scm.bstein.dev/bstein/veles-content", href: "https://scm.bstein.dev/bstein/cassandra-content",
}, },
], ],
}, },
@ -639,8 +682,10 @@ export const homepageContent = {
title: "Senior Remote Engineering Role", title: "Senior Remote Engineering Role",
duration: "Full-time remote roles where the fit is strong.", duration: "Full-time remote roles where the fit is strong.",
description: description:
homepageFocus === "devops" focus === "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 DevOps Automation, platform engineering, CI/CD, infrastructure automation, Kubernetes, Linux, or systems reliability roles. SDET and test automation depth comes with it."
: focus === "developer"
? "I am open to well-aligned remote developer, full-stack, platform product, AI service, Python, Vue, automation, or internal tooling roles. SDET and DevOps 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.", : "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) => { ].sort((a, b) => {
@ -782,9 +827,9 @@ export const homepageContent = {
description: "Ask the Titan Lab AI assistant for help.", description: "Ask the Titan Lab AI assistant for help.",
}, },
{ {
name: "Veles", name: "Cassandra",
icon: "🃏", icon: "🃏",
href: "https://veles.bstein.dev/", href: "https://cassandra.bstein.dev/",
description: "Test Magic: The Gathering deck ideas with AI playtesting.", description: "Test Magic: The Gathering deck ideas with AI playtesting.",
}, },
{ {
@ -795,9 +840,12 @@ export const homepageContent = {
}, },
], ],
}; };
}
export function contactMailto() { export const homepageContent = homepageContentForMode(homepageFocus);
const email = homepageContent.primaryContact.email;
const subject = encodeURIComponent(homepageContent.primaryContact.subject); export function contactMailto(content = homepageContent) {
const email = content.primaryContact.email;
const subject = encodeURIComponent(content.primaryContact.subject);
return `mailto:${email}?subject=${subject}`; return `mailto:${email}?subject=${subject}`;
} }

View File

@ -234,12 +234,12 @@ export function fallbackServices() {
status: "live", status: "live",
}, },
{ {
name: "Veles", name: "Cassandra",
icon: "🃏", icon: "🃏",
category: "ai", category: "ai",
summary: "Magic: The Gathering AI playtester for deck construction.", summary: "Magic: The Gathering AI playtester for deck construction.",
link: "https://veles.bstein.dev/", link: "https://cassandra.bstein.dev/",
host: "veles.bstein.dev", host: "cassandra.bstein.dev",
status: "live", status: "live",
}, },
{ {

View File

@ -12,8 +12,11 @@ import OnboardingView from "./views/OnboardingView.vue";
export default createRouter({ export default createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
{ path: "/", name: "home", component: HomeView }, { path: "/", name: "home", component: HomeView, meta: { homepageFocus: "sdet" } },
{ path: "/about", name: "about", component: AboutView }, { path: "/about", name: "about", component: AboutView, meta: { homepageFocus: "sdet" } },
{ path: "/dev", redirect: "/developer" },
{ path: "/:focus(devops|developer|sdet)", name: "home-focus", component: HomeView },
{ path: "/:focus(devops|developer|sdet)/about", name: "about-focus", component: AboutView },
{ path: "/ai", redirect: "/ai/chat" }, { path: "/ai", redirect: "/ai/chat" },
{ path: "/ai/chat", name: "ai-chat", component: AiView }, { path: "/ai/chat", name: "ai-chat", component: AiView },
{ path: "/ai/roadmap", name: "ai-roadmap", component: AiPlanView }, { path: "/ai/roadmap", name: "ai-roadmap", component: AiPlanView },

View File

@ -89,9 +89,9 @@
</p> </p>
<p> <p>
<span class="mono">Oceanus</span> and <span class="mono">Tethys</span> are now part of Atlas capacity instead of separate public status surfaces. <span class="mono">Oceanus</span> and <span class="mono">Tethys</span> are now part of Atlas capacity instead of separate public status surfaces.
The dedicated non-cluster hosts are the control-plane database node and the jumphost. <span class="mono">Veles</span>, The dedicated non-cluster hosts are the control-plane database node and the jumphost. <span class="mono">Cassandra</span>,
the MTG AI playtester, is deployed at the MTG AI playtester, is deployed at
<a href="https://veles.bstein.dev/" target="_blank" rel="noopener noreferrer">veles.bstein.dev</a>. <a href="https://cassandra.bstein.dev/" target="_blank" rel="noopener noreferrer">cassandra.bstein.dev</a>.
</p> </p>
</div> </div>
</section> </section>
@ -115,9 +115,13 @@
</template> </template>
<script setup> <script setup>
import { homepageContent } from "../data/homepageContent"; import { computed } from "vue";
import { useRoute } from "vue-router";
import { homepageContentForMode, homepageModeFromRoute } from "../data/homepageContent";
const about = homepageContent.about; const route = useRoute();
const content = computed(() => homepageContentForMode(homepageModeFromRoute(route)));
const about = computed(() => content.value.about);
const timeline = [ const timeline = [
{ {

View File

@ -39,13 +39,14 @@
<script setup> <script setup>
import { computed } from "vue"; import { computed } from "vue";
import { useRoute } from "vue-router";
import HomeContactFooter from "../components/HomeContactFooter.vue"; import HomeContactFooter from "../components/HomeContactFooter.vue";
import HomeHeroStatus from "../components/HomeHeroStatus.vue"; import HomeHeroStatus from "../components/HomeHeroStatus.vue";
import HomeMemberAccess from "../components/HomeMemberAccess.vue"; import HomeMemberAccess from "../components/HomeMemberAccess.vue";
import HomeProfessionalSections from "../components/HomeProfessionalSections.vue"; import HomeProfessionalSections from "../components/HomeProfessionalSections.vue";
import PlatformSection from "../components/PlatformSection.vue"; import PlatformSection from "../components/PlatformSection.vue";
import { auth } from "../auth"; import { auth } from "../auth";
import { contactMailto, homepageContent } from "../data/homepageContent"; import { contactMailto, homepageContentForMode, homepageModeFromRoute } from "../data/homepageContent";
import { useMemberAccessState } from "../member/useMemberAccessState"; import { useMemberAccessState } from "../member/useMemberAccessState";
const props = defineProps({ const props = defineProps({
@ -58,10 +59,12 @@ const props = defineProps({
error: String, error: String,
}); });
const content = homepageContent; const route = useRoute();
const homepageMode = computed(() => homepageModeFromRoute(route));
const content = computed(() => homepageContentForMode(homepageMode.value));
const { memberState, memberAction } = useMemberAccessState(); const { memberState, memberAction } = useMemberAccessState();
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const mailtoHref = contactMailto(); const mailtoHref = computed(() => contactMailto(content.value));
const statusItems = computed(() => [ const statusItems = computed(() => [
{ {
@ -134,8 +137,8 @@ const lastCheckedLabel = computed(() => {
}); });
const memberPanelCopy = computed(() => { const memberPanelCopy = computed(() => {
if (auth.enabled && auth.authenticated) return content.memberAccessCopy.authenticated; if (auth.enabled && auth.authenticated) return content.value.memberAccessCopy.authenticated;
if (memberAction.value?.kind === "restricted") return content.memberAccessCopy.restricted; if (memberAction.value?.kind === "restricted") return content.value.memberAccessCopy.restricted;
return ""; return "";
}); });

View File

@ -77,13 +77,13 @@ test("shows Brad's professional homepage and opens the platform diagram overlay"
await page.goto("/", { waitUntil: "domcontentloaded" }); await page.goto("/", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Brad Stein" })).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("heading", { name: "Senior SDET / DevOps Automation Engineer" })).toBeVisible();
await expect(page.getByRole("link", { name: "Discuss a Project" })).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: "View Résumé" })).toBeVisible();
await expect(page.getByRole("link", { name: "Explore the Live Platform" })).toBeVisible(); await expect(page.getByRole("link", { name: "Explore the Live Platform" })).toBeVisible();
await expect(page.getByRole("button", { name: "Login" })).toBeVisible(); await expect(page.getByRole("button", { name: "Login" })).toBeVisible();
await expect(page.getByRole("link", { name: "Register" })).toBeVisible(); await expect(page.getByRole("link", { name: "Register" })).toBeVisible();
await expect(page.getByRole("heading", { name: "From 0 to 100" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Quality Systems I Can Take From Zero to Reliable" })).toBeVisible();
await expect(page.getByRole("link", { name: "Nextcloud", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Nextcloud", exact: true })).toBeVisible();
await expect(page.getByText("2 of 2 online")).toBeVisible(); await expect(page.getByText("2 of 2 online")).toBeVisible();
await expect(page.getByText("Database machine")).toBeVisible(); await expect(page.getByText("Database machine")).toBeVisible();
@ -121,3 +121,21 @@ test("keeps the homepage usable when public status fails", async ({ page }) => {
await expect(page.getByText("Live status is temporarily unavailable").first()).toBeVisible(); await expect(page.getByText("Live status is temporarily unavailable").first()).toBeVisible();
await expect(page.getByText("status 503")).toHaveCount(0); await expect(page.getByText("status 503")).toHaveCount(0);
}); });
test("serves developer and SDET homepage modes", async ({ page }) => {
await page.goto("/developer", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Full-Stack Platform Engineer / Senior SDET" })).toBeVisible();
await expect(page.getByText("Product and Platform Showcase")).toBeVisible();
await expect(page.getByRole("link", { name: "Developer", exact: true })).toHaveClass(/active/);
await page.getByRole("link", { name: "View Résumé" }).click();
await expect(page).toHaveURL(/\/developer\/about$/);
await expect(page.getByText("AI Services · Vue · Python · Kubernetes")).toBeVisible();
await page.goto("/sdet", { waitUntil: "domcontentloaded" });
await expect(page.getByRole("heading", { name: "Senior SDET / DevOps Automation Engineer" })).toBeVisible();
await expect(page.getByText("Testing, Automation, and Platform Work")).toBeVisible();
await expect(page.getByRole("link", { name: "SDET", exact: true })).toHaveClass(/active/);
await page.getByRole("link", { name: "View Résumé" }).click();
await expect(page).toHaveURL(/\/about$/);
await expect(page.getByText("Test Automation · Release Quality · CI/CD")).toBeVisible();
});

View File

@ -53,6 +53,9 @@ describe("frontend entrypoint and router", () => {
expect(router.routes.map((route) => route.path)).toEqual([ expect(router.routes.map((route) => route.path)).toEqual([
"/", "/",
"/about", "/about",
"/dev",
"/:focus(devops|developer|sdet)",
"/:focus(devops|developer|sdet)/about",
"/ai", "/ai",
"/ai/chat", "/ai/chat",
"/ai/roadmap", "/ai/roadmap",
@ -63,6 +66,9 @@ describe("frontend entrypoint and router", () => {
"/onboarding", "/onboarding",
]); ]);
expect(router.routes.find((route) => route.path === "/ai")).toMatchObject({ redirect: "/ai/chat" }); expect(router.routes.find((route) => route.path === "/ai")).toMatchObject({ redirect: "/ai/chat" });
expect(router.routes.find((route) => route.path === "/dev")).toMatchObject({ redirect: "/developer" });
expect(router.routes.find((route) => route.name === "home-focus").component).toBeDefined();
expect(router.routes.find((route) => route.name === "about-focus").component).toBeDefined();
expect(router.routes.find((route) => route.name === "account").component).toBeDefined(); expect(router.routes.find((route) => route.name === "account").component).toBeDefined();
}); });
}); });

View File

@ -3,8 +3,20 @@ import { RouterLinkStub, flushPromises, mount, shallowMount } from "@vue/test-ut
import PlatformSection from "../../../frontend/src/components/PlatformSection.vue"; import PlatformSection from "../../../frontend/src/components/PlatformSection.vue";
import { auth } from "../../../frontend/src/auth.js"; import { auth } from "../../../frontend/src/auth.js";
import { homepageContentForMode, normalizeHomepageFocus } from "../../../frontend/src/data/homepageContent.js";
import HomeView from "../../../frontend/src/views/HomeView.vue"; import HomeView from "../../../frontend/src/views/HomeView.vue";
let mockRoute = { meta: { homepageFocus: "sdet" }, params: {}, query: {} };
jest.mock("vue-router", () => ({
RouterLink: {
name: "RouterLink",
props: ["to"],
template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>",
},
useRoute: () => mockRoute,
}), { virtual: true });
function resetAuth() { function resetAuth() {
auth.ready = true; auth.ready = true;
auth.enabled = true; auth.enabled = true;
@ -18,6 +30,7 @@ function resetAuth() {
describe("HomeView", () => { describe("HomeView", () => {
beforeEach(() => { beforeEach(() => {
mockRoute = { meta: { homepageFocus: "sdet" }, params: {}, query: {} };
global.fetch = jest.fn(async (resource) => { global.fetch = jest.fn(async (resource) => {
const url = typeof resource === "string" ? resource : resource?.url || ""; const url = typeof resource === "string" ? resource : resource?.url || "";
if (url.includes("/api/account/member-state")) { if (url.includes("/api/account/member-state")) {
@ -72,14 +85,14 @@ describe("HomeView", () => {
}); });
expect(wrapper.text()).toContain("Brad Stein"); expect(wrapper.text()).toContain("Brad Stein");
expect(wrapper.text()).toContain("DevOps Automation Engineer / Senior SDET"); expect(wrapper.text()).toContain("Senior SDET / DevOps Automation Engineer");
expect(wrapper.text()).toContain("Discuss a Project"); expect(wrapper.text()).toContain("Discuss a Project");
expect(wrapper.text()).toContain("View Résumé"); expect(wrapper.text()).toContain("View Résumé");
expect(wrapper.text()).toContain("Explore the Live Platform"); expect(wrapper.text()).toContain("Explore the Live Platform");
expect(wrapper.text()).toContain("From 0 to 100"); expect(wrapper.text()).toContain("Quality Systems I Can Take From Zero to Reliable");
expect(wrapper.text()).toContain("Platform Showcase"); expect(wrapper.text()).toContain("Testing, Automation, and Platform Work");
expect(wrapper.text()).toContain("Atlas operator tools"); expect(wrapper.text()).toContain("Atlas operator tools");
expect(wrapper.text()).toContain("Veles"); expect(wrapper.text()).toContain("Cassandra");
expect(wrapper.text()).not.toContain("TaskWatcher"); expect(wrapper.text()).not.toContain("TaskWatcher");
expect(wrapper.text()).toContain("family, friends, and the lucky few"); expect(wrapper.text()).toContain("family, friends, and the lucky few");
expect(wrapper.text()).toContain("Register"); expect(wrapper.text()).toContain("Register");
@ -152,6 +165,50 @@ describe("HomeView", () => {
expect(wrapper.text()).not.toContain("app_password"); expect(wrapper.text()).not.toContain("app_password");
expect(wrapper.text()).not.toContain("mailu_app_password"); expect(wrapper.text()).not.toContain("mailu_app_password");
}); });
it("builds developer and SDET homepage modes", () => {
const devops = homepageContentForMode("devops");
const developer = homepageContentForMode("developer");
const sdet = homepageContentForMode("sdet");
expect(normalizeHomepageFocus("dev")).toBe("developer");
expect(devops.summary).toContain("custom software, tooling, and infrastructure");
expect(devops.techStack).toEqual([
"Kubernetes",
"Docker",
"Linux",
"Terraform",
"Ansible",
"Flux",
"Helm",
"Jenkins",
"Harbor",
"Keycloak",
"Vault",
"Grafana",
"Prometheus",
"Python",
"Bash",
"Pytest",
"Selenium",
"Playwright",
]);
expect(developer.focus).toBe("developer");
expect(developer.role).toBe("Full-Stack Platform Engineer / Senior SDET");
expect(developer.resumeUrl).toBe("/developer/about");
expect(developer.techStack).toContain("Vue");
expect(developer.techStack.indexOf("Selenium")).toBeLessThan(developer.techStack.indexOf("Playwright"));
expect(developer.workSection.heading).toBe("Product and Platform Showcase");
expect(developer.selectedWork[0].title).toBe("Cassandra");
expect(sdet.focus).toBe("sdet");
expect(sdet.role).toBe("Senior SDET / DevOps Automation Engineer");
expect(sdet.resumeUrl).toBe("/about");
expect(sdet.techStack).toContain("Selenium");
expect(sdet.techStack.indexOf("Selenium")).toBeLessThan(sdet.techStack.indexOf("Playwright"));
expect(sdet.capabilities[0].title).toBe("Test Automation & SDET");
expect(sdet.selectedWork[0].title).toBe("Boeing");
});
}); });
describe("PlatformSection", () => { describe("PlatformSection", () => {

View File

@ -20,7 +20,7 @@ describe("sample data builders", () => {
expect(hardware.specialty.map((node) => node.alias)).toEqual(["atlas-db", "theia"]); 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 === "Keycloak")).toBe(true);
expect(services.services.some((service) => service.name === "Oceanus")).toBe(false); 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 === "Cassandra")).toBe(true);
expect(services.services.some((service) => service.name === "AI Chat")).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 === "AI Chat").link).toBe("https://bstein.dev/ai/chat");
expect(services.services.find((service) => service.name === "Monero").link).toBe("https://bstein.dev/monero"); expect(services.services.find((service) => service.name === "Monero").link).toBe("https://bstein.dev/monero");

View File

@ -27,6 +27,7 @@ jest.mock("vue-router", () => ({
props: ["to"], props: ["to"],
template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>", template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>",
}, },
useRoute: () => ({ meta: { homepageFocus: "sdet" }, params: {}, query: {} }),
}), { virtual: true }); }), { virtual: true });
describe("static shell views and components", () => { describe("static shell views and components", () => {
@ -44,7 +45,7 @@ describe("static shell views and components", () => {
const about = shallowMount(AboutView); const about = shallowMount(AboutView);
expect(about.text()).toContain("About Me"); expect(about.text()).toContain("About Me");
expect(about.text()).toContain("Titan Lab"); expect(about.text()).toContain("Titan Lab");
expect(about.text()).toContain("DevOps Automation Engineer / Senior SDET"); expect(about.text()).toContain("Senior SDET / DevOps Automation Engineer");
expect(about.text()).toContain("Kubernetes"); expect(about.text()).toContain("Kubernetes");
expect(about.text()).toContain("IBM Cloud"); expect(about.text()).toContain("IBM Cloud");
expect(about.text()).toContain("TradeHat"); expect(about.text()).toContain("TradeHat");
@ -130,10 +131,16 @@ describe("static shell views and components", () => {
try { try {
expect(wrapper.text()).toContain("Contact"); expect(wrapper.text()).toContain("Contact");
expect(wrapper.text()).toContain("DevOps");
expect(wrapper.text()).toContain("Developer");
expect(wrapper.text()).toContain("SDET");
expect(wrapper.text()).toContain("Login"); expect(wrapper.text()).toContain("Login");
expect(wrapper.text()).toContain("Register"); expect(wrapper.text()).toContain("Register");
expect(wrapper.text()).not.toContain("Reset Password"); expect(wrapper.text()).not.toContain("Reset Password");
expect(wrapper.find("a[href='/#contact']").exists()).toBe(true); expect(wrapper.find("a[href='/#contact']").exists()).toBe(true);
expect(wrapper.find("a[href='/devops']").exists()).toBe(true);
expect(wrapper.find("a[href='/developer']").exists()).toBe(true);
expect(wrapper.find(".mode-link.active").text()).toBe("SDET");
await wrapper.find("button.menu-toggle").trigger("click"); await wrapper.find("button.menu-toggle").trigger("click");
const nav = wrapper.find("nav"); const nav = wrapper.find("nav");