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
16 changes: 16 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ UPSTASH_REDIS_REST_TOKEN=
# Abuse prevention — server-side PostHog events (posthog-node)
# Same project as NEXT_PUBLIC_POSTHOG_KEY below
POSTHOG_API_KEY=
# Server-side PostHog host (abuse alerts + OTLP log export via instrumentation.ts)
POSTHOG_HOST=https://us.i.posthog.com

# Abuse prevention — alert webhooks (optional; skipped when unset)
Expand Down Expand Up @@ -85,3 +86,18 @@ ALLOWED_DEV_ORIGINS=

# DevSprint quiz — referral URL shown to high scorers (≥80% accuracy)
NEXT_PUBLIC_REFERRAL_URL=https://example.com

# Twitter/X integration (src/lib/twitter.ts)
TWITTER_API_KEY=
TWITTER_API_SECRET=
TWITTER_ACCESS_TOKEN=
TWITTER_ACCESS_SECRET=

# LinkedIn integration (src/lib/linkedin.ts)
LINKEDIN_ACCESS_TOKEN=
LINKEDIN_PERSON_ID=

# GitHub Discussions integration (src/lib/github-discussion.ts)
GITHUB_TOKEN=
GITHUB_REPO_ID=
GITHUB_DISCUSSION_CATEGORY_ID=
2 changes: 0 additions & 2 deletions .gitmessage
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
# Footer:
# Closes #ISSUE ← links issue and auto-closes on merge
#
# Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#
# ---------------------------------------------------------------------------
# Conventional Commits: https://www.conventionalcommits.org/
# Enforced by: The Doorman
Expand Down
19 changes: 11 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# flabs.tech — portfolio site

Next.js 16 App Router with TypeScript, SCSS Modules, Once UI design system.
Next.js 16 App Router with TypeScript, SCSS Modules, Once UI + Mantine.

## Commands

Expand All @@ -15,12 +15,15 @@ npm test # vitest run
npm run test:watch # vitest (watch mode)
npm run test:coverage # vitest with v8 coverage
npm run test:e2e # playwright test (all browsers)
npm run test:e2e:ui # playwright interactive UI mode
npm run test:e2e:chrome # playwright chromium only (local)
npm run test:e2e:ci # playwright chromium only, no visual snapshots
npm run test:e2e:update-snapshots # update visual baselines
npm run storybook # Storybook dev server
npm run build-storybook # static Storybook export
npm run analyze # bundle analyzer
npm run lhci # local Lighthouse CI run
npm run crosspost:devto # cross-post blog MDX to Dev.to
```

## Verification order (CI)
Expand All @@ -37,13 +40,13 @@ Run all three (lint → typecheck → test) before committing.

```
src/
app/ Next.js App Router pages (about, blog, projects, work)
components/ Shared components (layout/, ui/, shared/)
config/ Once UI theme and site config
app/ Next.js App Router pages (about, blog, projects, work, quiz) + admin/ + api/
components/ Shared components (layout/, ui/, shared/, ai/, admin/)
config/ App config, Mantine theme, projects, icons
content/ MDX source for blog, projects, work entries
features/ Feature modules (about, blog, projects, work)
features/ Feature modules (about, blog, projects, work, quiz)
hooks/ Custom React hooks
lib/ Utilities (mdx, formatDate, rateLimiter, abuse/)
lib/ Utilities (mdx, formatDate, rateLimiter, abuse/, ai/)
styles/ Global SCSS variables and breakpoints
test/ Vitest setup (jsdom, RTL)
types/ TypeScript type definitions
Expand Down Expand Up @@ -71,14 +74,14 @@ Required env vars: `OPENCODE_API_KEY`, `UPSTASH_REDIS_REST_URL/TOKEN`, `POSTHOG_
- SCSS Modules for styling (`.module.scss`), global styles in `src/styles/`
- Tests co-located in `__tests__/` dirs next to source files
- Conventional Commits: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`, `test:`, `ci:`, `perf:`
- No pre-commit hooks; CI enforces quality gates
- Git hooks (`.githooks/`) enforce conventional commits (commitlint) and block direct commits to `main`; CI enforces quality gates
- PRs require CI to pass (lint → typecheck → test → e2e)

## Testing

- **Unit:** Vitest + React Testing Library, jsdom environment
- `npm test` to run all, `npx vitest run -t "test name"` for a single test
- **E2E:** Playwright with chromium + webkit (locally), chromium only (CI)
- **E2E:** Playwright with chromium + webkit + mobile-chrome (locally), chromium only (CI)
- E2E tests in `e2e/`, a11y via axe-core, visual snapshots in `e2e/screenshots/`
- Coverage tracked for `src/lib/`, `src/hooks/`, `src/components/`, `src/features/`

Expand Down
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Contributing to flabs.tech

Short version — the full conventions live in [AGENTS.md](AGENTS.md).

## Workflow

1. Open an issue first describing the change.
2. Create a branch: `<type>/<issue-number>-<slug>` (e.g. `feat/131-dark-mode`).
3. Make changes with [Conventional Commits](https://www.conventionalcommits.org/) (enforced by commitlint via `.githooks/`; direct commits to `main` are blocked).
4. Open a PR using the template at [.github/pull_request_template.md](.github/pull_request_template.md) — link the issue with `Closes #N`.

## Before you push

```sh
npm run lint
npm run typecheck
npm test
```

CI runs lint → typecheck → test → e2e → Lighthouse; all must pass before merge.

## Docs that must stay in sync

- `README.md` — features, stack, commands, project structure
- `AGENTS.md` — agent commands, architecture, conventions
- `.env.example` — every env var the app reads
- [`docs/blog-post-pattern.md`](docs/blog-post-pattern.md) — rules for new blog posts (blocking checklist)
43 changes: 28 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ Live at **[flabs.tech](https://flabs.tech)**

### Pages
- **Home** — Split hero with animated headline + CTAs, 3-column project grid, recent posts section
- **Work** — Professional experience timeline: 7 roles across 6 companies in the USA, Europe, and Brazil, plus education
- **Projects** — Featured projects with MDX detail pages and GitHub links, sourced from the fworks-tech GitHub profile: Agenthood, Agenthood Site, ArXiv Manager, atlaslink (coming soon), HashEyes, LogRoute, flabs.tech
- **Work** — Professional experience timeline: 8 roles across 7 companies in the USA, Europe, and Brazil, plus education
- **Projects** — Featured projects with MDX detail pages and GitHub links, sourced from the fworks-tech GitHub profile: Agenthood, Agenthood Site, ArXiv Manager, atlaslink, HashEyes, LogRoute, flabs.tech
- **Blog** — Engineering blog with MDX posts on GraphQL Federation, multi-agent AI, and skills registries
- **About** — Full professional bio, location, social links, and skill tags across Frontend · Backend & APIs · AI & Agents
- **Quiz (DevSprint)** — Timed dev-trivia game: 20s per question, streaks, achievements, weekly leaderboard (Upstash Redis), referral sharing
- **AI Assistant** — Chat widget on every page; answers about the site's content and author via OpenCode Zen (`mimo-v2.5`). Equipped with tools: GitHub repo stats, authorized URL fetching, and content search across blog/projects
- **AI Assistant** — Chat widget on every page; answers about the site's content and author via OpenCode Go (`glm-5.3-flash`). Equipped with tools: GitHub repo stats, authorized URL fetching, and content search across blog/projects

### Technical
- **Next.js 16** App Router with full TypeScript
Expand All @@ -32,6 +32,7 @@ Live at **[flabs.tech](https://flabs.tech)**
- **Dynamic OG images** via `next/og` — auto-generated for every page with 1200×630 (1.91:1)
- **Profile photo favicon** generated server-side via `icon.tsx`, plus a static `favicon.ico` built from the same avatar photo (what Google/browsers fetch at `/favicon.ico`)
- **Abuse prevention** for the AI chat endpoint — deterministic pipeline (`src/lib/abuse/`): signal scoring → quarantine tiers → shadow/enforce modes; privacy-first HMAC keyed identities
- **Dev.to cross-posting** — `npm run crosspost:devto` + `crosspost.yml` workflow backfills article ids into MDX frontmatter after merge
- **AGENTS.md** — AI agent instructions (build/test commands, conventions, git workflow)
- Deployed on **Vercel** with PR preview deployments

Expand All @@ -46,12 +47,12 @@ Live at **[flabs.tech](https://flabs.tech)**
| Language | TypeScript |
| Content | MDX + gray-matter |
| Styling | SCSS Modules |
| AI Runtime | Vercel AI SDK v7 + OpenCode Zen (OpenAI-compatible, `mimo-v2.5`) |
| AI Runtime | Vercel AI SDK v7 + OpenCode Go (OpenAI-compatible, `glm-5.3-flash`) |
| Storage | Upstash Redis (leaderboard, sessions, abuse signals) |
| Observability | PostHog · pino + OpenTelemetry logs |
| Observability | PostHog · pino + OpenTelemetry (OTLP log export to PostHog) |
| Linting | ESLint 9 (flat config) + Prettier |
| Bundler | Turbopack |
| Type Checking | TypeScript 5.8 (`tsc --noEmit`) |
| Type Checking | TypeScript 5 (`tsc --noEmit`) |
| Testing | Vitest 4 · Playwright · axe-core · Lighthouse CI |
| Storybook | Storybook 10 |
| Bundle Audit | @next/bundle-analyzer |
Expand All @@ -65,24 +66,29 @@ Live at **[flabs.tech](https://flabs.tech)**
```
src/
├── app/ # Next.js App Router (routes, API, layout)
│ ├── api/ # chat, quiz/*, authenticate, analytics, og, rss, auth
│ ├── admin/ # GitHub-SSO admin area (analytics, AI stats, drafts, publishing)
│ ├── api/ # chat, quiz/*, authenticate, analytics, og, rss, auth, crosspost, check-auth
│ └── quiz/ # DevSprint quiz game
├── components/ # Presentational components by role
│ ├── admin/ # Admin dashboard charts
│ ├── ai/ # AiAssistant chat widget
│ ├── layout/ # Header, Footer, Providers, RouteGuard
│ ├── ui/ # Mailchimp, HeadingLink, ProjectCard
│ └── shared/ # MDX renderer, shared utilities
├── config/ # App configuration (Once UI, icons, barrel)
├── config/ # App configuration (app config, Mantine theme, projects, icons, barrel)
├── content/ # Editorial data (bio, experience, MDX posts)
│ ├── blog/ # Blog post MDX files
│ ├── work/ # Work experience MDX files
│ └── projects/ # Project detail MDX files
├── features/ # Domain-specific components (by page)
├── features/ # Domain-specific components (about, blog, projects, work, quiz)
├── hooks/ # Custom React hooks
├── lib/ # Pure utility functions
│ ├── abuse/ # AI chat abuse-prevention pipeline
│ └── ai/ # Chat tool definitions + web search
├── styles/ # Global SCSS/CSS
└── types/ # Shared TypeScript types
├── types/ # Shared TypeScript types
├── proxy.ts # Request-logging middleware (+ Server-Timing header)
└── auth.ts # NextAuth 5 config (GitHub SSO, Upstash Redis sessions)
```

Layered dependency rule: inner layers (`lib/`, `config/`) never import from outer layers (`features/`, `app/`).
Expand All @@ -106,9 +112,11 @@ npm run storybook # Start at http://localhost:6006
npm run build-storybook # Static build
```

Stories cover `AnimatedHeadline`, `HeadingLink`, `ProjectCard`, `ScrollToHash`, `ZoomableImage`, `Post`, and `ShareSection`.

### Env vars

Required: `OPENCODE_API_KEY`, `UPSTASH_REDIS_REST_URL/TOKEN`, `POSTHOG_API_KEY`; optional: `SLACK_WEBHOOK_URL`, `DISCORD_WEBHOOK_URL`, `ABUSE_KEY_SECRET`, `ABUSE_RESPONSE_MODE`, `ABUSE_TRACK_IP`, `ABUSE_RETENTION_MS` (see `.env.example`).
Required: `OPENCODE_API_KEY`, `UPSTASH_REDIS_REST_URL/TOKEN`, `POSTHOG_API_KEY`; optional: `SLACK_WEBHOOK_URL`, `DISCORD_WEBHOOK_URL`, `ABUSE_KEY_SECRET`, `ABUSE_RESPONSE_MODE`, `ABUSE_TRACK_IP`, `ABUSE_RETENTION_MS`. See `.env.example` for the full list with descriptions.

---

Expand All @@ -122,7 +130,7 @@ Required: `OPENCODE_API_KEY`, `UPSTASH_REDIS_REST_URL/TOKEN`, `POSTHOG_API_KEY`;
| `npm run test:watch` | Run tests in watch mode |
| `npm run test:coverage` | Run tests with v8 coverage report |

**Stack:** Vitest 4 · React Testing Library · jsdom · v8 coverage · 514 tests across 81 test files
**Stack:** Vitest 4 · React Testing Library · jsdom · v8 coverage · 725 tests across 103 test files

**Convention:** Tests live in `__tests__/` directories next to the files they cover.

Expand All @@ -148,7 +156,7 @@ src/features/about/TableOfContents.tsx → src/features/about/__tests__/TableOfC

**Stack:** Playwright 1.x · axe-core · navigation, pages, a11y, visual snapshots, API routes, responsive, AI assistant, sign-in

**Browsers:** Chromium + WebKit (local) · Chromium only (CI)
**Browsers:** Chromium + WebKit + mobile-chrome (Pixel 5, local) · Chromium only (CI)

**Structure:**
```
Expand All @@ -164,13 +172,16 @@ e2e/
├── signin.spec.ts # Auth redirect flow
├── ai-assistant.spec.ts # Chat open/send/tool responses
├── ai-assistant.screenshots.spec.ts # Chat visual snapshots
├── admin.spec.ts # Admin area (auth-gated)
├── pages/
│ ├── home.spec.ts # Title, favicon, OG meta
│ ├── about.spec.ts # Title, social links
│ ├── blog.spec.ts # Listing, post nav
│ ├── work.spec.ts # Timeline
│ ├── work-detail.spec.ts # Case study pages
│ └── projects.spec.ts # Grid, detail nav
├── fixtures.ts # Shared Playwright fixtures
├── global-setup.ts # Playwright global setup
└── screenshots/
└── pages.spec.ts # Full-page desktop snapshots
```
Expand All @@ -179,6 +190,7 @@ e2e/

```
push/PR to main
├── secrets job: gitleaks secret scanning
├── test job: npm install → npm run lint → npm run typecheck → vitest
├── e2e job: npm install → playwright install chromium → playwright test
└── lighthouse job: npm install → npm run build → lhci autorun (needs: test)
Expand All @@ -197,6 +209,7 @@ push/PR to main
- **Rate limiting** on all public APIs — `/api/authenticate` (5 req/60s/IP), `/api/chat` (10–30 req/60s/IP), `/api/analytics/event`, and quiz endpoints
- **Session auth** — NextAuth 5 with Upstash Redis sessions; httpOnly, SameSite: strict, Secure cookies
- **AI abuse pipeline** (`src/lib/abuse/`) — deterministic logistic scoring over a decaying feature vector (30-min half-life, actors auto-recover); two-tier prompt-injection detection (block vs. signal); quarantine tiers throttle → soft-quarantine → hard-block; `ABUSE_RESPONSE_MODE=shadow` (observe, default) or `enforce` (block)
- **Admin area** (`/admin`) — GitHub SSO (NextAuth 5, Upstash Redis sessions); analytics and AI-stats dashboards, quiz management, draft preview, publishing tools
- **Privacy** — `ABUSE_TRACK_IP=false` → HMAC-keyed identities (`ABUSE_KEY_SECRET`); alert recipients (PostHog/webhooks) only see masked keys; client IP read from the rightmost `X-Forwarded-For` entry to defeat spoofing

---
Expand All @@ -216,15 +229,15 @@ push/PR to main
| `npm run lhci` | Run Lighthouse CI locally |
| `npm run analyze` | Bundle analyzer (opens HTML report) |

Budgets: performance ≥0.8, a11y ≥0.9, best-practices ≥0.9, SEO ≥0.9 · LCP ≤3000ms, CLS ≤0.1
Budgets: performance ≥0.8, a11y ≥0.9, best-practices ≥0.9, SEO ≥0.9 · LCP ≤3000ms, CLS ≤0.1, TBT ≤300ms, FCP ≤1800ms

---

## Content files

| File | Purpose |
|------|---------|
| `src/config/once-ui.config.ts` | Theme, colors, routes, SEO schema, newsletter |
| `src/config/app.config.ts` | Theme, colors, routes, SEO schema, newsletter |
| `src/config/icons.ts` | Icon registry |
| `src/content/index.tsx` | Bio, work experience, skills, social links |
| `src/content/blog/*.mdx` | Blog posts |
Expand Down
4 changes: 2 additions & 2 deletions docs/blog-post-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ Verify against code, never from memory:

| # | Rule |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| T1 | Footer model MUST equal `MODEL_ID` in `src/app/api/chat/route.ts:39` (today `mimo-v2.5`); grep before writing. |
| T1 | Footer model MUST equal `MODEL_ID` in `src/app/api/chat/route.ts:50` (today `glm-5.3-flash`); grep before writing. |
| T2 | Count claims MUST match the table and code — an allowlist gate is not a timeout. |
| T3 | Name exact timeout scope: `AbortSignal.timeout(10_000)` (`src/lib/ai/tools.ts:78`) covers `fetchUrlContent` ONLY — `fetchGitHubRepo` has no timeout; never claim wider scope. |
| T3 | Name exact timeout scope: `AbortSignal.timeout(10_000)` (`src/lib/ai/tools.ts:19`) covers `fetchUrlContent`, `fetchGitHubRepo`, and `listGitHubRepos` — never claim wider scope. |
| T4 | State exactly which inputs the model controls: `fetchGitHubRepo` hardcodes `owner="fworks-tech"`; only `repo` is model-supplied. |
| T5 | State total tool counts accurately: 4 tools (`fetchGitHubRepo`, `fetchUrlContent`, `searchContent`, `listGitHubRepos`). |
| T6 | Checklist (§7) MUST cover every shipped behavior in the diff/ADR, including guards (400 on empty/malformed messages). |
Expand Down
1 change: 0 additions & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"value": "camera=(), microphone=(), geolocation=()"
},
{
"key": "Content-Security-Policy",
"key": "Content-Security-Policy",
"value": "default-src 'self'; script-src 'self' 'unsafe-inline' https://*.vercel-insights.com https://*.vercel-analytics.com https://*.posthog.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://fonts.googleapis.com; img-src 'self' data: https:; connect-src 'self' https://*.vercel-insights.com https://*.vercel-analytics.com https://us.i.posthog.com; worker-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' https://github.com"
}
Expand Down
Loading