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
32 changes: 32 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,36 @@ jobs:
run: npx tsx scripts/test-insights.ts
env:
INSIGHTS_TEST_DATABASE_URL: mongodb://127.0.0.1:27017/studio_insights_test?replicaSet=rs0
- name: Verify transactional content recovery with MongoDB
run: npm run test:recovery
env:
RECOVERY_TEST_DATABASE_URL: mongodb://127.0.0.1:27017/studio_recovery_test?replicaSet=rs0
- name: Seed synthetic browser-test content
run: npx tsx scripts/seed-e2e.ts
env:
E2E_DATABASE_URL: mongodb://127.0.0.1:27017/studio_e2e_test?replicaSet=rs0
- name: Create browser-test indexes
run: npx prisma db push --skip-generate
env:
DATABASE_URL: mongodb://127.0.0.1:27017/studio_e2e_test?replicaSet=rs0
- name: Verify content check refuses an unavailable database
run: |
if DATABASE_URL='' npm run content:check; then
echo 'Content check unexpectedly accepted an unavailable database'
exit 1
fi
- run: npm run build
env:
DATABASE_URL: mongodb://127.0.0.1:27017/studio_e2e_test?replicaSet=rs0
- run: npx playwright install --with-deps chromium
- run: npm run test:e2e
env:
E2E_DATABASE_URL: mongodb://127.0.0.1:27017/studio_e2e_test?replicaSet=rs0
- uses: actions/upload-artifact@v4
if: failure()
with:
name: browser-test-results
path: |
playwright-report/
test-results/
retention-days: 7
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ firestore-debug.log
/src/generated/prisma

!.env.example

# Browser test output
/playwright-report/
/test-results/
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,9 +268,12 @@ The admin console, first login and day‑to‑day content workflow are covered i

1. Import the repo in Vercel. The Next.js preset is picked up automatically.
2. Add the variables above under **Settings → Environment Variables**.
3. Deploy. `postinstall` generates the Prisma client during the build.
4. Once, from your machine, run `DATABASE_URL="<production uri>" npm run db:push` to create the collections and indexes.
5. Log in, set a new PIN in **Security**, then press **GitHub → Sync now**.
3. From your machine, run `DATABASE_URL="<production uri>" npm run db:push` to create the collections and indexes, including content history.
4. For a new database, use the local admin console (`npm run dev`) to save your real profile (name, title and bio) and at least one project or visible repository. Existing populated installations can skip this step. Do not seed example content into production.
5. Deploy. `postinstall` generates Prisma; `prebuild` verifies the content/database before a release. A failed check keeps the previous successful deployment active.
6. Log in, set a new PIN in **Security**, then press **GitHub → Sync now**.

See [availability, admin recovery and browser tests](docs/portfolio-improvements.md) for Trash, revisions, backup limits and test commands.

> [!TIP]
> Seeing `Server selection timeout` or `bad auth`? In Atlas, open **Network Access** to Vercel (for example `0.0.0.0/0`), and check that the database name is part of `DATABASE_URL`.
Expand Down
4 changes: 4 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ npm test # unit tests
npm run typecheck
npm run lint
```

## Content availability and recovery

Builds now require a reachable, populated database to avoid releasing an empty portfolio. See [portfolio improvements](docs/portfolio-improvements.md) for the content check, schema/index setup, admin Trash/history, backup limits, and browser tests.
54 changes: 54 additions & 0 deletions docs/portfolio-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Portfolio backlog: selected October improvements

Implements issue #20 items 1, 2, 4–10. The Naruto/cyber design remains. Authentication changes and portfolio case-study content are separate work.

## Publishing and availability

- The 90-day archive job targets only slugs in the durable Security Insights import ledger. Original write-ups, including older posts discussing CVEs, are preserved. Imported content is archived reversibly.
- `npm run build` first runs `content:check`. A working database, a populated public profile (name/title/bio), and at least one visible project/repository are required. A connection failure or incomplete source data fails the release; Vercel keeps the previous successful deployment. No demo data is inserted by this check.
- Home-page database errors now fail static generation/revalidation rather than silently generating an empty portfolio. Runtime ISR can retain its last successful page. This protects releases; it does not make dynamic pages available during a database outage.
- The blog reads 12 summaries per page, never whole article bodies in listing queries. Search matches titles/excerpts case-insensitively; tag filtering matches an exact tag. GET forms and pagination URLs preserve filters and work without client-side JavaScript.

## Accessibility and mobile projects

The entrance dialog is static and defaults to safe mode before paint. It has no loading delay or flicker. Keyboard focus starts on Safe Mode, remains in the dialog, and moves to the page's main content on dismissal. Escape selects safe mode. Reduced-motion preferences override a stored glitch choice and update when the OS setting changes. Storage-denied browsing still works.

Shared reveals, text decoding, parallax and tilt honor the same mode; existing WebGL viewport pausing and reduced-motion fallbacks remain. Mobile grid and list cards expose Source/Visit, Demo (when present), and Details links as siblings of selection buttons, without nesting interactive elements. Keyboard selection updates the preview too.

## Search discovery and fonts

- Canonical URLs use the public production origin. Home-page Person and article BlogPosting JSON-LD escape content-controlled markup. Articles include publication/modification metadata and a social image.
- `/sitemap.xml` includes only public pages/published articles. `/robots.txt` and noindex headers exclude API/admin/decoy paths. Filtered searches are noindex, follow.
- The four existing font families remain. Requested weights drop from 15 to 9; Japanese UI labels use a 106-glyph subset. When adding Japanese labels, extend the `text=` subset in layout.tsx. Google Fonts still uses `display=swap` and preconnects.
- `scripts/measure-mobile.cjs` records LCP, CLS, observed long-task blocking, TTFB, font bytes, page errors and overflow using Chromium Pixel 7 emulation, 4× CPU slowdown, 1.6 Mbps download and 150 ms latency. Results are laboratory measurements against synthetic local data, not real-user Core Web Vitals or a production speed guarantee.

## Admin history, Trash and backups

Open **History & recovery** in the real admin console. New writes to profile, projects, skills, certificates, education, experience, CTFs, GitHub repositories, blog, bounty findings and audio settings store full before/after revisions in the same Mongo transaction as the content edit. Automatic content syncs are recorded too. Existing records have no historical revisions until their next write. Actor labels are session-token hash prefixes or `automation`; raw session tokens are never logged.

Deleting these content records sets optional `deletedAt`; normal reads/counts exclude both newly deleted records and retain compatibility with older Mongo documents that omit the field. Trash shows retained records by collection. Review a revision, compare current/before/after, and explicitly restore a side. If the record changed since review, restoration fails with 409. Every restore creates a new undo revision; conflicting writes or a failed audit insert roll back the transaction.

The **Download content backup** action exports the 11 content collections (including drafts/Trash/private bounty details) plus the import deduplication ledger. The upload path validates the entire file in a dry run before enabling restore. Restore merges by durable ID, keeps records absent from the file, rejects unique-field/singleton conflicts and records every overwrite. It is atomic; it never partially restores a file.

Limits: 3 MB / 1,000 records per content backup. Credentials, sessions, login attempts, inbox messages, client requests and old revision history are excluded. Uploaded photos/resumes are URL references, not copied media binaries. Keep downloaded files private. Use the database provider's separate backups and media retention for full disaster recovery or larger datasets. Content history itself is retained without automatic pruning.

Schema changes: optional `deletedAt` on the 11 content models and a new `ContentRevision` collection with indexes. Run the normal `npx prisma db push` deployment/schema setup to create its indexes; existing content fields need no data rewrite. Prisma generation remains the existing install step.

## Reproduce verification

The integration scripts refuse any URL except their exact localhost disposable database. They never fall back to `DATABASE_URL`.

1. Start a local MongoDB 7 replica set on port 27017 named `rs0`.
2. `npm ci`; `npm run typecheck`; `npm run lint`; `npm test -- --runInBand`.
3. Set `RECOVERY_TEST_DATABASE_URL=mongodb://127.0.0.1:27017/studio_recovery_test?replicaSet=rs0`; run `npm run test:recovery`.
4. Set `INSIGHTS_TEST_DATABASE_URL=mongodb://127.0.0.1:27017/studio_insights_test?replicaSet=rs0`; run `npx tsx scripts/test-insights.ts`.
5. Set `E2E_DATABASE_URL=mongodb://127.0.0.1:27017/studio_e2e_test?replicaSet=rs0`; run `npx tsx scripts/seed-e2e.ts`.
6. Set `DATABASE_URL` to that same e2e database; run `npx prisma db push --skip-generate` and `npm run build`.
7. `npx playwright install --with-deps chromium`; `npm run test:e2e`. The runner starts the production server, uses a fixture PIN, and tests desktop/mobile against real APIs and Mongo. Each browser project resets only synthetic test data, preserving indexes. Screenshots/traces are retained on failure.
8. To measure mobile performance, start the production server on port 3100 with the e2e database and run `node scripts/measure-mobile.cjs`. `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` can select an already installed Chromium.

CI performs the database integrations and all 12 browser scenarios. Missing-database release refusal is checked explicitly. CI stores browser failure artifacts for seven days.

## Recorded laboratory check (16 October 2026)

With the conditions above, the synthetic home page recorded LCP 888 ms, CLS 0.0167, local-server TTFB 53 ms, and five font requests totaling 131,626 transferred bytes. Fonts finished loading, safe mode was active, and there were no page errors or horizontal overflow. Observed long-task blocking over the sample remained 2,268 ms under 4× CPU throttling; this is a remaining scripting/rendering cost, not a claim that all mobile performance work is complete. No comparable production before/after field measurement was available.
4 changes: 3 additions & 1 deletion docs/security-insights.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Uses the official CISA KEV JSON mirror. No AI key, paid scheduler, or publishing
- First successful run publishes up to eight briefs from the preceding 30 days, distributed across weeks. Existing manual CVE coverage is skipped.
- Subsequent runs publish at most three new briefs per UTC day. Past-month imports also publish automatically. The publishing approval switch has been removed.
- On the first successful sync after this update, existing unpublished imported briefs from the past 90 days publish automatically. Archived posts and manual drafts are excluded. Later syncs preserve editorial changes, including deliberate unpublishing.
- Every successful sync archives blog posts older than 90 days using their publication date, or creation date for drafts without a publication date. Archived content is retained in the database and admin blog list, marked with `archivedAt`, and hidden from the homepage, `/blog`, and individual public post URLs. Nothing is permanently deleted.
- Every successful sync archives imported briefs older than 90 days using their publication date, or creation date for drafts without a publication date. Archived content is retained in the database and admin blog list, marked with `archivedAt`, and hidden from the homepage, `/blog`, and individual public post URLs. Nothing is permanently deleted.
- Admin → Blog: daily collection switch, Sync now, Import past month, last success and errors. The manual editor still controls manually authored posts.
- A shared ten-minute MongoDB lease prevents concurrent cron/admin imports. Automatic retries have a twenty-minute cooldown. The endpoint runs for at most five minutes. Paused collection also pauses automatic archiving; Sync now can still run it.
- Stable slugs and a durable CVE ledger prevent duplicates, preserve edits, and keep deleted imported posts from returning.
Expand All @@ -23,3 +23,5 @@ Daily collection and publishing are enabled by default. Existing pause settings
## Verification

Unit tests cover source validation, date windows, budgets, automatic publishing, deletion tracking, lease skips, failures, admin authentication and cron authentication. CI runs `scripts/test-insights.ts` against a disposable MongoDB replica set to verify atomic concurrency, persisted caps, automatic backfill publishing, legacy documents with missing fields, one-time draft release, retained archived content, deletion tracking and editorial preservation. It cannot connect to production.

Retention applies only to slugs recorded in InsightImport. Original write-ups are never selected by the 90-day automatic archive.
Loading
Loading