From 2d1d1a559d8edcc692e2aa76fa95431d179e21c6 Mon Sep 17 00:00:00 2001 From: Jordan Winters Date: Tue, 21 Jul 2026 11:19:04 -0500 Subject: [PATCH 1/3] test: align career-guidance tests with current page content The test suite still asserted the original generated marketing copy (testimonials, pricing tiers, success-rate stats) that was removed when the page was rewritten before merge. 18 of 19 tests failed on main, blocking every subsequent PR pipeline. Rewritten against the current page: section headings, service and engagement offerings, FAQ entries, link safety (target/rel), heading hierarchy, deep-link anchors, axe accessibility, and metadata. Co-Authored-By: Claude Fable 5 --- app/(main)/career-guidance/page.test.tsx | 175 +++++++---------------- 1 file changed, 55 insertions(+), 120 deletions(-) diff --git a/app/(main)/career-guidance/page.test.tsx b/app/(main)/career-guidance/page.test.tsx index e25589a..315718f 100644 --- a/app/(main)/career-guidance/page.test.tsx +++ b/app/(main)/career-guidance/page.test.tsx @@ -2,109 +2,92 @@ import { axeTest, render, screen } from 'lib/test-utils' import { describe, expect, it } from 'vitest' import CareerGuidancePage from './page' +const LINKEDIN_URL = 'https://www.linkedin.com/in/wintersjordan/' + describe('Career Guidance Page', () => { it('renders the hero section with main heading', () => { render() - expect( - screen.getByRole('heading', { level: 1, name: /transform your career with expert guidance/i }) - ).toBeInTheDocument() - }) - - it('displays call-to-action buttons in hero', () => { - render() - - expect(screen.getByText(/schedule free consultation/i)).toBeInTheDocument() - expect(screen.getByText(/view services/i)).toBeInTheDocument() + expect(screen.getByRole('heading', { level: 1, name: /career guidance/i })).toBeInTheDocument() + expect(screen.getByText(/career coaching/i)).toBeInTheDocument() }) - it('includes professional bio and credentials', () => { + it('displays the hero call-to-action', () => { render() - expect( - screen.getByRole('heading', { name: /expert career guidance from a seasoned professional/i }) - ).toBeInTheDocument() - expect(screen.getByText(/10\+/)).toBeInTheDocument() - expect(screen.getByText(/years experience/i)).toBeInTheDocument() + const heroCta = screen.getByRole('link', { name: /get in touch on linkedin/i }) + expect(heroCta).toHaveAttribute('href', LINKEDIN_URL) }) it('displays all service offerings', () => { render() - expect(screen.getByRole('heading', { name: /1-on-1 career coaching/i })).toBeInTheDocument() - expect( - screen.getByRole('heading', { name: /resume & linkedin optimization/i }) - ).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /interview preparation/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /career transition strategy/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /leadership development/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /salary negotiation/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /services offered/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /1-on-1 coaching/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /resume & linkedin review/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /interview prep/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /career transitions/i })).toBeInTheDocument() }) - it('renders the process section', () => { + it('displays service benefits', () => { render() - expect(screen.getByRole('heading', { name: /how it works/i })).toBeInTheDocument() - expect(screen.getByText(/book consultation/i)).toBeInTheDocument() - expect(screen.getByText(/create strategy/i)).toBeInTheDocument() - expect(screen.getByText(/take action/i)).toBeInTheDocument() - expect(screen.getByText(/achieve goals/i)).toBeInTheDocument() + expect(screen.getByText(/personalized advice/i)).toBeInTheDocument() + expect(screen.getByText(/ats optimization/i)).toBeInTheDocument() + expect(screen.getByText(/mock interviews/i)).toBeInTheDocument() + expect(screen.getByText(/transition planning/i)).toBeInTheDocument() }) - it('includes client testimonials', () => { + it('displays engagement options with durations', () => { render() - expect(screen.getByRole('heading', { name: /client success stories/i })).toBeInTheDocument() - expect( - screen.getByText(/working with bidwell consulting transformed my career trajectory/i) - ).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /ways to work together/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /discovery call/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /^single session$/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /ongoing mentorship/i })).toBeInTheDocument() + expect(screen.getByText('30 minutes')).toBeInTheDocument() + expect(screen.getByText('60 minutes')).toBeInTheDocument() + expect(screen.getByText('3 months')).toBeInTheDocument() }) - it('displays pricing tiers', () => { + it('renders a reach-out link for every engagement option', () => { render() - expect(screen.getByRole('heading', { name: /pricing & packages/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /discovery session/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /^single session$/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /career accelerator/i })).toBeInTheDocument() - expect(screen.getByText(/most popular/i)).toBeInTheDocument() + const reachOutLinks = screen.getAllByRole('link', { name: /reach out/i }) + expect(reachOutLinks).toHaveLength(3) + for (const link of reachOutLinks) { + expect(link).toHaveAttribute('href', LINKEDIN_URL) + } }) - it('includes FAQ section', () => { + it('includes FAQ section with all questions', () => { render() expect(screen.getByRole('heading', { name: /frequently asked questions/i })).toBeInTheDocument() - expect(screen.getByText(/how long are the coaching sessions\?/i)).toBeInTheDocument() - expect(screen.getByText(/what if i'm not sure which service i need\?/i)).toBeInTheDocument() + expect(screen.getByText(/how long are sessions\?/i)).toBeInTheDocument() + expect(screen.getByText(/not sure what you need\?/i)).toBeInTheDocument() + expect(screen.getByText(/packages available\?/i)).toBeInTheDocument() + expect(screen.getByText(/industry focus\?/i)).toBeInTheDocument() + expect(screen.getByText(/remote work\?/i)).toBeInTheDocument() }) it('renders contact section', () => { render() - expect( - screen.getByRole('heading', { name: /ready to transform your career\?/i }) - ).toBeInTheDocument() - expect(screen.getByText(/multiple ways to connect/i)).toBeInTheDocument() + expect(screen.getByRole('heading', { name: /get in touch/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /connect on linkedin/i })).toBeInTheDocument() }) - it('includes LinkedIn links', () => { + it('opens all external links safely in a new tab', () => { render() - const linkedinLink = screen.getByRole('link', { name: /connect on linkedin/i }) - expect(linkedinLink).toHaveAttribute('href', 'https://www.linkedin.com/in/wintersjordan/') - expect(linkedinLink).toHaveAttribute('target', '_blank') - expect(linkedinLink).toHaveAttribute('rel', 'noopener noreferrer') - }) - - it('displays trust building section', () => { - render() - - expect( - screen.getByRole('heading', { name: /why choose bidwell consulting/i }) - ).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /real-world experience/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /proven methodologies/i })).toBeInTheDocument() - expect(screen.getByRole('heading', { name: /ongoing support/i })).toBeInTheDocument() + const links = screen.getAllByRole('link') + expect(links.length).toBeGreaterThan(0) + for (const link of links) { + expect(link).toHaveAttribute('href', LINKEDIN_URL) + expect(link).toHaveAttribute('target', '_blank') + expect(link).toHaveAttribute('rel', 'noopener noreferrer') + } }) it('has proper heading hierarchy', () => { @@ -114,36 +97,16 @@ describe('Career Guidance Page', () => { const h1Elements = screen.getAllByRole('heading', { level: 1 }) expect(h1Elements).toHaveLength(1) - // Should have multiple h2 headings for main sections + // One h2 per main section: services, engagement, FAQ, contact const h2Elements = screen.getAllByRole('heading', { level: 2 }) - expect(h2Elements.length).toBeGreaterThan(5) - }) - - it('includes anchor links for navigation', () => { - render() - - const servicesLinks = screen.getAllByRole('link', { name: /view services/i }) - expect(servicesLinks[0]).toHaveAttribute('href', '#services') - - const contactLinks = screen.getAllByRole('link', { name: /schedule free consultation/i }) - expect(contactLinks[0]).toHaveAttribute('href', '#contact') - }) - - it('displays service benefits with checkmarks', () => { - render() - - expect(screen.getByText(/customized career roadmap/i)).toBeInTheDocument() - expect(screen.getByText(/ats-optimized resume writing/i)).toBeInTheDocument() - expect(screen.getByText(/mock interview sessions/i)).toBeInTheDocument() + expect(h2Elements).toHaveLength(4) }) - it('shows statistics in about section', () => { - render() + it('exposes anchor targets for deep links', () => { + const { container } = render() - expect(screen.getByText(/100\+/)).toBeInTheDocument() - expect(screen.getByText(/clients helped/i)).toBeInTheDocument() - expect(screen.getByText(/95%/)).toBeInTheDocument() - expect(screen.getByText(/success rate/i)).toBeInTheDocument() + expect(container.querySelector('#services')).toBeInTheDocument() + expect(container.querySelector('#contact')).toBeInTheDocument() }) it('is accessible', async () => { @@ -153,37 +116,9 @@ describe('Career Guidance Page', () => { it('has proper metadata', async () => { const { metadata } = await import('./page') - expect(metadata.title).toBe('Career Guidance & Coaching Services') - expect(metadata.description).toContain('Professional career coaching') + expect(metadata.title).toBe('Career Guidance') + expect(metadata.description).toContain('career coaching') expect(metadata.keywords).toContain('career coach') expect(metadata.keywords).toContain('career guidance') }) - - it('displays all FAQ questions', () => { - render() - - expect(screen.getByText(/how long are the coaching sessions\?/i)).toBeInTheDocument() - expect(screen.getByText(/what if i'm not sure which service i need\?/i)).toBeInTheDocument() - expect(screen.getByText(/do you offer packages or subscriptions\?/i)).toBeInTheDocument() - expect(screen.getByText(/how quickly can i expect results\?/i)).toBeInTheDocument() - expect(screen.getByText(/do you specialize in any particular industry\?/i)).toBeInTheDocument() - expect(screen.getByText(/can you help with remote job searches\?/i)).toBeInTheDocument() - }) - - it('includes all pricing tier features', () => { - render() - - expect(screen.getByText(/initial assessment/i)).toBeInTheDocument() - expect(screen.getByText(/no commitment required/i)).toBeInTheDocument() - expect(screen.getByText(/dedicated 1-on-1 time/i)).toBeInTheDocument() - expect(screen.getByText(/weekly coaching sessions/i)).toBeInTheDocument() - expect(screen.getByText(/priority scheduling/i)).toBeInTheDocument() - }) - - it('has multiple CTA buttons throughout the page', () => { - render() - - const getStartedButtons = screen.getAllByRole('link', { name: /get started/i }) - expect(getStartedButtons.length).toBeGreaterThan(0) - }) }) From 64dc77f118e9f3666a8f2bab8db21085a0a3ad57 Mon Sep 17 00:00:00 2001 From: Jordan Winters Date: Tue, 21 Jul 2026 11:19:04 -0500 Subject: [PATCH 2/3] ci: run health check on a weekly schedule The health check only ran after deployments, so an outage between deploys (e.g. GitHub Pages being disabled) went unnoticed for months. A weekly scheduled run fails loudly when the site is unreachable. Co-Authored-By: Claude Fable 5 --- .github/workflows/health-check.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/health-check.yml b/.github/workflows/health-check.yml index 2b30be1..09080a3 100644 --- a/.github/workflows/health-check.yml +++ b/.github/workflows/health-check.yml @@ -9,6 +9,11 @@ on: workflows: ["Deploy to Production"] types: [completed] + # Scheduled uptime monitoring — catches outages that happen between + # deploys (e.g. Pages being disabled), not just broken deploys + schedule: + - cron: '0 8 * * 1' # Mondays 08:00 UTC + # Manual health checks workflow_dispatch: @@ -16,7 +21,7 @@ jobs: health-check: name: Verify Site Health runs-on: ubuntu-latest - if: github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' + if: github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' steps: - name: Checkout uses: actions/checkout@v4 From 537493cab11a88fd3d41d3a894a917534dce9b07 Mon Sep 17 00:00:00 2001 From: Jordan Winters Date: Tue, 21 Jul 2026 11:22:06 -0500 Subject: [PATCH 3/3] docs: add post-mortem for bidwell.info outage Co-Authored-By: Claude Fable 5 --- artifacts/postmortem_site_outage_2026-07.md | 98 +++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 artifacts/postmortem_site_outage_2026-07.md diff --git a/artifacts/postmortem_site_outage_2026-07.md b/artifacts/postmortem_site_outage_2026-07.md new file mode 100644 index 0000000..f598685 --- /dev/null +++ b/artifacts/postmortem_site_outage_2026-07.md @@ -0,0 +1,98 @@ +# Post-Mortem: bidwell.info Outage (detected 2026-07-21) + +## Summary + +The production site (https://bidwell.info) served GitHub's 404 page for an +extended period — every route, including the apex domain, the www subdomain, +and the underlying https://dralgorhythm.github.io/bidwell/ URL. Detected +2026-07-21 during a routine repository check; the outage began silently at an +unknown point after 2026-02-28. + +## Root Cause + +**GitHub Pages was disabled because the repository is private and the account +plan does not support GitHub Pages on private repositories.** + +Confirmed by the Pages API, which rejects both deployments and configuration +updates with: + +> `Page is disabled because current plan does not support private GitHub Pages` (HTTP 422) + +When Pages was disabled, GitHub also removed the custom-domain binding +(`cname: null` in the Pages config), which is why bidwell.info 404s even +though DNS still points correctly at GitHub Pages +(A records 185.199.108–111.153, `www` CNAME to `dralgorhythm.github.io`). + +The site last deployed successfully on 2025-12-08 and worked at that time, so +the repository visibility (or the account plan) changed sometime afterward. + +## Contributing Causes + +1. **No independent uptime monitoring.** The Post-Deploy Health Check workflow + only ran via `workflow_run` after a deploy. With no deploys happening, an + outage between deploys could never be detected. The site was down for + months with zero signal. +2. **CI was red on `main` since December, halting all merges (and therefore + all deploys).** PR #32 (career-guidance page) was merged on 2025-12-08 with + a failing pipeline: its test suite asserted generated marketing copy that + had been rewritten before merge, leaving 18 permanently failing tests on + `main`. All three subsequent dependabot PRs inherited the failing test leg + and could not go green. +3. **Merging with a red pipeline** (PR #32) established the broken baseline. + No branch protection requires checks to pass before merge. + +## Timeline + +| Date | Event | +|------|-------| +| 2025-12-07/08 | PR #32 pipeline fails 3×; merged anyway. Deploy succeeds; site healthy. Failing tests now on `main`. | +| 2025-12-12 → 2026-02-28 | Dependabot PRs (#35 lodash-es, #36 next, #37 rollup) all fail the inherited test leg; nothing merges; no deploys. | +| After 2026-02-28 | Repo private / plan state disables GitHub Pages; custom domain binding removed; site 404s. Exact date unknown (no monitoring). | +| 2026-07-21 | Outage detected. Triage, CI fix, and monitoring added (PR #38). Redeploy blocked pending account-level decision. | + +## Resolution + +- **CI restored** (PR #38): career-guidance tests rewritten against the actual + page content — structural assertions (headings, services, engagement + options, FAQ, link safety, a11y via axe, metadata) instead of verbatim + marketing copy. Full suite green: 189/189. +- **Weekly scheduled health check added** (PR #38): the health-check workflow + now runs Mondays 08:00 UTC in addition to post-deploy, so a dead site fails + the workflow and GitHub emails the failure notification. +- **Dependabot PRs unblocked**: verified locally against the fixed test suite + (including a full typecheck/test/build pass on the `next` 16.1.5 bump). + +## Remaining Action (account-level decision required) + +Restoring the site requires one of: + +1. **Make the repository public** — free; Pages works immediately. Full git + history was scanned with gitleaks (166 commits): no secrets found. Content + is a portfolio site plus site-planning docs. +2. **Upgrade the GitHub plan** (Pro) — keeps the repo private with Pages. +3. **Move hosting** (e.g. Cloudflare Pages) — supports private repos on free + tier, but deviates from the tech-strategy golden path (GitHub Pages for + static frontend). + +After option 1 or 2, recovery is: + +```bash +# Re-attach the custom domain (may require re-enabling Pages in Settings → Pages first) +gh api -X PUT repos/dralgorhythm/bidwell/pages -f build_type=workflow -f cname=bidwell.info +# Redeploy +gh workflow run deploy.yml --ref main +# Verify +gh workflow run health-check.yml +``` + +HTTPS for the custom domain may take a few minutes while GitHub re-provisions +the certificate. + +## Lessons + +- Uptime monitoring must be independent of the deploy pipeline; an outage + that only manifests between deploys is otherwise invisible. +- Never merge red: consider branch protection requiring the validate checks. +- Note: GitHub disables `schedule:` workflow triggers after ~60 days of + repository inactivity — the weekly health check protects an active repo, + but a fully dormant one still needs external monitoring.