diff --git a/.env.example b/.env.example index e4a7c2f..79156de 100644 --- a/.env.example +++ b/.env.example @@ -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) @@ -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= diff --git a/.gitmessage b/.gitmessage index 1ff1ad9..cc4b2a5 100644 --- a/.gitmessage +++ b/.gitmessage @@ -10,8 +10,6 @@ # Footer: # Closes #ISSUE ← links issue and auto-closes on merge # -# Co-Authored-By: Claude Sonnet 4.6 -# # --------------------------------------------------------------------------- # Conventional Commits: https://www.conventionalcommits.org/ # Enforced by: The Doorman diff --git a/AGENTS.md b/AGENTS.md index b14560f..efc9ed8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 @@ -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) @@ -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 @@ -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/` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..04913ed --- /dev/null +++ b/CONTRIBUTING.md @@ -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: `/-` (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) diff --git a/README.md b/README.md index eae2746..796408b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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 | @@ -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/`). @@ -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. --- @@ -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. @@ -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:** ``` @@ -164,6 +172,7 @@ 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 @@ -171,6 +180,8 @@ e2e/ │ ├── 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 ``` @@ -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) @@ -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 --- @@ -216,7 +229,7 @@ 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 --- @@ -224,7 +237,7 @@ Budgets: performance ≥0.8, a11y ≥0.9, best-practices ≥0.9, SEO ≥0.9 · L | 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 | diff --git a/docs/blog-post-pattern.md b/docs/blog-post-pattern.md index 4d8978e..9cec8cd 100644 --- a/docs/blog-post-pattern.md +++ b/docs/blog-post-pattern.md @@ -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). | diff --git a/vercel.json b/vercel.json index 7383f2b..ec6f8c1 100644 --- a/vercel.json +++ b/vercel.json @@ -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" }