Switch homepage focus to SDET
This commit is contained in:
parent
824a2187ea
commit
b29cdfde20
@ -32,7 +32,7 @@
|
||||
{
|
||||
"@type": "Person",
|
||||
"name": "Brad Stein",
|
||||
"jobTitle": "DevOps Automation Engineer / Senior SDET",
|
||||
"jobTitle": "Senior SDET / DevOps Automation Engineer",
|
||||
"url": "https://bstein.dev/",
|
||||
"email": "mailto:brad@bstein.dev",
|
||||
"sameAs": ["https://www.linkedin.com/in/steinbradley/", "https://scm.bstein.dev/bstein"]
|
||||
|
||||
@ -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">
|
||||
<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>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
|
||||
@ -20,7 +20,7 @@
|
||||
<circle cx="880" cy="174" r="6" fill="#00e5c5"/>
|
||||
<text x="80" y="196" fill="#00e5c5" font-family="JetBrains Mono, monospace" font-size="26" letter-spacing="3">BSTEIN.DEV</text>
|
||||
<text x="80" y="286" fill="#f7fbff" font-family="Space Grotesk, Inter, sans-serif" font-size="74" font-weight="700">Brad Stein</text>
|
||||
<text x="80" y="352" fill="#d7e5ff" font-family="Space Grotesk, Inter, sans-serif" font-size="40" font-weight="600">DevOps Automation Engineer / Senior SDET</text>
|
||||
<text x="80" y="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="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>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@ -350,7 +350,7 @@ function pickIcon(name) {
|
||||
if (h.includes("translation")) return "TR";
|
||||
if (h.includes("grafana")) return "GF";
|
||||
if (h.includes("pegasus")) return "PG";
|
||||
if (h.includes("veles")) return "VL";
|
||||
if (h.includes("cassandra")) return "CA";
|
||||
if (h.includes("ai chat")) return "AI";
|
||||
if (h.includes("ai image") || h.includes("vision")) return "VI";
|
||||
if (h.includes("ai speech")) return "SP";
|
||||
@ -366,7 +366,7 @@ function serviceRank(service) {
|
||||
"Flux",
|
||||
"Grafana",
|
||||
"OpenSearch",
|
||||
"Veles",
|
||||
"Cassandra",
|
||||
"Nextcloud",
|
||||
"Outline",
|
||||
"Planka",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<header class="topbar">
|
||||
<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">
|
||||
<img src="@/assets/profile-avatar.jpg" alt="" />
|
||||
</div>
|
||||
@ -36,7 +36,20 @@
|
||||
<a v-for="link in sectionLinks" :key="link.href" class="nav-link" :href="link.href" @click="closeMenu">
|
||||
{{ link.label }}
|
||||
</a>
|
||||
<a class="nav-link professional-action" href="/#contact" @click="closeMenu">Contact</a>
|
||||
<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">
|
||||
<template v-if="!auth.ready">
|
||||
@ -62,21 +75,29 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onUnmounted, ref, watch } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from "vue";
|
||||
import { RouterLink, useRoute } from "vue-router";
|
||||
import { auth, login, logout } from "@/auth";
|
||||
import { homepageContent } from "@/data/homepageContent";
|
||||
import {
|
||||
homepageContentForMode,
|
||||
homepageModeFromRoute,
|
||||
homepageModes,
|
||||
homepagePathForMode,
|
||||
} from "@/data/homepageContent";
|
||||
import { useMemberAccessState } from "@/member/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 = [
|
||||
{ label: "Work", href: "/#work" },
|
||||
{ label: "Capabilities", href: "/#capabilities" },
|
||||
{ label: "Platform", href: "/#platform" },
|
||||
{ label: "Process", href: "/#process" },
|
||||
];
|
||||
const sectionLinks = computed(() => [
|
||||
{ label: "Work", href: `${homePath.value}#work` },
|
||||
{ label: "Capabilities", href: `${homePath.value}#capabilities` },
|
||||
{ label: "Platform", href: `${homePath.value}#platform` },
|
||||
{ label: "Process", href: `${homePath.value}#process` },
|
||||
]);
|
||||
|
||||
const menuOpen = ref(false);
|
||||
const navEl = ref(null);
|
||||
@ -239,6 +260,15 @@ onUnmounted(() => {
|
||||
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 {
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
@ -253,6 +283,18 @@ onUnmounted(() => {
|
||||
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 {
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
@ -340,6 +382,16 @@ onUnmounted(() => {
|
||||
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 {
|
||||
justify-content: flex-start;
|
||||
white-space: normal;
|
||||
|
||||
@ -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 = {
|
||||
devops: {
|
||||
@ -6,21 +6,27 @@ const focusProfiles = {
|
||||
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.",
|
||||
"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:
|
||||
"Engineering experience across Boeing, IBM, deployment automation, infrastructure tooling, system testing, and production-minded platform operations.",
|
||||
techStack: [
|
||||
"Jenkins",
|
||||
"Docker",
|
||||
"Kubernetes",
|
||||
"Docker",
|
||||
"Linux",
|
||||
"Terraform",
|
||||
"Ansible",
|
||||
"Flux",
|
||||
"Helm",
|
||||
"Jenkins",
|
||||
"Harbor",
|
||||
"Keycloak",
|
||||
"Vault",
|
||||
"Grafana",
|
||||
"Prometheus",
|
||||
"Python",
|
||||
"Bash",
|
||||
"Pytest",
|
||||
"Selenium",
|
||||
"Playwright",
|
||||
],
|
||||
capabilityOrder: [
|
||||
@ -57,7 +63,7 @@ const focusProfiles = {
|
||||
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"],
|
||||
selectedWorkOrder: ["Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing", "Cassandra"],
|
||||
engagementOrder: [
|
||||
"Fixed-Scope Technical Rescue",
|
||||
"Short-Term Remote Contract",
|
||||
@ -69,7 +75,7 @@ const focusProfiles = {
|
||||
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: {
|
||||
cassandra: {
|
||||
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"],
|
||||
@ -123,14 +129,15 @@ const focusProfiles = {
|
||||
"Pytest",
|
||||
"Selenium",
|
||||
"Playwright",
|
||||
"API testing",
|
||||
"API Testing",
|
||||
"E2E Testing",
|
||||
"CI/CD",
|
||||
"Jenkins",
|
||||
"Docker",
|
||||
"Kubernetes",
|
||||
"Linux",
|
||||
"Grafana",
|
||||
"Prometheus",
|
||||
"Terraform",
|
||||
],
|
||||
capabilityOrder: [
|
||||
"Test Automation & SDET",
|
||||
@ -166,7 +173,7 @@ const focusProfiles = {
|
||||
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"],
|
||||
selectedWorkOrder: ["Boeing", "IBM", "Titan Lab + titan-iac", "Atlas operator tools", "Cassandra"],
|
||||
engagementOrder: [
|
||||
"Short-Term Remote Contract",
|
||||
"Fixed-Scope Technical Rescue",
|
||||
@ -178,7 +185,7 @@ const focusProfiles = {
|
||||
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: {
|
||||
cassandra: {
|
||||
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"],
|
||||
@ -219,7 +226,7 @@ const focusProfiles = {
|
||||
],
|
||||
},
|
||||
},
|
||||
dev: {
|
||||
developer: {
|
||||
eyebrow: "AVAILABLE FOR REMOTE PRODUCT, PLATFORM, AND AI SERVICE WORK",
|
||||
role: "Full-Stack Platform Engineer / Senior SDET",
|
||||
shortRole: "Product / Platform Engineering",
|
||||
@ -230,15 +237,17 @@ const focusProfiles = {
|
||||
techStack: [
|
||||
"Vue",
|
||||
"Python",
|
||||
"AI integration",
|
||||
"API design",
|
||||
"Flask",
|
||||
"API Design",
|
||||
"PostgreSQL",
|
||||
"Redis",
|
||||
"Workers",
|
||||
"AI Integration",
|
||||
"Docker",
|
||||
"Kubernetes",
|
||||
"Jenkins",
|
||||
"Flux",
|
||||
"Grafana",
|
||||
"PostgreSQL",
|
||||
"CI/CD",
|
||||
"Pytest",
|
||||
"Selenium",
|
||||
"Playwright",
|
||||
],
|
||||
capabilityOrder: [
|
||||
@ -275,7 +284,7 @@ const focusProfiles = {
|
||||
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"],
|
||||
selectedWorkOrder: ["Cassandra", "Titan Lab + titan-iac", "Atlas operator tools", "IBM", "Boeing"],
|
||||
engagementOrder: [
|
||||
"Short-Term Remote Contract",
|
||||
"Fixed-Scope Technical Rescue",
|
||||
@ -287,7 +296,7 @@ const focusProfiles = {
|
||||
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: {
|
||||
cassandra: {
|
||||
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"],
|
||||
@ -315,7 +324,7 @@ const focusProfiles = {
|
||||
},
|
||||
{
|
||||
label: "Testing",
|
||||
items: ["Pytest", "Playwright", "Selenium", "Jest", "API", "E2E", "Regression", "Contract"],
|
||||
items: ["Pytest", "Selenium", "Playwright", "Jest", "API", "E2E", "Regression", "Contract"],
|
||||
},
|
||||
{
|
||||
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 = [
|
||||
{
|
||||
@ -421,8 +463,6 @@ function orderedBy(items, order) {
|
||||
return [...items].sort((a, b) => (rank.get(a.title) ?? 99) - (rank.get(b.title) ?? 99));
|
||||
}
|
||||
|
||||
const selectedFocus = focusProfiles[homepageFocus];
|
||||
|
||||
const workDetailsByFocus = {
|
||||
devops: {
|
||||
titan: {
|
||||
@ -483,7 +523,7 @@ const workDetailsByFocus = {
|
||||
points: ["Python", "Selenium", "system E2E", "microservices", "web UI", "quality gates", "test architecture"],
|
||||
},
|
||||
},
|
||||
dev: {
|
||||
developer: {
|
||||
titan: {
|
||||
kind: "Live platform",
|
||||
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",
|
||||
role: selectedFocus.role,
|
||||
shortRole: selectedFocus.shortRole,
|
||||
focus: homepageFocus,
|
||||
focus,
|
||||
eyebrow: selectedFocus.eyebrow,
|
||||
title: "Brad Stein | SDET, DevOps Automation & Platform Engineering",
|
||||
description:
|
||||
@ -532,7 +575,7 @@ export const homepageContent = {
|
||||
email: "brad@bstein.dev",
|
||||
subject: "Project, contract, or engineering inquiry from bstein.dev",
|
||||
},
|
||||
resumeUrl: "/about",
|
||||
resumeUrl: homepageAboutPathForMode(focus),
|
||||
resumePdfUrl: "",
|
||||
linkedInUrl: "https://www.linkedin.com/in/steinbradley/",
|
||||
sourceUrl: "https://scm.bstein.dev/bstein",
|
||||
@ -597,18 +640,18 @@ export const homepageContent = {
|
||||
{
|
||||
icon: "AI",
|
||||
kind: "Community AI service",
|
||||
title: "Veles",
|
||||
description: selectedFocus.veles.description,
|
||||
points: selectedFocus.veles.points,
|
||||
title: "Cassandra",
|
||||
description: selectedFocus.cassandra.description,
|
||||
points: selectedFocus.cassandra.points,
|
||||
weight: "minor",
|
||||
links: [
|
||||
{
|
||||
label: "Live app",
|
||||
href: "https://veles.bstein.dev/",
|
||||
href: "https://cassandra.bstein.dev/",
|
||||
},
|
||||
{
|
||||
label: "Content seed",
|
||||
href: "https://scm.bstein.dev/bstein/veles-content",
|
||||
href: "https://scm.bstein.dev/bstein/cassandra-content",
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -639,9 +682,11 @@ export const homepageContent = {
|
||||
title: "Senior Remote Engineering Role",
|
||||
duration: "Full-time remote roles where the fit is strong.",
|
||||
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 SDET, QA Automation, systems testing, release quality, CI/CD, or DevOps Automation roles. Platform engineering 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.",
|
||||
},
|
||||
].sort((a, b) => {
|
||||
const rank = new Map(selectedFocus.engagementOrder.map((title, index) => [title, index]));
|
||||
@ -782,9 +827,9 @@ export const homepageContent = {
|
||||
description: "Ask the Titan Lab AI assistant for help.",
|
||||
},
|
||||
{
|
||||
name: "Veles",
|
||||
name: "Cassandra",
|
||||
icon: "🃏",
|
||||
href: "https://veles.bstein.dev/",
|
||||
href: "https://cassandra.bstein.dev/",
|
||||
description: "Test Magic: The Gathering deck ideas with AI playtesting.",
|
||||
},
|
||||
{
|
||||
@ -794,10 +839,13 @@ export const homepageContent = {
|
||||
description: "Use the private RPC endpoint through the Monero guide.",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function contactMailto() {
|
||||
const email = homepageContent.primaryContact.email;
|
||||
const subject = encodeURIComponent(homepageContent.primaryContact.subject);
|
||||
export const homepageContent = homepageContentForMode(homepageFocus);
|
||||
|
||||
export function contactMailto(content = homepageContent) {
|
||||
const email = content.primaryContact.email;
|
||||
const subject = encodeURIComponent(content.primaryContact.subject);
|
||||
return `mailto:${email}?subject=${subject}`;
|
||||
}
|
||||
|
||||
@ -234,12 +234,12 @@ export function fallbackServices() {
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
name: "Veles",
|
||||
name: "Cassandra",
|
||||
icon: "🃏",
|
||||
category: "ai",
|
||||
summary: "Magic: The Gathering AI playtester for deck construction.",
|
||||
link: "https://veles.bstein.dev/",
|
||||
host: "veles.bstein.dev",
|
||||
link: "https://cassandra.bstein.dev/",
|
||||
host: "cassandra.bstein.dev",
|
||||
status: "live",
|
||||
},
|
||||
{
|
||||
|
||||
@ -12,8 +12,11 @@ import OnboardingView from "./views/OnboardingView.vue";
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: "/", name: "home", component: HomeView },
|
||||
{ path: "/about", name: "about", component: AboutView },
|
||||
{ path: "/", name: "home", component: HomeView, meta: { homepageFocus: "sdet" } },
|
||||
{ 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/chat", name: "ai-chat", component: AiView },
|
||||
{ path: "/ai/roadmap", name: "ai-roadmap", component: AiPlanView },
|
||||
|
||||
@ -89,9 +89,9 @@
|
||||
</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.
|
||||
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
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
@ -115,9 +115,13 @@
|
||||
</template>
|
||||
|
||||
<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 = [
|
||||
{
|
||||
|
||||
@ -39,13 +39,14 @@
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import HomeContactFooter from "../components/HomeContactFooter.vue";
|
||||
import HomeHeroStatus from "../components/HomeHeroStatus.vue";
|
||||
import HomeMemberAccess from "../components/HomeMemberAccess.vue";
|
||||
import HomeProfessionalSections from "../components/HomeProfessionalSections.vue";
|
||||
import PlatformSection from "../components/PlatformSection.vue";
|
||||
import { auth } from "../auth";
|
||||
import { contactMailto, homepageContent } from "../data/homepageContent";
|
||||
import { contactMailto, homepageContentForMode, homepageModeFromRoute } from "../data/homepageContent";
|
||||
import { useMemberAccessState } from "../member/useMemberAccessState";
|
||||
|
||||
const props = defineProps({
|
||||
@ -58,10 +59,12 @@ const props = defineProps({
|
||||
error: String,
|
||||
});
|
||||
|
||||
const content = homepageContent;
|
||||
const route = useRoute();
|
||||
const homepageMode = computed(() => homepageModeFromRoute(route));
|
||||
const content = computed(() => homepageContentForMode(homepageMode.value));
|
||||
const { memberState, memberAction } = useMemberAccessState();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const mailtoHref = contactMailto();
|
||||
const mailtoHref = computed(() => contactMailto(content.value));
|
||||
|
||||
const statusItems = computed(() => [
|
||||
{
|
||||
@ -134,8 +137,8 @@ const lastCheckedLabel = computed(() => {
|
||||
});
|
||||
|
||||
const memberPanelCopy = computed(() => {
|
||||
if (auth.enabled && auth.authenticated) return content.memberAccessCopy.authenticated;
|
||||
if (memberAction.value?.kind === "restricted") return content.memberAccessCopy.restricted;
|
||||
if (auth.enabled && auth.authenticated) return content.value.memberAccessCopy.authenticated;
|
||||
if (memberAction.value?.kind === "restricted") return content.value.memberAccessCopy.restricted;
|
||||
return "";
|
||||
});
|
||||
|
||||
|
||||
@ -77,13 +77,13 @@ test("shows Brad's professional homepage and opens the platform diagram overlay"
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
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: "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("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.getByText("2 of 2 online")).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("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();
|
||||
});
|
||||
|
||||
@ -53,6 +53,9 @@ describe("frontend entrypoint and router", () => {
|
||||
expect(router.routes.map((route) => route.path)).toEqual([
|
||||
"/",
|
||||
"/about",
|
||||
"/dev",
|
||||
"/:focus(devops|developer|sdet)",
|
||||
"/:focus(devops|developer|sdet)/about",
|
||||
"/ai",
|
||||
"/ai/chat",
|
||||
"/ai/roadmap",
|
||||
@ -63,6 +66,9 @@ describe("frontend entrypoint and router", () => {
|
||||
"/onboarding",
|
||||
]);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,8 +3,20 @@ import { RouterLinkStub, flushPromises, mount, shallowMount } from "@vue/test-ut
|
||||
|
||||
import PlatformSection from "../../../frontend/src/components/PlatformSection.vue";
|
||||
import { auth } from "../../../frontend/src/auth.js";
|
||||
import { homepageContentForMode, normalizeHomepageFocus } from "../../../frontend/src/data/homepageContent.js";
|
||||
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() {
|
||||
auth.ready = true;
|
||||
auth.enabled = true;
|
||||
@ -18,6 +30,7 @@ function resetAuth() {
|
||||
|
||||
describe("HomeView", () => {
|
||||
beforeEach(() => {
|
||||
mockRoute = { meta: { homepageFocus: "sdet" }, params: {}, query: {} };
|
||||
global.fetch = jest.fn(async (resource) => {
|
||||
const url = typeof resource === "string" ? resource : resource?.url || "";
|
||||
if (url.includes("/api/account/member-state")) {
|
||||
@ -72,14 +85,14 @@ describe("HomeView", () => {
|
||||
});
|
||||
|
||||
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("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("Quality Systems I Can Take From Zero to Reliable");
|
||||
expect(wrapper.text()).toContain("Testing, Automation, and Platform Work");
|
||||
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()).toContain("family, friends, and the lucky few");
|
||||
expect(wrapper.text()).toContain("Register");
|
||||
@ -152,6 +165,50 @@ describe("HomeView", () => {
|
||||
expect(wrapper.text()).not.toContain("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", () => {
|
||||
|
||||
@ -20,7 +20,7 @@ describe("sample data builders", () => {
|
||||
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 === "Cassandra")).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");
|
||||
|
||||
@ -27,6 +27,7 @@ jest.mock("vue-router", () => ({
|
||||
props: ["to"],
|
||||
template: "<a :href=\"typeof to === 'string' ? to : '#'\" @click=\"$emit('click', $event)\"><slot /></a>",
|
||||
},
|
||||
useRoute: () => ({ meta: { homepageFocus: "sdet" }, params: {}, query: {} }),
|
||||
}), { virtual: true });
|
||||
|
||||
describe("static shell views and components", () => {
|
||||
@ -44,7 +45,7 @@ describe("static shell views and components", () => {
|
||||
const about = shallowMount(AboutView);
|
||||
expect(about.text()).toContain("About Me");
|
||||
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("IBM Cloud");
|
||||
expect(about.text()).toContain("TradeHat");
|
||||
@ -130,10 +131,16 @@ describe("static shell views and components", () => {
|
||||
|
||||
try {
|
||||
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("Register");
|
||||
expect(wrapper.text()).not.toContain("Reset Password");
|
||||
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");
|
||||
const nav = wrapper.find("nav");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user