Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/health-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ 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:

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
Expand Down
175 changes: 55 additions & 120 deletions app/(main)/career-guidance/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<CareerGuidancePage />)

expect(
screen.getByRole('heading', { level: 1, name: /transform your career with expert guidance/i })
).toBeInTheDocument()
})

it('displays call-to-action buttons in hero', () => {
render(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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', () => {
Expand All @@ -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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)
it('exposes anchor targets for deep links', () => {
const { container } = render(<CareerGuidancePage />)

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 () => {
Expand All @@ -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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

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(<CareerGuidancePage />)

const getStartedButtons = screen.getAllByRole('link', { name: /get started/i })
expect(getStartedButtons.length).toBeGreaterThan(0)
})
})
98 changes: 98 additions & 0 deletions artifacts/postmortem_site_outage_2026-07.md
Original file line number Diff line number Diff line change
@@ -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.
Loading