67 Commits
Author SHA1 Message Date
venus 0967d540cf feat: archive page 2026-10-27 17:33:34 -05:00
venus 59f807c45b fix: not starting 2026-10-20 18:02:32 -05:00
venus 07de3b2782 feat: live timer since rebuild 2026-10-20 17:01:05 -05:00
venus c8cd5fd0d9 Merge branch 'feat-linked-articles' into dev 2026-10-20 16:49:50 -05:00
venus 7f12861511 feat: linked articles in header 2026-10-20 16:39:36 -05:00
venus 515e52b274 Merge branch 'feat-local-header-links' into dev 2026-10-20 16:25:52 -05:00
venus 963521b3a9 feat: markdown links render in preview 2026-10-20 16:25:13 -05:00
venus efb0fbea94 feat: added headers to links in local pages 2026-10-20 16:11:11 -05:00
venus 6c1384b661 feat: upadted 404 page 2026-10-08 01:13:18 -05:00
venus 98e46644c0 feat: dynamic ish terminal preview 2026-10-06 17:06:08 -05:00
venus 569152a7be fix: fixed broken nav links 2026-10-06 16:16:13 -05:00
venus b2fc93c8e6 fix: fixed some copy and broken links 2026-10-06 16:11:35 -05:00
venus b239f61480 fix: removed a typo and some old code 2026-10-06 16:06:22 -05:00
venus 93fe924b74 test: added obsidian test 2026-10-06 16:00:36 -05:00
venus c211ad3a04 feat: added a hidden toggle availible in frontmatter which renders the note unlisted, also updated agent behavior with a compliance skill 2026-09-23 21:58:54 -05:00
venus dd866427f0 feat: modified agent behavior for a more test centric workflow 2026-09-23 21:52:28 -05:00
venus 2c22255855 feat: added a tag to allow for featured notes 2026-09-22 16:52:58 -05:00
venus f77b43c766 patch: fixed sticky sidebar on mobile 2026-09-15 09:18:27 -05:00
venus 835ac40cd2 feat: added remote compilation to the docker build pipeline 2026-09-14 23:38:13 -05:00
venus 31f93f559d fix: new posts were not rendering css properly 2026-09-14 23:25:16 -05:00
venus 6f3bf4bf89 fixed a typo 2026-09-14 01:37:36 -05:00
venus 8448c16ef7 feat: added git push to docker build script 2026-09-14 01:30:40 -05:00
venus 90955fed5b feat: added a script for building and pushing docker container 2026-09-14 01:30:07 -05:00
venus 23bd6f6ba7 fixed a typo 2026-09-14 01:26:41 -05:00
venus 22f78da4cb fix(ingestion): trigger deterministic Eleventy builds on publish and unpublish 2026-09-14 01:24:07 -05:00
venus 417ec30bfd updated lorem ipsum in some spots 2026-09-14 01:17:42 -05:00
venus f30d636bba fix(ingestion): file deletion of old articles now syncs correctly 2026-09-14 01:17:19 -05:00
venus cf16e1f60d working on obsidian plugin 2026-09-14 01:03:23 -05:00
venus 3a21998d60 added an api for obsidian integration 2026-09-14 00:47:08 -05:00
venus 9aa0ada81d added whoami to the available services 2026-09-13 23:47:07 -05:00
venus 7bf4d98fc0 updated hero tile with 16px bottom margin 2026-09-13 22:18:15 -05:00
venus 73603bc61b updated readme with more relevant details, gemini wrote 2026-09-13 22:13:53 -05:00
venus bd6358cf1c fixed some of the copy and modified a css margin 2026-09-13 22:11:33 -05:00
venus 42e7e0f1ff added engine seperation from posts and entrypoint for compose building 2026-09-13 21:12:06 -05:00
venus 04a39ca852 fix(healthcheck): correct arrow syntax and add unit tests 2026-09-13 15:09:11 -05:00
venus eddbf5470e updated healthcheck with a dedicated function 2026-09-13 14:38:49 -05:00
venus acd47b80aa test(docker): remove mandatory healthcheck requirement from compose test 2026-09-13 02:38:29 -05:00
venus 900014f506 fixed a failing test case on the server 2026-09-13 02:35:16 -05:00
venus 737a6e3e51 updated gitignore 2026-09-13 02:15:48 -05:00
venus 2423546ee6 updated gitignore 2026-09-13 02:11:40 -05:00
venus d55297f1dd fixed bug in compose file 2026-09-13 02:09:45 -05:00
venus d3aefa343e feat(docker): unify dynamic PORT across Dockerfile, compose, and dev server 2026-09-13 02:04:37 -05:00
venus 78b1e0b485 feat(docker): build and serve with Node and npm instead of Nginx 2026-09-13 01:59:49 -05:00
venus e733f479dd rewrote boilerplate site.js files 2026-09-13 01:50:38 -05:00
venus 1079b956d3 docs(rules): enforce User Content Sovereignty and remove specific content bias from guidelines and skills 2026-09-13 01:33:30 -05:00
venus 125b3e8839 fix(nav): update gateway navigation label from Gateway 3 to Gateway 2026-09-13 01:29:47 -05:00
venus cedd8194f6 feat: replace Forgejo references with Gitea and generalize content with Lorem Ipsum templates 2026-09-13 01:27:37 -05:00
venus e4601dbbcb feat: adopt strict TDD framework and add adversarial test suite
- Update AGENTS.md and .agents/rules/blog-system.md with strict TDD mandate
- Extract Eleventy custom filters into modular src/_filters/index.js for unit testing
- Add npm test script using Node.js native test runner (node:test)
- Create test/site-config.test.js for environment validation and fatal error assertions
- Create test/filters.test.js for adversarial filter edge cases (dates, limits, reading time, headings)
- Create test/frontmatter-schema.test.js enforcing post metadata contracts
- Create test/design-rules.test.js enforcing strict anti-patterns (no glassmorphism, no shadows, max 4px radius)
- Create test/build-integrity.test.js checking static output, zero 404 links, and TOC anchor targets
2026-09-13 01:16:20 -05:00
venus fc6c17b541 feat: throw explicit error when SITE_DOMAIN is unset
- Throw fatal configuration error in src/_data/site.js if SITE_DOMAIN is missing
- Require SITE_DOMAIN in docker-compose.yml without default fallback
- Require SITE_DOMAIN ARG in Dockerfile with no default fallback
2026-09-13 01:11:28 -05:00
venus 1491d3fa5c feat: make SITE_URL and SITE_DOMAIN dynamic via .env and docker compose
- Create .env and .env.example with SITE_DOMAIN=riverrooks.dev and SITE_URL=https://riverrooks.dev
- Migrate src/_data/site.json to src/_data/site.js reading environment variables dynamically
- Update subdomains across Forgejo, CyberChef, Stirling PDF, and Uptime Kuma to inherit site.domain
- Update docker-compose.yml and Dockerfile to accept dynamic SITE_URL and SITE_DOMAIN build args
- Add .env to .gitignore
2026-09-13 01:10:20 -05:00
venus b13a50ccee feat: set Gateway 3 as root index and move dispatches to /dispatches/
- Update src/index.njk with Gateway 3 homelab services router and cluster telemetry
- Create src/dispatches.njk for the full-width technical dispatches feed and tag filters
- Update src/_data/site.json navigation hierarchy to reflect root Gateway and /dispatches/
- Update breadcrumbs and return links in post.njk and hero-tile.njk
2026-09-13 01:08:42 -05:00
venus d8fb8a3699 feat: implement dynamic Eleventy tiling blog, modular CSS, and agent customizations
- Add project foundation README.md and root AGENTS.md for pair programming
- Configure .agents/ rules and skills for Eleventy tiling and Obsidian publishing
- Decompose monolithic styles into modular tokens, reset, layout, and component CSS
- Implement Nunjucks layout inheritance (base.njk, post.njk, page.njk) and reusable partials
- Configure Eleventy 3 with markdown-it-anchor, syntax highlighting, and dynamic collections
- Migrate sample prototypes into dynamic dispatches lander, gateway, and Markdown posts
- Add multi-stage Dockerfile and clean-URL nginx.conf for production serving
2026-09-13 00:49:27 -05:00
venus c0f67e75a3 resetting for dev now that samples are selected 2026-09-12 22:49:47 -05:00
venus 70134d2e00 added more samples and identified ideal candidates 2026-09-12 22:47:46 -05:00
venus f3ae84ad79 feat(samples): align split panel sample with canonical layout specs 2026-09-05 15:15:48 -05:00
venus 451fb34f52 feat(samples): align dual pane explorer with canonical system specs and logs 2026-09-05 15:15:46 -05:00
venus 9f4758039a feat(samples): align sample dashboard with canonical telemetry and metrics 2026-09-05 15:15:45 -05:00
venus 6a0f4347f9 feat(samples): align sample command center with canonical telemetry and nodes 2026-09-05 15:15:44 -05:00
venus 0ee5fae6be feat(samples): synchronize public services directory with canonical specs 2026-09-05 15:15:43 -05:00
venus be516f8848 feat(samples): update portfolio layout with canonical engineering projects 2026-09-05 15:15:41 -05:00
venus 59bac4bac9 feat(samples): expand samples catalog index with canonical layout showcase 2026-09-05 15:15:39 -05:00
venus e68322f182 feat(samples): update homepage sample with canonical metadata and layout updates 2026-09-05 15:15:38 -05:00
venus bf351e9755 feat(samples): align homelab specs and nodes with canonical dataset 2026-09-05 15:15:36 -05:00
venus 801ba3a533 feat(samples): update document flow layout with canonical dataset references 2026-09-05 15:15:35 -05:00
venus 65ad51560c feat(samples): update companion layout with canonical persona and links 2026-09-05 15:15:34 -05:00
venus 0f80bf4e9b feat(index): update tags and footer with canonical contact info and samples link 2026-09-05 15:15:33 -05:00
venus 424041a827 docs: add canonical sample content dataset 2026-09-05 15:15:29 -05:00
97 changed files with 10535 additions and 6421 deletions
+91
View File
@@ -0,0 +1,91 @@
# Technical Blog System Rules & Architecture Directives
## 1. Core Invariant: User Content Sovereignty (Highest Priority)
1. **User Edits Are Absolute Ground Truth:**
- The user's edits, custom writing, post titles, navigation labels, terminology, and naming choices are the absolute highest level of truth in this codebase.
- They strictly take precedence over any background content description, sample design mockup (`sample-designs/*`), skill instructions, or default boilerplate.
2. **Strict Prohibition on Unsolicited Rewriting or Renaming:**
- You must **NEVER** revert, alter, overwrite, or substitute user-authored text, titles, navigation labels, tags, or naming conventions unless the user directly and explicitly requests that specific modification.
- Do not reintroduce removed text or names from prior mockups or commits.
3. **Mandatory Clarification Gate:**
- If you notice a potential inconsistency, typo, or naming ambiguity in user-authored content, you must **NOT** unilaterally modify it.
- You must ask the user for specific clarification before altering any user-authored writing or naming.
4. **Sample Designs Are Layout References Only:**
- Files in `sample-designs/` serve solely as visual, CSS, and structural layout references.
- Never extract or re-inject titles, domain names, service labels, or technical copy from `sample-designs/` into active templates or site data without explicit instruction.
---
## 2. Pedagogical Explanations (Interactive Learning)
1. **Explain the Code:** Whenever creating or refactoring code (Eleventy configuration, Nunjucks templates, CSS modules, build scripts), provide a clear, educational breakdown.
2. **Break Down Key Concepts:** Explain *why* a particular Eleventy filter, Nunjucks tag, or CSS variable structure was used so the user learns the foundational concepts.
3. **No Magic:** Avoid opaque or overly clever abstractions; prioritize readable, idiomatic code with commentary explaining tricky points.
---
## 3. CSS Architecture & Isolation
1. **Zero Monolithic Inlines:** Do not inject raw `<style>` blocks into HTML templates.
2. **Modular File Structure:**
- Put global tokens in `src/assets/css/tokens.css`.
- Put reset and base typography in `src/assets/css/reset.css`.
- Put layout structures (tiling layout, split grids) in `src/assets/css/layout.css`.
- Put scoped component styles in `src/assets/css/components/<component-name>.css`.
3. **Design System Token Integrity:** Always use CSS custom properties defined in `tokens.css`:
- Canvas: `var(--bg-canvas)` (`#0d0f12`)
- Surface: `var(--bg-surface)` (`#121417`)
- Elevated: `var(--bg-elevated)` (`#1c1f26`)
- Borders: `var(--border-color)` (`#2b303b`)
- Text: `var(--text-primary)` (`#e0e6f0`), `var(--text-muted)` (`#8a95a5`)
- Accents: `var(--accent-cyan)` (`#00f0ff`), `var(--accent-green)` (`#00e676`), `var(--accent-amber)` (`#ffb700`)
4. **Hard Aesthetic Constraints:**
- No glassmorphism (`backdrop-filter`).
- No box-shadows or glows.
- No linear/radial gradients.
- Border radius strictly bounded between `0px` and `4px`.
- Transition durations set to `none` or `0s` for instant snap responses on hover.
---
## 4. Templating Guidelines (Eleventy + Nunjucks)
1. **Layout Hierarchy:**
- Root template: `src/_includes/layouts/base.njk` (contains HTML structure, `<head>`, metadata, scripts, stylesheets).
- Content layouts: `src/_includes/layouts/post.njk` (for articles), `src/_includes/layouts/page.njk` (for landing/catalog).
2. **Component Partials:**
- Isolated partials in `src/_includes/partials/` (`global-bar.njk`, `hero-tile.njk`, `post-card.njk`, `stats-sidebar.njk`, `code-box.njk`, `footer.njk`).
- Pass dynamic data via Nunjucks macros or contextual variables.
---
## 5. Obsidian Content Pipeline
1. **Frontmatter Specification:** Every post ingested from Obsidian must adhere to:
```yaml
title: "Article Title"
date: YYYY-MM-DD
tags: ["#tag1", "#tag2"]
excerpt: "Single paragraph summary"
readingTime: "X MIN READ"
draft: false
hidden: false # optional: true unlists post from public feeds/lists, accessible via direct link only
```
2. **Obsidian Normalization:**
- Wikilinks `[[slug]]` or `[[slug|Label]]` must be transformed into standard web URLs (`/posts/slug/`).
- Callout blocks (`> [!NOTE]`, `> [!WARNING]`, etc.) must render as terminal-style alert callouts.
- Images must be processed into `src/assets/images/posts/` and linked via standard `<img>` or Markdown image syntax.
---
## 6. Test-Driven Development (TDD) & Adversarial Verification
1. **Strict Pre-Implementation Testing (Tests Before Functional Code):**
- Test cases MUST ALWAYS be written in `test/*.test.js` before writing or modifying any functional code (filters, templates, server routes, ingestion pipeline, data loaders, scripts).
- Never implement or modify functional or production code prior to writing and establishing corresponding automated tests.
2. **Mandatory Adversarial Test Coverage:**
- Standard behavior for any functional change MUST ALWAYS include adversarial testing specifically designed to stress assumptions and break the system:
- **Data & Configuration:** Test that missing required environment variables (e.g. `SITE_DOMAIN`) throw fatal errors.
- **Filters & Logic:** Test edge cases: empty strings, undefined/null values, unexpected types, single-word inputs, massive word counts, non-date objects.
- **Security & Integrity:** Test path traversals, malformed JSON, unauthorized tokens, malicious payloads.
- **Content Schemas:** Validate all Markdown files in `src/posts/` against frontmatter schemas.
- **Design Spec Compliance:** Automate regex/AST assertions on `src/assets/css/` ensuring zero violations of the "Do Not Use" list (`backdrop-filter`, `box-shadow`, `text-shadow`, gradients, floaty transitions).
- **Link & Anchor Integrity:** Verify that every compiled page has zero broken internal links and that all Table of Contents anchor links resolve to valid heading IDs.
3. **Pre-Presentation Verification Gate:**
- Before presenting the final output to the user, run `npm test`.
- All test cases must pass before presenting the final output. Presenting unverified output or code with failing tests is strictly prohibited.
@@ -0,0 +1,118 @@
---
name: eleventy-tiling-blog
description: >-
Use this skill when developing, refactoring, or building the Eleventy blog, modularizing CSS into tokens and components, creating Nunjucks layouts and partials from sample designs, or configuring Eleventy build pipelines.
---
# Eleventy Tiling Blog Development Skill
This skill guides the agent in modularizing templates and CSS from `sample-designs/` into a clean, DRY Eleventy (11ty) static publication site.
> [!IMPORTANT]
> **User Content Sovereignty:** Files in `sample-designs/` are layout and CSS references ONLY. Never extract or re-inject titles, service names, or text from `sample-designs/` over user-defined or sanitized content. User-authored text and naming conventions are the highest level of truth.
---
## 1. CSS Modularization Workflow
When extracting CSS from `sample-designs/*.html` into `src/assets/css/`:
1. **Tokens (`src/assets/css/tokens.css`):**
Extract all `:root` custom properties:
```css
:root {
--bg-canvas: #0d0f12;
--bg-surface: #121417;
--bg-elevated: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent-cyan: #00f0ff;
--accent-green: #00e676;
--accent-amber: #ffb700;
--accent-red: #ff5252;
--font-mono: 'IBM Plex Mono', monospace;
--font-code: 'JetBrains Mono', monospace;
--font-prose: 'Inter', sans-serif;
}
```
2. **Reset & Base (`src/assets/css/reset.css`):**
- Universal box-sizing reset (`box-sizing: border-box; margin: 0; padding: 0;`).
- Body font defaults, base line-height (`1.6`), instant hover transitions (`transition: none;`).
- Explicit 1px/2px focus ring styling for keyboard accessibility.
3. **Layout Grid (`src/assets/css/layout.css`):**
- `.tiling-layout`: Centered flex column, `max-width: 1120px` to `1160px`.
- `.split-grid`: 2-column or multi-tile layout with rigid gaps (`16px`).
- `.article-workspace-grid`: Left sticky sidebar (`290px`) + main article reading view (`1fr`).
4. **Component Modules (`src/assets/css/components/`):**
- `global-bar.css`: `.global-bar`, `.brand-group`, `.status-indicator`, `.bar-nav`, `.bar-link`.
- `tiles.css`: `.tile`, `.tile-header`, `.tile-title`, `.tile-meta`, `.hero-tile`.
- `code-box.css`: `.terminal-code-box`, `.code-titlebar`, `.code-content`.
- `article.css`: Prose typography capped at `65ch`, inline `<kbd>`, tables, callouts.
---
## 2. Nunjucks (`.njk`) Component Architecture
1. **Layout Inheritance:**
- `src/_includes/layouts/base.njk`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }} | {{ site.title or site.name }}</title>
<link rel="stylesheet" href="/assets/css/main.css">
</head>
<body>
<main class="tiling-layout">
{% include "partials/global-bar.njk" %}
{{ content | safe }}
{% include "partials/footer.njk" %}
</main>
</body>
</html>
```
- `src/_includes/layouts/post.njk`:
Extends `layouts/base.njk`, injecting the `.article-workspace-grid` with the table of contents sidebar and article body.
2. **Reusable Partials:**
- `src/_includes/partials/global-bar.njk`
- `src/_includes/partials/hero-tile.njk`
- `src/_includes/partials/post-card.njk`
- `src/_includes/partials/stats-sidebar.njk`
- `src/_includes/partials/code-box.njk`
---
## 3. Explaining Changes to the User
When writing or modifying Eleventy files:
- Explain **how Nunjucks blocks and layout inheritance prevent template duplication**.
- Explain **how CSS custom properties cascade and ensure dark-mode consistency**.
- Point out how the resulting build outputs raw static HTML in `_site/` with zero runtime JavaScript overhead.
---
## 4. Validation & Verification
Always follow the project TDD standard: write automated tests (including adversarial edge cases) in `test/` before modifying functional templates, filters, or configuration. Ensure all test cases pass before presenting the final output.
Run these commands to test and verify builds:
```bash
# Run complete test suite (unit tests, filters, server, design rules, build integrity)
npm test
# Validate Eleventy configuration and build static output
npx @11ty/eleventy --dryrun
# Build to _site directory
npx @11ty/eleventy
# Launch local dev server with auto-refresh
npx @11ty/eleventy --serve
```
+113
View File
@@ -0,0 +1,113 @@
---
name: obsidian-sync
description: >-
Use this skill when designing, building, or troubleshooting the Obsidian vault publishing integration, the server upload API, note normalization (wikilinks, callouts, frontmatter), or the custom Obsidian plugin.
---
# Obsidian Vault Publishing & Sync Skill
This skill guides the agent in implementing the custom Obsidian plugin and server-side ingestion workflow for publishing blog posts directly from an Obsidian vault.
> [!IMPORTANT]
> **User Content Sovereignty:** User-authored content, post titles, tags, and formatting are the highest level of truth. The sync and normalization pipelines must preserve user-authored text verbatim and never overwrite or mutate user writing without explicit instruction.
---
## 1. System Architecture
```text
+-------------------+ +-----------------------+
| Obsidian Vault | | Blog Server |
| | | |
| [Active Markdown] | POST /api/posts | /api/posts Endpoint |
| + | -------------------> | + |
| Custom Plugin | (Payload: JSON/MD) | Frontmatter Validator |
+-------------------+ | + |
| Wikilink Normalizer |
| + |
+-------------------+ | Write to `src/posts/` |
| Self-Hosted | CouchDB Sync | + |
| LiveSync Server | ===================> | 11ty Build Trigger |
+-------------------+ +-----------------------+
```
---
## 2. Ingestion API Specification
### Endpoint: `POST /api/posts`
- **Headers:**
- `Content-Type: application/json`
- `Authorization: Bearer <API_SECRET>`
- **Request Body:**
```json
{
"slug": "sample-post",
"content": "---\ntitle: \"Sample Post Title\"\ndate: 2026-09-12\ntags: [\"#tag1\"]\n---\n\n## Content...",
"assets": [
{
"filename": "diagram.png",
"data": "<base64-encoded-image>"
}
]
}
```
- **Response:**
- `200 OK`: `{"status": "published", "path": "/posts/sample-post/"}`
- `400 Bad Request`: `{"error": "Missing required frontmatter: title, date"}`
---
## 3. Obsidian Markdown Normalization Rules
1. **Frontmatter Invariants:**
- Must contain `title`, `date`, `tags` (array), and optional `excerpt`, `readingTime`, `draft`, `hidden`.
2. **Wikilinks Translation:**
- `[[article-slug]]` $\rightarrow$ `[article-slug](/posts/article-slug/)`
- `[[article-slug|Custom Label]]` $\rightarrow$ `[Custom Label](/posts/article-slug/)`
3. **Callout Translation:**
- Obsidian callouts:
```markdown
> [!NOTE]
> Informational notice
```
Must be rendered with corresponding terminal alert styling.
4. **Internal Attachments:**
- `![[screenshot.png]]` $\rightarrow$ translated to `![screenshot](/assets/images/posts/screenshot.png)` and written to `src/assets/images/posts/`.
---
## 4. Custom Obsidian Plugin Development
The Obsidian plugin runs inside Obsidian desktop/mobile and interacts with the Obsidian API:
1. Registers a ribbon icon (e.g. `send` or `terminal`) and a command: `Publish Current Note to Blog`.
2. Reads the active file:
```typescript
const activeFile = this.app.workspace.getActiveFile();
const content = await this.app.vault.read(activeFile);
```
3. Parses frontmatter using Obsidian's metadata cache:
```typescript
const cache = this.app.metadataCache.getFileCache(activeFile);
```
4. Sends the HTTP payload to the configured blog server URL with the Bearer token.
5. Displays an Obsidian `Notice('Published successfully to Blog!')`.
---
## 5. Self-Hosted LiveSync Compatibility
If using CouchDB-based self-hosted LiveSync:
- A local watcher or CouchDB change listener daemon can be configured to detect updates to notes marked with `published: true` or in a dedicated `Blog/` folder.
- When an update event fires, the daemon pulls the latest revision from the CouchDB database, runs the normalization pipeline, and saves it to `src/posts/`.
---
## 6. Validation & Verification
Always write automated test cases in `test/ingestion-api.test.js` before modifying or adding any functional synchronization or endpoint code:
- **Mandatory Adversarial Testing:** Include adversarial tests covering unauthorized tokens, invalid/missing frontmatter schemas, path traversals in slugs, and malformed base64 asset payloads.
- **Pre-Presentation Verification:** Run `npm test` and ensure all test cases pass before presenting the final output.
@@ -0,0 +1,94 @@
---
name: standards-verification
description: >-
Use this skill sparingly when a new major function or feature has been implemented, when the user explicitly asks to verify standards, or as a debugging tool to analyze code issues. Targets only modified or relevant files via git status/diff and evaluates them against the project standards document.
---
# Standards Verification Skill
This skill performs targeted architectural and stylistic compliance verification for new or modified code in this repository.
> [!IMPORTANT]
> **Invocation Policy:** Call this skill **sparingly and only when needed**.
> **Valid Triggers:**
> 1. A new major function or architectural feature has been implemented.
> 2. The user asks to call it or requests standards verification.
> 3. An error, test failure, or regression has occurred, and you are using this skill as a debugging tool to determine where the code diverges from project standards.
---
## 1. Targeted Inspection Scope (Not the Whole Codebase)
Do **NOT** scan or audit the whole codebase unless specifically requested by the user or strictly necessary for whole-system refactors.
Instead, narrow the scope to targeted sections using Git:
1. **Check Modified Files:**
```bash
git status -s
```
2. **Inspect Exact Changes:**
```bash
git diff HEAD
```
Or for staged changes:
```bash
git diff --staged
```
3. **Identify Changed Sections:**
Focus only on the files, functions, templates, or styles touched in the current task.
---
## 2. Standards Evaluation Workflow
To verify compliance, read the dedicated project standards document:
- Authoritative document: [`.agents/standards.md`](file:///home/venus/code/blog/.agents/standards.md) (or local skill reference: [`references/standards.md`](file:///home/venus/code/blog/.agents/skills/standards-verification/references/standards.md)).
Evaluate the targeted changes against the core standard pillars:
1. **Design System & CSS Rules:**
- Are any anti-patterns present? Check for `backdrop-filter`, `box-shadow`, `text-shadow` (other than `none`), gradients, or transitions with delays/curves.
- Are border radii strictly bounded between `0px` and `4px`?
- Are colors utilizing CSS custom properties from `tokens.css`?
- Is styling modularized without inline `<style>` tags?
2. **Templating & Eleventy Architecture:**
- Does post rendering adhere to layout inheritance (`base.njk` -> `post.njk`)?
- Are collections (`posts`, `tagList`) properly filtered using `isPostPublic`?
- If a post is marked `hidden`, does the layout emit `<meta name="robots" content="noindex, nofollow">`?
3. **Content & Frontmatter Contracts:**
- Are required frontmatter keys (`title`, `date`, `tags`, `excerpt`) valid and conforming to schema length invariants?
- Are wikilinks and Obsidian attachments normalized?
4. **Server & Security Invariants:**
- Does `server.js` preserve security headers (`X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`)?
- Is path traversal protection in place for all file-serving and API endpoints?
5. **Test-Driven Development (TDD) Invariants:**
- Were automated test cases written *before* functional code was modified?
- Are adversarial test cases included (boundary inputs, nulls, invalid configs)?
- Do all tests pass (`npm test`)?
---
## 3. Stylistic & Architectural Reporting
Compile a structured report highlighting:
- **Conformant Highlights:** Key standards cleanly satisfied.
- **Stylistic Issues & Deviations:** Any formatting inconsistencies, unneeded inline styles, or naming anomalies.
- **Remediation Steps:** Concrete, minimal code adjustments to bring the implementation into full compliance.
---
## 4. Debugging with Standards Verification
When using this skill as a debugging tool to troubleshoot broken builds, failed tests, or runtime bugs:
1. **Locate the Error:** Inspect test failure outputs or server logs to isolate the failing component.
2. **Correlate with Standards:** Compare the problematic implementation against [`.agents/standards.md`](file:///home/venus/code/blog/.agents/standards.md).
3. **Explain the Discrepancy:** Specifically identify where the code is out of touch with project standards (e.g. missing input guards for `null`, collections bypassing `isPostPublic`, un-sanitized slug paths).
4. **Suggest Root Cause & Fix:** Suggest why those issues cause an error and provide the targeted fix.
@@ -0,0 +1,59 @@
# Technical Blog Project Standards & Architectural Specification
This document is a direct reference for the `standards-verification` skill, mirroring the authoritative project standards defined in [`.agents/standards.md`](file:///home/venus/code/blog/.agents/standards.md).
---
## 1. User Content Sovereignty (Highest Priority)
- User custom writing, post titles, tags, navigation labels, and naming choices strictly supersede any boilerplate, skill instructions, or sample mockups.
- Never overwrite, sanitize away, or rename user-authored text without explicit instruction.
- Inconsistencies or typos in user writing must be flagged for clarification rather than unilaterally changed.
---
## 2. Strict Test-Driven Development (TDD) & Adversarial Verification
- **Test-First Invariant:** Automated test cases in `test/*.test.js` MUST ALWAYS be written and established *before writing or modifying any functional code* (filters, templates, server routes, ingestion logic, build scripts). Never write functional code before tests.
- **Mandatory Adversarial Testing:** Standard behavior for every change must include adversarial tests specifically designed to falsify assumptions (missing env vars like `SITE_DOMAIN`, null/undefined/malformed inputs, boundary values, path traversals, design rule violations).
- **Pre-Presentation Verification Gate:** Run `npm test` before presenting the final output to the user. All test cases must pass before presenting the final output. Delivering unverified or failing code is strictly forbidden.
---
## 3. Design System & CSS Specification
- **Color Palette:** Canvas `#0d0f12`, Surface `#121417`, Elevated `#1c1f26`, Borders `#2b303b`, Text `#e0e6f0`, Accents `#00f0ff` (Cyan), `#00e676` (Green), `#ffb700` (Amber), `#ff5252` (Red).
- **Hard Anti-Patterns:**
- NO glassmorphism (`backdrop-filter`).
- NO drop shadows or glows (`box-shadow`, `text-shadow`).
- NO linear or radial gradients.
- Border-radius capped at `0px` to `4px` maximum.
- Transitions must be instant binary snaps (`none` or `0s`).
- **Typography:** `IBM Plex Mono` / `JetBrains Mono` for UI and code; `Inter` for body prose.
- **CSS Modularity:** Zero inline `<style>` tags. Decompose into `tokens.css`, `reset.css`, `layout.css`, `components/*.css`.
---
## 4. Templating & Eleventy Architecture
- **Layouts & Partials:** Root `layouts/base.njk`, `layouts/post.njk`, `layouts/page.njk`. Isolated partials in `_includes/partials/`.
- **Collection Filtering:** `collections.posts` and `collections.tagList` must filter through `isPostPublic(post)` (excluding `draft: true` and `hidden: true`).
- **Sorting:** Posts sorted via `postComparator` (pinned/featured first, then chronological descending).
- **Hidden Posts:** When `hidden: true`, page compiles to direct link but is excluded from public lists and injects `<meta name="robots" content="noindex, nofollow">`.
---
## 5. Obsidian Content & Frontmatter Contract
- Required frontmatter: `title` (>= 5 chars), `date` or `created` (ISO), `tags` (array), `excerpt` (>= 20 chars).
- Optional: `readingTime`, `draft`, `hidden`, `pinned`, `featured`.
- Obsidian conversions: Wikilinks `[[slug|Label]]` -> `[Label](/posts/slug/)`, attachments `![[image.png]]` -> `![image](/assets/images/posts/image.png)`.
---
## 6. Server & Security Invariants
- `server.js`: Native Node HTTP, zero external runtime dependencies.
- Security headers: `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin`.
- Path traversal defense: Sanitize and verify paths remain inside root.
- Healthcheck: `/healthz` returns 200 OK JSON `{ status: "ok", uptime: ... }`.
---
## 7. Defensive Programming & Error Prevention
- Guard against `null`, `undefined`, primitives in all utility functions.
- Docker: `node:22-alpine`, non-root / clean entrypoint script as PID 1.
@@ -0,0 +1,76 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
console.log("\n[Standards Verification] Running targeted inspection on modified files...\n");
let statusOutput = "";
try {
statusOutput = execSync("git status -s", { encoding: "utf-8" }).trim();
} catch (err) {
console.error("Error executing git status:", err.message);
process.exit(1);
}
if (!statusOutput) {
console.log("✔ Working tree is clean. No changed files to inspect.");
process.exit(0);
}
const lines = statusOutput.split("\n");
const modifiedFiles = lines
.map((line) => line.slice(3).trim())
.filter((f) => fs.existsSync(f) && fs.statSync(f).isFile());
console.log(`Targeting ${modifiedFiles.length} modified file(s):`);
modifiedFiles.forEach((f) => console.log(` - ${f}`));
console.log("");
let issuesFound = 0;
for (const file of modifiedFiles) {
const content = fs.readFileSync(file, "utf-8");
// 1. CSS anti-patterns
if (file.endsWith(".css")) {
if (/backdrop-filter\s*:/i.test(content)) {
console.error(`✖ Violation in ${file}: Glassmorphism (backdrop-filter) is strictly forbidden.`);
issuesFound++;
}
const shadowMatches = content.match(/(box-shadow|text-shadow)\s*:\s*([^;]+);/gi) || [];
for (const match of shadowMatches) {
if (!/(box-shadow|text-shadow)\s*:\s*none\s*;/i.test(match)) {
console.error(`✖ Violation in ${file}: Drop shadows/glows forbidden: '${match}'`);
issuesFound++;
}
}
if (/(linear-gradient|radial-gradient|conic-gradient)/i.test(content)) {
console.error(`✖ Violation in ${file}: Gradients are strictly forbidden.`);
issuesFound++;
}
const radiusMatches = content.match(/border-radius\s*:\s*([^;]+);/gi) || [];
for (const match of radiusMatches) {
const pxMatch = match.match(/(\d+)\s*px/i);
if (pxMatch && parseInt(pxMatch[1], 10) > 4) {
console.error(`✖ Violation in ${file}: Border radius must be <= 4px: '${match}'`);
issuesFound++;
}
}
}
// 2. Monolithic inline styles in templates
if (file.endsWith(".njk") || file.endsWith(".html")) {
if (/<style[^>]*>[\s\S]*?<\/style>/i.test(content)) {
console.error(`✖ Violation in ${file}: Monolithic inline <style> tags are forbidden. Modularize into src/assets/css/`);
issuesFound++;
}
}
}
if (issuesFound > 0) {
console.error(`\n✖ Standards Verification failed: ${issuesFound} issue(s) detected.\n`);
process.exit(1);
} else {
console.log("✔ Targeted standards verification passed with 0 violations.\n");
}
+124
View File
@@ -0,0 +1,124 @@
# Technical Blog Project Standards & Architectural Specification
This document defines the authoritative engineering, design, templating, and security standards for this technical publication codebase. All new implementations, pull requests, refactors, and debugging tasks must conform to these rules.
---
## 1. User Content Sovereignty (Highest Priority)
1. **User Edits Are Absolute Ground Truth:**
- The user's custom writing, post titles, tags, navigation labels, terminology, and naming choices strictly supersede any boilerplate, skill instructions, or sample mockups (`sample-designs/*`).
2. **No Unsolicited Modifications:**
- Never overwrite, sanitize away, or rename user-authored text without explicit instruction.
3. **Mandatory Clarification Gate:**
- Inconsistencies or typos in user writing must be flagged for clarification rather than unilaterally changed.
---
## 2. Strict Test-Driven Development (TDD) & Adversarial Verification
1. **Test-First Invariant (Tests Before Code):**
- Automated test cases in `test/*.test.js` MUST ALWAYS be written and established *before writing or modifying any functional code* (filters, templates, server routes, ingestion logic, build scripts).
- Never implement or modify production code prior to establishing tests.
2. **Mandatory Adversarial Testing:**
- Every functional implementation must include adversarial tests designed to actively falsify assumptions:
- Missing or malformed configuration (e.g., unset `SITE_DOMAIN` must throw).
- Corrupt or unexpected inputs (empty strings, `null`, `undefined`, unexpected types, HTML injections, path traversals, giant payloads).
- Boundary values and edge cases.
- Design rule violation assertions (0 instances of forbidden CSS patterns).
- Link and anchor integrity checks.
3. **Pre-Presentation Verification Gate:**
- Before presenting the final output to the user, run `npm test`.
- All test cases must pass before presenting the final output. Delivering unverified or failing code is strictly forbidden.
---
## 3. Design System & CSS Specification
Strict adherence to `sample-designs/design-specification.md` is mandatory.
### 3.1 Color Palette & Token Variables
All colors must use standard CSS custom properties defined in `src/assets/css/tokens.css`:
- Canvas: `var(--bg-canvas)` (`#0d0f12`)
- Surface: `var(--bg-surface)` (`#121417`)
- Elevated: `var(--bg-elevated)` (`#1c1f26`)
- Borders: `var(--border-color)` (`#2b303b`)
- Text: `var(--text-primary)` (`#e0e6f0`), `var(--text-muted)` (`#8a95a5`)
- Accents: `var(--accent-cyan)` (`#00f0ff`), `var(--accent-green)` (`#00e676`), `var(--accent-amber)` (`#ffb700`), `var(--accent-red)` (`#ff5252`)
### 3.2 Hard Anti-Patterns (Forbidden)
- **NO Glassmorphism:** Never use `backdrop-filter: blur(...)`.
- **NO Drop Shadows or Glows:** `box-shadow: none;` and `text-shadow: none;` only.
- **NO Gradients:** Never use `linear-gradient`, `radial-gradient`, or `conic-gradient`. Flat solid surfaces only.
- **NO Rounded Bubbles:** `border-radius` is strictly capped between `0px` and `4px` maximum.
- **NO Floaty / Delayed Transitions:** Hover and state transitions must be instant binary snaps (`transition: none;` or `transition: 0s;`).
### 3.3 Typography & Modularity
- UI & Code: `IBM Plex Mono`, `JetBrains Mono`.
- Body Prose: `Inter`.
- CSS Modularity: Zero monolithic inline `<style>` tags in HTML. Decompose styles into `tokens.css`, `reset.css`, `layout.css`, and `components/*.css`.
---
## 4. Templating & Eleventy Architecture
1. **Layout Inheritance (Nunjucks):**
- Root layout: `src/_includes/layouts/base.njk`.
- Post layout: `src/_includes/layouts/post.njk`.
- Page layout: `src/_includes/layouts/page.njk`.
- Modular reusable partials in `src/_includes/partials/` (`global-bar.njk`, `hero-tile.njk`, `post-card.njk`, `stats-sidebar.njk`, `code-box.njk`, `footer.njk`).
2. **Collection Invariants:**
- `collections.posts`: Must filter through `isPostPublic(post)` (excluding both `draft: true` and `hidden: true`) and sort using `postComparator` (prioritizing pinned/featured posts, then chronological descending).
- `collections.tagList`: Must filter through `isPostPublic(post)` so draft or unlisted tags never leak into public category filters.
3. **Hidden / Unlisted Post Contract:**
- When frontmatter specifies `hidden: true`:
- Excluded from public lists, indices, and tag aggregates.
- Static page STILL compiles to `/posts/<slug>/index.html` and is accessible via direct URL.
- Emits `<meta name="robots" content="noindex, nofollow">` in `<head>` to prevent search indexing.
---
## 5. Obsidian Content & Frontmatter Contract
1. **Frontmatter Schema:**
```yaml
title: "Article Title" # Required: string (>= 5 chars)
date: 2026-09-23 # Required: ISO YYYY-MM-DD (or 'created')
tags: ["#tag1", "#tag2"] # Required: non-empty YAML array
excerpt: "Article summary..." # Required: string (>= 20 chars)
readingTime: "5 MIN READ" # Optional: explicit override
draft: false # Optional: boolean
hidden: false # Optional: boolean (share by link only)
pinned: false # Optional: boolean (prioritized to top)
featured: false # Optional: boolean (featured pillar)
```
2. **Obsidian Syntax Normalization:**
- Wikilinks: `[[target-slug|Custom Label]]` $\rightarrow$ `[Custom Label](/posts/target-slug/)`.
- Wikilinks simple: `[[target-slug]]` $\rightarrow$ `[target-slug](/posts/target-slug/)`.
- Attachments: `![[image.png]]` $\rightarrow$ `![image](/assets/images/posts/image.png)`.
- Terminal alert callouts for `> [!NOTE]`, `> [!WARNING]`, etc.
---
## 6. Server & Security Invariants
1. **Native Runtime:**
- `server.js` uses native Node standard library (`node:http`, `node:fs`, `node:path`) with zero external runtime dependencies.
2. **Security Headers on All Responses:**
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: DENY`
- `Referrer-Policy: strict-origin-when-cross-origin`
3. **Path Traversal Defense:**
- Sanitize all requested file paths using `path.resolve` and verify the resolved path remains rooted inside the target directory. Block `..` and malicious slug sequences.
4. **Healthcheck Telemetry:**
- `/healthz` and `/health` must respond with HTTP 200 JSON `{ status: "ok", uptime: <seconds> }`.
---
## 7. Defensive Programming & Error Prevention
1. **Input Guarding:**
- Never assume incoming arguments have expected shape. Always guard against `null`, `undefined`, non-objects, and primitive inputs (e.g., `item.data ?? item`).
2. **Clean Container Contracts:**
- Container images must use `node:22-alpine`.
- Dynamic entrypoint (`entrypoint.sh`) must build markdown on boot, handle optional watcher (`WATCH=true`), and execute server as PID 1 (`exec node server.js`).
+5 -3
View File
@@ -1,5 +1,7 @@
.git
.gitignore
Dockerfile
docker-compose.yml
*.md
.agents
node_modules
_site
sample-designs
*.log
+5
View File
@@ -0,0 +1,5 @@
# Environment Template
# Copy to .env and adjust variables as needed:
SITE_DOMAIN=riverrooks.dev
SITE_URL=https://riverrooks.dev
PORT=8080
+10
View File
@@ -0,0 +1,10 @@
node_modules/
_site/
.cache/
.DS_Store
*.log
.env
docker-compose.yml
# Local browser fixtures for document-based development
src/posts/*.md
+103
View File
@@ -0,0 +1,103 @@
# AGENTS.md: Technical Blog Agent Guidelines
You are the dedicated Senior Pair Architect, Lead Developer, and Pedagogical Coding Mentor for this technical blog.
---
## 1. Core Invariant: User Content Sovereignty (Highest Priority)
1. **User Edits Are Absolute Ground Truth:**
- The user's edits, custom writing, post titles, navigation labels, terminology, and naming choices are the absolute highest level of truth in this codebase.
- They strictly take precedence over any background content description, sample design mockup (`sample-designs/*`), skill instructions, or default boilerplate.
2. **Strict Prohibition on Unsolicited Rewriting or Renaming:**
- You must **NEVER** revert, alter, overwrite, or substitute user-authored text, titles, navigation labels, tags, or naming conventions unless the user directly and explicitly requests that specific modification.
- Do not reintroduce removed text or names from prior mockups or commits.
3. **Mandatory Clarification Gate:**
- If you notice a potential inconsistency, typo, or naming ambiguity in user-authored content, you must **NOT** unilaterally modify it.
- You must ask the user for specific clarification before altering any user-authored writing or naming.
4. **Sample Designs Are Layout References Only:**
- Files in `sample-designs/` serve solely as visual, CSS, and structural layout references.
- Never extract or re-inject titles, domain names, service labels, or technical copy from `sample-designs/` into active templates or site data without explicit instruction.
---
## 2. Operating Persona & Responsibilities
### 2.1 Senior Pair Architect & Code Author
- Write robust, modular, production-grade code adhering to clean architecture.
- Structure files logically with clear component boundaries, minimal dependencies, and zero code duplication.
- Build static rendering workflows with **Eleventy (11ty)** and **Nunjucks (`.njk`)**.
### 2.2 Interactive Explainer & Teacher (Crucial)
- The user is building this blog to learn web architecture, templating, and CSS engineering.
- **Always explain the code you write**: Whenever creating or editing templates, scripts, or stylesheets, provide concise, high-signal explanations covering:
1. **How it works:** The mechanical flow, syntax, and internal lifecycle (e.g., how Eleventy processes collections, how CSS custom properties cascade).
2. **Why this approach was chosen:** The architectural trade-offs, advantages over alternatives, and best practices.
3. **Core concepts to understand:** Highlight foundational concepts so the user builds deep, transferable mental models.
### 2.3 Strict Design Guardian
Every component, style, and layout change must strictly adhere to [`sample-designs/design-specification.md`](./sample-designs/design-specification.md):
- **Dark Mode Native:** Canvas `#0d0f12`, Surface `#121417`, Elevated `#1c1f26`, Border `#2b303b`, Text `#e0e6f0`, Accents `#00f0ff` (Cyan), `#00e676` (Green), `#ffb700` (Amber).
- **Hard Anti-Patterns:**
- **NO** glassmorphism (`backdrop-filter: blur()`).
- **NO** drop shadows or glows (`box-shadow`, `text-shadow`).
- **NO** gradients (linear or radial).
- **NO** spring animations or floaty delays.
- **NO** rounded bubbles (border-radius capped at 0px to 4px).
- **Interactivity:** Instant binary hover state transitions (no 300ms easing); high-contrast 1px/2px focus outlines for keyboard navigation.
- **Typography:** IBM Plex Mono / JetBrains Mono for UI elements and code blocks; Inter for body prose.
### 2.4 Strict Test-Driven Development (TDD) & Adversarial Verification Mandate
- **Strict Test-First Invariant (Tests Before Code):**
- Test cases MUST ALWAYS be written and established *before writing or modifying any functional code* (filters, templates, server routes, ingestion logic, build scripts).
- Never write or modify functional or production code before the corresponding automated tests are created.
- **Mandatory Adversarial Testing:**
- Standard behavior for any functional change MUST ALWAYS include adversarial testing specifically designed to falsify assumptions and break the implementation:
- Missing or malformed environment variables (e.g., unset `SITE_DOMAIN` must throw).
- Malformed frontmatter (missing required keys, invalid dates, malformed tags, extra/empty fields).
- Edge-case and malicious inputs to filters and functions (empty strings, undefined/null values, unexpected types, HTML injections, path traversals, giant payloads).
- Design rule violations (asserting 0 instances of `backdrop-filter`, `box-shadow`, gradients, or floaty animations).
- Broken internal links, missing assets, and broken TOC anchor tags.
- **Pre-Presentation Verification Gate:**
- Before presenting the final output to the user, the agent MUST run the full test suite (`npm test`).
- All test cases must pass before presenting the final output. Never deliver, present, or claim completion of work if any test case is failing or unverified.
---
## 3. Code Modularity & Isolation Guidelines
### 3.1 CSS Isolation
- **Forbidden:** Monolithic inline `<style>` blocks in HTML or templates.
- **Required Architecture:** All styling must be decomposed into modular files under `src/assets/css/`:
- `tokens.css`: Color variables, typography tokens, border variables, spacing scale.
- `reset.css`: CSS reset, typography baseline, instant transition overrides.
- `layout.css`: `.tiling-layout`, `.split-grid`, `.article-workspace-grid`.
- `components/*.css`: Isolated per-component stylesheets (`global-bar.css`, `tiles.css`, `code-box.css`, `article.css`).
### 3.2 Template Isolation (Nunjucks)
- Use layout inheritance: `base.njk` defines the outer shell and head assets; specialized layouts (`post.njk`, `page.njk`) extend `base.njk`.
- Reusable UI chunks must live in `src/_includes/partials/` (`global-bar.njk`, `hero-tile.njk`, `post-card.njk`, `code-box.njk`, `stats-sidebar.njk`).
- Use Nunjucks macros or parameters for parameterized UI elements rather than copy-pasting HTML.
---
## 4. Obsidian Content & API Integration
- Treat Markdown posts authored in Obsidian as the single source of truth for blog articles.
- Ingestion pipeline must validate frontmatter schemas:
```yaml
title: "Title of Post"
date: 2026-09-12
tags: ["#tag1", "#tag2"]
excerpt: "Short synopsis of article."
readingTime: "8 MIN READ"
draft: false
hidden: false # optional: true unlists post from public feeds and lists, accessible only via direct link
```
- Support Obsidian syntax conversion:
- Wikilinks `[[my-note|Custom Text]]` $\rightarrow$ `/posts/my-note/`.
- Obsidian callouts `> [!NOTE]` $\rightarrow$ semantic HTML alerts.
- Media/attachments $\rightarrow$ routed to static asset directories.
+28 -10
View File
@@ -1,15 +1,33 @@
FROM nginx:1.25-alpine
FROM node:22-alpine
# Remove default Nginx configuration
RUN rm /etc/nginx/conf.d/default.conf
WORKDIR /app
# Copy custom Nginx security & performance config
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Dynamic build arguments & environment variables
ARG SITE_DOMAIN
ARG SITE_URL
ARG PORT=8080
ENV SITE_DOMAIN=$SITE_DOMAIN
ENV SITE_URL=$SITE_URL
ENV PORT=$PORT
ENV NODE_ENV=production
# Copy static website files into Nginx server root
COPY . /usr/share/nginx/html
# Install dependencies
COPY package*.json ./
RUN npm ci
# Expose standard HTTP port
EXPOSE 80
# Copy application source files
COPY . .
RUN chmod +x /app/entrypoint.sh
CMD ["nginx", "-g", "daemon off;"]
# Pre-compile baseline static assets
RUN npm run build
# Expose web server port dynamically
EXPOSE ${PORT}
# Native container healthcheck probing IPv4 loopback
HEALTHCHECK --interval=30s --timeout=5s --start-period=3s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 8080) + '/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
# Serve site using dynamic entrypoint
ENTRYPOINT ["/app/entrypoint.sh"]
+230
View File
@@ -0,0 +1,230 @@
# River Rooks | Technical Publication & Systems Architecture Blog
> Developer-centric, high-signal, low-noise personal technical publication and systems architecture blog.
Inspired by native terminal environments, modal text editors (Zed, Ghostty), and tiling window managers, this site prioritizes raw performance, keyboard navigability, information density, and structured craftsmanship over generic marketing aesthetics.
---
## 1. Quickstart: Production Server Deployment
The production deployment uses a **decoupled container architecture**. The server does not need Node.js, git, or build dependencies—it only pulls the prebuilt engine from the registry and mounts a local folder containing your Markdown posts.
### 1.1 Server Directory Structure (`~/docker/blog`)
```text
~/docker/blog/
├── .env # Environment variables (SITE_DOMAIN, etc.)
├── docker-compose.yml # Production container definition
└── posts/ # Your live Markdown articles (.md)
```
### 1.2 Production `docker-compose.yml`
```yaml
services:
web:
image: git.riverrooks.dev/venus/river-rooks-blog:latest
container_name: river_rooks_web
restart: unless-stopped
volumes:
- "./posts:/app/src/posts:ro"
environment:
- "SITE_DOMAIN=${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env}"
- "SITE_URL=${SITE_URL:-https://${SITE_DOMAIN}}"
- "PORT=${PORT:-8080}"
- "WATCH=${WATCH:-false}"
ports:
- "${HOST_PORT:-${PORT:-8080}}:${PORT:-8080}"
networks:
- backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`riverrooks.dev`) || Host(`www.riverrooks.dev`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=basic"
- "traefik.http.services.blog.loadbalancer.server.port=8080"
- "traefik.docker.network=backend"
networks:
backend:
external: true
```
### 1.3 Deployment Commands
```bash
# 1. Pull the prebuilt engine from Gitea registry
docker compose pull
# 2. Launch the container
docker compose up -d
# 3. Verify healthcheck status (transitions to healthy in 3-5 seconds)
docker compose ps
```
---
## 2. Local Development & Verification
All templates, styles, and engine code are developed in this repository (`~/code/blog`).
### 2.1 Native Node.js Workflow
```bash
# Install dependencies
npm install
# Run automated test suite (unit tests, filters, server, and docker assertions)
npm test
# Launch Eleventy dev server with hot-reload (binds to http://localhost:8080)
npm run dev
# Compile static assets to _site/
npm run build
# Start the native static server locally
npm start
```
### 2.2 Local Docker Compose Workflow
The local `docker-compose.yml` provides sensible defaults (`localhost`, port 8080, and mounts `./src/posts`):
```bash
# Build and run the local container stack
docker compose up -d --build
# View container logs (Eleventy compilation + server output)
docker compose logs -f
# Stop and clean up
docker compose down
```
### 2.3 Development Standards: Strict TDD & Adversarial Verification
This project enforces a strict test-first development lifecycle and verification standard:
1. **Tests Written Before Functional Code:** Automated test cases in `test/` must always be written before writing or modifying any functional code, filters, templates, server endpoints, or scripts.
2. **Mandatory Adversarial Testing:** Standard behavior for any functional change must always include adversarial test cases (boundary values, malformed/null inputs, unauthorized requests, path traversals, corrupt frontmatter, and design spec violations).
3. **Pre-Presentation Verification Gate:** Every change must be verified by running `npm test`. All test cases must pass before presenting the final output or committing code.
---
## 3. Push & Update Cycles
Because the system separates the **Engine** (templates/code) from the **Content** (markdown notes), updates follow two distinct paths:
### 3.1 Publishing or Editing Articles (Content Workflow)
When you write a new note or edit an existing article:
1. **Add or update the Markdown file** in `posts/` (e.g. `posts/my-new-post.md`):
```yaml
---
title: "My Article Title"
date: 2026-09-14
tags: ["#systems", "#architecture"]
excerpt: "A short synopsis of the article."
hidden: false # (Optional) Set to true to hide from public lists while keeping direct link active
---
# Article Content...
```
2. **Recompile the site:**
```bash
docker compose restart web
```
*Eleventy compiles the mounted markdown in ~150ms and starts serving immediately.*
> [!TIP]
> **Zero-Restart Live Reload:** Set `WATCH=true` in your server's `docker-compose.yml` environment. Eleventy will watch the `posts/` directory in the background. Whenever a note is saved or synced (e.g., from Obsidian), the site automatically updates in memory without running any commands.
### 3.2 Updating Blog Templates, CSS, or Features (Engine Workflow)
When you modify design tokens, Nunjucks templates, server routes, or dependencies:
1. **Develop and verify locally:**
```bash
npm test
```
2. **Build and push the new engine image to the Gitea container registry:**
```bash
# Build the engine image
docker build -t git.riverrooks.dev/venus/river-rooks-blog:latest .
# Push to Gitea
docker push git.riverrooks.dev/venus/river-rooks-blog:latest
```
3. **Update the production server:**
```bash
docker compose pull
docker compose up -d
```
---
## 4. System Architecture & Structure
### 4.1 High-Level Architecture
```text
┌─────────────────────────────────────────────────────────────┐
│ Dev Machine / Workstation │
│ src/ (templates, CSS, config) + Dockerfile + entrypoint.sh │
└──────────────────────────────┬──────────────────────────────┘
│ docker push
▼
┌─────────────────────────────────────────────────────────────┐
│ Gitea Container Registry (git.riverrooks.dev) │
│ image: venus/river-rooks-blog:latest │
└──────────────────────────────┬──────────────────────────────┘
│ docker pull
▼
┌─────────────────────────────────────────────────────────────┐
│ Production Server │
│ │
│ ~/docker/blog/posts/ ──(volume)──> [ river_rooks_web ] │
│ (Live Markdown Notes) ├── entrypoint.sh │
│ ├── 11ty compiles │
│ Traefik (SSL / Proxy) <──────────── └── server.js (:8080) │
└─────────────────────────────────────────────────────────────┘
```
### 4.2 Key Engine Components
- **`entrypoint.sh`**: Dynamic container entrypoint. Compiles mounted markdown notes with Eleventy on boot, starts optional background file watcher, and spawns `server.js` as PID 1 for clean signal handling.
- **`Dockerfile`**: Minimal `node:22-alpine` container with pre-installed dependencies, baseline static pre-compilation, and baked-in `HEALTHCHECK` querying `http://127.0.0.1:8080/healthz`.
- **`server.js`**: Zero-dependency native Node static web server supporting clean directory URLs, MIME caching, security headers, and the `/healthz` telemetry endpoint.
- **`eleventy.config.js`**: Eleventy 3.x configuration, custom collections (`posts`, `tagList`), markdown-it anchor plugins, and custom date/reading-time filters.
### 4.3 Directory Map
```text
.
├── .agents/ # Antigravity agent configurations & rules
├── sample-designs/ # Design specifications & HTML/CSS prototypes
├── src/ # Blog engine source
│ ├── _data/ # Site metadata & navigation
│ ├── _filters/ # Modular Eleventy filters (date, readingTime, headings)
│ ├── _includes/
│ │ ├── layouts/ # Base & post Nunjucks templates
│ │ └── partials/ # Global bar, hero tile, post cards, code box
│ ├── assets/
│ │ └── css/ # Modular CSS (tokens, reset, layout, components)
│ └── posts/ # Default/fallback markdown posts
├── test/ # Automated test suite (node:test)
├── docker-compose.yml # Local development compose file
├── sample-docker-compose.yml # Production-ready compose template
├── Dockerfile # Production container image definition
├── entrypoint.sh # Container startup & compilation entrypoint
├── server.js # Native Node static HTTP server
├── eleventy.config.js # Eleventy configuration
├── package.json # Project scripts & dependencies
└── README.md # Documentation & runbook
```
---
## 5. Strict Design Specification
All UI components and stylesheets strictly follow [`sample-designs/design-specification.md`](./sample-designs/design-specification.md):
| Rule | Constraint |
| :--- | :--- |
| **Color Scheme** | Dark-mode native only. Canvas: `#0d0f12`, Surface: `#121417`, Elevated: `#1c1f26`, Borders: `#2b303b`. |
| **Accents** | Terminal accents: Cyan (`#00f0ff`), Green (`#00e676`), Amber (`#ffb700`), Red (`#ff5252`). |
| **Borders & Radius** | Crisp 1px solid borders. Zero to 4px maximum border-radius (sharp corners). |
| **Strict Anti-Patterns** | **NO** glassmorphism (`backdrop-filter`), **NO** drop shadows or glows, **NO** gradients, **NO** spring animations. |
| **Typography** | Monospace for UI & code (`IBM Plex Mono`, `JetBrains Mono`); clean sans-serif for body prose (`Inter`). |
| **Interactivity** | Instant binary hover snaps (no slow fade transitions); high-contrast focus outlines for keyboard navigation. |
Executable
+7
View File
@@ -0,0 +1,7 @@
#! /usr/bin/env zsh
echo building docker image and pushing to remote server
docker build -t git.riverrooks.dev/venus/river-rooks-blog:latest /home/venus/code/blog/
docker push git.riverrooks.dev/venus/river-rooks-blog:latest
echo docker image built and pushed, now updating remote server
ssh sol "cd docker/blog && ./docker-update.zsh"
-17
View File
@@ -1,17 +0,0 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
image: river-rooks-blog:latest
container_name: river_rooks_web
restart: unless-stopped
ports:
- "8080:80"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:80/"]
interval: 30s
timeout: 5s
retries: 3
+111
View File
@@ -0,0 +1,111 @@
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
import markdownIt from "markdown-it";
import markdownItAnchor from "markdown-it-anchor";
import { filters } from "./src/_filters/index.js";
import {
postComparator,
isPostPublic,
isPostArchived,
isPostDraft,
isPostHidden
} from "./src/_utils/sort-posts.js";
import { validateLinkedArticles } from "./src/_utils/linked-articles.js";
export default function (eleventyConfig) {
let buildTimestamp = new Date().toISOString();
eleventyConfig.on("eleventy.before", () => {
buildTimestamp = new Date().toISOString();
});
eleventyConfig.addGlobalData("buildTimestamp", () => buildTimestamp);
// Build ignored local post fixtures without making them Git-tracked.
eleventyConfig.setUseGitIgnore(false);
// 1. Static Asset Passthrough Copy
eleventyConfig.addPassthroughCopy({ "src/assets": "assets" });
// 2. Syntax Highlighting Plugin
eleventyConfig.addPlugin(syntaxHighlight);
// 3. Markdown-It Library Customization
const markdownOptions = {
html: true,
breaks: false,
linkify: true
};
const markdownLib = markdownIt(markdownOptions).use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.headerLink({
safariReaderFix: true
}),
slugify: (s) =>
s
.trim()
.toLowerCase()
.replace(/[^a-z0-9 -]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
});
eleventyConfig.setLibrary("md", markdownLib);
// 4. Custom Filters (imported from modular src/_filters/index.js)
for (const [name, fn] of Object.entries(filters)) {
eleventyConfig.addFilter(name, fn);
}
// 5. Custom Collections
// Pinned / featured prioritized, chronological blog posts collection (excludes drafts, hidden, and archived posts)
eleventyConfig.addCollection("posts", (collectionApi) => {
const allPosts = collectionApi.getFilteredByGlob("src/posts/*.md");
validateLinkedArticles(allPosts);
return allPosts
.filter((post) => isPostPublic(post))
.sort(postComparator);
});
// Chronological archived blog posts collection (excludes drafts and hidden posts)
eleventyConfig.addCollection("archivedPosts", (collectionApi) => {
return collectionApi
.getFilteredByGlob("src/posts/*.md")
.filter((post) => !isPostDraft(post) && !isPostHidden(post) && isPostArchived(post))
.sort(postComparator);
});
// Dynamic Tag List Collection across all public posts (excludes drafts and hidden posts)
eleventyConfig.addCollection("tagList", (collectionApi) => {
const tagSet = new Set();
collectionApi
.getFilteredByGlob("src/posts/*.md")
.filter((post) => isPostPublic(post))
.forEach((item) => {
if ("tags" in item.data) {
let tags = item.data.tags;
if (typeof tags === "string") tags = [tags];
for (const tag of tags) {
const cleanTag = tag.replace(/^#/, "");
tagSet.add(cleanTag);
}
}
});
return [...tagSet].sort();
});
// 6. Dev Server Options (dynamic port binding)
eleventyConfig.setServerOptions({
port: parseInt(process.env.PORT || "8080", 10)
});
return {
dir: {
input: "src",
includes: "_includes",
data: "_data",
output: "_site"
},
templateFormats: ["njk", "md", "html"],
htmlTemplateEngine: "njk",
markdownTemplateEngine: "njk"
};
}
Executable
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
# Pass-through mode: if arguments were provided, execute them directly
if [ "$#" -gt 0 ]; then
exec "$@"
fi
# Ensure posts directory data file exists for layout inheritance
if [ ! -f src/posts/posts.json ]; then
echo '{"layout": "layouts/post.njk"}' > src/posts/posts.json
fi
# Build static site from mounted or packaged markdown posts
echo "[blog-engine] Compiling static site with Eleventy..."
npm run build || exit 1
# Optional watch mode for zero-restart live note updates
if [ "${WATCH:-false}" = "true" ]; then
echo "[blog-engine] Starting live watch daemon for markdown changes..."
npx @11ty/eleventy --watch &
fi
# Start production static web server
echo "[blog-engine] Starting static server on port ${PORT:-8080}..."
exec node server.js
-270
View File
@@ -1,270 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Systems Architecture & Cybersecurity</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg-canvas: #0d0f12; /* Deep slate/charcoal */
--bg-surface: #121417; /* Lighter surface background */
--border-color: #2b303b; /* Low-contrast 1px border */
--text-primary: #e0e6f0; /* Off-white prose */
--text-muted: #8a95a5; /* Monospaced metadata gray */
--accent: #00f0ff; /* High-visibility Cyan terminal accent */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.65;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 40px 24px;
}
.standalone-container {
width: 100%;
max-width: 65ch; /* Capped reading density */
}
/* Identity Header */
.author-name {
font-family: 'IBM Plex Mono', monospace;
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.status-line {
display: flex;
align-items: center;
gap: 10px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
}
/* 8x8px Solid Active State Indicator Dot */
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent);
display: inline-block;
flex-shrink: 0;
}
/* 1px Solid Dividers */
.section-divider {
border: 0;
border-top: 1px solid var(--border-color);
margin: 32px 0;
}
/* Terminal Status Block */
.terminal-block {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
margin-bottom: 32px;
}
.terminal-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
display: flex;
justify-content: space-between;
font-size: 0.78rem;
color: var(--text-muted);
}
.terminal-title {
color: var(--text-primary);
font-weight: 500;
}
.terminal-body {
padding: 16px;
font-size: 0.85rem;
line-height: 1.5;
}
.cmd-line {
color: var(--text-muted);
margin-bottom: 8px;
}
.prompt {
color: var(--accent);
font-weight: 700;
}
.cmd-output {
color: var(--text-primary);
}
.cursor-block {
display: inline-block;
width: 8px;
height: 15px;
background-color: var(--text-primary);
margin-left: 4px;
vertical-align: middle;
animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
to { visibility: hidden; }
}
/* Sections */
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.bio-section {
margin-bottom: 32px;
}
.bio-text {
font-size: 0.98rem;
color: var(--text-primary);
}
.stack-section {
margin-bottom: 32px;
}
.tag-cluster {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
}
.tag {
color: var(--text-muted);
}
/* Standalone Footer Links */
.footer-links {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.footer-link {
color: var(--text-primary);
text-decoration: none;
padding: 2px 6px;
transition: none; /* Instant snap */
}
.footer-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.footer-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.pipe {
color: var(--border-color);
}
</style>
</head>
<body>
<main class="standalone-container">
<!-- Identity Header -->
<header class="identity-header">
<h1 class="author-name">River Rooks</h1>
<div class="status-line">
<span class="status-dot"></span>
<span class="status-text">SYSTEMS ARCHITECTURE &amp; CYBERSECURITY</span>
</div>
</header>
<hr class="section-divider">
<!-- Under Construction Terminal Block -->
<section class="terminal-section" aria-label="Status Terminal">
<div class="terminal-block">
<div class="terminal-header">
<span class="terminal-title">/var/log/status.txt</span>
<span>STDOUT</span>
</div>
<div class="terminal-body">
<div class="cmd-line"><span class="prompt">$</span> cat /var/log/status.txt</div>
<div class="cmd-output">
Under construction. Full site infrastructure is currently being provisioned. Routing traffic to temporary landing page...<span class="cursor-block"></span>
</div>
</div>
</div>
</section>
<!-- Biography -->
<section class="bio-section">
<h2 class="section-title">whoami</h2>
<p class="bio-text">
Cybersecurity student at the University of Alabama College of Engineering, focusing on network forensics, self-hosted infrastructure, and systems architecture.
</p>
</section>
<!-- Active Stack Tags -->
<section class="stack-section">
<h2 class="section-title">active_stack</h2>
<div class="tag-cluster">
<span class="tag">[#network-forensics]</span>
<span class="tag">[#docker-traefik]</span>
<span class="tag">[#arch-linux]</span>
<span class="tag">[#neovim]</span>
<span class="tag">[#wireguard]</span>
<span class="tag">[#posix-sh]</span>
<span class="tag">[#python]</span>
</div>
</section>
<hr class="section-divider">
<!-- Piped Contact Footer -->
<footer class="footer-links">
<a href="https://github.com" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<span class="pipe">|</span>
<a href="mailto:river@example.com" class="footer-link">Email</a>
<span class="pipe">|</span>
<a href="#" class="footer-link">PGP Key</a>
</footer>
</main>
</body>
</html>
-34
View File
@@ -1,34 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html river-rooks.html;
# Disable Nginx version tokens
server_tokens off;
# Security Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Gzip Asset Compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 256;
# Clean URL Routing
location / {
try_files $uri $uri/ $uri.html =404;
}
# Static asset caching
location ~* \.(?:css|js|svg|png|jpg|jpeg|gif|ico)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
error_page 404 /index.html;
}
+1618
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "river-rooks-blog",
"version": "1.0.0",
"description": "Developer-centric personal technical publication and systems architecture blog",
"type": "module",
"main": "eleventy.config.js",
"scripts": {
"dev": "eleventy --serve --watch",
"build": "eleventy",
"serve": "node server.js",
"start": "node server.js",
"test": "node --test 'test/**/*.test.js'",
"clean": "rm -rf _site"
},
"keywords": [
"eleventy",
"blog",
"tiling",
"nunjucks",
"obsidian"
],
"author": "River Rooks",
"license": "MIT",
"dependencies": {
"@11ty/eleventy": "^3.1.6",
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2",
"luxon": "^3.5.0",
"markdown-it": "^14.1.0",
"markdown-it-anchor": "^9.2.0"
}
}
File diff suppressed because it is too large Load Diff
+781
View File
@@ -0,0 +1,781 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Blog Lander (Tiling Hero &amp; Feed)</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg-canvas: #0d0f12; /* Deep slate/charcoal canvas */
--bg-surface: #121417; /* Tile surface */
--bg-elevated: #1c1f26; /* Hovered tile surface */
--border-color: #2b303b; /* Crisp 1px solid border */
--text-primary: #e0e6f0; /* Light off-white prose */
--text-muted: #8a95a5; /* Monospace metadata */
--accent-cyan: #00f0ff; /* Primary active accent */
--accent-green: #00e676; /* Operational status indicator */
--accent-amber: #ffb700; /* Warning / highlight accent */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 48px 16px;
}
.tiling-layout {
width: 100%;
max-width: 1120px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Top Global Status Header Bar (Gateway 3 Style) */
.global-bar {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
flex-wrap: wrap;
gap: 10px;
}
.brand-group {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
font-weight: 700;
}
.status-indicator {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
}
.bar-nav {
display: flex;
gap: 14px;
align-items: center;
}
.bar-link {
color: var(--text-muted);
text-decoration: none;
transition: none; /* Instant snap */
}
.bar-link:hover {
color: var(--accent-cyan);
}
.bar-link.active {
color: var(--text-primary);
border-bottom: 1px solid var(--accent-cyan);
}
/* Hero Tile: Section Declaration & Synopsis */
.hero-tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-cyan);
border-radius: 4px;
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.hero-pretitle {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--accent-cyan);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.hero-main-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 2.1rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.2;
}
.hero-landing-text {
font-size: 0.98rem;
color: var(--text-primary);
line-height: 1.65;
max-width: 82ch;
}
.filter-pills-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 4px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
}
.filter-tag-btn {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
color: var(--text-muted);
padding: 3px 8px;
border-radius: 2px;
cursor: pointer;
transition: none;
}
.filter-tag-btn:hover {
border-color: var(--accent-cyan);
color: var(--text-primary);
background-color: var(--bg-elevated);
}
.filter-tag-btn.active {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
border-color: var(--accent-cyan);
font-weight: 700;
}
/* Asymmetric Split Grid */
.split-grid {
display: grid;
grid-template-columns: 1.25fr 0.75fr;
gap: 16px;
}
@media (max-width: 880px) {
.split-grid {
grid-template-columns: 1fr;
}
}
/* Standard Tile Container */
.tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.tile-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.92rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.tile-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.74rem;
color: var(--text-muted);
}
/* Featured Lead Article Card (Inside Tile) */
.featured-card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.featured-badge-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.76rem;
flex-wrap: wrap;
gap: 8px;
}
.badge-pinned {
color: var(--accent-amber);
font-weight: 700;
}
.featured-article-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
line-height: 1.3;
transition: none;
}
.featured-article-title:hover {
color: var(--accent-cyan);
}
.featured-prose {
font-size: 0.92rem;
color: var(--text-primary);
line-height: 1.6;
}
/* Terminal Code Block Component */
.terminal-code-box {
border: 1px solid var(--border-color);
border-radius: 3px;
background-color: var(--bg-canvas);
overflow: hidden;
font-family: 'JetBrains Mono', monospace;
}
.code-titlebar {
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
padding: 6px 12px;
font-size: 0.74rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
.code-content {
padding: 12px;
font-size: 0.78rem;
overflow-x: auto;
color: var(--text-primary);
line-height: 1.5;
}
.featured-cta-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--border-color);
flex-wrap: wrap;
gap: 10px;
font-family: 'IBM Plex Mono', monospace;
}
.cta-button {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 6px 14px;
font-weight: 700;
font-size: 0.8rem;
text-decoration: none;
border-radius: 2px;
border: 1px solid var(--accent-cyan);
transition: none;
}
.cta-button:hover {
background-color: transparent;
color: var(--accent-cyan);
}
/* Sidebar Tile Elements */
.sidebar-stats-box {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
}
.stat-row {
display: flex;
justify-content: space-between;
}
.stat-val {
color: var(--text-primary);
font-weight: 700;
}
.topic-track-item {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
text-decoration: none;
transition: none;
}
.topic-track-item:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.topic-track-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.84rem;
font-weight: 700;
color: var(--text-primary);
}
.topic-track-item:hover .topic-track-title {
color: var(--accent-cyan);
}
.topic-track-desc {
font-size: 0.78rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Full-Width Chronological Article Feed Tile */
.feed-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.feed-card {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 8px;
transition: none;
}
.feed-card:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.feed-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.76rem;
color: var(--text-muted);
}
.feed-date {
color: var(--accent-cyan);
}
.feed-heading {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
line-height: 1.35;
transition: none;
}
.feed-card:hover .feed-heading {
color: var(--accent-cyan);
}
.feed-excerpt {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.6;
}
.feed-tags-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 6px;
border-top: 1px solid var(--border-color);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.74rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 6px;
}
.tag-cluster {
display: flex;
gap: 8px;
}
/* Global Footer Tile (Gateway 3 Standard) */
.footer-tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 10px;
}
.footer-links-cluster {
display: flex;
gap: 12px;
align-items: center;
}
.footer-btn {
color: var(--text-primary);
text-decoration: none;
padding: 2px 4px;
transition: none;
}
.footer-btn:hover {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
}
.pipe {
color: var(--border-color);
}
</style>
</head>
<body>
<main class="tiling-layout">
<!-- Top Global Status Bar (Gateway 3 Match) -->
<header class="global-bar">
<div class="brand-group">
<span class="status-indicator"></span>
<span>RIVER ROOKS // BLOG &amp; RESEARCH ARCHIVE</span>
</div>
<nav class="bar-nav" aria-label="Quick Navigation">
<a href="service-gateway-tiling.html" class="bar-link">Gateway 3 ↗</a>
<a href="#" class="bar-link active">&gt; Dispatches</a>
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="bar-link">Git Forge</a>
<a href="feed.xml" class="bar-link">RSS Feed</a>
<a href="index.html" class="bar-link">Catalog ↗</a>
</nav>
</header>
<!-- Hero Tile: Blog Mission & Topic Filters -->
<section class="hero-tile" aria-label="Blog Mission & Overview">
<div class="hero-pretitle">TECHNICAL PUBLICATION REPOSITORY &bull; ~/NOTES/INDEX.MD</div>
<h1 class="hero-main-title">Engineering Notes, CTF Write-Ups &amp; Systems Architecture</h1>
<p class="hero-landing-text">
Documenting bare-metal systems, embedded hardware audits, and network isolation architectures. Writing zero-allocation POSIX scripts, reverse engineering production microcontrollers, and analyzing memory allocator exploitation primitives.
</p>
<div class="filter-pills-row">
<span style="color: var(--text-muted); margin-right: 4px;">TOPIC FILTER:</span>
<button class="filter-tag-btn active" onclick="filterTopic('all', this)">[All Dispatches (5)]</button>
<button class="filter-tag-btn" onclick="filterTopic('firmware', this)">[#firmware (1)]</button>
<button class="filter-tag-btn" onclick="filterTopic('homelab', this)">[#homelab (2)]</button>
<button class="filter-tag-btn" onclick="filterTopic('security', this)">[#security (3)]</button>
<button class="filter-tag-btn" onclick="filterTopic('posix-sh', this)">[#posix-sh (1)]</button>
</div>
</section>
<!-- Split Grid: Featured Article + Research Index -->
<div class="split-grid">
<!-- Left Column: Featured Article Tile -->
<section class="tile" aria-label="Featured Technical Article">
<div class="tile-header">
<div class="tile-title">
<span>// PINNED RESEARCH WRITE-UP</span>
</div>
<span class="tile-meta">SPI FLASH &bull; 8 MIN READ</span>
</div>
<div class="featured-card-body">
<div class="featured-badge-row">
<span class="badge-pinned">[FEATURED DEEP DIVE]</span>
<span style="color: var(--text-muted);">2026-07-20 &bull; ~/notes/iot-firmware-spi-extraction.md</span>
</div>
<h2 style="margin: 0;">
<a href="article-tiling-firmware.html" class="featured-article-title">Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
</h2>
<p class="featured-prose">
In security audits of embedded systems, retrieving firmware is the critical first step. When over-the-air updates are encrypted and debug ports (UART, JTAG) are disabled in production silicon, dumping physical SPI Flash memory becomes necessary. This writeup covers extracting firmware from an 8-pin SOP SPI flash chip using a Bus Pirate and flashrom to bypass bootloader lockdowns.
</p>
<!-- Terminal Code Box -->
<div class="terminal-code-box">
<div class="code-titlebar">
<span>extract_firmware.sh</span>
<span>POSIX sh</span>
</div>
<div class="code-content">
<pre><code># Query the SPI flash details using flashrom via Bus Pirate
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump.bin
# Verify the integrity of the binary dump by comparing hashes
if [ $? -eq 0 ]; then
echo "Dump successful. Running md5sum..."
md5sum firmware_dump.bin
fi</code></pre>
</div>
</div>
<div class="featured-cta-row">
<div class="tag-cluster">
<span style="color: var(--text-muted); font-size: 0.75rem;">[#firmware] [#hardware] [#security] [#bus-pirate]</span>
</div>
<a href="article-tiling-firmware.html" class="cta-button">READ FULL WRITEUP &rarr;</a>
</div>
</div>
</section>
<!-- Right Column: Quick Research Tracks & Telemetry -->
<div style="display: flex; flex-direction: column; gap: 16px;">
<!-- Publication Stats Tile -->
<section class="tile" aria-label="Archive Statistics">
<div class="tile-header">
<div class="tile-title">
<span>// ARCHIVE TELEMETRY</span>
</div>
<span class="tile-meta">STATIC HTML</span>
</div>
<div class="sidebar-stats-box">
<div class="stat-row">
<span>TOTAL ARTICLES:</span>
<span class="stat-val">5 PUBLISHED</span>
</div>
<div class="stat-row">
<span>TOTAL READING TIME:</span>
<span class="stat-val">51 MINUTES</span>
</div>
<div class="stat-row">
<span>ACTIVE TOPICS:</span>
<span class="stat-val">7 CATEGORIES</span>
</div>
<div class="stat-row">
<span>FORMAT COMPLIANCE:</span>
<span class="stat-val">IEEE STD 1003.1</span>
</div>
</div>
</section>
<!-- Curated Focus Tracks -->
<section class="tile" aria-label="Curated Topic Tracks">
<div class="tile-header">
<div class="tile-title">
<span>// RESEARCH TRACKS</span>
</div>
<span class="tile-meta">CURATED</span>
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<a href="#" class="topic-track-item">
<div class="topic-track-title">&gt; Embedded &amp; Hardware Forensics</div>
<div class="topic-track-desc">Flash memory dumping, bus tapping, silicon pinout discovery.</div>
</a>
<a href="#" class="topic-track-item">
<div class="topic-track-title">&gt; Network Routing &amp; Homelab</div>
<div class="topic-track-desc">Traefik reverse proxying, WireGuard full-mesh, VLAN firewalls.</div>
</a>
<a href="#" class="topic-track-item">
<div class="topic-track-title">&gt; Low-Level Memory Exploitation</div>
<div class="topic-track-desc">glibc tcache safe-linking, heap corruption, House of Apple.</div>
</a>
</div>
</section>
</div>
</div>
<!-- Full-Width Article Feed Tile -->
<section class="tile" aria-label="Chronological Article Stream">
<div class="tile-header">
<div class="tile-title">
<span>// CHRONOLOGICAL DISPATCHES FEED</span>
</div>
<span class="tile-meta">ALL ARTICLES ORDERED BY DATE</span>
</div>
<div class="feed-list" id="feedList">
<!-- Post 2: Traefik -->
<article class="feed-card" data-tags="homelab networking">
<div class="feed-meta-row">
<time datetime="2026-07-15" class="feed-date">2026-07-15 &bull; 12 min read</time>
<span>~/notes/ha-traefik-docker.md</span>
</div>
<h3 style="margin: 0;">
<a href="article-tiling-traefik.html" class="feed-heading">&gt; Building a High-Availability Traefik Router with Docker Compose</a>
</h3>
<p class="feed-excerpt">
Architecting automated TLS wildcard certificate propagation, Docker provider label routing, zero-downtime container updates, and backend health-check failover routes on bare-metal infrastructure.
</p>
<div class="feed-tags-row">
<div class="tag-cluster">
<span>[#homelab]</span>
<span>[#docker-traefik]</span>
<span>[#networking]</span>
</div>
<a href="article-tiling-traefik.html" style="color: var(--accent-cyan); text-decoration: none;">Read Entry &rarr;</a>
</div>
</article>
<!-- Post 3: POSIX sh -->
<article class="feed-card" data-tags="posix-sh security">
<div class="feed-meta-row">
<time datetime="2026-07-10" class="feed-date">2026-07-10 &bull; 6 min read</time>
<span>~/notes/posix-sh-vs-bash.md</span>
</div>
<h3 style="margin: 0;">
<a href="#" class="feed-heading">&gt; POSIX sh vs. Bash: Portability in Modern Scripts</a>
</h3>
<p class="feed-excerpt">
Analyzing edge-case incompatibilities, syntax overhead, eliminating bashisms, and writing fast, bulletproof bootstrapping scripts for Alpine and embedded targets conforming to IEEE Std 1003.1.
</p>
<div class="feed-tags-row">
<div class="tag-cluster">
<span>[#posix-sh]</span>
<span>[#shell]</span>
<span>[#unix]</span>
</div>
<a href="#" style="color: var(--accent-cyan); text-decoration: none;">Read Entry &rarr;</a>
</div>
</article>
<!-- Post 4: Heap Exploitation -->
<article class="feed-card" data-tags="security">
<div class="feed-meta-row">
<time datetime="2026-06-28" class="feed-date">2026-06-28 &bull; 15 min read</time>
<span>~/notes/ctf-heap-glibc-2-35.md</span>
</div>
<h3 style="margin: 0;">
<a href="article-tiling-heap.html" class="feed-heading">&gt; CTF Write-up: Heap Exploitation in glibc 2.35</a>
</h3>
<p class="feed-excerpt">
Deconstructing modern glibc memory allocator defenses: tcache safe-linking pointer mangling, bypass primitives, and House of Apple file structure hijacking for arbitrary code execution.
</p>
<div class="feed-tags-row">
<div class="tag-cluster">
<span>[#ctf]</span>
<span>[#binary-exploitation]</span>
<span>[#security]</span>
</div>
<a href="article-tiling-heap.html" style="color: var(--accent-cyan); text-decoration: none;">Read Entry &rarr;</a>
</div>
</article>
<!-- Post 5: Zero-Trust WireGuard -->
<article class="feed-card" data-tags="networking security homelab">
<div class="feed-meta-row">
<time datetime="2026-06-14" class="feed-date">2026-06-14 &bull; 10 min read</time>
<span>~/notes/zero-trust-wireguard-opnsense.md</span>
</div>
<h3 style="margin: 0;">
<a href="#" class="feed-heading">&gt; Zero-Trust Network Isolation with WireGuard &amp; OPNsense VLANs</a>
</h3>
<p class="feed-excerpt">
Designing strict zone-based firewall rules across isolated VLANs (Management, Server DMZ, IoT, Trusted Clients) with point-to-point WireGuard cryptographic routing.
</p>
<div class="feed-tags-row">
<div class="tag-cluster">
<span>[#networking]</span>
<span>[#wireguard]</span>
<span>[#security]</span>
</div>
<a href="#" style="color: var(--accent-cyan); text-decoration: none;">Read Entry &rarr;</a>
</div>
</article>
</div>
</section>
<!-- Global Footer Tile (Matching Gateway 3 Exactly) -->
<footer class="footer-tile">
<div>River Rooks &bull; University of Alabama College of Engineering</div>
<nav class="footer-links-cluster" aria-label="Global Footer Links">
<a href="service-gateway-tiling.html" class="footer-btn">Gateway Lander</a>
<span class="pipe">|</span>
<a href="feed.xml" class="footer-btn">RSS</a>
<span class="pipe">|</span>
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-btn">Forgejo</a>
<span class="pipe">|</span>
<a href="mailto:river@rooks.dev" class="footer-btn">river@rooks.dev</a>
<span class="pipe">|</span>
<a href="#" title="Fingerprint: 0x4A8E92F1C0B3D4E7" class="footer-btn">PGP</a>
<span class="pipe">|</span>
<a href="index.html" class="footer-btn">Catalog ↗</a>
</nav>
</footer>
</main>
<script>
function filterTopic(topic, btn) {
document.querySelectorAll('.filter-tag-btn').forEach(el => el.classList.remove('active'));
btn.classList.add('active');
const cards = document.querySelectorAll('#feedList .feed-card');
cards.forEach(card => {
if (topic === 'all') {
card.style.display = 'flex';
} else {
const tags = card.getAttribute('data-tags') || '';
if (tags.includes(topic)) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
}
});
}
</script>
</body>
</html>
+592
View File
@@ -0,0 +1,592 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Homelab Gateway (Tiling Workspace)</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg-canvas: #0d0f12; /* Deep slate/charcoal canvas */
--bg-surface: #121417; /* Tile surface */
--bg-elevated: #1c1f26; /* Hovered tile */
--border-color: #2b303b; /* Crisp 1px border */
--text-primary: #e0e6f0; /* Prose off-white */
--text-muted: #8a95a5; /* Monospace metadata */
--accent-cyan: #00f0ff; /* Primary active accent */
--accent-green: #00e676; /* Online indicator */
--accent-amber: #ffb700; /* Warning / internal indicator */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 48px 16px;
}
.tiling-layout {
width: 100%;
max-width: 1120px;
display: flex;
flex-direction: column;
gap: 16px;
}
/* Top Global Status Header Bar */
.global-bar {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
flex-wrap: wrap;
gap: 10px;
}
.brand-group {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
font-weight: 700;
}
.status-indicator {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
}
.bar-nav {
display: flex;
gap: 14px;
align-items: center;
}
.bar-link {
color: var(--text-muted);
text-decoration: none;
transition: none; /* Instant snap */
}
.bar-link:hover {
color: var(--accent-cyan);
}
/* Primary Hero Landing Tile (Main Focus of Homepage) */
.hero-tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-left: 3px solid var(--accent-cyan);
border-radius: 4px;
padding: 28px 32px;
display: flex;
flex-direction: column;
gap: 16px;
}
.hero-pretitle {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--accent-cyan);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.hero-main-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.2;
}
.hero-landing-text {
font-size: 1.02rem;
color: var(--text-primary);
line-height: 1.7;
max-width: 80ch;
}
.hero-landing-text p {
margin-bottom: 14px;
}
.hero-landing-text p:last-child {
margin-bottom: 0;
}
.hero-directive-box {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 14px 18px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.84rem;
color: var(--text-muted);
line-height: 1.6;
}
.hero-directive-box strong {
color: var(--text-primary);
}
/* Tiling Grid Section: Split Columns */
.split-grid {
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 16px;
}
@media (max-width: 880px) {
.split-grid {
grid-template-columns: 1fr;
}
}
/* Tile Container Style */
.tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.tile-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.95rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.tile-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.74rem;
color: var(--text-muted);
}
/* Featured Service Tile: The Blog */
.blog-service-feature {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.blog-feature-top {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
}
.badge-core {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 2px 6px;
font-weight: 700;
border-radius: 2px;
}
.blog-feature-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
display: inline-block;
transition: none;
}
.blog-feature-title:hover {
color: var(--accent-cyan);
}
.blog-feature-desc {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.5;
}
.blog-sublinks {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
border-top: 1px solid var(--border-color);
padding-top: 10px;
}
.blog-sublink-item {
display: flex;
justify-content: space-between;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-primary);
text-decoration: none;
padding: 4px 6px;
border-radius: 2px;
transition: none;
}
.blog-sublink-item:hover {
background-color: var(--bg-elevated);
color: var(--accent-cyan);
}
/* Homelab Services Grid */
.services-mini-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 540px) {
.services-mini-grid {
grid-template-columns: 1fr;
}
}
.service-tile-item {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
padding: 12px;
text-decoration: none;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 6px;
transition: none;
}
.service-tile-item:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.service-item-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
}
.service-tile-item:hover .service-item-title {
color: var(--accent-cyan);
}
.service-item-tag {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.7rem;
color: var(--text-muted);
}
.service-item-desc {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Infrastructure Nodes Telemetry Tile */
.node-stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 3px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
}
.node-name {
color: var(--text-primary);
font-weight: 700;
}
.node-load {
color: var(--accent-green);
}
/* Bottom Global Footer Tile */
.footer-tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 10px;
}
.footer-links-cluster {
display: flex;
gap: 12px;
align-items: center;
}
.footer-btn {
color: var(--text-primary);
text-decoration: none;
padding: 2px 4px;
transition: none;
}
.footer-btn:hover {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
}
.pipe {
color: var(--border-color);
}
</style>
</head>
<body>
<main class="tiling-layout">
<!-- Top Status Bar -->
<header class="global-bar">
<div class="brand-group">
<span class="status-indicator"></span>
<span>RIVER ROOKS // NODE GATEWAY</span>
</div>
<nav class="bar-nav" aria-label="Quick Nav">
<a href="blog-lander-cards.html" class="bar-link">&gt; Blog &amp; Notes</a>
<a href="homelab.html" class="bar-link">Infrastructure</a>
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="bar-link">Git Forge</a>
<a href="index.html" class="bar-link">Catalog ↗</a>
</nav>
</header>
<!-- Main Hero Landing Tile (Central Focus of Homepage) -->
<section class="hero-tile" aria-label="Homepage Mission & Landing Declaration">
<div class="hero-pretitle">SYSTEM DECLARATION &bull; RACK-01 NODE GATEWAY</div>
<h1 class="hero-main-title">Cybersecurity &amp; Dev Ops notes</h1>
<div class="hero-landing-text">
<p>
I am River Rooks, a cybersecurity student at the University of Alabama. My work centers on self-hosted system architecture, network security, and capture the flag style skills. I maintain a personal homelab cluster for research, experimentation, and development of open-source tools.
</p>
<p>
This website functions as the unified frontend for my physical homelab cluster. Rather than hosting projects on opaque third-party clouds, everything here runs on bare metal in my personal server rack—orchestrated through Proxmox VE, partitioned across isolated VLANs via OPNsense, and protected by point-to-point WireGuard cryptographic meshes.
</p>
<p>
<strong>Navigation Guide:</strong> If you are here to read technical research, the <strong>Engineering Blog</strong> is linked below with deep write-ups on firmware dumping, heap exploitation, and container routing. If you are here to use tools, the <strong>Public Services</strong> provide locally hosted instances of CyberChef, Stirling PDF, and Git.
</p>
</div>
<div class="hero-directive-box">
<strong>PRIMARY DESTINATIONS:</strong>
<span style="color: var(--accent-cyan); margin-left: 8px;">[1] Technical Blog (5 Articles)</span> &bull;
<span>[2] Forgejo Git Repos</span> &bull;
<!-- <span>[3] Public Cryptographic &amp; PDF Tools</span> &bull; -->
<!-- <span>[4] Cluster Telemetry</span> -->
</div>
</section>
<!-- Split Tiling Columns -->
<div class="split-grid">
<!-- Left Column: Featured Service (Blog) + Recent Articles -->
<div style="display: flex; flex-direction: column; gap: 16px;">
<!-- Featured Service Tile: The Blog -->
<section class="tile" aria-label="Featured Service: Technical Blog">
<div class="tile-header">
<div class="tile-title">
<span>// PRIMARY SERVICE: THE TECHNICAL BLOG</span>
</div>
<span class="tile-meta">~/notes/ &bull; 5 ARTICLES</span>
</div>
<div class="blog-service-feature">
<div class="blog-feature-top">
<span class="badge-core">[CORE PUBLICATION SERVICE]</span>
<span style="color: var(--text-muted);">2026-07-20 LAST SYNC</span>
</div>
<a href="blog-lander-cards.html" class="blog-feature-title">&gt; River Rooks Technical Notes &amp; Research</a>
<p class="blog-feature-desc">
In-depth security post-mortems, CTF write-ups, shell portability benchmarks, and bare-metal homelab architecture guides.
</p>
<div class="blog-sublinks">
<a href="blog-lander-cards.html" class="blog-sublink-item">
<span>1. Reverse Engineering IoT Firmware (SPI Flash)</span>
<span style="color: var(--accent-cyan);">[8 min] &rarr;</span>
</a>
<a href="blog-lander-cards.html" class="blog-sublink-item">
<span>2. High-Availability Traefik with Docker Compose</span>
<span style="color: var(--accent-cyan);">[12 min] &rarr;</span>
</a>
<a href="blog-lander-cards.html" class="blog-sublink-item">
<span>3. POSIX sh vs. Bash: Portability in Modern Scripts</span>
<span style="color: var(--accent-cyan);">[6 min] &rarr;</span>
</a>
<a href="blog-lander-cards.html" class="blog-sublink-item">
<span>4. CTF Write-up: Heap Exploitation in glibc 2.35</span>
<span style="color: var(--accent-cyan);">[15 min] &rarr;</span>
</a>
</div>
</div>
</section>
</div>
<!-- Right Column: Homelab Hosted Services & Node Telemetry -->
<div style="display: flex; flex-direction: column; gap: 16px;">
<!-- Hosted Services Tile -->
<section class="tile" aria-label="Homelab Services Directory">
<div class="tile-header">
<div class="tile-title">
<span>// HOMELAB SERVICES ROUTER</span>
</div>
<span class="tile-meta">4 PUBLIC TOOLS</span>
</div>
<div class="services-mini-grid">
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="service-tile-item">
<div>
<div class="service-item-title">Forgejo Git</div>
<div class="service-item-tag">git.rooks.dev:2222</div>
</div>
<p class="service-item-desc">Self-hosted code repositories &amp; dotfiles.</p>
</a>
<a href="https://chef.rooks.dev" target="_blank" rel="noopener" class="service-tile-item">
<div>
<div class="service-item-title">CyberChef</div>
<div class="service-item-tag">chef.rooks.dev:443</div>
</div>
<p class="service-item-desc">Hex decoding, cryptography &amp; forensics.</p>
</a>
<a href="https://pdf.rooks.dev" target="_blank" rel="noopener" class="service-tile-item">
<div>
<div class="service-item-title">Stirling PDF</div>
<div class="service-item-tag">pdf.rooks.dev:443</div>
</div>
<p class="service-item-desc">Private document OCR and manipulation.</p>
</a>
<a href="https://status.rooks.dev" target="_blank" rel="noopener" class="service-tile-item">
<div>
<div class="service-item-title">Uptime Kuma</div>
<div class="service-item-tag">status.rooks.dev:3001</div>
</div>
<p class="service-item-desc">Live synthetic latency &amp; uptime telemetry.</p>
</a>
</div>
</section>
<!-- Bare-Metal Node Telemetry Tile -->
<section class="tile" aria-label="Physical Hardware Nodes">
<div class="tile-header">
<div class="tile-title">
<span>// HARDWARE NODES (RACK-01)</span>
</div>
<span class="tile-meta">10GbE SFP+</span>
</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div class="node-stat-row">
<div>
<span class="node-name">gw-node01.lan</span>
<span style="color: var(--text-muted); font-size: 0.72rem; margin-left: 6px;">OPNsense 24.7</span>
</div>
<span class="node-load">CPU: 18% | RAM: 32%</span>
</div>
<div class="node-stat-row">
<div>
<span class="node-name">pve-node02.lan</span>
<span style="color: var(--text-muted); font-size: 0.72rem; margin-left: 6px;">Proxmox VE (AMD 5900X)</span>
</div>
<span class="node-load">CPU: 42% | RAM: 68%</span>
</div>
<div class="node-stat-row">
<div>
<span class="node-name">nas-node03.lan</span>
<span style="color: var(--text-muted); font-size: 0.72rem; margin-left: 6px;">TrueNAS (48TB RaidZ2)</span>
</div>
<span class="node-load">CPU: 14% | RAM: 82%</span>
</div>
</div>
</section>
</div>
</div>
<!-- Global Footer Bar -->
<footer class="footer-tile">
<div>River Rooks &bull; University of Alabama College of Engineering</div>
<nav class="footer-links-cluster" aria-label="Global Footer Links">
<a href="blog-lander-cards.html" class="footer-btn">Blog Lander</a>
<span class="pipe">|</span>
<a href="feed.xml" class="footer-btn">RSS</a>
<span class="pipe">|</span>
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-btn">Forgejo</a>
<span class="pipe">|</span>
<a href="mailto:river@rooks.dev" class="footer-btn">river@rooks.dev</a>
<span class="pipe">|</span>
<a href="#" title="Fingerprint: 0x4A8E92F1C0B3D4E7" class="footer-btn">PGP</a>
<span class="pipe">|</span>
<a href="index.html" class="footer-btn">Catalog ↗</a>
</nav>
</footer>
</main>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
services:
web:
# Pull prebuilt engine image from your Gitea registry
image: git.riverrooks.dev/venus/river-rooks-blog:latest
# Local build fallback (optional when building image locally)
# build:
# context: .
# dockerfile: Dockerfile
# args:
# SITE_DOMAIN: "${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env}"
# SITE_URL: "${SITE_URL:-https://${SITE_DOMAIN}}"
# PORT: "${PORT:-8080}"
container_name: river_rooks_web
restart: unless-stopped
# Healthcheck is natively baked into the Dockerfile image metadata
environment:
- "SITE_DOMAIN=${SITE_DOMAIN:?Error: SITE_DOMAIN is not set. Please define SITE_DOMAIN in .env}"
- "SITE_URL=${SITE_URL:-https://${SITE_DOMAIN}}"
- "PORT=${PORT:-8080}"
- "WATCH=${WATCH:-false}"
volumes:
# Mount external markdown posts directly into the container
- "./posts:/app/src/posts"
ports:
- "${HOST_PORT:-${PORT:-8080}}:${PORT:-8080}"
networks:
- backend
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`riverrooks.dev`) || Host(`www.riverrooks.dev`)"
- "traefik.http.routers.blog.entrypoints=websecure"
- "traefik.http.routers.blog.tls.certresolver=basic"
- "traefik.http.services.blog.loadbalancer.server.port=8080"
- "traefik.docker.network=backend"
networks:
backend:
external: true
-470
View File
@@ -1,470 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample 3: Companion Layout</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
/* Design Spec Token Definition */
:root {
--bg-canvas: #0d0f12; /* Deep charcoal slate */
--bg-surface: #121417; /* Slightly lighter dark surface */
--border-color: #2b303b; /* Low contrast static borders */
--text-primary: #e0e6f0; /* Off-white prose */
--text-muted: #8a95a5; /* Timestamp/status information */
--accent: #00f0ff; /* High-contrast active Cyan */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Sticky Navigation Header at the top */
.sticky-header {
position: sticky;
top: 0;
height: 56px;
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
z-index: 100;
font-family: 'IBM Plex Mono', monospace;
}
.logo-container {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-primary);
}
.logo-prompt {
color: var(--accent);
}
.header-nav {
display: flex;
gap: 16px;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.85rem;
padding: 4px 10px;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
transition: none; /* No spring/delayed transitions */
}
/* Snappy Hover Inversion */
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* Structural Indicator for Active Navigation Items */
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 4px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Split Container Layout */
.split-container {
display: flex;
flex: 1;
height: calc(100vh - 56px);
overflow: hidden;
}
/* Left Pane - Scrollable Content Feed */
.left-pane {
width: 50%;
overflow-y: auto;
background-color: var(--bg-canvas);
padding: 40px;
border-right: 1px solid var(--border-color);
}
/* Right Pane - Fixed Centered ASCII Widget */
.right-pane {
width: 50%;
background-color: var(--bg-canvas);
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
overflow: hidden;
}
.scroll-content {
width: 100%;
max-width: 65ch; /* Cap article readability width */
margin: 0 auto;
}
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 1.3rem;
color: var(--text-primary);
margin-bottom: 24px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
/* Cards Feed Component */
.card-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.project-card {
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 20px;
background-color: var(--bg-canvas); /* inherits canvas background */
cursor: pointer;
outline: none;
transition: none; /* Interaction suppression */
}
/* Instant Typography Snap on Hover/Focus */
.project-card:hover .card-title,
.project-card:focus .card-title {
color: var(--accent);
}
.project-card:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.card-title {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 1rem;
color: var(--text-primary);
margin-bottom: 8px;
transition: none;
}
.card-description {
font-size: 0.9rem;
color: var(--text-muted);
line-height: 1.5;
}
/* Companion Terminal Widget */
.companion-terminal {
width: 100%;
max-width: 420px;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
font-family: 'IBM Plex Mono', monospace;
overflow: hidden;
}
.terminal-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--text-muted);
}
.tab-title {
color: var(--text-primary);
font-weight: 500;
}
.tab-status {
font-weight: 700;
color: var(--accent);
}
.terminal-body {
padding: 20px;
}
.mascot-pre {
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
line-height: 1.35;
color: var(--text-primary);
background-color: var(--bg-canvas);
padding: 16px;
border: 1px solid var(--border-color);
border-radius: 2px;
overflow: hidden;
white-space: pre;
margin-bottom: 16px;
}
/* Terminal Blinking Block Cursor */
.cursor-block {
display: inline-block;
width: 8px;
height: 15px;
background-color: var(--text-primary);
animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
to {
visibility: hidden;
}
}
.status-log-panel {
border-top: 1px solid var(--border-color);
padding-top: 12px;
font-size: 0.75rem;
}
.log-output {
color: var(--text-muted);
word-break: break-all;
}
/* Footer styling */
.pane-footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
/* Mobile Layout (Width <= 768px) */
@media (max-width: 768px) {
body {
overflow-y: auto;
height: auto;
}
.split-container {
flex-direction: column;
height: auto;
overflow: visible;
}
.right-pane {
width: 100%;
height: 300px;
padding: 20px;
order: 1; /* Companion moves to top of stack */
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 56px;
z-index: 90;
}
.left-pane {
width: 100%;
height: auto;
overflow-y: visible;
padding: 24px;
order: 2; /* Content feed follows below */
border-right: none;
}
.companion-terminal {
max-width: 100%;
height: 100%;
}
.terminal-body {
padding: 12px;
display: flex;
flex-direction: column;
height: calc(100% - 33px);
}
.mascot-pre {
padding: 8px;
font-size: 0.75rem;
line-height: 1.25;
flex: 1;
margin-bottom: 8px;
}
.status-log-panel {
padding-top: 8px;
}
}
</style>
</head>
<body>
<!-- Sticky Top Header -->
<header class="sticky-header">
<div class="logo-container">
<span class="logo-prompt">$</span> agy --layout=companion
</div>
<nav class="header-nav">
<a href="index.html" class="nav-link">Catalog</a>
<a href="homepage.html" class="nav-link">Sample 1</a>
<a href="split-panel.html" class="nav-link">Sample 2</a>
<a href="companion.html" class="nav-link active">Sample 3</a>
<a href="document-flow.html" class="nav-link">Sample 4</a>
<a href="portfolio.html" class="nav-link">Sample 5</a>
</nav>
</header>
<!-- Split View Container -->
<div class="split-container">
<!-- Left Pane: Scrollable Project Cards -->
<main class="left-pane" aria-label="Project Feed">
<div class="scroll-content">
<h2 class="section-title">System Modules</h2>
<div class="card-list">
<!-- Card 1 -->
<div class="project-card" tabindex="0" data-target="/dev/posix-shell-parser">
<h3 class="card-title">Custom POSIX Shell Parser</h3>
<p class="card-description">
A lightweight Lexer and AST parser written in Lua for parsing POSIX-compliant shell scripts. Highly optimized for syntax coloring inside modal environments.
</p>
</div>
<!-- Card 2 -->
<div class="project-card" tabindex="0" data-target="/dev/wireguard-mesh">
<h3 class="card-title">WireGuard Mesh Topology Builder</h3>
<p class="card-description">
Automated configuration generator for setting up full-mesh WireGuard private overlays across cloud instances and local homelab nodes.
</p>
</div>
<!-- Card 3 -->
<div class="project-card" tabindex="0" data-target="/dev/zero-alloc-logger">
<h3 class="card-title">Zero-Allocation Logger (Go)</h3>
<p class="card-description">
A high-performance JSON structure logger that uses sync.Pool buffers to avoid memory allocation overhead during intensive disk operations.
</p>
</div>
<!-- Card 4 -->
<div class="project-card" tabindex="0" data-target="/dev/kernel-bootstrap">
<h3 class="card-title">Embedded Kernel Boot Strap</h3>
<p class="card-description">
Minimalist boot sector and initial kernel bootstrap for ARM Cortex-M microcontrollers to handle memory map initialization and early UART status printouts.
</p>
</div>
<!-- Card 5 -->
<div class="project-card" tabindex="0" data-target="/dev/ddns-daemon">
<h3 class="card-title">Traefik Cloudflare DDNS Daemon</h3>
<p class="card-description">
Background controller daemon designed to sync Docker container network labels with Cloudflare DNS records to route traffic to local homelab subdomains.
</p>
</div>
</div>
<footer class="pane-footer">
<span>Companion Spec Sample</span>
<span>© 2026 Developer System</span>
</footer>
</div>
</main>
<!-- Right Pane: Centered ASCII Companion -->
<aside class="right-pane" aria-label="System Companion">
<div class="companion-terminal">
<div class="terminal-header">
<span class="tab-title">sys-daemon</span>
<span class="tab-status">ACTIVE</span>
</div>
<div class="terminal-body">
<pre id="mascot-display" class="mascot-pre"></pre>
<div class="status-log-panel">
<div id="log-output" class="log-output"></div>
</div>
</div>
</div>
</aside>
</div>
<!-- Companion State Script -->
<script>
const IDLE_ART = ` /\\___/\\
( o o ) sys-daemon
( =^= ) STATUS: IDLE
\\__*__/ LOG: awaiting input<span class="cursor-block"></span>`;
const READING_ART = ` /\\___/\\
( - - ) sys-daemon
( =o= ) STATUS: READING
\\__*__/ LOG: reading module_`;
const mascot = document.getElementById('mascot-display');
const logOutput = document.getElementById('log-output');
const cards = document.querySelectorAll('.project-card');
function setIdle() {
mascot.innerHTML = IDLE_ART;
logOutput.innerText = `> sys-daemon initialized`;
}
function setReading(cardTarget) {
mascot.innerHTML = READING_ART;
logOutput.innerText = `[sys-daemon] target: ${cardTarget}`;
}
// Set Initial State
setIdle();
// Event hooks for Instant State swaps
cards.forEach(card => {
const target = card.getAttribute('data-target');
// Mouse triggers
card.addEventListener('mouseenter', () => setReading(target));
card.addEventListener('mouseleave', setIdle);
// Keyboard accessibility focus/blur triggers
card.addEventListener('focus', () => setReading(target));
card.addEventListener('blur', setIdle);
});
</script>
</body>
</html>
-604
View File
@@ -1,604 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample 4: Document Flow Layout</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
/* Design Tokens */
:root {
--bg-canvas: #0d0f12; /* Deep charcoal slate */
--bg-surface: #121417; /* Slightly lighter dark surface */
--border-color: #2b303b; /* Low-contrast static 1px borders */
--text-primary: #e0e6f0; /* Off-white reading text */
--text-muted: #8a95a5; /* Monospaced metadata / timestamps */
--accent: #00f0ff; /* High-contrast Cyan indicator */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.65;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Sticky Navigation Header */
.sticky-header {
position: sticky;
top: 0;
height: 56px;
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
z-index: 100;
font-family: 'IBM Plex Mono', monospace;
}
.logo-container {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-primary);
}
.logo-prompt {
color: var(--accent);
}
.header-nav {
display: flex;
gap: 16px;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.85rem;
padding: 4px 10px;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
transition: none;
}
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 4px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Main Split Layout */
.split-container {
display: flex;
flex: 1;
height: calc(100vh - 56px);
overflow: hidden;
}
/* Left Pane - Fixed Terminal Scroll-Tracker Daemon */
.left-pane {
width: 380px;
background-color: var(--bg-canvas);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px;
flex-shrink: 0;
}
.companion-terminal {
width: 100%;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
font-family: 'IBM Plex Mono', monospace;
overflow: hidden;
}
.terminal-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--text-muted);
}
.tab-title {
color: var(--text-primary);
font-weight: 500;
}
.tab-status {
font-weight: 700;
color: var(--accent);
}
.terminal-body {
padding: 20px;
}
.mascot-pre {
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
line-height: 1.35;
color: var(--text-primary);
background-color: var(--bg-canvas);
padding: 16px;
border: 1px solid var(--border-color);
border-radius: 2px;
overflow: hidden;
white-space: pre;
margin-bottom: 16px;
}
/* Blinking Block Cursor */
.cursor-block {
display: inline-block;
width: 8px;
height: 15px;
background-color: var(--text-primary);
animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
to {
visibility: hidden;
}
}
.status-log-panel {
border-top: 1px solid var(--border-color);
padding-top: 12px;
font-size: 0.75rem;
}
.log-output {
color: var(--text-muted);
word-break: break-all;
}
/* Right Pane - Continuous Document Stream */
.right-pane {
flex: 1;
overflow-y: auto;
background-color: var(--bg-canvas);
padding: 48px 32px;
display: flex;
justify-content: center;
scroll-behavior: smooth;
}
.document-stream {
width: 100%;
max-width: 65ch; /* Optimal prose line length */
}
.document-header {
margin-bottom: 40px;
padding-bottom: 24px;
border-bottom: 1px solid var(--border-color);
}
.doc-title {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 1.8rem;
color: var(--text-primary);
margin-bottom: 12px;
}
.doc-subtitle {
font-size: 1rem;
color: var(--text-muted);
line-height: 1.5;
}
/* Continuous Section Flow (No 4-sided Bounding Boxes) */
.doc-section {
padding: 32px 0;
border-bottom: 1px solid var(--border-color);
scroll-margin-top: 20px;
}
.doc-section:last-of-type {
border-bottom: none;
}
.section-header-wrap {
display: flex;
align-items: baseline;
gap: 10px;
margin-bottom: 8px;
}
/* Structural Indicator for Active Scroll Section */
.active-indicator {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
color: var(--accent);
opacity: 0;
transition: none;
}
.doc-section.is-active .active-indicator {
opacity: 1;
}
.section-heading {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 1.25rem;
color: var(--text-primary);
}
.doc-section.is-active .section-heading {
color: var(--accent);
}
.section-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 16px;
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.meta-tag {
color: var(--text-muted);
}
.doc-section p {
color: var(--text-primary);
font-size: 0.95rem;
margin-bottom: 16px;
}
.doc-section p:last-child {
margin-bottom: 0;
}
/* Terminal Code Block Component */
.code-block {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
margin: 20px 0;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
}
.code-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 6px 14px;
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-muted);
}
.code-content {
padding: 14px;
overflow-x: auto;
font-size: 0.82rem;
line-height: 1.5;
}
.code-content pre {
margin: 0;
}
.pane-footer {
margin-top: 48px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
/* Responsive Mobile Layout (Width <= 768px) */
@media (max-width: 768px) {
body {
overflow-y: auto;
height: auto;
}
.split-container {
flex-direction: column;
height: auto;
overflow: visible;
}
.left-pane {
width: 100%;
height: 280px;
padding: 16px;
border-right: none;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 56px;
z-index: 90;
}
.right-pane {
padding: 24px 16px;
overflow-y: visible;
}
}
</style>
</head>
<body>
<!-- Sticky Navigation Header -->
<header class="sticky-header">
<div class="logo-container">
<span class="logo-prompt">$</span> agy --layout=document-flow
</div>
<nav class="header-nav">
<a href="index.html" class="nav-link">Catalog</a>
<a href="homepage.html" class="nav-link">Sample 1</a>
<a href="split-panel.html" class="nav-link">Sample 2</a>
<a href="companion.html" class="nav-link">Sample 3</a>
<a href="document-flow.html" class="nav-link active">Sample 4</a>
<a href="portfolio.html" class="nav-link">Sample 5</a>
</nav>
</header>
<!-- Split View Container -->
<div class="split-container">
<!-- Left Pane: Fixed Terminal Scroll-Tracker Daemon -->
<aside class="left-pane" aria-label="Scroll Tracker Daemon">
<div class="companion-terminal">
<div class="terminal-header">
<span class="tab-title">sys-daemon (scroll-tracker)</span>
<span class="tab-status">ONLINE</span>
</div>
<div class="terminal-body">
<pre id="mascot-display" class="mascot-pre"></pre>
<div class="status-log-panel">
<div id="log-output" class="log-output">> continuous document flow initialized</div>
</div>
</div>
</div>
</aside>
<!-- Right Pane: Continuous Document Reading Stream -->
<main id="scroll-target" class="right-pane" aria-label="Technical Documentation">
<div class="document-stream">
<!-- Document Header -->
<header class="document-header">
<h1 class="doc-title">Engineering Architecture & Portfolio</h1>
<p class="doc-subtitle">
A high-density technical reading flow. Pinned left pane contains an automated terminal tracker daemon observing scroll depth across sections.
</p>
</header>
<!-- Section 1 -->
<section id="section-shell-parser" class="doc-section" data-target="/dev/posix-shell-parser">
<div class="section-header-wrap">
<span class="active-indicator">></span>
<h2 class="section-heading">Custom POSIX Shell Parser</h2>
</div>
<div class="section-meta">
<span>Version: v1.2.0</span>
<span>•</span>
<span>Module: /dev/posix-shell-parser</span>
<span>•</span>
<span class="meta-tag">[#lua]</span>
<span class="meta-tag">[#ast-parser]</span>
</div>
<p>
This section details a lightweight Lexer and AST parser written in Lua for parsing POSIX-compliant shell scripts. The architecture avoids runtime allocations during tokenization, enabling real-time syntax coloring inside terminal buffers.
</p>
<p>
The execution pipeline operates in two distinct phases: string scanning into token stream vectors, followed by recursive descent operator precedence tree construction.
</p>
<div class="code-block">
<div class="code-header">
<span>lexer.lua</span>
<span>Lua</span>
</div>
<div class="code-content">
<pre><code>local function tokenize(src)
local tokens = {}
for pos, char in ipairs(src) do
if char == ";" or char == "|" then
table.insert(tokens, { type = "OPERATOR", value = char })
end
end
return tokens
end</code></pre>
</div>
</div>
</section>
<!-- Section 2 -->
<section id="section-wireguard-mesh" class="doc-section" data-target="/dev/wireguard-mesh">
<div class="section-header-wrap">
<span class="active-indicator">></span>
<h2 class="section-heading">WireGuard Mesh Topology Builder</h2>
</div>
<div class="section-meta">
<span>Version: v0.8.4</span>
<span>•</span>
<span>Module: /dev/wireguard-mesh</span>
<span>•</span>
<span class="meta-tag">[#networking]</span>
<span class="meta-tag">[#wireguard]</span>
</div>
<p>
Automated configuration generator designed to provision full-mesh WireGuard overlay networks across distributed cloud nodes and homelab infrastructure.
</p>
<p>
Instead of manually calculating public/private keypairs and Endpoint IP pairings per host node, this utility computes matrix topologies and generates atomic configuration files.
</p>
</section>
<!-- Section 3 -->
<section id="section-zero-alloc-logger" class="doc-section" data-target="/dev/zero-alloc-logger">
<div class="section-header-wrap">
<span class="active-indicator">></span>
<h2 class="section-heading">Zero-Allocation Logger (Go)</h2>
</div>
<div class="section-meta">
<span>Version: v2.1.1</span>
<span>•</span>
<span>Module: /dev/zero-alloc-logger</span>
<span>•</span>
<span class="meta-tag">[#go]</span>
<span class="meta-tag">[#performance]</span>
</div>
<p>
A high-throughput structured JSON logger implemented in Go that leverages <code>sync.Pool</code> byte buffers to completely eliminate heap allocations during intensive I/O operations.
</p>
<p>
Benchmark comparisons demonstrate zero GC overhead when processing over 500,000 log events per second across multi-threaded worker routines.
</p>
</section>
<!-- Section 4 -->
<section id="section-kernel-bootstrap" class="doc-section" data-target="/dev/kernel-bootstrap">
<div class="section-header-wrap">
<span class="active-indicator">></span>
<h2 class="section-heading">Embedded Kernel Boot Strap</h2>
</div>
<div class="section-meta">
<span>Version: v0.1.0</span>
<span>•</span>
<span>Module: /dev/kernel-bootstrap</span>
<span>•</span>
<span class="meta-tag">[#arm-cortex]</span>
<span class="meta-tag">[#assembly]</span>
</div>
<p>
Bare-metal boot sector and initial C runtime setup for ARM Cortex-M microcontrollers. Handles vector table offset initialization, memory region copy routines, and low-level UART debugging initialization.
</p>
</section>
<!-- Section 5 -->
<section id="section-ddns-daemon" class="doc-section" data-target="/dev/ddns-daemon">
<div class="section-header-wrap">
<span class="active-indicator">></span>
<h2 class="section-heading">Traefik Cloudflare DDNS Daemon</h2>
</div>
<div class="section-meta">
<span>Version: v1.0.2</span>
<span>•</span>
<span>Module: /dev/ddns-daemon</span>
<span>•</span>
<span class="meta-tag">[#docker]</span>
<span class="meta-tag">[#traefik]</span>
</div>
<p>
Background controller daemon designed to sync Docker container network labels with Cloudflare DNS records to route traffic to local homelab subdomains dynamically.
</p>
</section>
<footer class="pane-footer">
<span>Sample 4: Document Flow Layout</span>
<span>© 2026 Developer System</span>
</footer>
</div>
</main>
</div>
<!-- Scroll-Tracker IntersectionObserver Script -->
<script>
const IDLE_ART = ` /\\___/\\
( o o ) sys-daemon
( =^= ) STATUS: IDLE
\\__*__/ LOG: awaiting input<span class="cursor-block"></span>`;
const READING_ART = ` /\\___/\\
( - - ) sys-daemon
( =o= ) STATUS: TRACKING
\\__*__/ LOG: reading module_`;
const mascot = document.getElementById('mascot-display');
const logOutput = document.getElementById('log-output');
const sections = document.querySelectorAll('.doc-section');
const rightPane = document.getElementById('scroll-target');
function setIdle() {
mascot.innerHTML = IDLE_ART;
logOutput.innerText = `> sys-daemon (idle scroll-tracker)`;
}
function setTracking(targetPath) {
mascot.innerHTML = READING_ART;
logOutput.innerText = `[sys-daemon] active_section: ${targetPath}`;
}
setIdle();
// IntersectionObserver to observe document sections in scroll view
const observerOptions = {
root: rightPane,
rootMargin: '-20% 0px -60% 0px', // Active threshold zone
threshold: 0
};
let activeSection = null;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (activeSection) {
activeSection.classList.remove('is-active');
}
activeSection = entry.target;
activeSection.classList.add('is-active');
const target = activeSection.getAttribute('data-target');
setTracking(target);
}
});
}, observerOptions);
sections.forEach(section => observer.observe(section));
</script>
</body>
</html>
-869
View File
@@ -1,869 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Homelab Lander & Infrastructure Catalog</title>
<meta name="description" content="Public landing page and operational directory for River Rooks self-hosted homelab infrastructure, services, hardware nodes, and network topology.">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg-canvas: #0d0f12; /* Deep slate/charcoal */
--bg-surface: #121417; /* Solid surface container */
--bg-surface-alt: #181b21; /* Slightly lighter surface card */
--border-color: #2b303b; /* Low-contrast 1px border */
--border-active: #00f0ff; /* Active Cyan border */
--text-primary: #e0e6f0; /* Off-white prose */
--text-muted: #8a95a5; /* Monospaced metadata gray */
--accent: #00f0ff; /* High-visibility Cyan terminal accent */
--accent-green: #00ff66; /* ANSI Green for online status */
--accent-amber: #ffb000; /* Amber for alerts / tags */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.65;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Sticky Navigation Header */
.sticky-header {
position: sticky;
top: 0;
height: 56px;
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
z-index: 100;
font-family: 'IBM Plex Mono', monospace;
}
.logo-container {
font-weight: 700;
font-size: 0.92rem;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.logo-prompt {
color: var(--accent);
}
.header-nav {
display: flex;
gap: 10px;
overflow-x: auto;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.82rem;
padding: 4px 8px;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
transition: none; /* Instant snap */
white-space: nowrap;
}
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 4px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Outer Wrapper */
.page-wrapper {
flex: 1;
display: flex;
justify-content: center;
padding: 40px 24px 60px 24px;
}
.content-container {
width: 100%;
max-width: 1000px;
}
/* Identity Header */
.identity-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 24px;
}
.header-titles {
flex: 1;
min-width: 280px;
}
.site-h1 {
font-family: 'IBM Plex Mono', monospace;
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 6px;
letter-spacing: -0.02em;
}
.status-line {
display: flex;
align-items: center;
gap: 10px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
flex-shrink: 0;
}
.system-badge-box {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
padding: 10px 14px;
border-radius: 4px;
color: var(--text-muted);
line-height: 1.5;
}
.system-badge-box strong {
color: var(--text-primary);
}
/* Dividers */
.section-divider {
border: 0;
border-top: 1px solid var(--border-color);
margin: 32px 0;
}
/* Terminal Status Block */
.terminal-block {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
margin-bottom: 36px;
}
.terminal-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
display: flex;
justify-content: space-between;
font-size: 0.78rem;
color: var(--text-muted);
}
.terminal-title {
color: var(--text-primary);
font-weight: 500;
}
.terminal-body {
padding: 16px;
font-size: 0.84rem;
line-height: 1.55;
}
.cmd-line {
color: var(--text-muted);
margin-bottom: 10px;
}
.prompt {
color: var(--accent);
font-weight: 700;
}
.cmd-output {
color: var(--text-primary);
white-space: pre-wrap;
}
.highlight-green {
color: var(--accent-green);
}
.highlight-cyan {
color: var(--accent);
}
.highlight-amber {
color: var(--accent-amber);
}
.cursor-block {
display: inline-block;
width: 8px;
height: 15px;
background-color: var(--text-primary);
margin-left: 4px;
vertical-align: middle;
animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
to { visibility: hidden; }
}
/* Section Headings */
.section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 8px;
}
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
}
.section-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
}
/* Interactive Filter Controls */
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
font-family: 'IBM Plex Mono', monospace;
}
.filter-btn {
background-color: var(--bg-surface);
color: var(--text-muted);
border: 1px solid var(--border-color);
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
font-family: inherit;
border-radius: 4px;
transition: none; /* Instant snap */
}
.filter-btn:hover {
color: var(--text-primary);
border-color: var(--accent);
}
.filter-btn.active {
background-color: var(--accent);
color: var(--bg-canvas);
border-color: var(--accent);
font-weight: 700;
}
.filter-btn:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* Services Card Grid */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
margin-bottom: 36px;
}
.service-card {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 18px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: none;
}
.service-card:hover {
border-color: var(--border-active);
}
.service-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.service-name {
font-family: 'IBM Plex Mono', monospace;
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
}
.service-card:hover .service-name {
color: var(--accent);
}
.service-tag {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
}
.service-desc {
font-size: 0.88rem;
color: var(--text-muted);
margin-bottom: 16px;
line-height: 1.5;
}
.service-footer {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--border-color);
}
.service-port {
color: var(--text-muted);
}
.service-status {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
}
.status-text-online {
color: var(--accent-green);
}
.status-text-active {
color: var(--accent);
}
/* Hardware Nodes & Metrics Table */
.nodes-container {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
overflow-x: auto;
margin-bottom: 36px;
}
.nodes-table {
width: 100%;
border-collapse: collapse;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.83rem;
text-align: left;
}
.nodes-table th {
background-color: var(--bg-canvas);
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
padding: 10px 14px;
font-weight: 700;
}
.nodes-table td {
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
color: var(--text-primary);
vertical-align: middle;
}
.nodes-table tr:last-child td {
border-bottom: none;
}
.nodes-table tr:hover td {
background-color: var(--bg-surface-alt);
}
.node-hostname {
font-weight: 700;
color: var(--accent);
}
.metric-bar {
color: var(--accent-green);
}
/* Network Topology ASCII Block */
.topology-block {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
font-family: 'JetBrains Mono', monospace;
padding: 18px;
font-size: 0.8rem;
line-height: 1.4;
overflow-x: auto;
color: var(--text-muted);
margin-bottom: 36px;
}
.topology-title {
color: var(--text-primary);
font-weight: 700;
margin-bottom: 12px;
}
/* Piped Footer Links */
.footer-links {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.footer-link {
color: var(--text-primary);
text-decoration: none;
padding: 2px 6px;
transition: none; /* Zero-delay snap */
}
.footer-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.footer-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.pipe {
color: var(--border-color);
}
</style>
</head>
<body>
<!-- Sticky Header -->
<header class="sticky-header">
<div class="logo-container">
<span class="logo-prompt">$</span> agy --page=homelab
</div>
<nav class="header-nav" aria-label="Main Navigation">
<a href="index.html" class="nav-link">Catalog</a>
<a href="homepage.html" class="nav-link">Sample 1</a>
<a href="split-panel.html" class="nav-link">Sample 2</a>
<a href="companion.html" class="nav-link">Sample 3</a>
<a href="document-flow.html" class="nav-link">Sample 4</a>
<a href="portfolio.html" class="nav-link">Sample 5</a>
<a href="homelab.html" class="nav-link active">Sample 6 (Homelab)</a>
</nav>
</header>
<!-- Page Content Container -->
<div class="page-wrapper">
<main class="content-container">
<!-- Identity & Status Banner -->
<header class="identity-header">
<div class="header-titles">
<h1 class="site-h1">Homelab Lander &amp; Infrastructure</h1>
<div class="status-line">
<span class="status-dot"></span>
<span>CLUSTER ONLINE | 10GbE BACKBONE | TRAEFIK V3 EDGE PROXY</span>
</div>
</div>
<div class="system-badge-box">
<div><strong>SYSTEM UPTIME:</strong> 99.98% (42d 14h)</div>
<div><strong>LOCATION:</strong> ALABAMA DCs / RACK-01</div>
</div>
</header>
<hr class="section-divider">
<!-- Under Construction / System Telemetry Terminal Block -->
<section aria-label="System Diagnostics Terminal">
<div class="terminal-block">
<div class="terminal-header">
<span class="terminal-title">/var/log/homelab-status.log</span>
<span>STDOUT [TTY1]</span>
</div>
<div class="terminal-body">
<div class="cmd-line"><span class="prompt">$</span> homelab-ctl status --output=summary</div>
<div class="cmd-output">[<span class="highlight-green">OK</span>] Edge Router: OPNsense 24.7 (10GbE SFP+ WAN/LAN)
[<span class="highlight-green">OK</span>] Reverse Proxy: Traefik v3.1 (Auto Let's Encrypt TLS Wildcard)
[<span class="highlight-green">OK</span>] Virtualization: Proxmox VE 8.2 Cluster (3 Bare-Metal Nodes)
[<span class="highlight-green">OK</span>] ZFS Array: TrueNAS SCALE (48TB RaidZ2 Pool, 0 Degradation)
[<span class="highlight-cyan">INFO</span>] Active Wireguard Mesh Peers: 14 Connected
[<span class="highlight-amber">WARN</span>] Sonarr Queue: 2 downloads pending verification<span class="cursor-block"></span></div>
</div>
</div>
</section>
<!-- Homelab Services Directory -->
<section aria-label="Services Directory">
<div class="section-header">
<h2 class="section-title">services_directory</h2>
<span class="section-meta">12 Active Micro-services</span>
</div>
<!-- Filter Bar -->
<div class="filter-bar" id="filterBar" role="tablist" aria-label="Filter Services">
<button class="filter-btn active" data-filter="all" role="tab" aria-selected="true">All Services</button>
<button class="filter-btn" data-filter="core" role="tab">Core &amp; Network</button>
<button class="filter-btn" data-filter="compute" role="tab">Compute &amp; Storage</button>
<button class="filter-btn" data-filter="media" role="tab">Media &amp; Automation</button>
<button class="filter-btn" data-filter="monitoring" role="tab">Monitoring &amp; Sec</button>
</div>
<!-- Services Cards Grid -->
<div class="services-grid" id="servicesGrid">
<!-- Traefik -->
<article class="service-card" data-category="core">
<div class="service-card-header">
<h3 class="service-name">Traefik Proxy</h3>
<span class="service-tag">[#core]</span>
</div>
<p class="service-desc">Edge HTTP reverse proxy and load balancer with dynamic Docker provider discovery and ACME TLS certificate management.</p>
<div class="service-footer">
<span class="service-port">443/tcp (TLS)</span>
<span class="service-status status-text-online"><span class="status-dot"></span> ONLINE</span>
</div>
</article>
<!-- OPNsense -->
<article class="service-card" data-category="core">
<div class="service-card-header">
<h3 class="service-name">OPNsense Firewall</h3>
<span class="service-tag">[#core]</span>
</div>
<p class="service-desc">Hardened FreeBSD firewall routing 10GbE traffic with VLAN isolation, Suricata IDS/IPS, and Unbound DNS.</p>
<div class="service-footer">
<span class="service-port">VLAN 10 / Mgmt</span>
<span class="service-status status-text-online"><span class="status-dot"></span> ACTIVE</span>
</div>
</article>
<!-- Proxmox VE -->
<article class="service-card" data-category="compute">
<div class="service-card-header">
<h3 class="service-name">Proxmox VE</h3>
<span class="service-tag">[#compute]</span>
</div>
<p class="service-desc">Enterprise hypervisor cluster hosting Debian LXC containers and KVM virtual machines with Ceph &amp; ZFS backend.</p>
<div class="service-footer">
<span class="service-port">8006/tcp</span>
<span class="service-status status-text-online"><span class="status-dot"></span> ONLINE</span>
</div>
</article>
<!-- TrueNAS SCALE -->
<article class="service-card" data-category="compute">
<div class="service-card-header">
<h3 class="service-name">TrueNAS SCALE</h3>
<span class="service-tag">[#storage]</span>
</div>
<p class="service-desc">Primary network attached storage managing 48TB ZFS RaidZ2 array, NFS shares, SMB datasets, and daily scrub jobs.</p>
<div class="service-footer">
<span class="service-port">NFS / SMB</span>
<span class="service-status status-text-online"><span class="status-dot"></span> ONLINE</span>
</div>
</article>
<!-- AdGuard Home -->
<article class="service-card" data-category="core">
<div class="service-card-header">
<h3 class="service-name">AdGuard Home</h3>
<span class="service-tag">[#dns]</span>
</div>
<p class="service-desc">Network-wide DNS sinkhole filtering trackers and ad domains across all VLANs with DNS-over-HTTPS upstream resolvers.</p>
<div class="service-footer">
<span class="service-port">53/udp, 853/tcp</span>
<span class="service-status status-text-active"><span class="status-dot"></span> PROTECTED</span>
</div>
</article>
<!-- WireGuard Mesh -->
<article class="service-card" data-category="core">
<div class="service-card-header">
<h3 class="service-name">WireGuard Mesh</h3>
<span class="service-tag">[#vpn]</span>
</div>
<p class="service-desc">High-performance encrypted point-to-point VPN tunnel connecting remote laptops and mobile endpoints to homelab subnet.</p>
<div class="service-footer">
<span class="service-port">51820/udp</span>
<span class="service-status status-text-online"><span class="status-dot"></span> 14 PEERS</span>
</div>
</article>
<!-- Jellyfin Media -->
<article class="service-card" data-category="media">
<div class="service-card-header">
<h3 class="service-name">Jellyfin Media</h3>
<span class="service-tag">[#media]</span>
</div>
<p class="service-desc">FOSS media streaming engine leveraging Intel QuickSync GPU hardware acceleration for 4K HDR transcoding.</p>
<div class="service-footer">
<span class="service-port">8096/tcp</span>
<span class="service-status status-text-online"><span class="status-dot"></span> READY</span>
</div>
</article>
<!-- Arr Automation Suite -->
<article class="service-card" data-category="media">
<div class="service-card-header">
<h3 class="service-name">Arr Stack (Sonarr/Radarr)</h3>
<span class="service-tag">[#automation]</span>
</div>
<p class="service-desc">Automated content indexing, Prowlarr torrent/Usenet management, and SABnzbd queue downloader pipeline.</p>
<div class="service-footer">
<span class="service-port">8989 / 7878</span>
<span class="service-status status-text-online"><span class="status-dot"></span> RUNNING</span>
</div>
</article>
<!-- Prometheus + Grafana -->
<article class="service-card" data-category="monitoring">
<div class="service-card-header">
<h3 class="service-name">Grafana &amp; Prometheus</h3>
<span class="service-tag">[#telemetry]</span>
</div>
<p class="service-desc">Time-series metric collection with Node Exporter, Telegraf agents, and live cluster performance dashboards.</p>
<div class="service-footer">
<span class="service-port">3000 / 9090</span>
<span class="service-status status-text-active"><span class="status-dot"></span> SCRAPING</span>
</div>
</article>
<!-- Uptime Kuma -->
<article class="service-card" data-category="monitoring">
<div class="service-card-header">
<h3 class="service-name">Uptime Kuma</h3>
<span class="service-tag">[#status]</span>
</div>
<p class="service-desc">Synthetic HTTP, Ping, and TCP health monitor broadcasting real-time service status alerts to Discord webhooks.</p>
<div class="service-footer">
<span class="service-port">3001/tcp</span>
<span class="service-status status-text-online"><span class="status-dot"></span> 100% HEALTH</span>
</div>
</article>
<!-- Vaultwarden -->
<article class="service-card" data-category="monitoring">
<div class="service-card-header">
<h3 class="service-name">Vaultwarden</h3>
<span class="service-tag">[#security]</span>
</div>
<p class="service-desc">Lightweight Rust implementation of the Bitwarden API hosting encrypted credentials, SSH keys, and TOTP tokens.</p>
<div class="service-footer">
<span class="service-port">443/tcp (E2EE)</span>
<span class="service-status status-text-active"><span class="status-dot"></span> LOCKED</span>
</div>
</article>
<!-- Forgejo Git -->
<article class="service-card" data-category="compute">
<div class="service-card-header">
<h3 class="service-name">Forgejo Git Server</h3>
<span class="service-tag">[#git]</span>
</div>
<p class="service-desc">Self-hosted Git forge hosting homelab infrastructure-as-code repos, Ansible playbooks, and Docker compose files.</p>
<div class="service-footer">
<span class="service-port">3000/tcp, 2222/ssh</span>
<span class="service-status status-text-online"><span class="status-dot"></span> ONLINE</span>
</div>
</article>
</div>
</section>
<hr class="section-divider">
<!-- Bare-Metal Hardware Nodes Grid -->
<section aria-label="Hardware Topology">
<div class="section-header">
<h2 class="section-title">hardware_nodes</h2>
<span class="section-meta">3 Physical Bare-Metal Servers</span>
</div>
<div class="nodes-container">
<table class="nodes-table">
<thead>
<tr>
<th>Hostname</th>
<th>Role &amp; OS</th>
<th>Hardware Spec</th>
<th>CPU Load</th>
<th>RAM Util</th>
</tr>
</thead>
<tbody>
<tr>
<td class="node-hostname">gw-node01.lan</td>
<td>OPNsense 24.7</td>
<td>Intel N100 (4C/4T) | 16GB DDR5 | 2.5GbE</td>
<td><span class="metric-bar">[██░░░░░░░░]</span> 18%</td>
<td><span class="metric-bar">[███░░░░░░░]</span> 32%</td>
</tr>
<tr>
<td class="node-hostname">pve-node02.lan</td>
<td>Proxmox VE 8.2</td>
<td>AMD Ryzen 9 5900X (12C/24T) | 128GB ECC | NVMe</td>
<td><span class="metric-bar">[████░░░░░░]</span> 42%</td>
<td><span class="metric-bar">[███████░░░]</span> 68%</td>
</tr>
<tr>
<td class="node-hostname">nas-node03.lan</td>
<td>TrueNAS SCALE</td>
<td>Intel Xeon E-2278G (8C/16T) | 64GB ECC | 6x16TB ZFS</td>
<td><span class="metric-bar">[██░░░░░░░░]</span> 24%</td>
<td><span class="metric-bar">[████████░░]</span> 81%</td>
</tr>
</tbody>
</table>
</div>
</section>
<!-- Network Topology Diagram -->
<section aria-label="Network Architecture">
<div class="section-header">
<h2 class="section-title">network_topology</h2>
<span class="section-meta">ASCII Architecture Map</span>
</div>
<div class="topology-block">
<div class="topology-title">// Network Edge &amp; Traffic Routing Flow</div>
[ WAN / INTERNET ]
│
▼
┌─────────────────────────┐
│ OPNsense Edge Gateway │ (10GbE SFP+ / Firewall / IDS)
└───────────┬─────────────┘
│
├───────────────► [ WireGuard Mesh VPN ] (51820/udp Remote Access)
│
▼
┌─────────────────────────┐
│ Traefik v3 Reverse Proxy│ (Auto TLS Certs / Authelia 2FA)
└───────────┬─────────────┘
│
┌─────────┴───────────────────────┬─────────────────────────┐
▼ ▼ ▼
┌─────────────────────────┐ ┌───────────────────────┐ ┌─────────────────────────┐
│ VLAN 10: Management │ │ VLAN 20: Services │ │ VLAN 30: Storage Pool │
│ • Proxmox VE (8006) │ │ • Grafana (3000) │ │ • TrueNAS ZFS (SMB/NFS) │
│ • AdGuard Home (53) │ │ • Jellyfin Media │ │ • MinIO S3 Bucket │
└─────────────────────────┘ └───────────────────────┘ └─────────────────────────┘
</div>
</section>
<hr class="section-divider">
<!-- Piped Contact & Catalog Footer -->
<footer class="footer-links">
<a href="index.html" class="footer-link">Spec Catalog</a>
<span class="pipe">|</span>
<a href="https://github.com" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<span class="pipe">|</span>
<a href="mailto:river@example.com" class="footer-link">Email</a>
<span class="pipe">|</span>
<a href="#" class="footer-link">PGP Key</a>
<span class="pipe">|</span>
<span style="font-size:0.78rem; color:var(--text-muted);">HOSTED ON SELF-HEALED K3S CLUSTER</span>
</footer>
</main>
</div>
<!-- Pure Vanilla JS Filter Logic -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const filterBtns = document.querySelectorAll('#filterBar .filter-btn');
const serviceCards = document.querySelectorAll('#servicesGrid .service-card');
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
// Remove active class from all buttons
filterBtns.forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
// Add active class to clicked button
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
const filter = btn.getAttribute('data-filter');
// Toggle service cards visibility based on category
serviceCards.forEach(card => {
const category = card.getAttribute('data-category');
if (filter === 'all' || category === filter) {
card.style.display = 'flex';
} else {
card.style.display = 'none';
}
});
});
});
});
</script>
</body>
</html>
-498
View File
@@ -1,498 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terminal/Minimalist Blog - Home</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
/* CSS Variables & Core Layout */
:root {
--bg-canvas: #121417;
--bg-surface: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent: #ffb000; /* Amber */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
display: flex;
justify-content: center;
}
/* Rigid Grid System & App Wrapper */
.app-container {
display: grid;
grid-template-columns: 260px 1fr;
width: 100%;
max-width: 1200px;
min-height: 100vh;
border-left: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
}
@media (max-width: 900px) {
.app-container {
grid-template-columns: 1fr;
border-left: none;
border-right: none;
}
.sidebar {
border-right: none !important;
border-bottom: 1px solid var(--border-color);
}
}
/* Sidebar Navigation (Monospaced UI) */
.sidebar {
background-color: var(--bg-canvas);
border-right: 1px solid var(--border-color);
padding: 24px;
display: flex;
flex-direction: column;
font-family: 'IBM Plex Mono', monospace;
}
.profile-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.profile-subtitle {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 24px;
}
.nav-list {
list-style: none;
padding: 0;
margin-bottom: auto;
}
.nav-item {
margin-bottom: 8px;
}
.nav-link {
display: flex;
align-items: center;
color: var(--text-primary);
text-decoration: none;
font-size: 0.9rem;
padding: 6px 8px;
border: 1px solid transparent;
}
/* Snap instant hovers */
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* Structural Indicator for Active Nav Items */
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 6px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Keybindings Panel */
.keybindings-panel {
border: 1px solid var(--border-color);
background-color: var(--bg-surface);
padding: 16px;
border-radius: 4px;
margin-top: 24px;
}
.keybindings-title {
font-size: 0.8rem;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.keybinding-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
margin-bottom: 8px;
}
.keybinding-row:last-child {
margin-bottom: 0;
}
kbd {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-bottom: 1.5px solid var(--text-muted);
border-radius: 3px;
padding: 1px 5px;
font-size: 0.75rem;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
}
/* Main Content Area */
.main-content {
padding: 40px;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
}
.prose-container {
width: 100%;
max-width: 65ch; /* Optimal readability cap */
}
/* Typography & Headers */
h1, h2, h3 {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
color: var(--text-primary);
margin-top: 0;
}
h1 {
font-size: 1.8rem;
margin-bottom: 16px;
}
h2 {
font-size: 1.3rem;
margin-top: 32px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
p {
margin-bottom: 20px;
color: var(--text-primary);
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Dividers */
hr {
border: 0;
border-top: 1px solid var(--border-color);
margin: 32px 0;
width: 100%;
}
/* Metadata Tags */
.post-metadata {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
align-items: center;
}
.tag {
color: var(--accent);
background-color: transparent;
padding: 0;
font-size: 0.85rem;
text-decoration: none;
}
.tag:hover {
text-decoration: underline;
}
/* Terminal-Style Code Block Component */
.code-block {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
margin: 24px 0;
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
}
.code-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: var(--text-muted);
}
.code-filename {
color: var(--text-primary);
font-weight: 500;
}
.code-content {
padding: 16px;
overflow-x: auto;
font-size: 0.85rem;
line-height: 1.5;
}
.code-content pre {
margin: 0;
}
/* Simple Monospace Syntax Colors */
.syn-comment { color: var(--text-muted); }
.syn-keyword { color: #ff5555; }
.syn-string { color: #50fa7b; }
.syn-type { color: #8be9fd; }
.syn-var { color: #ffb86c; }
/* Dense Archive / Tabular Posts List */
.post-table {
width: 100%;
border-collapse: collapse;
margin-top: 16px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.9rem;
}
.post-table th, .post-table td {
text-align: left;
padding: 10px 12px;
border-bottom: 1px solid var(--border-color);
}
.post-table th {
color: var(--text-muted);
font-weight: 500;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.post-table tbody tr {
cursor: pointer;
}
/* Tabular Snap Hover - Instantly inverts row */
.post-table tbody tr:hover td {
background-color: var(--accent);
color: var(--bg-canvas);
}
.post-table tbody tr:hover td a {
color: var(--bg-canvas);
}
.post-table td a {
color: var(--text-primary);
font-weight: 500;
}
.post-table .post-date {
color: var(--text-muted);
width: 110px;
}
.post-table tbody tr:hover .post-date {
color: var(--bg-canvas);
}
/* Footer */
footer {
margin-top: 48px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<div class="app-container">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="profile-title">[node-01]</div>
<div class="profile-subtitle">dev-blog@system:~$</div>
<nav>
<ul class="nav-list">
<li class="nav-item">
<a href="homepage.html" class="nav-link active">Home</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">Posts</a>
</li>
<li class="nav-item">
<a href="#" class="nav-link">Projects</a>
</li>
<li class="nav-item">
<a href="index.html" class="nav-link">Spec Catalog</a>
</li>
</ul>
</nav>
<!-- Keybindings Panel Widget -->
<div class="keybindings-panel">
<div class="keybindings-title">Keyboard Shortcuts</div>
<div class="keybinding-row">
<span>Search posts</span>
<kbd>Ctrl</kbd> + <kbd>K</kbd>
</div>
<div class="keybinding-row">
<span>Go to Catalog</span>
<kbd>g</kbd> + <kbd>c</kbd>
</div>
<div class="keybinding-row">
<span>Toggle Terminal</span>
<kbd>~</kbd>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<div class="prose-container">
<!-- Post Title & Metadata Header -->
<article>
<h1>Reverse Engineering IoT Firmware: SPI Flash Extraction</h1>
<div class="post-metadata">
<span>Posted: 2026-07-20</span>
<span>•</span>
<span>Reading: 8 mins</span>
<span>•</span>
<a href="#" class="tag">[#firmware]</a>
<a href="#" class="tag">[#hardware]</a>
<a href="#" class="tag">[#security]</a>
</div>
<p>
In security audits of embedded systems, retrieving the firmware is usually the critical first step. When over-the-air updates are encrypted and the debug ports (UART, JTAG) are disabled in production silicon, dumping the physical SPI Flash memory chip becomes necessary.
</p>
<p>
In this write-up, we extract firmware directly from an 8-pin SOP SPI flash chip using a Bus Pirate. This hardware-level approach bypasses any bootloader lockdowns and extracts the raw partition table directly from the silicon.
</p>
<!-- Component 1: Terminal-Style Code Block with syntax styling -->
<div class="code-block">
<div class="code-header">
<span class="code-filename">extract_firmware.sh</span>
<span>Bash</span>
</div>
<div class="code-content">
<pre><code><span class="syn-comment">#!/bin/sh</span>
<span class="syn-comment"># Query the SPI flash details using flashrom via Bus Pirate</span>
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump.bin
<span class="syn-comment"># Verify the integrity of the binary dump by comparing hashes</span>
<span class="syn-keyword">if</span> [ <span class="syn-var">$?</span> -eq 0 ]; <span class="syn-keyword">then</span>
echo <span class="syn-string">"Dump successful. Running md5sum..."</span>
md5sum firmware_dump.bin
<span class="syn-keyword">else</span>
echo <span class="syn-string">"Failed to read flash memory."</span> >&2
<span class="syn-keyword">exit</span> 1
<span class="syn-keyword">fi</span></code></pre>
</div>
</div>
<p>
After standard verification, we can proceed to unpack the bin file using tools like <code>binwalk</code>. If the partition table contains standard filesystem formats (e.g. SquashFS, jffs2), binwalk extracts it automatically. Otherwise, manual offset carving is required.
</p>
</article>
<hr>
<!-- Tabular Archive Index Section -->
<section>
<h2>Recent Writes & Archive</h2>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 20px;">
Dense, terminal-style index of prior posts. Press <kbd>Tab</kbd> to focus and navigate rows instantly.
</p>
<table class="post-table">
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
<tr onclick="location.href='#'">
<td class="post-date">2026-07-15</td>
<td><a href="#">Building a High-Availability Traefik Router with Docker Compose</a></td>
<td><span style="color: var(--accent);">[#homelab]</span></td>
</tr>
<tr onclick="location.href='#'">
<td class="post-date">2026-07-10</td>
<td><a href="#">POSIX sh vs. Bash: Portability in Modern Scripts</a></td>
<td><span style="color: var(--accent);">[#shell]</span></td>
</tr>
<tr onclick="location.href='#'">
<td class="post-date">2026-06-28</td>
<td><a href="#">CTF Write-up: Heap Exploitation in glibc 2.35</a></td>
<td><span style="color: var(--accent);">[#ctf]</span></td>
</tr>
</tbody>
</table>
</section>
<!-- Footer -->
<footer>
<span>© 2026 Developer Blog</span>
<span>Built with Design-Specification.md</span>
</footer>
</div>
</main>
</div>
</body>
</html>
-370
View File
@@ -1,370 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Design Spec Samples Catalog | Dynamic Registry</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=Inter:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--bg-canvas: #0d0f12;
--bg-surface: #121417;
--bg-elevated: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent-cyan: #00f0ff;
--accent-green: #00e676;
--accent-amber: #ffb700;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
max-width: 860px;
margin: 40px auto;
padding: 0 20px;
}
/* Header Identity */
.catalog-header {
border-bottom: 1px solid var(--border-color);
padding-bottom: 16px;
margin-bottom: 24px;
}
.catalog-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.6rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 12px;
}
.status-dot {
width: 10px;
height: 10px;
background-color: var(--accent-cyan);
display: inline-block;
}
.catalog-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
color: var(--text-muted);
margin-top: 6px;
display: flex;
justify-content: space-between;
}
/* Search Filter Input Bar */
.filter-bar {
margin-bottom: 24px;
display: flex;
gap: 12px;
}
.search-input {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
padding: 10px 14px;
width: 100%;
outline: none;
}
.search-input:focus {
border-color: var(--accent-cyan);
}
/* Dynamic Catalog Cards List */
.samples-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.sample-card {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 16px;
transition: none; /* Instant snap */
}
.sample-card:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.sample-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
}
.sample-link {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.05rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
transition: none;
}
.sample-link:hover {
color: var(--accent-cyan);
}
.badge {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
padding: 2px 6px;
border-radius: 2px;
font-weight: 700;
}
.badge-html { background-color: rgba(0, 240, 255, 0.15); color: var(--accent-cyan); }
.badge-md { background-color: rgba(255, 183, 0, 0.15); color: var(--accent-amber); }
.sample-filename {
font-family: 'JetBrains Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
margin-bottom: 8px;
}
.sample-desc {
font-size: 0.88rem;
color: var(--text-muted);
}
/* Footer */
.catalog-footer {
margin-top: 40px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<header class="catalog-header">
<h1 class="catalog-title">
<span class="status-dot"></span>
DESIGN SPEC SAMPLES CATALOG
</h1>
<div class="catalog-meta">
<span>PATH: /samples/</span>
<span id="file-counter">SCANNING DIRECTORY...</span>
</div>
</header>
<!-- Filter Input -->
<div class="filter-bar">
<input type="text" id="search-box" class="search-input" placeholder="Search samples by name, keyword, or filename... ($ grep -i)" oninput="filterSamples()">
</div>
<!-- Dynamic Catalog Container -->
<main id="samples-container" class="samples-list">
<!-- Rendered dynamically by JavaScript -->
</main>
<footer class="catalog-footer">
<div>Unified Design Language Compliant</div>
<div>POSIX / Terminal Minimalist Spec</div>
</footer>
<script>
// Known files catalog with fallback metadata definitions
const knownFiles = [
{
file: 'unified-design-specification.md',
title: 'Unified Design Specification (Master Brief)',
type: 'md',
desc: 'The master specification uniting Homelab Services Directory, Technical Blog/Notes, and Resume/Portfolio into a dark slate (#0d0f12), dual-typography (IBM Plex Mono + Inter), instant snap interaction language.'
},
{
file: 'sample-dashboard.html',
title: 'Sample 8: Terminal Workspace Dashboard (3-Tile Split)',
type: 'html',
desc: 'A 3-tile modal editor workspace layout (Neovim/Tmux style) featuring live homelab services, resume bio, article log stream, and interactive vim command bar.'
},
{
file: 'sample-command-center.html',
title: 'Sample 9: Command Center (Zed/Ghostty Tabbed Utility)',
type: 'html',
desc: 'A tabbed single-pane utility layout inspired by Zed Editor and Ghostty. Features keyboard navigation hotkeys [1], [2], [3] for Services, Blog, and Resume tabs.'
},
{
file: 'sample-dual-pane-explorer.html',
title: 'Sample 10: Dual-Pane Tree Explorer (Helix File Tree)',
type: 'html',
desc: 'A split-screen file-tree explorer featuring a left tree sidebar, document view container, and an interactive ASCII daemon mascot (sys-daemon) reacting to item selection.'
},
{
file: 'homepage.html',
title: 'Sample 1: Developer Homepage (Terminal Minimalist Spec)',
type: 'html',
desc: 'A dark-mode native developer homepage modeled after modern, high-density terminal setups and IDEs. Adheres strictly to zero glassmorphism and solid color layouts.'
},
{
file: 'split-panel.html',
title: 'Sample 2: Split Panel Layout (ASCII Animation & Scrollable Tiles)',
type: 'html',
desc: 'A dashboard layout with a sticky site nav header, left panel with animated terminal ASCII character, and right panel with scrollable developer project cards.'
},
{
file: 'companion.html',
title: 'Sample 3: Companion Layout (Interactive Daemon & Minimal Cards)',
type: 'html',
desc: 'A 50/50 vertical split layout with content cards on the left and a persistent, reactive ASCII companion on the right with discrete state updates.'
},
{
file: 'document-flow.html',
title: 'Sample 4: Document Flow Layout (Scroll-Tracker Daemon)',
type: 'html',
desc: 'A continuous document reading flow removing 4-sided bounding boxes, featuring a left pane with an IntersectionObserver scroll-tracker daemon.'
},
{
file: 'portfolio.html',
title: 'Sample 5: River Rooks Portfolio (Single-Page Business Card)',
type: 'html',
desc: 'A single-column digital business card landing page conforming to portfolio-spec.md with terminal stdout block, active stack tags, and piped footer.'
},
{
file: 'homelab.html',
title: 'Sample 6: Homelab Lander & Infrastructure Directory',
type: 'html',
desc: 'A public-facing lander for self-hosted homelab infrastructure featuring category filters, service cards, telemetry, and ASCII network topology routing map.'
},
{
file: 'public-services.html',
title: 'Sample 7: Public Services & Applications Portal',
type: 'html',
desc: 'A quick-access launcher portal for self-hosted web tools (Git forge, CyberChef, Stirling PDF, media) with search, filters, and access level badges.'
},
{
file: 'design-specification.md',
title: 'Design Specification (Primary Technical Brief)',
type: 'md',
desc: 'Primary specification document detailing technical web architecture guidelines, typography rules, color palette, and global anti-pattern constraints.'
},
{
file: 'portfolio-spec.md',
title: 'Portfolio Specification (Landing Page Brief)',
type: 'md',
desc: 'Specification sheet for the single-page under-construction portfolio card layout.'
},
{
file: 'sample3-spec-sheet.md',
title: 'Sample 3 Specification (Companion Layout Brief)',
type: 'md',
desc: 'Specification sheet detailing the split-screen ASCII companion interaction suppression policy.'
}
];
let allItems = [];
async function initCatalog() {
allItems = [...knownFiles];
// Attempt dynamic discovery via fetch directory listing if web server supports autoindex
try {
const response = await fetch('./');
if (response.ok) {
const htmlText = await textResponse(response);
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
const links = Array.from(doc.querySelectorAll('a'))
.map(a => a.getAttribute('href'))
.filter(href => href && (href.endsWith('.html') || href.endsWith('.md')) && href !== 'index.html');
// Merge dynamically discovered files if any are missing from knownFiles
links.forEach(href => {
const cleanName = href.replace('./', '');
if (!allItems.some(item => item.file === cleanName)) {
const isHtml = cleanName.endsWith('.html');
allItems.push({
file: cleanName,
title: cleanName,
type: isHtml ? 'html' : 'md',
desc: `Dynamically discovered ${isHtml ? 'HTML sample' : 'Markdown specification'} file.`
});
}
});
}
} catch (err) {
console.log('Directory autoindex fetch skipped, rendering fallback manifest.');
}
renderCatalog(allItems);
}
async function textResponse(res) {
return await res.text();
}
function renderCatalog(items) {
const container = document.getElementById('samples-container');
const counter = document.getElementById('file-counter');
counter.innerText = `${items.length} FILES INDEXED`;
container.innerHTML = '';
if (items.length === 0) {
container.innerHTML = `<div style="color: var(--text-muted); font-family: monospace;">No matching sample files found.</div>`;
return;
}
items.forEach(item => {
const card = document.createElement('div');
card.className = 'sample-card';
const badgeClass = item.type === 'html' ? 'badge-html' : 'badge-md';
const badgeLabel = item.type === 'html' ? 'HTML SAMPLE' : 'MARKDOWN SPEC';
card.innerHTML = `
<div class="sample-header">
<a href="${item.file}" class="sample-link">${item.title}</a>
<span class="badge ${badgeClass}">${badgeLabel}</span>
</div>
<div class="sample-filename">file:///samples/${item.file}</div>
<div class="sample-desc">${item.desc}</div>
`;
container.appendChild(card);
});
}
function filterSamples() {
const query = document.getElementById('search-box').value.toLowerCase().trim();
const filtered = allItems.filter(item =>
item.title.toLowerCase().includes(query) ||
item.file.toLowerCase().includes(query) ||
item.desc.toLowerCase().includes(query)
);
renderCatalog(filtered);
}
document.addEventListener('DOMContentLoaded', initCatalog);
</script>
</body>
</html>
-49
View File
@@ -1,49 +0,0 @@
# Project Brief: Temporary Single-Page Portfolio (Landing Page)
**Project Type:** Single-Page "Under Construction" / Digital Business Card
**Design Philosophy:** High-signal, low-noise. Developer-centric minimalism. (Matches primary site spec)
## 1. Executive Summary
This is a temporary, single-page landing site to serve as a digital presence while the full web architecture is being developed. It must adhere strictly to the global design rules established for the main site (no glassmorphism, no glows, monospace-heavy typography, dark charcoal palette). The layout should be a simple, single-column document flow.
## 2. Layout & Structure
The page will be a single, centered column with a maximum width of `65ch` (characters) to ensure optimal reading density.
### A. Header (Identity)
* **Main Title (H1):** "River Rooks"
* **Subtitle/Status:** A monospaced, muted status line (e.g., "SYSTEMS ARCHITECTURE & CYBERSECURITY") paired with a rigid, square, 8x8px solid active-state indicator dot (using the primary accent color).
### B. The "Under Construction" Terminal Block
* **Component:** A terminal-style code block.
* **Style:** 1px solid border, slightly lighter surface background.
* **Content:** Must present the "under construction" status as a terminal command and stdout output.
* *Example prompt:* `$ cat /var/log/status.txt`
* *Example output:* `Under construction. Full site infrastructure is currently being provisioned. Routing traffic to temporary landing page...`
### C. "Whoami" Section (Biography)
* **Header (H2):** Clean sans-serif or bold monospace.
* **Content:** Standard prose paragraph establishing background (e.g., studying cybersecurity at the University of Alabama College of Engineering, focus on network forensics and self-hosted infrastructure).
### D. Active Stack (Skills & Tech)
* **Component:** A dense cluster of inline metadata tags.
* **Style:** Small, muted, monospaced font. Bracketed format preferred (e.g., `[#network-forensics]`, `[#docker-traefik]`, `[#arch-linux]`, `[#neovim]`).
### E. Footer (Contact & Links)
* **Layout:** A single monospaced line separated by structural pipes (`|`).
* **Links:** GitHub, Email, PGP Key.
* **Interaction:** Links must have an immediate hover state (e.g., background color snap) with zero transition delay. No floaty animations.
## 3. Strict Typography & Color Application
* **Canvas:** Deep slate/charcoal (e.g., `#0d0f12`)
* **Primary Text:** Off-white/light ash (Sans-serif: Inter or Geist)
* **Metadata/Terminal Text:** Muted gray (Monospace: IBM Plex Mono or JetBrains Mono)
* **Accent (Designer's Choice):** A high-visibility terminal color for the status dot and hover states.
* **Dividers:** Use simple 1px solid horizontal rules to separate the Header, Main Content, and Footer.
-358
View File
@@ -1,358 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Systems Architecture & Cybersecurity</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg-canvas: #0d0f12; /* Deep slate/charcoal */
--bg-surface: #121417; /* Slightly lighter surface background */
--border-color: #2b303b; /* Low-contrast 1px border */
--text-primary: #e0e6f0; /* Off-white prose */
--text-muted: #8a95a5; /* Monospaced metadata gray */
--accent: #00f0ff; /* High-visibility Cyan terminal accent */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.65;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Sticky Navigation Header */
.sticky-header {
position: sticky;
top: 0;
height: 56px;
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
z-index: 100;
font-family: 'IBM Plex Mono', monospace;
}
.logo-container {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-primary);
}
.logo-prompt {
color: var(--accent);
}
.header-nav {
display: flex;
gap: 14px;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.85rem;
padding: 4px 8px;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
transition: none;
}
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 4px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Page Canvas Container */
.page-wrapper {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 60px 24px;
}
.content-container {
width: 100%;
max-width: 65ch; /* Capped reading width */
}
/* Header (Identity) */
.author-name {
font-family: 'IBM Plex Mono', monospace;
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.status-line {
display: flex;
align-items: center;
gap: 10px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
}
/* Rigid, square 8x8px active-state indicator dot */
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent);
display: inline-block;
flex-shrink: 0;
}
/* 1px Solid Dividers */
.section-divider {
border: 0;
border-top: 1px solid var(--border-color);
margin: 32px 0;
}
/* Under Construction Terminal Block */
.terminal-block {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
font-family: 'JetBrains Mono', monospace;
overflow: hidden;
margin-bottom: 32px;
}
.terminal-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
display: flex;
justify-content: space-between;
font-size: 0.78rem;
color: var(--text-muted);
}
.terminal-title {
color: var(--text-primary);
font-weight: 500;
}
.terminal-body {
padding: 16px;
font-size: 0.85rem;
line-height: 1.5;
}
.cmd-line {
color: var(--text-muted);
margin-bottom: 8px;
}
.prompt {
color: var(--accent);
font-weight: 700;
}
.cmd-output {
color: var(--text-primary);
}
.cursor-block {
display: inline-block;
width: 8px;
height: 15px;
background-color: var(--text-primary);
margin-left: 4px;
vertical-align: middle;
animation: blink 1s steps(2, start) infinite;
}
@keyframes blink {
to { visibility: hidden; }
}
/* Content Sections */
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.bio-section {
margin-bottom: 32px;
}
.bio-text {
font-size: 0.98rem;
color: var(--text-primary);
}
.stack-section {
margin-bottom: 32px;
}
.tag-cluster {
display: flex;
flex-wrap: wrap;
gap: 10px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
}
.tag {
color: var(--text-muted);
}
/* Piped Link Footer with Instant Color Snap */
.footer-links {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.footer-link {
color: var(--text-primary);
text-decoration: none;
padding: 2px 6px;
transition: none; /* Zero-delay snap */
}
.footer-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.footer-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.pipe {
color: var(--border-color);
}
</style>
</head>
<body>
<!-- Sticky Header -->
<header class="sticky-header">
<div class="logo-container">
<span class="logo-prompt">$</span> agy --page=portfolio
</div>
<nav class="header-nav">
<a href="index.html" class="nav-link">Catalog</a>
<a href="homepage.html" class="nav-link">Sample 1</a>
<a href="split-panel.html" class="nav-link">Sample 2</a>
<a href="companion.html" class="nav-link">Sample 3</a>
<a href="document-flow.html" class="nav-link">Sample 4</a>
<a href="portfolio.html" class="nav-link active">Sample 5</a>
</nav>
</header>
<!-- Page Content Area -->
<div class="page-wrapper">
<main class="content-container">
<!-- A. Header (Identity) -->
<header class="identity-header">
<h1 class="author-name">River Rooks</h1>
<div class="status-line">
<span class="status-dot"></span>
<span class="status-text">SYSTEMS ARCHITECTURE &amp; CYBERSECURITY</span>
</div>
</header>
<hr class="section-divider">
<!-- B. Under Construction Terminal Block -->
<section class="terminal-section" aria-label="Status Terminal">
<div class="terminal-block">
<div class="terminal-header">
<span class="terminal-title">/var/log/status.txt</span>
<span>STDOUT</span>
</div>
<div class="terminal-body">
<div class="cmd-line"><span class="prompt">$</span> cat /var/log/status.txt</div>
<div class="cmd-output">
Under construction. Full site infrastructure is currently being provisioned. Routing traffic to temporary landing page...<span class="cursor-block"></span>
</div>
</div>
</div>
</section>
<!-- C. "Whoami" Section (Biography) -->
<section class="bio-section">
<h2 class="section-title">whoami</h2>
<p class="bio-text">
Cybersecurity student at the University of Alabama College of Engineering, focusing on network forensics, self-hosted infrastructure, and systems architecture.
</p>
</section>
<!-- D. Active Stack (Skills & Tech) -->
<section class="stack-section">
<h2 class="section-title">active_stack</h2>
<div class="tag-cluster">
<span class="tag">[#network-forensics]</span>
<span class="tag">[#docker-traefik]</span>
<span class="tag">[#arch-linux]</span>
<span class="tag">[#neovim]</span>
<span class="tag">[#wireguard]</span>
<span class="tag">[#posix-sh]</span>
<span class="tag">[#python]</span>
</div>
</section>
<hr class="section-divider">
<!-- E. Footer (Contact & Links) -->
<footer class="footer-links">
<a href="https://github.com" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<span class="pipe">|</span>
<a href="mailto:river@example.com" class="footer-link">Email</a>
<span class="pipe">|</span>
<a href="#" class="footer-link">PGP Key</a>
<span class="pipe">|</span>
<a href="index.html" class="footer-link">Spec Catalog</a>
</footer>
</main>
</div>
</body>
</html>
-901
View File
@@ -1,901 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Public Services & Applications Portal</title>
<meta name="description" content="Public portal for River Rooks self-hosted web applications, developer utilities, media services, and public identity endpoints.">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
:root {
--bg-canvas: #0d0f12; /* Deep slate/charcoal */
--bg-surface: #121417; /* Solid surface container */
--bg-surface-alt: #181b21; /* Slightly lighter surface card */
--border-color: #2b303b; /* Low-contrast 1px border */
--border-active: #ffb000; /* Active Amber border */
--text-primary: #e0e6f0; /* Off-white prose */
--text-muted: #8a95a5; /* Monospaced metadata gray */
--accent: #ffb000; /* High-visibility Amber terminal accent */
--accent-cyan: #00f0ff; /* Cyan secondary accent */
--accent-green: #00ff66; /* Green status indicator */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.65;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Sticky Navigation Header */
.sticky-header {
position: sticky;
top: 0;
height: 56px;
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
z-index: 100;
font-family: 'IBM Plex Mono', monospace;
}
.logo-container {
font-weight: 700;
font-size: 0.92rem;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.logo-prompt {
color: var(--accent);
}
.header-nav {
display: flex;
gap: 10px;
overflow-x: auto;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.82rem;
padding: 4px 8px;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
transition: none; /* Zero-delay snap */
white-space: nowrap;
}
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 4px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Main Page Wrapper */
.page-wrapper {
flex: 1;
display: flex;
justify-content: center;
padding: 40px 24px 60px 24px;
}
.content-container {
width: 100%;
max-width: 1000px;
}
/* Hero Banner */
.hero-header {
margin-bottom: 28px;
}
.site-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
letter-spacing: -0.02em;
}
.site-subtitle {
font-size: 1rem;
color: var(--text-muted);
max-width: 70ch;
}
/* Dividers */
.section-divider {
border: 0;
border-top: 1px solid var(--border-color);
margin: 32px 0;
}
/* Search & Filter Controls */
.controls-panel {
display: flex;
flex-direction: column;
gap: 16px;
margin-bottom: 32px;
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 16px;
}
.search-box {
display: flex;
align-items: center;
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px 12px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.88rem;
}
.search-prompt {
color: var(--accent);
margin-right: 10px;
font-weight: 700;
}
.search-input {
background: none;
border: none;
color: var(--text-primary);
font-family: inherit;
font-size: inherit;
width: 100%;
outline: none;
}
.search-input::placeholder {
color: var(--text-muted);
}
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-family: 'IBM Plex Mono', monospace;
}
.filter-btn {
background-color: var(--bg-canvas);
color: var(--text-muted);
border: 1px solid var(--border-color);
padding: 6px 12px;
font-size: 0.8rem;
cursor: pointer;
font-family: inherit;
border-radius: 4px;
transition: none; /* Instant snap */
}
.filter-btn:hover {
color: var(--text-primary);
border-color: var(--accent);
}
.filter-btn.active {
background-color: var(--accent);
color: var(--bg-canvas);
border-color: var(--accent);
font-weight: 700;
}
.filter-btn:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* Section Headers */
.section-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 8px;
}
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
}
.section-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
}
/* Featured Services Grid */
.featured-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
gap: 16px;
margin-bottom: 36px;
}
.featured-card {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
}
.featured-card:hover {
border-color: var(--accent);
}
.featured-badge {
align-self: flex-start;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.72rem;
font-weight: 700;
color: var(--bg-canvas);
background-color: var(--accent);
padding: 2px 6px;
border-radius: 2px;
margin-bottom: 12px;
text-transform: uppercase;
}
.service-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 8px;
}
.featured-card:hover .service-title {
color: var(--accent);
}
.service-body {
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 20px;
line-height: 1.5;
}
.action-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.access-tag {
color: var(--text-muted);
}
.launch-btn {
display: inline-flex;
align-items: center;
gap: 4px;
background-color: var(--bg-canvas);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 6px 12px;
text-decoration: none;
font-weight: 500;
border-radius: 4px;
transition: none;
}
.launch-btn:hover {
background-color: var(--accent);
color: var(--bg-canvas);
border-color: var(--accent);
}
.launch-btn:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
/* All Public Services Grid */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
margin-bottom: 36px;
}
.service-card {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 18px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: none;
}
.service-card:hover {
border-color: var(--border-active);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.card-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
}
.service-card:hover .card-title {
color: var(--accent);
}
.card-category {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
}
.card-desc {
font-size: 0.88rem;
color: var(--text-muted);
margin-bottom: 16px;
line-height: 1.5;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
padding-top: 10px;
border-top: 1px solid var(--border-color);
}
.badge-access {
display: inline-flex;
align-items: center;
gap: 6px;
}
.dot-public {
width: 6px;
height: 6px;
background-color: var(--accent-green);
display: inline-block;
}
.dot-auth {
width: 6px;
height: 6px;
background-color: var(--accent);
display: inline-block;
}
.direct-link {
color: var(--text-primary);
text-decoration: none;
padding: 3px 8px;
border: 1px solid var(--border-color);
border-radius: 3px;
background-color: var(--bg-canvas);
}
.direct-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
border-color: var(--accent);
}
/* Public Identity & Quick Keys Box */
.identity-box {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 20px;
font-family: 'JetBrains Mono', monospace;
margin-bottom: 36px;
}
.identity-title {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 14px;
}
.key-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
font-size: 0.83rem;
flex-wrap: wrap;
gap: 10px;
}
.key-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.key-label {
color: var(--text-muted);
}
.key-value {
color: var(--text-primary);
}
.copy-btn {
background-color: var(--bg-canvas);
color: var(--text-primary);
border: 1px solid var(--border-color);
padding: 4px 8px;
font-family: inherit;
font-size: 0.75rem;
cursor: pointer;
border-radius: 3px;
transition: none;
}
.copy-btn:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
/* Piped Footer Links */
.footer-links {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--text-muted);
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.footer-link {
color: var(--text-primary);
text-decoration: none;
padding: 2px 6px;
transition: none; /* Zero-delay snap */
}
.footer-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.footer-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.pipe {
color: var(--border-color);
}
</style>
</head>
<body>
<!-- Sticky Header -->
<header class="sticky-header">
<div class="logo-container">
<span class="logo-prompt">$</span> agy --page=public-services
</div>
<nav class="header-nav" aria-label="Main Navigation">
<a href="index.html" class="nav-link">Catalog</a>
<a href="homepage.html" class="nav-link">Sample 1</a>
<a href="split-panel.html" class="nav-link">Sample 2</a>
<a href="companion.html" class="nav-link">Sample 3</a>
<a href="document-flow.html" class="nav-link">Sample 4</a>
<a href="portfolio.html" class="nav-link">Sample 5</a>
<a href="homelab.html" class="nav-link">Sample 6</a>
<a href="public-services.html" class="nav-link active">Sample 7 (Public Services)</a>
</nav>
</header>
<!-- Page Content Area -->
<div class="page-wrapper">
<main class="content-container">
<!-- Hero Header -->
<header class="hero-header">
<h1 class="site-title">Public Services Portal</h1>
<p class="site-subtitle">
Direct access to public self-hosted web applications, developer utilities, repository mirrors, and interactive tools operated by River Rooks.
</p>
</header>
<!-- Instant Search & Category Filter Controls -->
<section aria-label="Search and Filter Controls">
<div class="controls-panel">
<div class="search-box">
<span class="search-prompt">$ search_services</span>
<input type="text" id="searchInput" class="search-input" placeholder="type service name, tag, or technology..." aria-label="Search Public Services">
</div>
<div class="filter-bar" id="filterBar" role="tablist" aria-label="Service Categories">
<button class="filter-btn active" data-filter="all" role="tab" aria-selected="true">All Services</button>
<button class="filter-btn" data-filter="dev" role="tab">Developer Services</button>
<button class="filter-btn" data-filter="tools" role="tab">Utilities &amp; Tools</button>
<button class="filter-btn" data-filter="media" role="tab">Media &amp; Content</button>
<button class="filter-btn" data-filter="security" role="tab">Identity &amp; Security</button>
</div>
</div>
</section>
<!-- Featured Services Section -->
<section aria-label="Featured Public Services">
<div class="section-header">
<h2 class="section-title">featured_flagships</h2>
<span class="section-meta">Primary Public Endpoints</span>
</div>
<div class="featured-grid">
<!-- Forgejo -->
<article class="featured-card">
<div>
<span class="featured-badge">Open Source Git Forge</span>
<h3 class="service-title">Forgejo Repositories</h3>
<p class="service-body">
Self-hosted Git platform hosting public source code for network security scripts, Neovim dotfiles, Ansible playbooks, and Docker compose templates.
</p>
</div>
<div class="action-row">
<span class="access-tag">Access: Public Read</span>
<a href="#" class="launch-btn">Explore Repos ↗</a>
</div>
</article>
<!-- CyberChef -->
<article class="featured-card">
<div>
<span class="featured-badge">Security Utility</span>
<h3 class="service-title">CyberChef Workbench</h3>
<p class="service-body">
Web-based data transformation tool for hex decoding, Base64 operations, regex extraction, cryptographic hashing, and CTF payload analysis.
</p>
</div>
<div class="action-row">
<span class="access-tag">Access: Open Web</span>
<a href="#" class="launch-btn">Launch App ↗</a>
</div>
</article>
<!-- Stirling PDF -->
<article class="featured-card">
<div>
<span class="featured-badge">Document Tools</span>
<h3 class="service-title">Stirling PDF Studio</h3>
<p class="service-body">
Locally hosted document processing suite for merging, splitting, OCR extraction, password encryption, and PDF page manipulation.
</p>
</div>
<div class="action-row">
<span class="access-tag">Access: Open Web</span>
<a href="#" class="launch-btn">Launch App ↗</a>
</div>
</article>
</div>
</section>
<hr class="section-divider">
<!-- All Public Services Catalog Grid -->
<section aria-label="Complete Public Services Directory">
<div class="section-header">
<h2 class="section-title">services_catalog</h2>
<span class="section-meta" id="resultCount">Showing 9 Services</span>
</div>
<div class="services-grid" id="servicesGrid">
<!-- Forgejo Git -->
<article class="service-card" data-category="dev" data-search="forgejo git repo code source open">
<div>
<div class="card-header">
<h3 class="card-title">Forgejo Git Forge</h3>
<span class="card-category">[#dev-git]</span>
</div>
<p class="card-desc">Public repository mirror for software projects, dotfiles, and CTF writeups.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-public"></span> PUBLIC</span>
<a href="#" class="direct-link">git.rooks.dev ↗</a>
</div>
</article>
<!-- CyberChef -->
<article class="service-card" data-category="tools" data-search="cyberchef decode base64 crypto hex ctf">
<div>
<div class="card-header">
<h3 class="card-title">CyberChef Workbench</h3>
<span class="card-category">[#security-tool]</span>
</div>
<p class="card-desc">Cyber Swiss Army Knife for encoding, decoding, hashing, and data parsing.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-public"></span> PUBLIC</span>
<a href="#" class="direct-link">chef.rooks.dev ↗</a>
</div>
</article>
<!-- Stirling PDF -->
<article class="service-card" data-category="tools" data-search="pdf stirling merge split ocr document">
<div>
<div class="card-header">
<h3 class="card-title">Stirling PDF Studio</h3>
<span class="card-category">[#pdf-tools]</span>
</div>
<p class="card-desc">Complete PDF manipulation tool for merging, splitting, and OCR scanning.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-public"></span> PUBLIC</span>
<a href="#" class="direct-link">pdf.rooks.dev ↗</a>
</div>
</article>
<!-- Hedgedoc -->
<article class="service-card" data-category="dev" data-search="hedgedoc codimd markdown notes collab">
<div>
<div class="card-header">
<h3 class="card-title">HedgeDoc Collaborative</h3>
<span class="card-category">[#markdown]</span>
</div>
<p class="card-desc">Real-time collaborative Markdown editor for technical specs and meeting notes.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-public"></span> OPEN</span>
<a href="#" class="direct-link">pad.rooks.dev ↗</a>
</div>
</article>
<!-- Jellyfin Media -->
<article class="service-card" data-category="media" data-search="jellyfin video stream media movie tv">
<div>
<div class="card-header">
<h3 class="card-title">Jellyfin Media Portal</h3>
<span class="card-category">[#media-video]</span>
</div>
<p class="card-desc">Personal video library with 4K HDR hardware acceleration and mobile streaming.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-auth"></span> ACCOUNT REQ</span>
<a href="#" class="direct-link">watch.rooks.dev ↗</a>
</div>
</article>
<!-- Audiobookshelf -->
<article class="service-card" data-category="media" data-search="audiobookshelf audio book podcast library">
<div>
<div class="card-header">
<h3 class="card-title">Audiobookshelf</h3>
<span class="card-category">[#media-audio]</span>
</div>
<p class="card-desc">Self-hosted audiobook and podcast server with position syncing across devices.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-auth"></span> INVITE ONLY</span>
<a href="#" class="direct-link">listen.rooks.dev ↗</a>
</div>
</article>
<!-- Calibre-Web -->
<article class="service-card" data-category="media" data-search="calibre ebook pdf book reading library">
<div>
<div class="card-header">
<h3 class="card-title">Calibre-Web Library</h3>
<span class="card-category">[#ebooks]</span>
</div>
<p class="card-desc">E-book library catalog with e-reader OPDS feed and browser reading support.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-public"></span> PUBLIC READ</span>
<a href="#" class="direct-link">read.rooks.dev ↗</a>
</div>
</article>
<!-- Vaultwarden -->
<article class="service-card" data-category="security" data-search="vaultwarden bitwarden password vault secrets 2fa">
<div>
<div class="card-header">
<h3 class="card-title">Vaultwarden Password Vault</h3>
<span class="card-category">[#secrets]</span>
</div>
<p class="card-desc">End-to-end encrypted password manager endpoint for authorized client syncing.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-auth"></span> 2FA / AUTH</span>
<a href="#" class="direct-link">vault.rooks.dev ↗</a>
</div>
</article>
<!-- Matrix Synapse -->
<article class="service-card" data-category="security" data-search="matrix chat element federation identity messaging">
<div>
<div class="card-header">
<h3 class="card-title">Matrix Federated Chat</h3>
<span class="card-category">[#messaging]</span>
</div>
<p class="card-desc">Federated Matrix homeserver for encrypted messaging and public discussion rooms.</p>
</div>
<div class="card-footer">
<span class="badge-access"><span class="dot-public"></span> FEDERATED</span>
<a href="#" class="direct-link">matrix.rooks.dev ↗</a>
</div>
</article>
</div>
</section>
<hr class="section-divider">
<!-- Public Identity & Key Verification Section -->
<section aria-label="Identity and Public Keys">
<div class="section-header">
<h2 class="section-title">public_keys_&amp;_verification</h2>
<span class="section-meta">Cryptographic Identity</span>
</div>
<div class="identity-box">
<div class="identity-title">// Public Endpoints &amp; Cryptographic Fingerprints</div>
<div class="key-row">
<div>
<span class="key-label">PGP Key Fingerprint:</span><br>
<span class="key-value">3F8A B29C 7D1E 4056 9912 A8B0 C4D3 E2F1 8091 22A4</span>
</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText('3F8AB29C7D1E40569912A8B0C4D3E2F1809122A4')">Copy Key Fingerprint</button>
</div>
<div class="key-row">
<div>
<span class="key-label">Matrix Handle:</span><br>
<span class="key-value">@river:rooks.dev</span>
</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText('@river:rooks.dev')">Copy Matrix ID</button>
</div>
<div class="key-row">
<div>
<span class="key-label">Public SSH Key:</span><br>
<span class="key-value">ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8q882a... river@rooks.dev</span>
</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText('ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH8q882aZ9kX1m2pL3vN4qR5sT6uV7wX8yZ9river@rooks.dev')">Copy Public Key</button>
</div>
</div>
</section>
<hr class="section-divider">
<!-- Piped Contact Footer -->
<footer class="footer-links">
<a href="index.html" class="footer-link">Spec Catalog</a>
<span class="pipe">|</span>
<a href="https://github.com" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<span class="pipe">|</span>
<a href="mailto:river@example.com" class="footer-link">Email Contact</a>
<span class="pipe">|</span>
<a href="#" class="footer-link">Download PGP Key</a>
<span class="pipe">|</span>
<span style="font-size:0.78rem; color:var(--text-muted);">PUBLIC SERVICES PORTAL v2.4</span>
</footer>
</main>
</div>
<!-- Pure Vanilla JS Search & Filter Script -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('searchInput');
const filterBtns = document.querySelectorAll('#filterBar .filter-btn');
const serviceCards = document.querySelectorAll('#servicesGrid .service-card');
const resultCount = document.getElementById('resultCount');
let currentFilter = 'all';
let currentQuery = '';
function updateResults() {
let visibleCount = 0;
serviceCards.forEach(card => {
const category = card.getAttribute('data-category');
const searchData = card.getAttribute('data-search').toLowerCase();
const cardText = card.innerText.toLowerCase();
const matchesCategory = (currentFilter === 'all' || category === currentFilter);
const matchesQuery = currentQuery === '' || searchData.includes(currentQuery) || cardText.includes(currentQuery);
if (matchesCategory && matchesQuery) {
card.style.display = 'flex';
visibleCount++;
} else {
card.style.display = 'none';
}
});
resultCount.textContent = `Showing ${visibleCount} Service${visibleCount !== 1 ? 's' : ''}`;
}
// Filter Buttons Click Listener
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
currentFilter = btn.getAttribute('data-filter');
updateResults();
});
});
// Search Input Listener
searchInput.addEventListener('input', (e) => {
currentQuery = e.target.value.trim().toLowerCase();
updateResults();
});
});
</script>
</body>
</html>
-485
View File
@@ -1,485 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Command Center</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=Inter:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
--bg-canvas: #0d0f12;
--bg-surface: #121417;
--bg-elevated: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent-cyan: #00f0ff;
--accent-green: #00e676;
--accent-amber: #ffb700;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Command Center Top Navigation Header */
.app-header {
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
padding: 0 16px;
display: flex;
justify-content: space-between;
align-items: center;
height: 48px;
}
.brand-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.95rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent-cyan);
}
/* Zed-Style Editor Tabs */
.nav-tabs {
display: flex;
gap: 2px;
height: 100%;
align-items: flex-end;
}
.tab-btn {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
border-bottom: none;
color: var(--text-muted);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
padding: 8px 16px;
cursor: pointer;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
transition: none; /* Instant snap */
}
.tab-btn:hover {
color: var(--text-primary);
background-color: var(--bg-elevated);
}
.tab-btn.active {
background-color: var(--bg-surface);
color: var(--accent-cyan);
border-top: 2px solid var(--accent-cyan);
font-weight: 700;
}
/* Main Container */
.container {
max-width: 900px;
width: 100%;
margin: 32px auto;
padding: 0 20px;
flex: 1;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* Panel Component */
.panel {
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-surface);
overflow: hidden;
margin-bottom: 24px;
}
.panel-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 10px 16px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
.panel-body {
padding: 20px;
}
/* Services Grid (Tab 1) */
.services-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
}
.service-box {
border: 1px solid var(--border-color);
background-color: var(--bg-canvas);
padding: 14px;
border-radius: 2px;
}
.service-box:hover {
border-color: var(--accent-cyan);
}
.service-title {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 0.92rem;
display: flex;
justify-content: space-between;
margin-bottom: 6px;
}
.service-title a {
color: var(--text-primary);
text-decoration: none;
transition: none;
}
.service-title a:hover {
color: var(--accent-cyan);
}
.badge-status {
font-size: 0.75rem;
padding: 2px 6px;
border-radius: 2px;
font-family: 'IBM Plex Mono', monospace;
}
.badge-online { background-color: rgba(0, 230, 118, 0.15); color: var(--accent-green); }
.badge-internal { background-color: rgba(255, 183, 0, 0.15); color: var(--accent-amber); }
.service-desc {
font-size: 0.85rem;
color: var(--text-muted);
margin-bottom: 12px;
}
.service-stats {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
border-top: 1px solid var(--border-color);
padding-top: 8px;
display: flex;
justify-content: space-between;
}
/* Blog Archive Table (Tab 2) */
.blog-table {
width: 100%;
border-collapse: collapse;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
}
.blog-table th {
text-align: left;
border-bottom: 1px solid var(--border-color);
padding: 10px;
color: var(--text-muted);
font-size: 0.78rem;
}
.blog-table td {
padding: 12px 10px;
border-bottom: 1px solid var(--border-color);
}
.blog-table tr:hover td {
background-color: var(--bg-canvas);
}
.post-link {
color: var(--text-primary);
text-decoration: none;
font-weight: 500;
transition: none;
}
.post-link:hover {
color: var(--accent-cyan);
}
.tag-badge {
color: var(--text-muted);
font-size: 0.78rem;
}
/* Resume Config Document (Tab 3) */
.conf-block {
background-color: var(--bg-canvas);
border: 1px solid var(--border-color);
padding: 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
border-radius: 4px;
margin-bottom: 20px;
}
.conf-key { color: var(--accent-cyan); }
.conf-val { color: var(--text-primary); }
.conf-comment { color: var(--text-muted); }
.btn-action {
background-color: var(--bg-elevated);
border: 1px solid var(--border-color);
color: var(--text-primary);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
padding: 8px 16px;
cursor: pointer;
text-decoration: none;
display: inline-block;
transition: none;
}
.btn-action:hover {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
}
/* Footer */
.cmd-footer {
border-top: 1px solid var(--border-color);
background-color: var(--bg-surface);
padding: 12px 20px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<!-- Top App Bar & Zed-style Tabs -->
<header class="app-header">
<div class="brand-title">
<span class="status-dot"></span>
RIVER ROOKS // COMMAND CENTER
</div>
<nav class="nav-tabs">
<button class="tab-btn active" onclick="switchTab('services')">[1] ~/services</button>
<button class="tab-btn" onclick="switchTab('blog')">[2] ~/blog</button>
<button class="tab-btn" onclick="switchTab('resume')">[3] ~/resume</button>
</nav>
</header>
<!-- Main Container -->
<main class="container">
<!-- Tab 1: Homelab & Public Services Directory -->
<div id="tab-services" class="tab-pane active">
<div class="panel">
<div class="panel-header">
<span>/etc/services.d/manifest.json</span>
<span>4 SERVICES OPERATIONAL</span>
</div>
<div class="panel-body">
<div class="services-grid">
<div class="service-box">
<div class="service-title">
<a href="https://grafana.local" target="_blank">grafana.homelab</a>
<span class="badge-status badge-online">ONLINE</span>
</div>
<p class="service-desc">Real-time metrics, Prometheus scraping, &amp; homelab telemetry dashboards.</p>
<div class="service-stats">
<span>PING: 4ms</span>
<span>TRAEFIK HTTPS</span>
</div>
</div>
<div class="service-box">
<div class="service-title">
<a href="https://vault.local" target="_blank">vault.homelab</a>
<span class="badge-status badge-online">ONLINE</span>
</div>
<p class="service-desc">HashiCorp Vault secret storage, certificate authority, &amp; mTLS renewal.</p>
<div class="service-stats">
<span>PING: 2ms</span>
<span>SEALED: FALSE</span>
</div>
</div>
<div class="service-box">
<div class="service-title">
<a href="https://nextcloud.local" target="_blank">cloud.homelab</a>
<span class="badge-status badge-online">ONLINE</span>
</div>
<p class="service-desc">Encrypted private cloud file storage &amp; automated system backup target.</p>
<div class="service-stats">
<span>PING: 6ms</span>
<span>STORAGE: 1.2TB</span>
</div>
</div>
<div class="service-box">
<div class="service-title">
<a href="https://wireguard.local" target="_blank">wireguard.homelab</a>
<span class="badge-status badge-internal">INTERNAL</span>
</div>
<p class="service-desc">WireGuard mesh VPN gateway connecting cloud VPS instances to homelab node.</p>
<div class="service-stats">
<span>PING: 12ms</span>
<span>PEERS: 6 ACTIVE</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tab 2: Technical Notes & Blog Archive -->
<div id="tab-blog" class="tab-pane">
<div class="panel">
<div class="panel-header">
<span>/var/log/articles.index</span>
<span>POST COUNT: 4</span>
</div>
<div class="panel-body" style="padding: 0;">
<table class="blog-table">
<thead>
<tr>
<th>DATE</th>
<th>TITLE</th>
<th>TAGS</th>
<th>READ TIME</th>
</tr>
</thead>
<tbody>
<tr>
<td>2026-08-02</td>
<td><a href="#" class="post-link">Hardening Traefik Ingress with HashiCorp Vault mTLS</a></td>
<td><span class="tag-badge">[#homelab] [#traefik]</span></td>
<td>8 min</td>
</tr>
<tr>
<td>2026-07-18</td>
<td><a href="#" class="post-link">DEFCON CTF Qualifier: Memory Corruption &amp; Heap Exploitation</a></td>
<td><span class="tag-badge">[#ctf] [#binary]</span></td>
<td>14 min</td>
</tr>
<tr>
<td>2026-06-29</td>
<td><a href="#" class="post-link">Custom Linux Kernel Compilation for BPF Network Tracing</a></td>
<td><span class="tag-badge">[#linux] [#ebpf]</span></td>
<td>11 min</td>
</tr>
<tr>
<td>2026-05-14</td>
<td><a href="#" class="post-link">Minimalist Dotfiles &amp; Hyprland Keybindings Optimization</a></td>
<td><span class="tag-badge">[#hyprland] [#dots]</span></td>
<td>5 min</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Tab 3: Resume & Engineering Profile -->
<div id="tab-resume" class="tab-pane">
<div class="panel">
<div class="panel-header">
<span>/etc/river_rooks.conf</span>
<span>POSIX SPECIFICATION</span>
</div>
<div class="panel-body">
<div class="conf-block">
<span class="conf-comment"># Identity &amp; System Purpose</span><br>
<span class="conf-key">AUTHOR</span> = <span class="conf-val">"River Rooks"</span><br>
<span class="conf-key">ROLE</span> = <span class="conf-val">"Systems Architect &amp; Cybersecurity Researcher"</span><br>
<span class="conf-key">AFFILIATION</span> = <span class="conf-val">"University of Alabama College of Engineering"</span><br><br>
<span class="conf-comment"># Active Skillset Matrix</span><br>
<span class="conf-key">DOMAINS</span> = [<span class="conf-val">"Network Forensics"</span>, <span class="conf-val">"Self-Hosted Infrastructure"</span>, <span class="conf-val">"Linux Kernel Hardening"</span>]<br>
<span class="conf-key">LANGUAGES</span> = [<span class="conf-val">"POSIX sh"</span>, <span class="conf-val">"Python"</span>, <span class="conf-val">"C"</span>, <span class="conf-val">"Lua"</span>]<br>
<span class="conf-key">TOOLS</span> = [<span class="conf-val">"Docker"</span>, <span class="conf-val">"Traefik"</span>, <span class="conf-val">"WireGuard"</span>, <span class="conf-val">"Neovim"</span>, <span class="conf-val">"Arch Linux"</span>]
</div>
<p style="font-size: 0.92rem; margin-bottom: 16px;">
Focused on building resilient, zero-trust network architectures and low-noise self-hosted environments.
Proven CTF participant and open-source dotfiles maintainer.
</p>
<div style="display: flex; gap: 12px;">
<a href="#" class="btn-action">Download Resume (.PDF)</a>
<a href="#" class="btn-action">Fetch PGP Key</a>
</div>
</div>
</div>
</div>
</main>
<!-- Command Center Footer -->
<footer class="cmd-footer">
<div>Keyboard Navigation: Press <kbd>1</kbd>, <kbd>2</kbd>, or <kbd>3</kbd> to switch tabs</div>
<div>Ghostty / Zed Terminal Interface</div>
</footer>
<script>
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'));
if (tabId === 'services') {
document.querySelectorAll('.tab-btn')[0].classList.add('active');
document.getElementById('tab-services').classList.add('active');
} else if (tabId === 'blog') {
document.querySelectorAll('.tab-btn')[1].classList.add('active');
document.getElementById('tab-blog').classList.add('active');
} else if (tabId === 'resume') {
document.querySelectorAll('.tab-btn')[2].classList.add('active');
document.getElementById('tab-resume').classList.add('active');
}
}
document.addEventListener('keydown', (e) => {
if (e.key === '1') switchTab('services');
if (e.key === '2') switchTab('blog');
if (e.key === '3') switchTab('resume');
});
</script>
</body>
</html>
-434
View File
@@ -1,434 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Terminal Workspace Dashboard</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;700&family=Inter:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap');
:root {
--bg-canvas: #0d0f12;
--bg-surface: #121417;
--bg-elevated: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent-cyan: #00f0ff;
--accent-green: #00e676;
--accent-amber: #ffb700;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Top Mode/Status Header */
.top-bar {
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
padding: 8px 16px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.mode-tag {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 2px 8px;
font-weight: 700;
border-radius: 2px;
}
.filepath {
color: var(--text-primary);
margin-left: 12px;
}
.status-group {
display: flex;
align-items: center;
gap: 16px;
color: var(--text-muted);
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
}
/* Tiled Grid Layout */
.dashboard-grid {
display: grid;
grid-template-columns: 300px 1fr 340px;
gap: 16px;
padding: 16px;
flex: 1;
}
@media (max-width: 1024px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
}
/* Tile Container */
.tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
display: flex;
flex-direction: column;
}
.tile-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 8px 14px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
justify-content: space-between;
}
.tile-body {
padding: 16px;
flex: 1;
overflow-y: auto;
}
/* Services List (Left Tile) */
.service-card {
border: 1px solid var(--border-color);
background-color: var(--bg-canvas);
padding: 12px;
margin-bottom: 12px;
border-radius: 2px;
}
.service-head {
display: flex;
justify-content: space-between;
align-items: center;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
font-weight: 700;
margin-bottom: 4px;
}
.service-head a {
color: var(--text-primary);
text-decoration: none;
transition: none;
}
.service-head a:hover {
color: var(--accent-cyan);
}
.service-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
.badge-online { color: var(--accent-green); }
.badge-internal { color: var(--accent-amber); }
/* Resume / Main Content (Center Tile) */
.hero-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 6px;
}
.subtitle {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
color: var(--accent-cyan);
margin-bottom: 16px;
}
.bio-prose {
font-size: 0.95rem;
max-width: 65ch;
margin-bottom: 20px;
}
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1rem;
font-weight: 700;
border-bottom: 1px solid var(--border-color);
padding-bottom: 6px;
margin: 20px 0 12px 0;
}
.project-card {
border: 1px solid var(--border-color);
padding: 12px;
margin-bottom: 12px;
border-radius: 2px;
}
.project-card:hover {
background-color: var(--bg-elevated);
}
.project-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.9rem;
font-weight: 700;
color: var(--accent-cyan);
margin-bottom: 4px;
}
.tag-cluster {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
margin-top: 8px;
}
/* Notes / Log Stream (Right Tile) */
.article-item {
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
.article-item:last-child { border-bottom: none; }
.article-date {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
}
.article-link {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
display: block;
margin: 2px 0;
transition: none;
}
.article-link:hover {
color: var(--accent-cyan);
}
/* Command Prompt Bar */
.cmd-footer {
background-color: var(--bg-surface);
border-top: 1px solid var(--border-color);
padding: 8px 16px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
display: flex;
align-items: center;
gap: 10px;
}
.prompt-symbol { color: var(--accent-cyan); font-weight: bold; }
.cmd-input {
background: none;
border: none;
color: var(--text-primary);
font-family: inherit;
font-size: inherit;
width: 100%;
outline: none;
}
</style>
</head>
<body>
<!-- Top Status Navigation -->
<header class="top-bar">
<div>
<span class="mode-tag">NORMAL</span>
<span class="filepath">/home/river/dashboard.workspace</span>
</div>
<div class="status-group">
<span><span class="status-dot"></span> TRAEFIK: MESH_OK</span>
<span>SYS_LOAD: 0.14</span>
<span>UPTIME: 99.98%</span>
</div>
</header>
<!-- Main 3-Tile Dashboard -->
<main class="dashboard-grid">
<!-- Left Tile: Homelab Services Directory -->
<section class="tile">
<div class="tile-header">
<span>/etc/services</span>
<span>[4 ACTIVE]</span>
</div>
<div class="tile-body">
<div class="service-card">
<div class="service-head">
<a href="https://grafana.local" target="_blank">grafana.homelab</a>
<span class="badge-online">[ONLINE]</span>
</div>
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Telemetry & Prometheus monitoring dashboards.</p>
<div class="service-meta">
<span>PORT: :3000</span>
<span>HTTPS</span>
</div>
</div>
<div class="service-card">
<div class="service-head">
<a href="https://vault.local" target="_blank">vault.homelab</a>
<span class="badge-online">[ONLINE]</span>
</div>
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">HashiCorp Vault secret storage & certificates.</p>
<div class="service-meta">
<span>PORT: :8200</span>
<span>mTLS</span>
</div>
</div>
<div class="service-card">
<div class="service-head">
<a href="https://nextcloud.local" target="_blank">cloud.homelab</a>
<span class="badge-online">[ONLINE]</span>
</div>
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Private encrypted document & data storage.</p>
<div class="service-meta">
<span>PORT: :443</span>
<span>HTTPS</span>
</div>
</div>
<div class="service-card">
<div class="service-head">
<a href="https://wireguard.local" target="_blank">vpn.homelab</a>
<span class="badge-internal">[INTERNAL]</span>
</div>
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">WireGuard mesh overlay network ingress.</p>
<div class="service-meta">
<span>UDP: :51820</span>
<span>VPN</span>
</div>
</div>
</div>
</section>
<!-- Center Tile: Engineering Resume & Bio -->
<section class="tile">
<div class="tile-header">
<span>~/whoami.md</span>
<span>RESUME</span>
</div>
<div class="tile-body">
<h1 class="hero-title">River Rooks</h1>
<div class="subtitle">SYSTEMS ARCHITECT &amp; CYBERSECURITY RESEARCHER</div>
<p class="bio-prose">
Computer Science &amp; Cybersecurity student at the University of Alabama College of Engineering.
Specializing in low-level network forensics, self-hosted infrastructure resilience, and hardened Linux kernel configurations.
</p>
<h2 class="section-title">active_stack</h2>
<div class="tag-cluster">
<span>[#network-forensics]</span>
<span>[#docker-traefik]</span>
<span>[#arch-linux]</span>
<span>[#neovim]</span>
<span>[#wireguard]</span>
<span>[#posix-sh]</span>
<span>[#python]</span>
<span>[#hashicorp-vault]</span>
</div>
<h2 class="section-title">featured_projects</h2>
<div class="project-card">
<div class="project-title">mesh-topology-builder</div>
<p style="font-size: 0.85rem; color: var(--text-primary);">
Automated WireGuard mesh network provisioner written in POSIX shell and Python. Generates deterministic keypairs and Traefik routing rules.
</p>
</div>
<div class="project-card">
<div class="project-title">pcap-forensics-parser</div>
<p style="font-size: 0.85rem; color: var(--text-primary);">
High-throughput PCAP packet analyzer built for CTF competitions to detect anomalous TCP handshake flags and hidden ICMP payloads.
</p>
</div>
</div>
</section>
<!-- Right Tile: Technical Notes & Blog Stream -->
<section class="tile">
<div class="tile-header">
<span>/var/log/articles.log</span>
<span>NOTES</span>
</div>
<div class="tile-body">
<div class="article-item">
<div class="article-date">2026-08-02 | #HOMELAB</div>
<a href="#" class="article-link">Hardening Traefik Ingress with HashiCorp Vault mTLS</a>
<p style="font-size: 0.78rem; color: var(--text-muted);">Zero-trust certificate auto-renewal pipeline configuration.</p>
</div>
<div class="article-item">
<div class="article-date">2026-07-18 | #CTF</div>
<a href="#" class="article-link">DEFCON CTF Qualifier: Memory Corruption &amp; Heap Exploitation</a>
<p style="font-size: 0.78rem; color: var(--text-muted);">Deconstructing glibc malloc chunk metadata manipulation.</p>
</div>
<div class="article-item">
<div class="article-date">2026-06-29 | #LINUX</div>
<a href="#" class="article-link">Custom Linux Kernel Compilation for BPF Network Tracing</a>
<p style="font-size: 0.78rem; color: var(--text-muted);">Configuring eBPF probe hooks for real-time socket inspection.</p>
</div>
<div class="article-item">
<div class="article-date">2026-05-14 | #HYPRLAND</div>
<a href="#" class="article-link">Minimalist Dotfiles &amp; Hyprland Keybindings Optimization</a>
<p style="font-size: 0.78rem; color: var(--text-muted);">Ergonomic modal editing &amp; window tiling configuration setup.</p>
</div>
</div>
</section>
</main>
<!-- Interactive Terminal Footer Bar -->
<footer class="cmd-footer">
<span class="prompt-symbol">$</span>
<input type="text" class="cmd-input" placeholder="Type :help, :open <service>, or press Ctrl+K for commands..." readonly>
</footer>
</body>
</html>
-384
View File
@@ -1,384 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>River Rooks | Dual Pane Explorer</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap');
:root {
--bg-canvas: #0d0f12;
--bg-surface: #121417;
--bg-elevated: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent-cyan: #00f0ff;
--accent-green: #00e676;
--accent-amber: #ffb700;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Top Breadcrumb Bar */
.top-nav {
background-color: var(--bg-surface);
border-bottom: 1px solid var(--border-color);
height: 42px;
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
}
.status-dot {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
margin-right: 6px;
}
/* Dual Pane Layout Container */
.split-view {
display: flex;
flex: 1;
height: calc(100vh - 42px - 28px);
}
/* Left Sidebar Tree (280px) */
.sidebar {
width: 280px;
background-color: var(--bg-surface);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.82rem;
}
.sidebar-header {
padding: 10px 14px;
border-bottom: 1px solid var(--border-color);
font-weight: 700;
color: var(--text-muted);
}
.tree-view {
padding: 12px;
flex: 1;
overflow-y: auto;
}
.tree-folder {
font-weight: 700;
color: var(--text-primary);
margin: 8px 0 4px 0;
}
.tree-item {
padding: 4px 8px 4px 16px;
color: var(--text-muted);
cursor: pointer;
border-radius: 2px;
display: flex;
justify-content: space-between;
transition: none;
}
.tree-item:hover {
color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.tree-item.active {
color: var(--accent-cyan);
background-color: var(--bg-canvas);
border-left: 2px solid var(--accent-cyan);
font-weight: 700;
}
/* Bottom ASCII Companion Panel */
.sidebar-companion {
border-top: 1px solid var(--border-color);
padding: 10px;
background-color: var(--bg-canvas);
font-family: 'JetBrains Mono', monospace;
font-size: 0.72rem;
color: var(--text-muted);
line-height: 1.2;
}
.companion-art {
color: var(--accent-cyan);
white-space: pre;
}
/* Right Document Viewer */
.main-pane {
flex: 1;
padding: 28px;
overflow-y: auto;
background-color: var(--bg-canvas);
}
.doc-container {
max-width: 65ch;
margin: 0 auto;
}
.doc-path {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 12px;
}
.doc-title {
font-family: 'IBM Plex Mono', monospace;
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 16px;
color: var(--text-primary);
}
.code-block {
border: 1px solid var(--border-color);
background-color: var(--bg-surface);
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
border-radius: 4px;
margin: 16px 0;
overflow: hidden;
}
.code-header {
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
padding: 6px 12px;
font-size: 0.75rem;
color: var(--text-muted);
}
.code-content {
padding: 12px;
overflow-x: auto;
}
.tag-cluster {
display: flex;
gap: 8px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
color: var(--text-muted);
margin-top: 16px;
}
/* Bottom Status Bar */
.status-bar {
height: 28px;
background-color: var(--bg-surface);
border-top: 1px solid var(--border-color);
padding: 0 16px;
display: flex;
align-items: center;
justify-content: space-between;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.75rem;
color: var(--text-muted);
}
</style>
</head>
<body>
<!-- Top Breadcrumbs Bar -->
<header class="top-nav">
<div>
<span class="status-dot"></span>
<span>RIVER ROOKS // TREE EXPLORER</span>
</div>
<div>
<span>POSITION: /home/river/services/grafana.md</span>
</div>
</header>
<!-- Dual Split View -->
<div class="split-view">
<!-- Left Explorer Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">FILE SYSTEM EXPLORER</div>
<div class="tree-view">
<div class="tree-folder">▾ /services/</div>
<div class="tree-item active" onclick="selectItem(this, 'grafana')">grafana.md <span>[SVC]</span></div>
<div class="tree-item" onclick="selectItem(this, 'vault')">vault.md <span>[SVC]</span></div>
<div class="tree-item" onclick="selectItem(this, 'wireguard')">wireguard.md <span>[SVC]</span></div>
<div class="tree-folder">▾ /notes/</div>
<div class="tree-item" onclick="selectItem(this, 'traefik-note')">traefik-mtls.md <span>[BLOG]</span></div>
<div class="tree-item" onclick="selectItem(this, 'ctf-note')">defcon-ctf.md <span>[BLOG]</span></div>
<div class="tree-folder">▾ /resume/</div>
<div class="tree-item" onclick="selectItem(this, 'whoami')">whoami.md <span>[CV]</span></div>
<div class="tree-item" onclick="selectItem(this, 'stack')">active_stack.conf <span>[CV]</span></div>
</div>
<!-- ASCII Companion Panel -->
<div class="sidebar-companion">
<div class="companion-art" id="mascot-art">
/\___/\
( o o ) sys-daemon
( =^= ) STATUS: IDLE
\__*__/ LOG: ready█
</div>
</div>
</aside>
<!-- Right Main Document View -->
<main class="main-pane">
<div class="doc-container" id="doc-view">
<!-- Default Content: Grafana Service -->
<div class="doc-path">file:///home/river/services/grafana.md</div>
<h1 class="doc-title">Service: grafana.homelab</h1>
<p>
Primary telemetry and Prometheus monitoring dashboard. Aggregates host node resource usage, Docker container stats, and Traefik router throughput.
</p>
<div class="code-block">
<div class="code-header">docker-compose.yml</div>
<div class="code-content">
<span style="color: var(--accent-cyan);">services:</span>
<span style="color: var(--accent-cyan);">grafana:</span>
image: grafana/grafana-oss:latest
container_name: homelab_grafana
ports:
- "3000:3000"
restart: unless-stopped
</div>
</div>
<div class="tag-cluster">
<span>[#homelab]</span>
<span>[#prometheus]</span>
<span>[#telemetry]</span>
<span>[STATUS: ONLINE]</span>
</div>
</div>
</main>
</div>
<!-- Bottom Status Bar -->
<footer class="status-bar">
<div>UTF-8 | LF | POSIX Shell</div>
<div>Use Arrow Keys / Mouse to inspect workspace items</div>
</footer>
<script>
const docs = {
'grafana': `
<div class="doc-path">file:///home/river/services/grafana.md</div>
<h1 class="doc-title">Service: grafana.homelab</h1>
<p>Primary telemetry and Prometheus monitoring dashboard. Aggregates host node resource usage, Docker container stats, and Traefik router throughput.</p>
<div class="code-block">
<div class="code-header">docker-compose.yml</div>
<div class="code-content">
<span style="color: var(--accent-cyan);">services:</span>
<span style="color: var(--accent-cyan);">grafana:</span>
image: grafana/grafana-oss:latest
container_name: homelab_grafana
ports:
- "3000:3000"
restart: unless-stopped
</div>
</div>
<div class="tag-cluster"><span>[#homelab]</span><span>[#prometheus]</span><span>[#telemetry]</span><span>[STATUS: ONLINE]</span></div>
`,
'vault': `
<div class="doc-path">file:///home/river/services/vault.md</div>
<h1 class="doc-title">Service: vault.homelab</h1>
<p>HashiCorp Vault cluster managing secrets, SSH keys, internal PKI certificates, and mTLS auto-renewal for homelab nodes.</p>
<div class="code-block">
<div class="code-header">vault-config.hcl</div>
<div class="code-content">
storage "file" { path = "/vault/file" }
listener "tcp" { address = "0.0.0.0:8200" tls_disable = 0 }
ui = true
</div>
</div>
<div class="tag-cluster"><span>[#vault]</span><span>[#security]</span><span>[#mtls]</span><span>[STATUS: ONLINE]</span></div>
`,
'wireguard': `
<div class="doc-path">file:///home/river/services/wireguard.md</div>
<h1 class="doc-title">Service: wireguard.homelab</h1>
<p>Encrypted WireGuard mesh network ingress connecting offsite cloud VPS nodes directly to the internal homelab network.</p>
<div class="tag-cluster"><span>[#networking]</span><span>[#wireguard]</span><span>[#vpn]</span><span>[STATUS: INTERNAL]</span></div>
`,
'traefik-note': `
<div class="doc-path">file:///home/river/notes/traefik-mtls.md</div>
<h1 class="doc-title">Hardening Traefik Ingress with HashiCorp Vault mTLS</h1>
<p>Technical write-up detailing zero-trust certificate auto-renewal pipelines using Vault PKI backend and Traefik dynamic configuration reloads.</p>
<div class="tag-cluster"><span>[#blog]</span><span>[#traefik]</span><span>[#security]</span><span>[DATE: 2026-08-02]</span></div>
`,
'ctf-note': `
<div class="doc-path">file:///home/river/notes/defcon-ctf.md</div>
<h1 class="doc-title">DEFCON CTF Qualifier: Memory Corruption Write-Up</h1>
<p>Deep-dive analysis on exploitation techniques involving glibc malloc heap metadata corruption and arbitrary write primitives.</p>
<div class="tag-cluster"><span>[#ctf]</span><span>[#binary]</span><span>[#pwn]</span><span>[DATE: 2026-07-18]</span></div>
`,
'whoami': `
<div class="doc-path">file:///home/river/resume/whoami.md</div>
<h1 class="doc-title">River Rooks - Biography</h1>
<p>Cybersecurity student at the University of Alabama College of Engineering, focusing on network forensics, self-hosted infrastructure, and hardened kernel configs.</p>
<div class="tag-cluster"><span>[#cs]</span><span>[#cybersecurity]</span><span>[#resume]</span></div>
`,
'stack': `
<div class="doc-path">file:///home/river/resume/active_stack.conf</div>
<h1 class="doc-title">Active Engineering Stack</h1>
<p>System configuration & tech stack specifications:</p>
<div class="code-block">
<div class="code-header">stack.conf</div>
<div class="code-content">
LANGUAGES="POSIX sh, Python, C, Lua"
TOOLS="Docker, Traefik, WireGuard, HashiCorp Vault, Neovim"
OS="Arch Linux / POSIX Compliance"
</div>
</div>
`
};
function selectItem(element, docKey) {
document.querySelectorAll('.tree-item').forEach(el => el.classList.remove('active'));
element.classList.add('active');
document.getElementById('doc-view').innerHTML = docs[docKey] || '';
// Update mascot state
const mascot = document.getElementById('mascot-art');
mascot.innerText = ` /\\___/\\\n ( - - ) sys-daemon\n ( =o= ) STATUS: READING\n \\__*__/ LOG: ${docKey}_`;
setTimeout(() => {
mascot.innerText = ` /\\___/\\\n ( o o ) sys-daemon\n ( =^= ) STATUS: IDLE\n \\__*__/ LOG: ready█`;
}, 1200);
}
</script>
</body>
</html>
-106
View File
@@ -1,106 +0,0 @@
# Design Specification: Sample 3 (Companion Layout)
**Status:** Ready for Handoff
**Target Page:** `companion.html`
**Layout Type:** 50/50 Split Screen (Reactive Desktop, Stacked Mobile)
**Theme Vibe:** Hyper-minimalist terminal utility, clean information hierarchy, zero animation noise.
---
## 1. Design Philosophy & Guidelines
This sample modifies the primary design spec by enforcing a strict **"interaction suppression"** policy. Dynamic decorations (such as border color transitions or card expansion effects) are completely disallowed. Contrast and active states are communicated solely through instantaneous typography color changes.
The key feature is a persistent digital companion on the right half of the screen. The companion must feel like a quiet terminal process rather than an active distraction.
---
## 2. Layout Geometry & Grid
The layout divides the viewport down the middle on desktop screens.
### Desktop Layout (Width > 768px)
- **Top Header:** Sticky `56px` navigation bar containing breadcrumbs/catalog links.
- **Split Container:** Spans 100% viewport width and 100% viewport height minus header (`height: calc(100vh - 56px)`).
- **Columns:**
- **Left Pane (50%):** Scrollable container. Houses a single-column list of project cards.
- **Right Pane (50%):** Fixed/non-scrollable container. Houses the centered ASCII companion terminal widget.
### Mobile Layout (Width <= 768px)
- The layout collapses into a single vertical stack.
- The ASCII companion pane moves to the top (fixed height: `300px`).
- The content pane follows below, scrollable within the main document window.
---
## 3. Color Palette & Typography
### Base Colors
- **Canvas Background:** `#0d0f12` (Deep charcoal slate)
- **Surface Background:** `#121417` (Slightly lighter dark surface)
- **Static Borders:** `#2b303b` (1px solid, low contrast)
- **Primary Text:** `#e0e6f0` (Off-white prose)
- **Muted Text:** `#8a95a5` (Timestamp / status information)
- **Active Accent:** `#00f0ff` (High-contrast Cyan)
### Typography
- **UI & ASCII Content:** Monospace (`JetBrains Mono` or `IBM Plex Mono`).
- **Prose Content:** Sans-serif (`Inter` or `Geist Sans`) for high readability.
---
## 4. Content Cards (Left Column)
Each card is a clean, structural text panel.
### Structure
- 1px static border (`#2b303b`) with `4px` border-radius.
- No background color (inherits Canvas background) or a solid `#121417` surface background.
- Elements:
1. **Header (`h3`):** Monospaced, bold, `#e0e6f0`.
2. **Description (`p`):** Sans-serif, `#8a95a5`, capped at `65ch` length.
### Interactions (Ultra-Minimal)
- **No Border Shifts:** Borders must remain `#2b303b` at all times.
- **No Translation:** Hovering must not scale, shift, or offset the card.
- **Instant Typography Snap:** Hovering or focusing the card snaps the text color of the Header (`h3`) from `#e0e6f0` to Cyan (`#00f0ff`) instantly.
- **CSS Rule:** Use `transition: none;` on all hover effects.
---
## 5. ASCII Companion (Right Column)
The companion is rendered as block-level ASCII text inside a simulated terminal panel.
### Layout Widget
- Sits centered in the right pane.
- Styled to resemble a terminal output terminal: card box with a tab bar indicating the daemon name `sys-daemon`.
### Animation & States
The companion has two interactive states:
#### State 1: IDLE
- **Mascot Art:**
```text
/\___/\
( o o ) sys-daemon
( =^= ) STATUS: IDLE
\__*__/ LOG: awaiting input█
```
- **Animation:** The terminal block cursor (`█`) blinks continuously at 1Hz (`opacity: 0` to `1` instantly). The rest of the mascot is static.
#### State 2: READING (Triggered on Card Hover)
- **Mascot Art:**
```text
/\___/\
( - - ) sys-daemon
( =o= ) STATUS: READING
\__*__/ LOG: reading module_
```
- **Reaction Log:** Underneath the mascot, a mini status output log updates to display the title of the card currently hovered. For example:
`[sys-daemon] target: /dev/mesh-mesh-topology-builder`
### Behavior Scripting Rules
- Attach `mouseenter` and `mouseleave` event listeners to the cards in the left pane.
- On hover, swap the inner HTML of the ASCII container to the `READING` frame and append the card's target name to the log output.
- Revert back to the `IDLE` frame immediately when the cursor leaves the card.
-473
View File
@@ -1,473 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Minimalist Split Panel Layout</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;700&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap');
/* Design Spec Token Definition */
:root {
--bg-canvas: #121417;
--bg-surface: #1c1f26;
--border-color: #2b303b;
--text-primary: #e0e6f0;
--text-muted: #8a95a5;
--accent: #00ff66; /* ANSI Green */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: 'Inter', sans-serif;
line-height: 1.6;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Sticky Navigation Header at the top */
.sticky-header {
position: sticky;
top: 0;
height: 56px;
background-color: var(--bg-canvas);
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
z-index: 100;
font-family: 'IBM Plex Mono', monospace;
}
.logo-container {
font-weight: 700;
font-size: 1rem;
color: var(--text-primary);
}
.logo-prompt {
color: var(--accent);
}
.header-nav {
display: flex;
gap: 16px;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.9rem;
padding: 4px 10px;
border: 1px solid transparent;
display: inline-flex;
align-items: center;
}
/* Snappy Hover Inversion */
.nav-link:hover {
background-color: var(--accent);
color: var(--bg-canvas);
}
.nav-link:focus {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* Structural Indicator for Active Navigation Items */
.nav-link.active::before {
content: "> ";
color: var(--accent);
margin-right: 4px;
}
.nav-link.active:hover::before {
color: var(--bg-canvas);
}
/* Split Container Layout */
.split-container {
display: flex;
flex: 1;
height: calc(100vh - 56px);
overflow: hidden;
}
@media (max-width: 768px) {
.split-container {
flex-direction: column;
overflow-y: auto;
}
.left-pane {
width: 100% !important;
height: auto !important;
border-right: none !important;
border-bottom: 1px solid var(--border-color);
padding: 40px 24px !important;
}
.right-pane {
height: auto !important;
overflow-y: visible !important;
}
}
/* Left Pane - Sticky Terminal Mascot (non-scrolling) */
.left-pane {
width: 380px;
background-color: var(--bg-canvas);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px;
flex-shrink: 0;
}
.terminal-card {
width: 100%;
border: 1px solid var(--border-color);
background-color: var(--bg-surface);
border-radius: 4px;
padding: 20px;
font-family: 'IBM Plex Mono', monospace;
}
.terminal-card-header {
font-size: 0.75rem;
color: var(--text-muted);
margin-bottom: 16px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
display: flex;
justify-content: space-between;
}
.mascot-pre {
font-size: 0.8rem;
line-height: 1.2;
color: var(--accent);
background-color: var(--bg-canvas);
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 2px;
overflow: hidden;
white-space: pre;
}
.status-panel {
margin-top: 16px;
font-size: 0.8rem;
color: var(--text-primary);
}
.status-row {
display: flex;
justify-content: space-between;
margin-bottom: 4px;
}
.status-label {
color: var(--text-muted);
}
.status-value {
font-weight: 500;
}
/* Right Pane - Scrollable Content feed */
.right-pane {
flex: 1;
overflow-y: auto;
background-color: var(--bg-canvas);
padding: 40px 24px;
display: flex;
justify-content: center;
}
.scroll-content {
width: 100%;
max-width: 70ch; /* Cap readability width */
}
.section-title {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 1.4rem;
color: var(--text-primary);
margin-bottom: 24px;
border-bottom: 1px solid var(--border-color);
padding-bottom: 8px;
}
/* Tile Cards Component */
.tile-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.tile {
background-color: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 24px;
text-decoration: none;
color: inherit;
display: block;
transition: none; /* Instant snap interactions */
}
/* Snappy Hover State (Border highlight & accent title) */
.tile:hover {
border-color: var(--accent);
}
.tile:focus {
outline: 2px solid var(--accent);
outline-offset: 4px;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 8px;
}
.tile-title {
font-family: 'IBM Plex Mono', monospace;
font-weight: 700;
font-size: 1.1rem;
color: var(--text-primary);
}
.tile:hover .tile-title {
color: var(--accent);
}
.tile-meta {
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
}
.tile-description {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.5;
}
/* Footer styling inside the scroll view */
.pane-footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
font-family: 'IBM Plex Mono', monospace;
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: space-between;
}
</style>
</head>
<body>
<!-- Sticky Top Header -->
<header class="sticky-header">
<div class="logo-container">
<span class="logo-prompt">$</span> agy --layout=split
</div>
<nav class="header-nav">
<a href="index.html" class="nav-link">Catalog</a>
<a href="homepage.html" class="nav-link">Sample 1</a>
<a href="split-panel.html" class="nav-link active">Sample 2</a>
<a href="companion.html" class="nav-link">Sample 3</a>
<a href="document-flow.html" class="nav-link">Sample 4</a>
<a href="portfolio.html" class="nav-link">Sample 5</a>
</nav>
</header>
<!-- Split Panel Main Container -->
<div class="split-container">
<!-- Left Pane - Center-Aligned ASCII Art Terminal Mascot -->
<aside class="left-pane">
<div class="terminal-card">
<div class="terminal-card-header">
<span>sys_monitor.sh</span>
<span>ONLINE</span>
</div>
<pre id="ascii-mascot" class="mascot-pre">
.-----------------.
/ ^ ^ ^ /|
/ (_) (_) (_) / |
/__________________/ |
| ___ ___ | |
| [o_o] [o_o] | |
| `-' `-' | |
| ______ | |
| / \ | /
| | -- | | /
| \____/ |/
`------------------'
</pre>
<div class="status-panel">
<div class="status-row">
<span class="status-label">Target:</span>
<span class="status-value">AGY-Node-A2</span>
</div>
<div class="status-row">
<span class="status-label">Status:</span>
<span class="status-value" style="color: var(--accent);">ANIMATING</span>
</div>
<div class="status-row">
<span class="status-label">Engine:</span>
<span class="status-value">Vanilla JS</span>
</div>
</div>
</div>
</aside>
<!-- Right Pane - Scrollable List of Cards -->
<main class="right-pane">
<div class="scroll-content">
<h2 class="section-title">Developer Workspace Projects</h2>
<div class="tile-list">
<!-- Tile 1 -->
<a href="#" class="tile">
<div class="tile-header">
<h3 class="tile-title">Custom POSIX Shell Parser</h3>
<span class="tile-meta">v1.2.0</span>
</div>
<p class="tile-description">
A lightweight Lexer and AST parser written in Lua for parsing POSIX compliant shell scripts. Highly optimized for syntax coloring inside modal environments.
</p>
</a>
<!-- Tile 2 -->
<a href="#" class="tile">
<div class="tile-header">
<h3 class="tile-title">WireGuard Mesh Topology Builder</h3>
<span class="tile-meta">v0.8.4</span>
</div>
<p class="tile-description">
Automated configuration generator for setting up full-mesh WireGuard private overlays across cloud instances and local homelab nodes.
</p>
</a>
<!-- Tile 3 -->
<a href="#" class="tile">
<div class="tile-header">
<h3 class="tile-title">Zero-Allocation Logger (Go)</h3>
<span class="tile-meta">v2.1.1</span>
</div>
<p class="tile-description">
A high-performance JSON structure logger that uses sync.Pool buffers to avoid memory allocation overhead during intensive disk operations.
</p>
</a>
<!-- Tile 4 -->
<a href="#" class="tile">
<div class="tile-header">
<h3 class="tile-title">Embedded Kernel Boot Strap</h3>
<span class="tile-meta">v0.1.0</span>
</div>
<p class="tile-description">
Minimalist boot sector and initial kernel bootstrap for ARM Cortex-M microcontrollers to handle memory map initialization and early UART status printouts.
</p>
</a>
<!-- Tile 5 -->
<a href="#" class="tile">
<div class="tile-header">
<h3 class="tile-title">Traefik Cloudflare Dynamic DNS Daemon</h3>
<span class="tile-meta">v1.0.2</span>
</div>
<p class="tile-description">
Background controller daemon designed to sync Docker container network labels with Cloudflare DNS records to route traffic to local homelab subdomains.
</p>
</a>
</div>
<footer class="pane-footer">
<span>Split Panel Spec Sample</span>
<span>© 2026 Developer System</span>
</footer>
</div>
</main>
</div>
<!-- Lightweight Script for Mascot Animation (Binary Steps) -->
<script>
const mascot = document.getElementById('ascii-mascot');
const frames = [
// Frame 1: Standard Look
` .-----------------.
/ ^ ^ ^ /|
/ (_) (_) (_) / |
/__________________/ |
| ___ ___ | |
| [o_o] [o_o] | |
| `-' `-' | |
| ______ | |
| / \\ | /
| | -- | | /
| \\____/ |/
\`------------------'`,
// Frame 2: Blinking Eyes / Surprised Mouth
` .-----------------.
/ ^ ^ ^ /|
/ (_) (_) (_) / |
/__________________/ |
| ___ ___ | |
| [-_-] [-_-] | |
| \`-' \`-' | |
| ______ | |
| / \\ | /
| | O | | /
| \\____/ |/
\`------------------'`,
// Frame 3: Winking Eyes / Grinning Mouth
` .-----------------.
/ ^ ^ ^ /|
/ (_) (_) (_) / |
/__________________/ |
| ___ ___ | |
| [o_-] [o_-] | |
| \`-' \`-' | |
| ______ | |
| / \\ | /
| | == | | /
| \\____/ |/
\`------------------'`
];
let currentFrame = 0;
setInterval(() => {
currentFrame = (currentFrame + 1) % frames.length;
mascot.innerText = frames[currentFrame];
}, 850);
</script>
</body>
</html>
-86
View File
@@ -1,86 +0,0 @@
# Unified Design Specification: Homelab, Blog & Engineering Portfolio
**Target Platform:** Integrated Personal Site (Public Services Directory, Technical Blog/Notes, Engineering Resume)
**Design Philosophy:** High-signal, low-noise. Developer-centric minimalism, modal text editor & terminal emulator craftsmanship.
**Vibe References:** Zed Editor, Ghostty, Noctalia.dev, Tiling Window Managers (Hyprland/i3).
**Strict Anti-Patterns:** Zero glassmorphism, zero glows/shadows, zero gradients, zero spring animations, zero rounded SaaS "blobs".
---
## 1. Executive Summary & Unified Purpose
This specification unites the design language across three primary user goals:
1. **Services Directory:** Instant access, routing links, and status visibility for self-hosted homelab services and public utilities.
2. **Technical Notes & Blog:** High-density, comfortable reading experience for deep technical writing (CTF write-ups, architecture specs, config guides, code snippets).
3. **Engineering Resume & Portfolio:** A crisp, high-craft presentation of skills, project archives, and active tech stack designed to impress technical recruiters and engineering leaders.
---
## 2. Unified Design Language & Rules
### Global Constraints
* **NO Glassmorphism:** Zero `backdrop-filter: blur()`, frosted glass, or translucent overlays.
* **NO Glows or Drop Shadows:** Depth is constructed exclusively via subtle surface color contrast and 1px solid borders.
* **NO Gradients:** Pure, solid color tokens across canvas, surfaces, and accent elements.
* **NO Spring/Float Animations:** Interactions feature instant snapped state transitions (`transition: none;`) or binary 1Hz blinking cursors/indicators.
* **Minimal Radii:** Sharp corners preferred. Border radii capped strictly at `0px` to `4px`.
### Geometry & Spatial Hierarchy
* **Grid Baseline:** Strict 4pt / 8pt baseline grid for consistent spacing and padding.
* **Reading Density:** Article and prose containers capped at `65ch` max width.
* **Dividers:** Clean 1px solid horizontal lines (`<hr>`) used to delineate major structural sections.
---
## 3. Unified Color Palette
The color system is dark-mode native, built from muted charcoal slate tones reminiscent of modern terminal emulators and modal text editors.
| Token Key | Hex Value | Application & Role |
| :--- | :--- | :--- |
| `--bg-canvas` | `#0d0f12` | Main canvas background (deep charcoal slate). |
| `--bg-surface` | `#121417` | Card backgrounds, terminal blocks, sidebars. |
| `--bg-elevated` | `#1c1f26` | Hovered card state, active tab surface. |
| `--border-color` | `#2b303b` | Structural 1px grid lines, card borders, horizontal dividers. |
| `--text-primary` | `#e0e6f0` | Off-white primary text for headings and body prose. |
| `--text-muted` | `#8a95a5` | Monospaced metadata, timestamps, tags, labels, status text. |
| `--accent-cyan` | `#00f0ff` | Primary active accent (terminal prompt, active nav snap, focus outline). |
| `--accent-green` | `#00e676` | Status indicator: Online / Operational / Verified. |
| `--accent-amber` | `#ffb700` | Status indicator: Under Construction / Maintenance / Warning. |
---
## 4. Digital & Typographical Language
### Dual Typography Hierarchy
* **Monospace Layer (`IBM Plex Mono`, `JetBrains Mono`):**
- **Usage:** Headings (`H1`-`H3`), navigation links, breadcrumbs, tags, terminal code blocks, timestamps, metadata, and keyboard shortcuts (`<kbd>`).
- **Weights:** Medium (500), Bold (700).
* **Prose Layer (`Inter`, `Geist Sans`):**
- **Usage:** Article body prose, project summaries, resume biography.
- **Line Height:** `1.65` to `1.7`.
- **Weight:** Regular (400).
### Digital Interface Conventions
* **POSIX-Style Path Headers:** Titles and section markers formatted as file paths (e.g., `/var/log/status.txt`, `/etc/services`, `~/notes/ctf-2026.md`).
* **Active Status Dots:** Rigid 8x8px square indicators (`.status-dot`) signaling system state (Cyan/Green for operational, Amber for construction).
* **Metadata Tag Cluster:** Monospaced bracketed tags (e.g., `[#homelab]`, `[#docker-traefik]`, `[#cybersecurity]`).
* **Instantaneous Snap Hover:** Interactive elements transition without delay (`transition: none;`), snapping text to Cyan (`#00f0ff`) or inverting background colors.
* **Keyboard Shortcut Badges (`<kbd>`):** Crisp, 1px bordered boxes depicting keys (e.g., `Ctrl` + `K`, `Tab`).
---
## 5. Functional Layout Modules
### A. Homelab & Public Services Directory
* Dense tabular grid listing self-hosted apps and public APIs.
* Displays service name, description, status badge (`[ONLINE]`, `[INTERNAL]`, `[PUBLIC]`), and routing link.
### B. Technical Notes & Blog
* Tabular article archive indexed by date, title, reading duration, and tags.
* Terminal-style code snippets with filename headers (e.g., `traefik.yml`) and copy utility.
### C. Resume & Portfolio Showcase
* Header identity block with `whoami` summary and active stack cluster.
* Project showcase cards featuring instant snap state.
* Optional integrated ASCII companion daemon (`sys-daemon`) for interactive module inspection.
+413
View File
@@ -0,0 +1,413 @@
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { exec } from "node:child_process";
import { fileURLToPath } from "node:url";
const MIME_TYPES = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "application/javascript; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".xml": "application/xml; charset=utf-8",
".svg": "image/svg+xml",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".ttf": "font/ttf",
".txt": "text/plain; charset=utf-8"
};
/**
* Normalizes Obsidian-specific syntax (wikilinks, internal attachments)
* into standard web URLs and image references.
* @param {string} content Raw Markdown content from Obsidian
* @returns {string} Normalized Markdown content
*/
export function normalizeObsidianMarkdown(content) {
if (typeof content !== "string") return "";
let normalized = content;
// Bridge created -> date so Eleventy collections sorting and filters work seamlessly
const createdMatch = normalized.match(/^created:\s*([^\r\n]+)$/m);
const hasDate = /^date:\s*.+$/m.test(normalized);
if (createdMatch && !hasDate) {
normalized = normalized.replace(/^(---\r?\n)/, `$1date: ${createdMatch[1].trim()}\n`);
}
// Bridge layout -> layouts/post.njk so Obsidian notes render with the blog theme and CSS
const hasLayout = /^layout:\s*.+$/m.test(normalized);
if (!hasLayout) {
normalized = normalized.replace(/^(---\r?\n)/, "$1layout: layouts/post.njk\n");
}
// 1. Convert internal attachments: ![[image.png|alt]] or ![[image.png]]
normalized = normalized.replace(/!\[\[([^|\]]+)(?:\|([^\]]+))?\]\]/g, (match, filename, alt) => {
const cleanFilename = path.basename(filename.trim());
const altText = alt ? alt.trim() : cleanFilename;
return `![${altText}](/assets/images/posts/${cleanFilename})`;
});
// 2. Convert wikilinks with label: [[target-slug|Custom Label]] -> [Custom Label](/posts/target-slug/)
normalized = normalized.replace(/\[\[([^|\]]+)\|([^\]]+)\]\]/g, (match, target, label) => {
return `[${label.trim()}](${wikilinkUrl(target)})`;
});
// 3. Convert simple wikilinks: [[target-slug]] -> [target-slug](/posts/target-slug/)
normalized = normalized.replace(/\[\[([^\]]+)\]\]/g, (match, target) => {
const trimmed = target.trim();
return `[${trimmed}](${wikilinkUrl(trimmed)})`;
});
return normalized;
}
function wikilinkUrl(target) {
const trimmed = target.trim();
const fragmentIndex = trimmed.indexOf("#");
if (fragmentIndex === -1) {
return `/posts/${slugify(trimmed)}/`;
}
const noteSlug = slugify(trimmed.slice(0, fragmentIndex));
const headerSlug = slugify(trimmed.slice(fragmentIndex + 1));
const fragment = headerSlug ? `#${headerSlug}` : "";
return `/posts/${noteSlug}/${fragment}`;
}
function slugify(text) {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "");
}
let isBuilding = false;
let buildQueued = false;
/**
* Rebuilds the static site using Eleventy.
* Uses a build queue to prevent concurrent build races.
* @returns {Promise<void>}
*/
export function triggerEleventyBuild() {
if (isBuilding) {
buildQueued = true;
return Promise.resolve();
}
const root = process.cwd();
const hasConfig = fs.existsSync(path.join(root, "eleventy.config.js")) ||
fs.existsSync(path.join(root, ".eleventy.js")) ||
fs.existsSync(path.join(root, "node_modules/@11ty/eleventy"));
if (!hasConfig) {
return Promise.resolve();
}
isBuilding = true;
return new Promise((resolve) => {
exec("npx @11ty/eleventy", (err) => {
isBuilding = false;
if (err) {
console.error("[Ingestion API] Eleventy build error:", err);
} else {
console.log("[Ingestion API] Eleventy build completed successfully.");
}
if (buildQueued) {
buildQueued = false;
triggerEleventyBuild().then(resolve);
} else {
resolve();
}
});
});
}
/**
* Creates the HTTP server instance for serving static files and handling ingestion API
* @param {string} [customSiteDir] Optional custom directory for _site
* @param {string} [customPostsDir] Optional custom directory for posts
* @param {string} [customAssetsDir] Optional custom directory for uploaded post assets
* @returns {http.Server}
*/
export function createServer(customSiteDir, customPostsDir, customAssetsDir) {
const siteDir = path.resolve(customSiteDir || path.join(process.cwd(), "_site"));
const postsDir = path.resolve(customPostsDir || path.join(process.cwd(), "src/posts"));
const assetsDir = path.resolve(customAssetsDir || path.join(process.cwd(), "src/assets/images/posts"));
const isCustomEnv = Boolean(customSiteDir || customPostsDir || customAssetsDir);
// Ensure default directory data file exists for layout inheritance
try {
if (!fs.existsSync(postsDir)) {
fs.mkdirSync(postsDir, { recursive: true });
}
const postsJsonPath = path.join(postsDir, "posts.json");
if (!fs.existsSync(postsJsonPath)) {
fs.writeFileSync(postsJsonPath, JSON.stringify({ layout: "layouts/post.njk" }, null, 2));
}
} catch (err) {
console.warn("[Server] Could not initialize posts.json directory data file:", err.message);
}
return http.createServer(async (req, res) => {
// Standard security headers
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
const parsedUrl = new URL(req.url, "http://localhost");
// Health check endpoint for container orchestrators & telemetry
if (parsedUrl.pathname === "/healthz" || parsedUrl.pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ status: "ok", uptime: process.uptime() }));
return;
}
// Obsidian Ingestion API: /api/posts
if (parsedUrl.pathname === "/api/posts") {
const authHeader = req.headers["authorization"] || "";
const expectedSecret = process.env.API_SECRET;
if (!expectedSecret) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Server API_SECRET is not configured" }));
return;
}
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (!match || match[1] !== expectedSecret) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized: Invalid or missing Bearer token" }));
return;
}
if (req.method === "POST") {
let body = "";
req.on("data", (chunk) => {
body += chunk;
if (body.length > 50 * 1024 * 1024) {
res.writeHead(413, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Payload too large (50MB limit)" }));
req.destroy();
}
});
req.on("end", async () => {
let payload;
try {
payload = JSON.parse(body);
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid JSON payload" }));
return;
}
const { slug, content, assets } = payload;
if (!slug || typeof slug !== "string" || !content || typeof content !== "string") {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing required fields: 'slug' and 'content'" }));
return;
}
const cleanSlug = slug.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
if (!cleanSlug) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid slug format" }));
return;
}
const hasTitle = /^title:\s*.+$/m.test(content);
const hasDate = /^date:\s*.+$/m.test(content) || /^created:\s*.+$/m.test(content);
if (!hasTitle || !hasDate) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing required frontmatter: 'title' and 'created' (or 'date') are required" }));
return;
}
try {
fs.mkdirSync(postsDir, { recursive: true });
fs.mkdirSync(assetsDir, { recursive: true });
const normalizedContent = normalizeObsidianMarkdown(content);
const targetFilePath = path.join(postsDir, `${cleanSlug}.md`);
fs.writeFileSync(targetFilePath, normalizedContent, "utf-8");
if (Array.isArray(assets)) {
for (const asset of assets) {
if (asset && asset.filename && asset.data) {
const cleanName = path.basename(asset.filename).replace(/[^a-zA-Z0-9._-]/g, "");
if (cleanName) {
const assetPath = path.join(assetsDir, cleanName);
fs.writeFileSync(assetPath, Buffer.from(asset.data, "base64"));
}
}
}
}
// Rebuild static site with Eleventy if in standard environment
if (!isCustomEnv) {
await triggerEleventyBuild();
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
status: "published",
slug: cleanSlug,
path: `/posts/${cleanSlug}/`
}));
} catch (writeErr) {
console.error("[Ingestion API] Error saving post/assets:", writeErr);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: `Failed to write file to disk: ${writeErr.message}` }));
}
});
return;
}
if (req.method === "DELETE") {
const querySlug = parsedUrl.searchParams.get("slug");
if (!querySlug) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Missing 'slug' parameter" }));
return;
}
const cleanSlug = querySlug.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
if (!cleanSlug) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Invalid slug parameter" }));
return;
}
// 1. Remove source markdown post
const targetFilePath = path.join(postsDir, `${cleanSlug}.md`);
if (fs.existsSync(targetFilePath)) {
fs.unlinkSync(targetFilePath);
}
// 2. Remove compiled static site directory and file
const compiledDir = path.join(siteDir, "posts", cleanSlug);
if (fs.existsSync(compiledDir)) {
fs.rmSync(compiledDir, { recursive: true, force: true });
}
const compiledHtml = path.join(siteDir, "posts", `${cleanSlug}.html`);
if (fs.existsSync(compiledHtml)) {
fs.unlinkSync(compiledHtml);
}
// 3. Rebuild static collections (index.html, dispatches, tags) with Eleventy
if (!isCustomEnv) {
await triggerEleventyBuild();
}
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "deleted", slug: cleanSlug }));
return;
}
res.writeHead(405, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Method Not Allowed" }));
return;
}
// Path normalization & security validation against directory traversal
let decodedPath = "";
try {
decodedPath = decodeURIComponent(parsedUrl.pathname);
} catch {
res.writeHead(400, { "Content-Type": "text/plain" });
res.end("Bad Request");
return;
}
const resolvedPath = path.resolve(siteDir, `.${decodedPath}`);
// Boundary check: ensure target path is within siteDir
if (!resolvedPath.startsWith(siteDir)) {
res.writeHead(403, { "Content-Type": "text/plain" });
res.end("Forbidden");
return;
}
let targetFile = resolvedPath;
// Directory index resolution
if (fs.existsSync(targetFile) && fs.statSync(targetFile).isDirectory()) {
targetFile = path.join(targetFile, "index.html");
}
// Clean URL extensionless fallback (/posts/my-post -> /posts/my-post.html)
if (!fs.existsSync(targetFile) && fs.existsSync(`${targetFile}.html`)) {
targetFile = `${targetFile}.html`;
}
// Check if target file exists and is a regular file
if (fs.existsSync(targetFile) && fs.statSync(targetFile).isFile()) {
const ext = path.extname(targetFile).toLowerCase();
const contentType = MIME_TYPES[ext] || "application/octet-stream";
// Cache control: static media and webfonts get long-term caching; CSS, JS, and HTML get short revalidation
if (decodedPath.startsWith("/assets/") && !decodedPath.endsWith(".css") && !decodedPath.endsWith(".js")) {
res.setHeader("Cache-Control", "public, max-age=2592000, immutable");
} else {
res.setHeader("Cache-Control", "public, max-age=0, must-revalidate");
}
res.writeHead(200, { "Content-Type": contentType });
const stream = fs.createReadStream(targetFile);
stream.pipe(res);
return;
}
// 404 Not Found fallback
const custom404 = path.join(siteDir, "404.html");
if (fs.existsSync(custom404)) {
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
fs.createReadStream(custom404).pipe(res);
} else {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("404 Not Found");
}
});
}
// Direct CLI execution handler
const currentFilePath = fileURLToPath(import.meta.url);
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
const port = parseInt(process.env.PORT || "8080", 10);
const host = process.env.HOST || "0.0.0.0";
const siteDir = path.resolve(process.cwd(), "_site");
if (!fs.existsSync(siteDir)) {
console.warn(`[Static Server] Warning: '${siteDir}' does not exist yet. Run 'npm run build' first.`);
}
const server = createServer();
server.listen(port, host, () => {
console.log(`[Static Server] Serving '${siteDir}' on http://${host}:${port}`);
});
// Graceful termination handling
const shutdown = (signal) => {
console.log(`\n[Static Server] Received ${signal}. Shutting down gracefully...`);
server.close(() => {
process.exit(0);
});
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
}
+25
View File
@@ -0,0 +1,25 @@
---
layout: layouts/base.njk
permalink: /404.html
title: Page not found | River Rooks
excerpt: The requested page could not be found.
---
<section class="not-found tile" aria-labelledby="not-found-title">
<header class="tile-header">
<span class="tile-title">// ROUTE STATUS</span>
<span class="tile-meta">HTTP 404 &bull; NOT FOUND</span>
</header>
<div class="not-found-body">
<h1 class="not-found-code" id="not-found-title">404<span class="not-found-cursor" aria-hidden="true">_</span></h1>
<div class="not-found-message">
<p class="not-found-label">REQUEST FAILED / NO MATCHING ROUTE</p>
<h2>Page not found.</h2>
<p>The address you requested doesn't point to a page here. Check the URL, or choose a known route below.</p>
<nav class="not-found-links" aria-label="Recovery links">
<a href="/">&gt; Back to home</a>
<a href="/dispatches/">&gt; Browse dispatches</a>
</nav>
</div>
</div>
</section>
+130
View File
@@ -0,0 +1,130 @@
import fs from "node:fs";
import path from "node:path";
// Load .env variables into process.env if SITE_DOMAIN is not already defined
if (!process.env.SITE_DOMAIN) {
try {
const envPath = path.resolve(process.cwd(), ".env");
if (fs.existsSync(envPath)) {
const envContent = fs.readFileSync(envPath, "utf-8");
for (const line of envContent.split("\n")) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith("#") && trimmed.includes("=")) {
const [key, ...vals] = trimmed.split("=");
const value = vals.join("=").trim().replace(/^["']|["']$/g, "");
if (!process.env[key.trim()]) {
process.env[key.trim()] = value;
}
}
}
}
} catch (e) {
// Silent fallback
}
}
const siteDomain = process.env.SITE_DOMAIN;
if (!siteDomain) {
throw new Error(
"\n=======================================================================\n" +
"FATAL CONFIGURATION ERROR: SITE_DOMAIN environment variable is not set.\n" +
"The site domain must be explicitly defined in your .env file or environment.\n" +
"Example in .env:\n" +
" SITE_DOMAIN=riverrooks.dev\n" +
"=======================================================================\n"
);
}
const siteUrl = process.env.SITE_URL || `https://${siteDomain}`;
export default {
domain: siteDomain,
url: siteUrl,
name: "River Rooks",
title: "River Rooks | Homelab Gateway",
description: "CTF Writeups, Techincal Publucations, Homelab Journey, and Development Projects.",
status: "SYSTEMS NOMINAL",
author: "River Rooks",
nav: [
{
label: "Home",
url: "/"
},
{
label: "Dispatches",
url: "/dispatches/"
},
{
label: "Code",
url: `https://git.riverrooks.dev/venus`,
external: true
},
// {
// label: "RSS Feed",
// url: "/feed.xml"
// }
],
services: [
{
name: "Gitea",
url: `https://git.riverrooks.dev/venus`,
tag: `git.${siteDomain}:443`,
desc: "Self-hosted Git code repositories."
},
// {
// name: "CyberChef",
// url: `https://chef.${siteDomain}`,
// tag: `chef.${siteDomain}:443`,
// desc: "Industry tool for analyzing and decoding data."
// },
// {
// name: "Aperisolve",
// url: `https://aperisolve.${siteDomain}`,
// tag: `aperisolve.${siteDomain}:443`,
// desc: "Basic image steganography suite."
// },
{
name: "Streams",
url: `https://streams.${siteDomain}`,
tag: `streams.${siteDomain}:443`,
desc: "Self-hosted Steam aggregator for wathing live sports."
},
{
name: "Calibre",
url: `https://books.${siteDomain}`,
tag: `books.${siteDomain}:443`,
desc: `Calibre web frontend for managing and reading eBooks.`
},
{
name: "Wireguard",
url: `https://wg.${siteDomain}`,
tag: `wg.${siteDomain}:443`,
desc: `Self-hosted Wireguard VPN server.(Requires login)`
}
// {
// name: "Whoami",
// url: `https://whoami.${siteDomain}`,
// tag: `whoami.${siteDomain}:443`,
// desc: `Simple service to display request headers and connection information.`
// }
],
topics: [
{
title: "Systems Engineering",
slug: "systems",
desc: "Notes from my Journey building out interconnected systems.",
badge: "TRACK: SYSTEMS"
},
{
title: "Security Research",
slug: "security",
desc: "Notes from my CTF training and security research.",
badge: "TRACK: SECURITY"
},
{
title: "Homelab Infrastructure",
slug: "homelab",
desc: "Notes from building out my homelab.",
badge: "TRACK: INFRA"
}
]
};
+116
View File
@@ -0,0 +1,116 @@
import { DateTime } from "luxon";
import markdownIt from "markdown-it";
import { resolveLinkedArticles } from "../_utils/linked-articles.js";
/**
* Custom Eleventy Filters for River Rooks Blog
*/
const excerptMarkdown = markdownIt({ html: false });
export const renderExcerptMarkdown = (excerpt) => {
if (typeof excerpt !== "string") return "";
return excerptMarkdown.renderInline(excerpt);
};
export const postDate = (dateObj) => {
if (!dateObj) return "";
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
const dt = DateTime.fromJSDate(date, { zone: "utc" });
if (!dt.isValid) return "";
return dt.toFormat("LLL dd, yyyy").toUpperCase();
};
export const isoDate = (dateObj) => {
if (!dateObj) return "";
const date = typeof dateObj === "string" ? new Date(dateObj) : dateObj;
const dt = DateTime.fromJSDate(date, { zone: "utc" });
if (!dt.isValid) return "";
return dt.toISODate() || "";
};
export const limit = (arr, count) => {
if (!Array.isArray(arr)) return [];
if (count <= 0) return [];
return arr.slice(0, count);
};
export const filterTagList = (tags) => {
const internalTags = ["all", "nav", "post", "posts"];
if (!tags) return [];
const tagArray = Array.isArray(tags) ? tags : [tags];
return tagArray.filter((tag) => {
if (typeof tag !== "string") return false;
const clean = tag.replace(/^#/, "").toLowerCase();
return !internalTags.includes(clean);
});
};
export const readingTime = (text, frontmatterTime) => {
if (frontmatterTime) return frontmatterTime;
if (!text || typeof text !== "string") return "1 MIN READ";
const clean = text.replace(/<[^>]+>/g, "").trim();
if (!clean) return "1 MIN READ";
const words = clean.split(/\s+/).filter(Boolean).length;
const minutes = Math.max(1, Math.ceil(words / 200));
return `${minutes} MIN READ`;
};
export const extractHeadings = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== "string") return [];
const headingRegex = /<h([23])\s+id="([^"]+)"[^>]*>(.*?)<\/h\1>/gis;
const headings = [];
let match;
while ((match = headingRegex.exec(htmlContent)) !== null) {
headings.push({
level: parseInt(match[1], 10),
id: match[2],
title: match[3].replace(/<[^>]+>/g, "").trim()
});
}
return headings;
};
const leadingTerminalPreviewHeading = /^\s*<h1\b[^>]*\bid=(["'])terminal-preview\1[^>]*>[\s\S]*?<\/h1>/i;
const terminalPreviewBounds = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== "string") return null;
const heading = htmlContent.match(leadingTerminalPreviewHeading);
if (!heading) return null;
const contentAfterHeading = htmlContent.slice(heading[0].length);
const nextArticleHeading = contentAfterHeading.search(/<h[1-6]\b/i);
if (nextArticleHeading === -1) return null;
const leadingArticleWhitespace = contentAfterHeading
.slice(0, nextArticleHeading)
.match(/\s*$/)?.[0] || "";
return {
preview: contentAfterHeading.slice(0, nextArticleHeading),
article: leadingArticleWhitespace + contentAfterHeading.slice(nextArticleHeading)
};
};
export const extractTerminalPreview = (htmlContent) => terminalPreviewBounds(htmlContent)?.preview || "";
export const removeTerminalPreview = (htmlContent) => {
if (!htmlContent || typeof htmlContent !== "string") return "";
return terminalPreviewBounds(htmlContent)?.article || htmlContent;
};
export const filters = {
renderExcerptMarkdown,
postDate,
isoDate,
limit,
filterTagList,
readingTime,
extractHeadings,
extractTerminalPreview,
removeTerminalPreview,
resolveLinkedArticles
};
export default filters;
+35
View File
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title | default(site.title) }}</title>
<meta name="description" content="{{ excerpt | default(site.description) }}">
<!-- Webfonts: IBM Plex Mono, JetBrains Mono, Inter -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:ital,wght@0,400;0,500;0,700;1,400&family=JetBrains+Mono:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
<!-- Modular Bundled Stylesheet -->
<link rel="stylesheet" href="/assets/css/main.css?v=1.1">
{% block head %}{% endblock %}
</head>
<body>
<div class="tiling-layout">
{% include "partials/global-bar.njk" %}
<main id="main-content">
{% block content %}
{{ content | safe }}
{% endblock %}
</main>
{% include "partials/footer.njk" %}
</div>
<script type="module" src="/assets/js/build-timer.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
{% extends "layouts/base.njk" %}
{% block content %}
<div class="page-container">
{{ content | safe }}
</div>
{% endblock %}
+117
View File
@@ -0,0 +1,117 @@
{% extends "layouts/base.njk" %}
{% block head %}
{% if hidden and hidden != "false" and hidden != "0" and hidden != "no" %}
<meta name="robots" content="noindex, nofollow">
{% endif %}
{% endblock %}
{% block content %}
<div class="article-workspace-grid">
<!-- Left Sticky Sidebar: Document Outline & Telemetry -->
<aside class="article-sidebar" aria-label="Article Sidebar">
<!-- Dynamic Table of Contents -->
{% set headings = (content | removeTerminalPreview) | extractHeadings %}
{% if headings.length > 0 %}
<div class="sidebar-tile">
<div class="sidebar-heading">
<span>DOCUMENT OUTLINE</span>
<span style="color: var(--accent-cyan);">[{{ headings.length }}]</span>
</div>
<nav class="toc-nav" aria-label="Table of Contents">
{% for h in headings %}
<a href="#{{ h.id }}" class="toc-link {% if h.level == 3 %}toc-sub{% endif %}">
{{ h.title }}
</a>
{% endfor %}
</nav>
</div>
{% endif %}
<!-- Article Telemetry Tile -->
<div class="sidebar-tile">
<div class="sidebar-heading">DISPATCH TELEMETRY</div>
{% set linkedPosts = collections.posts | resolveLinkedArticles(linkedArticles) %}
{% if linkedPosts.length > 0 %}
<section aria-labelledby="other-articles-heading">
<div id="other-articles-heading" class="sidebar-heading">OTHER ARTICLES</div>
<nav class="toc-nav" aria-label="Other Articles">
{% for linkedPost in linkedPosts %}
<a href="{{ linkedPost.url }}" class="toc-link">{{ linkedPost.data.title }}</a>
{% endfor %}
</nav>
</section>
{% endif %}
<div class="sidebar-stats-box" style="padding: 0; background: transparent; border: none;">
{% if hidden and hidden != "false" and hidden != "0" and hidden != "no" %}
<div class="stat-row">
<span>VISIBILITY:</span>
<span class="stat-val" style="color: var(--accent-amber);">UNLISTED</span>
</div>
{% endif %}
<div class="stat-row">
<span>PUBLISHED:</span>
<span class="stat-val">{{ date | postDate }}</span>
</div>
<div class="stat-row">
<span>DURATION:</span>
<span class="stat-val">{{ (content | removeTerminalPreview) | readingTime(readingTime) }}</span>
</div>
{% if difficulty %}
<div class="stat-row">
<span>DIFFICULTY:</span>
<span class="stat-val" style="color: var(--accent-amber);">{{ difficulty }}</span>
</div>
{% endif %}
</div>
<div style="padding-top: 10px; border-top: var(--border-width) solid var(--border-color);">
<div style="font-family: var(--font-mono); font-size: 0.72rem; color: var(--text-muted); margin-bottom: 6px;">TOPIC TAGS:</div>
<div class="tag-cluster">
{% for tag in tags | filterTagList %}
<span class="tag-chip">#{{ tag }}</span>
{% endfor %}
</div>
</div>
<div style="padding-top: 10px; border-top: var(--border-width) solid var(--border-color);">
<a href="/dispatches/" class="filter-tag-btn" style="width: 100%; justify-content: center;">
&larr; Return to Dispatches
</a>
</div>
</div>
</aside>
<!-- Right Main Article Workspace -->
<article class="article-tile" aria-labelledby="article-title">
<header class="article-header-block">
<nav class="article-breadcrumbs" aria-label="Breadcrumb">
<a href="/dispatches/">~/NOTES</a>
<span>/</span>
<span>{{ (tags | filterTagList)[0] | default('DISPATCHES') | upper }}</span>
<span>/</span>
<span style="color: var(--accent-cyan);">{{ page.fileSlug | upper }}.MD</span>
</nav>
<h1 id="article-title" class="article-main-title">{{ title }}</h1>
<div class="article-meta-row">
<span class="meta-item">
<time datetime="{{ date | isoDate }}">{{ date | postDate }}</time>
</span>
<span>&bull;</span>
<span class="meta-item">{{ (content | removeTerminalPreview) | readingTime(readingTime) }}</span>
{% if difficulty %}
<span>&bull;</span>
<span class="meta-item" style="color: var(--accent-amber);">DIFFICULTY: {{ difficulty }}</span>
{% endif %}
</div>
</header>
<div class="markdown-prose">
{{ content | removeTerminalPreview | safe }}
</div>
</article>
</div>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{#
Reusable Tile Container Macro (macros/tile.njk)
Renders a terminal pane with header, title, meta pill, and custom body slot.
Usage:
{% import "macros/tile.njk" as ui %}
{% call ui.tile(title="STATISTICS", meta="STATIC HTML") %}
<p>Inside tile</p>
{% endcall %}
#}
{% macro tile(title="", meta="", class="", id="") %}
<section class="tile {{ class }}" {% if id %}id="{{ id }}"{% endif %}>
{% if title or meta %}
<header class="tile-header">
{% if title %}
<div class="tile-title">
<span class="status-indicator"></span>
{{ title }}
</div>
{% endif %}
{% if meta %}
<span class="tile-meta">{{ meta }}</span>
{% endif %}
</header>
{% endif %}
<div class="tile-content">
{{ caller() }}
</div>
</section>
{% endmacro %}
+63
View File
@@ -0,0 +1,63 @@
{% set featured = null %}
{% for p in collections.posts %}
{% if p.data.featured and not featured %}
{% set featured = p %}
{% endif %}
{% endfor %}
{% if not featured and collections.posts.length > 0 %}
{% set featured = collections.posts[0] %}
{% endif %}
{% if featured %}
<section class="tile" aria-label="Featured Technical Dispatch">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
FEATURED DISPATCH
</div>
<span class="tile-meta">{{ (featured.data.tags | filterTagList)[0] | default('SYSTEMS') | upper }} &bull; {{ featured.templateContent | readingTime(featured.data.readingTime) }}</span>
</header>
<div class="featured-card-body">
<div class="featured-badge-row">
<span class="badge-pinned">[FEATURED DEEP DIVE]</span>
<time datetime="{{ featured.date | isoDate }}">{{ featured.date | postDate }}</time>
</div>
<h2 style="margin: 0; font-size: inherit;">
<a href="{{ featured.url }}" class="featured-article-title">{{ featured.data.title }}</a>
</h2>
<p class="featured-prose">
{{ featured.data.excerpt | renderExcerptMarkdown | safe }}
</p>
{% if featured.data.terminal == true %}
{% set terminalPreview = featured.templateContent | extractTerminalPreview %}
{% if terminalPreview %}
<div class="terminal-code-box terminal-preview-box">
<div class="code-titlebar">
<span class="code-title">
<span style="color: var(--accent-cyan);">&gt;</span>
<span>TERMINAL PREVIEW</span>
</span>
<span>MARKDOWN</span>
</div>
<div class="terminal-preview-content">
{{ terminalPreview | safe }}
</div>
</div>
{% endif %}
{% endif %}
<div class="featured-cta-row">
<div class="tag-cluster">
{% for tag in featured.data.tags | filterTagList %}
<span class="tag-chip">#{{ tag }}</span>
{% endfor %}
</div>
<a href="{{ featured.url }}" class="cta-button">READ FULL WRITEUP &rarr;</a>
</div>
</div>
</section>
{% endif %}
+15
View File
@@ -0,0 +1,15 @@
<footer class="footer-tile" aria-label="Global Footer">
<div class="footer-left">
<span class="status-indicator"></span>
<span>RUNTIME: ELEVENTY 3.x STATIC</span>
<span>&bull;</span>
<span>THEME: NATIVE TERMINAL DARK</span>
<span>&bull;</span>
<time id="build-age" data-build-timestamp="{{ buildTimestamp }}" aria-live="polite">REBUILT: INITIALIZING</time>
</div>
<div class="footer-right">
<span>&copy; {{ currentYear or "2026" }} {{ site.author }}</span>
<a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" class="footer-link">Source Code ↗</a>
</div>
</footer>
+16
View File
@@ -0,0 +1,16 @@
<header class="global-bar">
<a href="/" class="brand-group" aria-label="{{ site.name }} Home">
<span class="status-indicator"></span>
<span>{{ site.name }}</span>
</a>
<nav class="bar-nav" aria-label="Quick Navigation">
{% for item in site.nav %}
<a href="{{ item.url }}"
class="bar-link {% if item.external %}external{% endif %} {% if page.url == item.url or (item.url != '/' and page.url.startsWith(item.url)) %}active{% endif %}"
{% if item.external %}target="_blank" rel="noopener noreferrer"{% endif %}>
{% if page.url == item.url %}&gt; {% endif %}{{ item.label }}
</a>
{% endfor %}
</nav>
</header>
+24
View File
@@ -0,0 +1,24 @@
<section class="hero-tile" aria-label="Blog Mission & Overview">
<div class="hero-pretitle">TECHNICAL PUBLICATION REPOSITORY &bull; ~/NOTES/INDEX.MD</div>
<h1 class="hero-main-title">{{ heroTitle or site.description }}</h1>
<p class="hero-landing-text">
{{ heroText or "Notes from my Journey building out interconnected systems." }}
</p>
<div class="filter-pills-row" aria-label="Topic Filters">
<a href="/dispatches/" class="filter-tag-btn {% if not currentTag %}active{% endif %}">
[All Dispatches ({{ collections.posts.length }})]
</a>
{% for tag in collections.tagList %}
{% set tagCount = 0 %}
{% for post in collections.posts %}
{% if tag in (post.data.tags or []) %}
{% set tagCount = tagCount + 1 %}
{% endif %}
{% endfor %}
<span class="filter-tag-btn {% if currentTag == tag %}active{% endif %}">
[#{{ tag }}{% if tagCount > 0 %} ({{ tagCount }}){% endif %}]
</span>
{% endfor %}
</div>
</section>
+27
View File
@@ -0,0 +1,27 @@
<article class="feed-card" aria-labelledby="post-title-{{ post.fileSlug }}">
<div class="feed-meta-row">
<time class="feed-date" datetime="{{ post.date | isoDate }}">
{{ post.date | postDate }}
</time>
<span>{{ post.templateContent | readingTime(post.data.readingTime) }}</span>
</div>
<h3 id="post-title-{{ post.fileSlug }}">
<a href="{{ post.url }}" class="feed-heading">{{ post.data.title }}</a>
</h3>
{% if post.data.excerpt %}
<p class="feed-excerpt">{{ post.data.excerpt | renderExcerptMarkdown | safe }}</p>
{% endif %}
<div class="feed-tags-row">
<div class="tag-cluster">
{% for tag in post.data.tags | filterTagList %}
<span class="tag-chip">#{{ tag }}</span>
{% endfor %}
</div>
{% if post.data.difficulty %}
<span>DIFFICULTY: {{ post.data.difficulty }}</span>
{% endif %}
</div>
</article>
+62
View File
@@ -0,0 +1,62 @@
<aside aria-label="Archive Statistics & Topics" style="display: flex; flex-direction: column; gap: var(--tile-gap);">
<!-- Archive Statistics Tile -->
<section class="tile" aria-label="Archive Telemetry">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
ARCHIVE TELEMETRY
</div>
<span class="tile-meta">STATIC HTML</span>
</header>
<div class="sidebar-stats-box">
<div class="stat-row">
<span>TOTAL DISPATCHES:</span>
<span class="stat-val">{{ collections.posts.length }} PUBLISHED</span>
</div>
<div class="stat-row">
<span>ARCHIVED NOTES:</span>
<span class="stat-val">{{ (collections.archivedPosts or []).length }} ARCHIVED</span>
</div>
<div class="stat-row">
<span>TOPIC DOMAINS:</span>
<span class="stat-val">{{ collections.tagList.length }} CATEGORIES</span>
</div>
<div class="stat-row">
<span>CORE SPECIFICATION:</span>
<span class="stat-val">{{ site.systemSpec }}</span>
</div>
<div class="stat-row">
<span>OPERATIONAL STATUS:</span>
<span class="stat-val" style="color: var(--accent-green);">{{ site.status }}</span>
</div>
</div>
<a href="/archive/" class="filter-tag-btn" style="width: 100%; justify-content: center;">
VIEW ARCHIVE ({{ (collections.archivedPosts or []).length }}) &rarr;
</a>
</section>
<!-- Curated Topic Tracks Tile -->
<section class="tile" aria-label="Curated Topic Tracks">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
TOPIC TRACKS
</div>
<span class="tile-meta">CURATED PATHS</span>
</header>
<div class="tile-content">
{% for topic in site.topics %}
<div class="topic-track-item">
<div style="display: flex; justify-content: space-between; align-items: baseline;">
<span class="topic-track-title">{{ topic.title }}</span>
<span style="font-family: var(--font-mono); font-size: 0.68rem; color: var(--accent-cyan);">{{ topic.badge }}</span>
</div>
<p class="topic-track-desc">{{ topic.desc }}</p>
</div>
{% endfor %}
</div>
</section>
</aside>
+59
View File
@@ -0,0 +1,59 @@
const internalPostLink = /^\[([^\]]+)\]\(\/posts\/[^)\s]+\/?\)$/;
const getLinkedArticleTitle = (value) => {
if (typeof value !== "string") return value;
const match = value.match(internalPostLink);
return match ? match[1].trim() : value;
};
export const validateLinkedArticles = (posts) => {
const titles = new Set(posts.map((post) => post.data.title));
for (const post of posts) {
const { linkedArticles } = post.data;
if (linkedArticles === undefined) continue;
if (!Array.isArray(linkedArticles)) {
throw new Error(
`Post '${post.inputPath}' must define 'linkedArticles' as a YAML list of article titles`
);
}
const linkedTitleSet = new Set();
for (const linkedArticle of linkedArticles) {
const title = getLinkedArticleTitle(linkedArticle);
if (typeof title !== "string" || title.trim() === "") {
throw new Error(
`Post '${post.inputPath}' must define 'linkedArticles' with non-empty article titles`
);
}
if (title === post.data.title) {
throw new Error(`Post '${post.inputPath}' cannot link to itself`);
}
if (linkedTitleSet.has(title)) {
throw new Error(
`Post '${post.inputPath}' cannot contain duplicate article titles in 'linkedArticles'`
);
}
linkedTitleSet.add(title);
if (!titles.has(title)) {
throw new Error(
`Post '${post.inputPath}' links to unknown article '${title}'`
);
}
}
}
};
export const resolveLinkedArticles = (posts, titles) => {
if (!Array.isArray(posts) || !Array.isArray(titles)) return [];
const postsByTitle = new Map(posts.map((post) => [post.data.title, post]));
return titles.map(getLinkedArticleTitle).map((title) => postsByTitle.get(title)).filter(Boolean);
};
+140
View File
@@ -0,0 +1,140 @@
/**
* Safely extracts a numeric timestamp from an Eleventy item or its frontmatter data.
* @param {object} item Eleventy page / post object
* @returns {number} Unix timestamp in ms
*/
export function getPostTimestamp(item) {
if (!item || typeof item !== "object") return 0;
const rawDate = item.date ?? item.data?.date ?? item.data?.created;
if (rawDate instanceof Date) {
const t = rawDate.getTime();
return Number.isNaN(t) ? 0 : t;
}
if (typeof rawDate === "string" || typeof rawDate === "number") {
const t = new Date(rawDate).getTime();
return Number.isNaN(t) ? 0 : t;
}
return 0;
}
/**
* Determines whether a post is flagged as pinned or featured.
* Explicit boolean true or truthy values are supported;
* missing, null, or false values return false.
*
* @param {object} item Eleventy page / post object
* @returns {boolean}
*/
export function isPostPinned(item) {
if (!item || typeof item !== "object") return false;
const data = item.data;
if (!data || typeof data !== "object") return false;
return Boolean(data.pinned || data.featured);
}
/**
* Comparator for sorting blog posts:
* 1. Pinned or featured posts are prioritized at the top of the collection.
* 2. Within the same priority group (both pinned or both unpinned), posts
* are sorted chronologically in descending order (newest first).
*
* @param {object} a First post
* @param {object} b Second post
* @returns {number} Negative if a comes before b, positive if b comes before a, 0 if equal
*/
export function postComparator(a, b) {
const aPinned = isPostPinned(a);
const bPinned = isPostPinned(b);
if (aPinned !== bPinned) {
return bPinned ? 1 : -1;
}
const timeA = getPostTimestamp(a);
const timeB = getPostTimestamp(b);
return timeB - timeA;
}
/**
* Determines whether a post is marked as hidden.
* Hidden posts are accessible via direct link/URL, but excluded
* from public post lists, indexes, and tag collections.
* Supports explicit boolean, string ("true", "yes", "1"), or numeric representations.
*
* @param {object} item Eleventy page / post object
* @returns {boolean}
*/
export function isPostHidden(item) {
if (!item || typeof item !== "object") return false;
const data = item.data ?? item;
if (!data || typeof data !== "object") return false;
const hidden = data.hidden;
if (typeof hidden === "string") {
const lower = hidden.trim().toLowerCase();
return lower === "true" || lower === "yes" || lower === "1";
}
if (typeof hidden === "number") {
return hidden === 1;
}
return Boolean(hidden);
}
/**
* Determines whether a post is marked as a draft.
*
* @param {object} item Eleventy page / post object
* @returns {boolean}
*/
export function isPostDraft(item) {
if (!item || typeof item !== "object") return false;
const data = item.data ?? item;
if (!data || typeof data !== "object") return false;
const draft = data.draft;
if (typeof draft === "string") {
const lower = draft.trim().toLowerCase();
return lower === "true" || lower === "yes" || lower === "1";
}
if (typeof draft === "number") {
return draft === 1;
}
return Boolean(draft);
}
/**
* Determines whether a post is marked as archived.
* Archived posts are excluded from the main Dispatches feed and listed on /archive/.
* Supports explicit boolean, string ("true", "yes", "1"), or numeric representations.
*
* @param {object} item Eleventy page / post object
* @returns {boolean}
*/
export function isPostArchived(item) {
if (!item || typeof item !== "object") return false;
const data = item.data ?? item;
if (!data || typeof data !== "object") return false;
const archive = data.archive;
if (typeof archive === "string") {
const lower = archive.trim().toLowerCase();
return lower === "true" || lower === "yes" || lower === "1";
}
if (typeof archive === "number") {
return archive === 1;
}
return Boolean(archive);
}
/**
* Determines whether a post is eligible for public collections and listings.
* A post must be non-draft, non-hidden, and non-archived.
*
* @param {object} item Eleventy page / post object
* @returns {boolean}
*/
export function isPostPublic(item) {
if (!item || typeof item !== "object") return false;
return !isPostDraft(item) && !isPostHidden(item) && !isPostArchived(item);
}
export default postComparator;
+28
View File
@@ -0,0 +1,28 @@
---
layout: layouts/base.njk
title: "River Rooks | Archived Dispatches"
---
<!-- 1. Lander Hero Tile with Dynamic Filter Tag Buttons -->
{% include "partials/hero-tile.njk" %}
<!-- 2. Full-Width Chronological Archived Dispatches Feed Tile -->
<section class="tile" aria-label="Archived Dispatches Feed Catalog">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
ARCHIVED DISPATCHES
</div>
<span class="tile-meta">ORDER: CHRONOLOGICAL DESC &bull; TOTAL: {{ collections.archivedPosts.length }}</span>
</header>
<div class="feed-list">
{% for post in collections.archivedPosts %}
{% include "partials/post-card.njk" %}
{% else %}
<p style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.85rem; padding: 16px;">
No archived dispatches found in repository.
</p>
{% endfor %}
</div>
</section>
+317
View File
@@ -0,0 +1,317 @@
/* ==========================================================================
Article Workspace & Markdown Prose (components/article.css)
========================================================================== */
/* Left Sticky Sidebar (Sticky on multi-column desktop, natural flow on mobile/single-column) */
.article-sidebar {
display: flex;
flex-direction: column;
gap: var(--tile-gap);
position: static;
}
@media (min-width: 961px) {
.article-sidebar {
position: sticky;
top: 16px;
max-height: calc(100vh - 32px);
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-color) transparent;
}
}
@media (max-width: 960px) {
.article-sidebar {
position: static !important;
top: auto !important;
max-height: none !important;
overflow: visible !important;
}
}
.sidebar-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 18px;
display: flex;
flex-direction: column;
gap: 12px;
}
.sidebar-heading {
font-family: var(--font-mono);
font-size: 0.82rem;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
/* Document Table of Contents */
.toc-nav {
display: flex;
flex-direction: column;
gap: 4px;
font-family: var(--font-mono);
font-size: 0.78rem;
}
.toc-link {
color: var(--text-muted);
text-decoration: none;
padding: 4px 6px;
border-radius: var(--border-radius-sm);
line-height: 1.4;
display: block;
}
.toc-link:hover {
background-color: var(--bg-elevated);
color: var(--accent-cyan);
}
.toc-link.active {
color: var(--accent-cyan);
background-color: var(--bg-canvas);
border-left: 2px solid var(--accent-cyan);
font-weight: 700;
}
.toc-sub {
padding-left: 16px;
color: var(--text-muted);
font-size: 0.74rem;
}
/* Main Article Reading Container */
.article-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 36px 40px;
display: flex;
flex-direction: column;
gap: 24px;
}
@media (max-width: 600px) {
.article-tile {
padding: 24px 18px;
}
}
/* Article Header */
.article-header-block {
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 24px;
display: flex;
flex-direction: column;
gap: 12px;
}
.article-breadcrumbs {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 8px;
}
.article-breadcrumbs a {
color: var(--text-muted);
text-decoration: none;
}
.article-breadcrumbs a:hover {
color: var(--accent-cyan);
}
.article-main-title {
font-family: var(--font-mono);
font-size: 2.2rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.25;
}
@media (max-width: 768px) {
.article-main-title {
font-size: 1.7rem;
}
}
.article-meta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 14px;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-muted);
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
}
/* Markdown Prose Hierarchy */
.markdown-prose {
max-width: var(--prose-max-width);
font-size: 1.02rem;
color: var(--text-primary);
line-height: 1.7;
}
.markdown-prose p {
margin-bottom: 18px;
}
.markdown-prose h2 {
font-family: var(--font-mono);
font-size: 1.45rem;
font-weight: 700;
color: var(--text-primary);
margin: 36px 0 16px 0;
padding-bottom: 8px;
border-bottom: var(--border-width) solid var(--border-color);
letter-spacing: -0.01em;
}
.markdown-prose h3 {
font-family: var(--font-mono);
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
margin: 28px 0 12px 0;
}
.markdown-prose a {
color: var(--accent-cyan);
text-decoration: underline;
text-underline-offset: 3px;
}
.markdown-prose a:hover {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
text-decoration: none;
}
/* Inline Code */
.markdown-prose code:not(pre code) {
font-family: var(--font-code);
font-size: 0.88rem;
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
padding: 2px 6px;
border-radius: var(--border-radius-xs);
color: var(--accent-cyan);
}
/* Keyboard Shortcut Hints */
kbd {
font-family: var(--font-mono);
font-size: 0.78rem;
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
padding: 2px 6px;
border-radius: var(--border-radius-xs);
color: var(--text-primary);
box-shadow: none;
}
/* Markdown Callouts */
.markdown-callout {
margin: 20px 0;
padding: 14px 18px;
border-radius: var(--border-radius-sm);
border: var(--border-width) solid var(--border-color);
background-color: var(--bg-canvas);
font-size: 0.94rem;
line-height: 1.6;
}
.callout-note {
border-left: 4px solid var(--accent-cyan);
}
.callout-warning {
border-left: 4px solid var(--accent-amber);
}
.callout-title {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.82rem;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.callout-note .callout-title {
color: var(--accent-cyan);
}
.callout-warning .callout-title {
color: var(--accent-amber);
}
/* Monospace Tables */
.table-container {
width: 100%;
overflow-x: auto;
margin: 22px 0;
}
.markdown-prose table,
.markdown-table {
width: 100%;
border-collapse: collapse;
font-size: 0.86rem;
font-family: var(--font-mono);
text-align: left;
margin: 20px 0;
border: var(--border-width) solid var(--border-color);
}
.markdown-prose th,
.markdown-table th {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
padding: 8px 12px;
color: var(--text-muted);
font-weight: 700;
}
.markdown-prose td,
.markdown-table td {
border: var(--border-width) solid var(--border-color);
padding: 8px 12px;
color: var(--text-primary);
}
.markdown-prose tr:hover,
.markdown-table tr:hover {
background-color: var(--bg-elevated);
}
/* Lists */
.markdown-prose ul, .markdown-prose ol {
margin: 16px 0 18px 24px;
}
.markdown-prose li {
margin-bottom: 6px;
}
+366
View File
@@ -0,0 +1,366 @@
/* ==========================================================================
Cards & Feeds (components/cards.css)
========================================================================== */
/* Featured Lead Article Card (Inside Tile) */
.featured-card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.featured-badge-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.76rem;
flex-wrap: wrap;
gap: 8px;
}
.badge-pinned {
color: var(--accent-amber);
font-weight: 700;
}
.featured-article-title {
font-family: var(--font-mono);
font-size: 1.35rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
line-height: 1.3;
}
.featured-article-title:hover {
color: var(--accent-cyan);
}
.featured-prose {
font-size: 0.92rem;
color: var(--text-primary);
line-height: 1.6;
}
.featured-cta-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: var(--border-width) solid var(--border-color);
flex-wrap: wrap;
gap: 10px;
font-family: var(--font-mono);
}
.cta-button {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 6px 14px;
font-weight: 700;
font-size: 0.8rem;
text-decoration: none;
border-radius: var(--border-radius-sm);
border: var(--border-width) solid var(--accent-cyan);
display: inline-block;
}
.cta-button:hover {
background-color: transparent;
color: var(--accent-cyan);
}
/* Sidebar Stats / Telemetry Box */
.sidebar-stats-box {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 12px 14px;
display: flex;
flex-direction: column;
gap: 8px;
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--text-muted);
}
.stat-row {
display: flex;
justify-content: space-between;
}
.stat-val {
color: var(--text-primary);
font-weight: 700;
}
/* Curated Topic Track Item */
.topic-track-item {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
text-decoration: none;
}
.topic-track-item:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.topic-track-title {
font-family: var(--font-mono);
font-size: 0.84rem;
font-weight: 700;
color: var(--text-primary);
}
.topic-track-item:hover .topic-track-title {
color: var(--accent-cyan);
}
.topic-track-desc {
font-size: 0.78rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Full-Width Chronological Article Feed */
.feed-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.feed-card {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 8px;
text-decoration: none;
}
.feed-card:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.feed-meta-row {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.76rem;
color: var(--text-muted);
}
.feed-date {
color: var(--accent-cyan);
}
.feed-heading {
font-family: var(--font-mono);
font-size: 1.1rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
line-height: 1.35;
}
.feed-card:hover .feed-heading {
color: var(--accent-cyan);
}
.feed-excerpt {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.6;
}
.feed-tags-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 6px;
border-top: var(--border-width) solid var(--border-color);
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 6px;
}
.tag-cluster {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-chip {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
padding: 2px 6px;
border-radius: var(--border-radius-sm);
color: var(--text-muted);
text-decoration: none;
font-family: var(--font-mono);
font-size: 0.74rem;
}
.tag-chip:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
/* Featured Service Card (Homelab Gateway) */
.blog-service-feature {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.blog-feature-top {
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.78rem;
}
.badge-core {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
padding: 2px 6px;
font-weight: 700;
border-radius: var(--border-radius-sm);
}
.blog-feature-title {
font-family: var(--font-mono);
font-size: 1.15rem;
font-weight: 700;
color: var(--text-primary);
text-decoration: none;
display: inline-block;
}
.blog-feature-title:hover {
color: var(--accent-cyan);
}
.blog-feature-desc {
font-size: 0.9rem;
color: var(--text-primary);
line-height: 1.5;
}
.blog-sublinks {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 4px;
border-top: var(--border-width) solid var(--border-color);
padding-top: 10px;
}
.blog-sublink-item {
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-primary);
text-decoration: none;
padding: 4px 6px;
border-radius: var(--border-radius-sm);
}
.blog-sublink-item:hover {
background-color: var(--bg-elevated);
color: var(--accent-cyan);
}
/* Homelab Services Mini-Grid */
.services-mini-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
@media (max-width: 540px) {
.services-mini-grid {
grid-template-columns: 1fr;
}
}
.service-tile-item {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 12px;
text-decoration: none;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 6px;
}
.service-tile-item:hover {
border-color: var(--accent-cyan);
background-color: var(--bg-elevated);
}
.service-item-title {
font-family: var(--font-mono);
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
}
.service-tile-item:hover .service-item-title {
color: var(--accent-cyan);
}
.service-item-tag {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-muted);
}
.service-item-desc {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.4;
}
/* Infrastructure Node Telemetry */
.node-stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-mono);
font-size: 0.78rem;
}
.node-name {
color: var(--text-primary);
font-weight: 700;
}
.node-load {
color: var(--accent-green);
}
+152
View File
@@ -0,0 +1,152 @@
/* ==========================================================================
Terminal Code Blocks & Syntax Highlighting (components/code-box.css)
========================================================================== */
/* Terminal Code Box Component */
.terminal-code-box,
.code-block-container {
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
background-color: var(--bg-canvas);
overflow: hidden;
font-family: var(--font-code);
margin: 18px 0;
}
.code-titlebar,
.code-block-header {
background-color: var(--bg-surface);
border-bottom: var(--border-width) solid var(--border-color);
padding: 6px 14px;
font-size: 0.74rem;
font-family: var(--font-mono);
color: var(--text-muted);
display: flex;
justify-content: space-between;
align-items: center;
}
.code-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-primary);
font-weight: 500;
}
.copy-code-btn {
background: transparent;
border: var(--border-width) solid var(--border-color);
color: var(--text-muted);
padding: 2px 8px;
font-family: inherit;
font-size: 0.72rem;
cursor: pointer;
border-radius: var(--border-radius-sm);
}
.copy-code-btn:hover {
border-color: var(--accent-cyan);
color: var(--accent-cyan);
}
.code-content,
.code-block-container pre,
.markdown-prose pre {
padding: 14px 16px;
font-size: 0.82rem;
overflow-x: auto;
color: var(--text-primary);
line-height: 1.55;
margin: 18px 0;
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-code);
}
.terminal-preview-box {
margin: 0;
}
.terminal-preview-content {
padding: 14px 16px;
color: var(--text-primary);
font-size: 0.82rem;
line-height: 1.55;
}
.terminal-preview-content > :first-child {
margin-top: 0;
}
.terminal-preview-content > :last-child {
margin-bottom: 0;
}
.terminal-preview-content pre {
margin: 10px 0;
padding: 10px 12px;
overflow-x: auto;
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
font-family: var(--font-code);
}
/* Syntax Highlighting Tokens (Prism / 11ty Syntax Highlight) */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #5c6370;
font-style: italic;
}
.token.punctuation {
color: #abb2bf;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: var(--accent-amber);
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: var(--accent-green);
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: var(--accent-cyan);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #c678dd;
}
.token.function,
.token.class-name {
color: #61afef;
}
.token.regex,
.token.important,
.token.variable {
color: var(--accent-amber);
}
+39
View File
@@ -0,0 +1,39 @@
/* ==========================================================================
Footer Tile (components/footer.css)
========================================================================== */
.footer-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 14px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-muted);
flex-wrap: wrap;
gap: 12px;
}
.footer-left {
display: flex;
align-items: center;
gap: 12px;
}
.footer-right {
display: flex;
gap: 16px;
align-items: center;
}
.footer-link {
color: var(--text-muted);
text-decoration: none;
}
.footer-link:hover {
color: var(--accent-cyan);
}
+63
View File
@@ -0,0 +1,63 @@
/* ==========================================================================
Global Status Header Bar (components/global-bar.css)
========================================================================== */
.global-bar {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 10px 18px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: var(--font-mono);
font-size: 0.8rem;
flex-wrap: wrap;
gap: 10px;
}
.brand-group {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-primary);
font-weight: 700;
text-decoration: none;
}
.brand-group:hover {
color: var(--accent-cyan);
}
.status-indicator {
width: 8px;
height: 8px;
background-color: var(--accent-green);
display: inline-block;
}
.bar-nav {
display: flex;
gap: 14px;
align-items: center;
}
.bar-link {
color: var(--text-muted);
text-decoration: none;
}
.bar-link:hover {
color: var(--accent-cyan);
}
.bar-link.active {
color: var(--text-primary);
border-bottom: 1px solid var(--accent-cyan);
}
.bar-link.external::after {
content: " ↗";
font-size: 0.75em;
color: var(--text-muted);
}
+86
View File
@@ -0,0 +1,86 @@
/* Missing route: a compact diagnostic panel in the site's tiling vocabulary. */
.not-found {
gap: 0;
padding: 20px 24px 28px;
}
.not-found-body {
display: grid;
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1fr);
align-items: center;
gap: 24px;
min-height: 320px;
}
.not-found-code {
font-family: var(--font-mono);
font-size: clamp(5rem, 14vw, 10rem);
line-height: 1;
letter-spacing: -0.09em;
color: var(--accent-amber);
white-space: nowrap;
}
.not-found-cursor {
color: var(--text-muted);
}
.not-found-message {
border-left: 1px solid var(--border-color);
padding: 16px 0 16px 24px;
max-width: 48ch;
}
.not-found-label {
font: 500 0.75rem var(--font-mono);
letter-spacing: 0.06em;
color: var(--accent-amber);
margin-bottom: 12px;
}
.not-found-message h2 {
font: 700 clamp(1.3rem, 3vw, 2rem) / 1.25 var(--font-mono);
margin-bottom: 12px;
}
.not-found-message > p:not(.not-found-label) {
color: var(--text-muted);
max-width: 42ch;
}
.not-found-links {
display: flex;
flex-wrap: wrap;
gap: 8px 24px;
margin-top: 24px;
font: 500 0.85rem var(--font-mono);
}
.not-found-links a {
color: var(--accent-cyan);
text-decoration: none;
}
.not-found-links a:hover {
color: var(--bg-canvas);
background: var(--accent-cyan);
}
@media (max-width: 680px) {
.not-found {
padding: 16px;
}
.not-found-body {
grid-template-columns: 1fr;
gap: 8px;
min-height: 0;
padding: 32px 0 8px;
}
.not-found-message {
border-left: 0;
border-top: 1px solid var(--border-color);
padding: 20px 0 0;
}
}
+141
View File
@@ -0,0 +1,141 @@
/* ==========================================================================
Tile Containers & Hero Tile (components/tiles.css)
========================================================================== */
/* Standard Tile Container */
.tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: baseline;
border-bottom: var(--border-width) solid var(--border-color);
padding-bottom: 10px;
flex-wrap: wrap;
gap: 8px;
}
.tile-title {
font-family: var(--font-mono);
font-size: 0.92rem;
font-weight: 700;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.tile-meta {
font-family: var(--font-mono);
font-size: 0.74rem;
color: var(--text-muted);
}
.tile-content {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Hero Lander Tile */
.hero-tile {
background-color: var(--bg-surface);
border: var(--border-width) solid var(--border-color);
border-left: 3px solid var(--accent-cyan);
border-radius: var(--border-radius);
padding: 24px 28px;
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.hero-pretitle {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--accent-cyan);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.hero-main-title {
font-family: var(--font-mono);
font-size: 2.1rem;
font-weight: 700;
color: var(--text-primary);
letter-spacing: -0.02em;
line-height: 1.2;
}
@media (max-width: 600px) {
.hero-main-title {
font-size: 1.6rem;
}
}
.hero-landing-text {
font-size: 0.98rem;
color: var(--text-primary);
line-height: 1.65;
max-width: 82ch;
}
/* Filter Pills Row in Hero Tile */
.filter-pills-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-top: 4px;
font-family: var(--font-mono);
font-size: 0.78rem;
}
.filter-tag-btn {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
color: var(--text-muted);
padding: 3px 8px;
border-radius: var(--border-radius-sm);
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
}
.filter-tag-btn:hover {
border-color: var(--accent-cyan);
color: var(--text-primary);
background-color: var(--bg-elevated);
}
.filter-tag-btn.active {
background-color: var(--accent-cyan);
color: var(--bg-canvas);
border-color: var(--accent-cyan);
font-weight: 700;
}
/* Directive Callout Box inside Hero */
.hero-directive-box {
background-color: var(--bg-canvas);
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius-sm);
padding: 12px 16px;
font-family: var(--font-mono);
font-size: 0.82rem;
color: var(--text-muted);
line-height: 1.6;
}
.hero-directive-box strong {
color: var(--text-primary);
}
+53
View File
@@ -0,0 +1,53 @@
/* ==========================================================================
Tiling Window Manager Grids & Layout (layout.css)
========================================================================== */
/* Outer Shell Container */
.tiling-layout {
width: 100%;
max-width: var(--site-max-width);
display: flex;
flex-direction: column;
gap: var(--tile-gap);
}
/* Asymmetric 2-Column Split Grid (Lander: Lead Article + Telemetry Sidebar) */
.split-grid {
display: grid;
grid-template-columns: 1.25fr 0.75fr;
gap: var(--tile-gap);
align-items: start;
}
@media (max-width: 880px) {
.split-grid {
grid-template-columns: 1fr;
}
}
/* Article Workspace Grid (Left Sidebar TOC + Main Reading Workspace) */
.article-workspace-grid {
display: grid;
grid-template-columns: 290px 1fr;
gap: var(--tile-gap);
align-items: start;
}
@media (max-width: 960px) {
.article-workspace-grid {
grid-template-columns: 1fr;
}
}
/* Generic Symmetrical Grids */
.grid-2col {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--tile-gap);
}
@media (max-width: 768px) {
.grid-2col {
grid-template-columns: 1fr;
}
}
+22
View File
@@ -0,0 +1,22 @@
/* ==========================================================================
Main Stylesheet (main.css)
River Rooks Blog - Modular CSS Architecture
========================================================================== */
/* 1. Design Tokens & CSS Custom Properties */
@import "./tokens.css";
/* 2. Global Reset & Baseline Typography */
@import "./reset.css";
/* 3. Tiling Window Manager Grids */
@import "./layout.css";
/* 4. Component Styles */
@import "./components/global-bar.css";
@import "./components/tiles.css";
@import "./components/cards.css";
@import "./components/code-box.css";
@import "./components/article.css";
@import "./components/footer.css";
@import "./components/not-found.css";
+52
View File
@@ -0,0 +1,52 @@
/* ==========================================================================
CSS Reset & Baseline (reset.css)
Strict adherence to developer-centric minimalism & instant transitions
========================================================================== */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
text-rendering: optimizeLegibility;
scroll-behavior: smooth;
}
body {
background-color: var(--bg-canvas);
color: var(--text-primary);
font-family: var(--font-prose);
line-height: 1.6;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 48px 16px;
overflow-x: hidden;
}
/* Hard anti-pattern enforcement: instant transitions across UI elements */
a, button, input, select, textarea {
transition: none !important;
}
/* High-contrast keyboard accessibility outline */
:focus-visible {
outline: 2px solid var(--accent-cyan);
outline-offset: 2px;
}
img, svg, video {
max-width: 100%;
height: auto;
display: block;
}
/* Monospace font assignment for code & structural elements */
code, pre, kbd, samp {
font-family: var(--font-code);
}
+38
View File
@@ -0,0 +1,38 @@
/* ==========================================================================
Design Tokens (tokens.css)
River Rooks Blog - Native Dark Mode Architecture
========================================================================== */
:root {
/* Surface & Canvas Colors */
--bg-canvas: #0d0f12; /* Deep slate/charcoal canvas */
--bg-surface: #121417; /* Tile & card surface */
--bg-elevated: #1c1f26; /* Hovered surface */
/* Borders & Dividers */
--border-color: #2b303b; /* Crisp 1px border */
--border-width: 1px;
--border-radius: 4px; /* Strictly capped at 4px */
--border-radius-sm: 2px;
--border-radius-xs: 1px;
/* Typography */
--font-mono: 'IBM Plex Mono', 'JetBrains Mono', monospace;
--font-code: 'JetBrains Mono', 'Fira Code', monospace;
--font-prose: 'Inter', system-ui, -apple-system, sans-serif;
/* Text Colors */
--text-primary: #e0e6f0; /* Light off-white prose */
--text-muted: #8a95a5; /* Monospace metadata */
/* High-Signal Terminal Accents */
--accent-cyan: #00f0ff; /* Primary active accent */
--accent-green: #00e676; /* Success / online indicator */
--accent-amber: #ffb700; /* Warning / highlight accent */
--accent-red: #ff5252; /* Danger / caution accent */
/* Structural Dimensions */
--site-max-width: 1140px;
--tile-gap: 16px;
--prose-max-width: 68ch;
}
+30
View File
@@ -0,0 +1,30 @@
export const formatBuildAge = (buildTimestamp, nowTimestamp = new Date().toISOString()) => {
const buildTime = Date.parse(buildTimestamp);
const now = Date.parse(nowTimestamp);
if (!Number.isFinite(buildTime) || !Number.isFinite(now)) return "unknown";
const elapsedSeconds = Math.max(0, Math.floor((now - buildTime) / 1000));
const days = Math.floor(elapsedSeconds / 86400);
const hours = Math.floor((elapsedSeconds % 86400) / 3600);
const minutes = Math.floor((elapsedSeconds % 3600) / 60);
const seconds = elapsedSeconds % 60;
if (days > 0) return `${days}d ${hours}h ago`;
if (hours > 0) return `${hours}h ${minutes}m ago`;
if (minutes > 0) return `${minutes}m ${seconds}s ago`;
return `${seconds}s ago`;
};
if (typeof document !== "undefined") {
const timer = document.getElementById("build-age");
if (timer) {
const updateBuildAge = () => {
timer.textContent = `REBUILT: ${formatBuildAge(timer.dataset.buildTimestamp)}`;
};
updateBuildAge();
window.setInterval(updateBuildAge, 1000);
}
}
+39
View File
@@ -0,0 +1,39 @@
---
layout: layouts/base.njk
title: "River Rooks | Engineering Notes & Systems Architecture"
---
<!-- 1. Lander Hero Tile with Dynamic Filter Tag Buttons -->
{% include "partials/hero-tile.njk" %}
<!-- 2. Asymmetric Split Grid: Featured Lead Post (Left) & Telemetry Sidebar (Right) -->
<div class="split-grid">
<div class="split-lead-col">
{% include "partials/featured-card.njk" %}
</div>
<div class="split-sidebar-col">
{% include "partials/stats-sidebar.njk" %}
</div>
</div>
<!-- 3. Full-Width Chronological Dispatches Feed Tile -->
<section class="tile" aria-label="Dispatches Feed Catalog">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
RECENT DISPATCHES
</div>
<span class="tile-meta">ORDER: CHRONOLOGICAL DESC &bull; TOTAL: {{ collections.posts.length }}</span>
</header>
<div class="feed-list">
{% for post in collections.posts %}
{% include "partials/post-card.njk" %}
{% else %}
<p style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.85rem; padding: 16px;">
No published dispatches found in repository.
</p>
{% endfor %}
</div>
</section>
+117
View File
@@ -0,0 +1,117 @@
---
layout: layouts/base.njk
title: "River Rooks | Homelab Gateway"
---
<!-- Main Hero Landing Tile -->
<section class="hero-tile" aria-label="Gateway Declaration">
<div class="hero-pretitle">SYSTEM DECLARATION &bull; NODE GATEWAY</div>
<h1 class="hero-main-title">Cybersecurity &amp; Dev Ops notes</h1>
<div class="hero-landing-text">
<p>
I am River Rooks, a cyber security student at the University of Alabama. This website is the public-facing gateway to my homelab, which hosts a variety of services and tools for personal and professional use. Here, you can find my technical blog and a collection of public tools that host.
</p>
<p>
I deploy and maintain a variety of services in my homelab for research, development, and learning purposes. This includes web applications, databases, and other tools that I use to enhance my skills in cybersecurity and DevOps.
</p>
<p>
My blog serves as a way to share some of my extensive notes with the public. I primarily focus on Capture The Flag (CTF) writeups, technical guides, and other cybersecurity-related content. I also use this platform to document my learning journey and share insights into the world of cybersecurity and DevOps.
</div>
<div class="hero-directive-box">
<strong>PRIMARY DESTINATIONS:</strong>
<a href="/dispatches/" style="color: var(--accent-cyan); text-decoration: none; margin-left: 8px;">[1] Technical Blog ({{ collections.posts.length }} Articles)</a> &bull;
<a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[2] Gitea Repos</a> &bull;
<a href="https://github.com/skoorrevir" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[3] GitHub Repos</a> &bull;
<a href="https://wg.{{ site.domain }}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[4] Wireguard VPN</a> &bull;
</div>
</section>
<!-- Split Tiling Columns -->
<div class="split-grid">
<!-- Left Column: Featured Service (Blog) + Dynamic Recent Articles -->
<div style="display: flex; flex-direction: column; gap: var(--tile-gap);">
<section class="tile" aria-label="Featured Service: Technical Blog">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
// PRIMARY SERVICE: THE TECHNICAL BLOG
</div>
<span class="tile-meta">~/notes/ &bull; {{ collections.posts.length }} ARTICLES</span>
</header>
<div class="blog-service-feature">
<div class="blog-feature-top">
<span class="badge-core">[CORE PUBLICATION SERVICE]</span>
<span style="color: var(--text-muted);">{{ collections.posts[0].date | postDate }} LAST SYNC</span>
</div>
<a href="/dispatches/" class="blog-feature-title">&gt; Technical Notes &amp; Engineering Dispatches</a>
<p class="blog-feature-desc">
Notes from my Journey building out interconnected systems.
</p>
<div class="blog-sublinks">
{% for post in collections.posts | limit(4) %}
<a href="{{ post.url }}" class="blog-sublink-item">
<span>{{ loop.index }}. {{ post.data.title }}</span>
<span style="color: var(--accent-cyan);">[{{ post.templateContent | readingTime(post.data.readingTime) }}] &rarr;</span>
</a>
{% endfor %}
</div>
</div>
</section>
</div>
<!-- Right Column: Homelab Hosted Services & Node Telemetry -->
<div style="display: flex; flex-direction: column; gap: var(--tile-gap);">
<section class="tile" aria-label="Homelab Services Directory">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
// HOMELAB SERVICES ROUTER
</div>
<span class="tile-meta">{{ site.services.length }} PUBLIC TOOLS</span>
</header>
<div class="services-mini-grid">
{% for svc in site.services %}
<a href="{{ svc.url }}" target="_blank" rel="noopener noreferrer" class="service-tile-item">
<div>
<div class="service-item-title">{{ svc.name }}</div>
<div class="service-item-tag">{{ svc.tag }}</div>
</div>
<p class="service-item-desc">{{ svc.desc }}</p>
</a>
{% endfor %}
</div>
</section>
<!-- Infrastructure Telemetry Tile -->
<section class="tile" aria-label="Infrastructure Nodes Telemetry">
<header class="tile-header">
<div class="tile-title">
<span class="status-indicator"></span>
// CLUSTER TELEMETRY
</div>
<span class="tile-meta">3 NODES ONLINE</span>
</header>
<div style="display: flex; flex-direction: column; gap: 8px;">
<div class="node-stat-row">
<span class="node-name">NODE-01 (Primary Cluster)</span>
<span class="node-load">ONLINE &bull; LOAD: 0.18</span>
</div>
<div class="node-stat-row">
<span class="node-name">NODE-02 (Storage Array)</span>
<span class="node-load">ONLINE &bull; POOL: 98% HEALTH</span>
</div>
<div class="node-stat-row">
<span class="node-name">GATEWAY-01 (Edge Ingress)</span>
<span class="node-load">ONLINE &bull; ROUTING ACTIVE</span>
</div>
</div>
</section>
</div>
</div>
+3
View File
@@ -0,0 +1,3 @@
{
"layout": "layouts/post.njk"
}
+250
View File
@@ -0,0 +1,250 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
import {
isPostArchived,
isPostDraft,
isPostHidden,
isPostPublic,
postComparator
} from "../src/_utils/sort-posts.js";
import { normalizeObsidianMarkdown } from "../server.js";
import configureEleventy from "../eleventy.config.js";
test("Archived Posts: isPostArchived accurately identifies archive flags across data shapes", () => {
// 1. Explicit boolean true
assert.equal(isPostArchived({ data: { archive: true } }), true, "Explicit boolean true in data");
assert.equal(isPostArchived({ archive: true }), true, "Root level archive true");
// 2. Truthy string and numeric representations (case-insensitive & trimmed)
assert.equal(isPostArchived({ data: { archive: "true" } }), true, "String 'true'");
assert.equal(isPostArchived({ data: { archive: "TRUE" } }), true, "Uppercase string 'TRUE'");
assert.equal(isPostArchived({ data: { archive: " true " } }), true, "Whitespace-padded 'true'");
assert.equal(isPostArchived({ data: { archive: "yes" } }), true, "String 'yes'");
assert.equal(isPostArchived({ data: { archive: "YES" } }), true, "String 'YES'");
assert.equal(isPostArchived({ data: { archive: "1" } }), true, "String '1'");
assert.equal(isPostArchived({ data: { archive: 1 } }), true, "Numeric 1");
// 3. Falsy representations
assert.equal(isPostArchived({ data: { archive: false } }), false, "Explicit boolean false");
assert.equal(isPostArchived({ data: { archive: "false" } }), false, "String 'false'");
assert.equal(isPostArchived({ data: { archive: "no" } }), false, "String 'no'");
assert.equal(isPostArchived({ data: { archive: "0" } }), false, "String '0'");
assert.equal(isPostArchived({ data: { archive: 0 } }), false, "Numeric 0");
assert.equal(isPostArchived({ data: { archive: "" } }), false, "Empty string");
assert.equal(isPostArchived({ data: { archive: null } }), false, "Null property");
assert.equal(isPostArchived({ data: {} }), false, "Missing archive property");
assert.equal(isPostArchived({}), false, "Empty post object");
// 4. Adversarial edge cases
assert.equal(isPostArchived(null), false, "Null input");
assert.equal(isPostArchived(undefined), false, "Undefined input");
assert.equal(isPostArchived("unexpected-string"), false, "Primitive string input");
assert.equal(isPostArchived(12345), false, "Primitive number input");
assert.equal(isPostArchived([]), false, "Array input");
assert.equal(isPostArchived({ data: null }), false, "Null data property");
});
test("Archived Posts: isPostPublic excludes archived posts from active public dispatches", () => {
assert.equal(
isPostPublic({ data: { title: "Active Post", draft: false, hidden: false, archive: false } }),
true,
"Non-archived public post is public"
);
assert.equal(
isPostPublic({ data: { title: "Archived Post", archive: true } }),
false,
"Archived post is excluded from active public dispatches"
);
assert.equal(
isPostPublic({ data: { title: "Archived String Post", archive: "true" } }),
false,
"Archived string post is excluded from active public dispatches"
);
});
test("Archived Posts: Eleventy collections separate active posts and archivedPosts while excluding drafts and hidden notes", () => {
const collections = new Map();
configureEleventy({
setUseGitIgnore() {},
addPassthroughCopy() {},
addPlugin() {},
setLibrary() {},
addFilter() {},
addCollection(name, callback) {
collections.set(name, callback);
},
addGlobalData() {},
on() {},
setServerOptions() {}
});
assert.ok(collections.has("posts"), "posts collection must be registered");
assert.ok(collections.has("archivedPosts"), "archivedPosts collection must be registered");
const samplePosts = [
{
inputPath: "src/posts/2026-10-06-active-new.md",
date: new Date("2026-10-06"),
data: { title: "Active New", tags: ["systems"] }
},
{
inputPath: "src/posts/2026-10-05-active-old.md",
date: new Date("2026-10-05"),
data: { title: "Active Old", archive: false, tags: ["notes"] }
},
{
inputPath: "src/posts/2026-10-03-archived-new.md",
date: new Date("2026-10-03"),
data: { title: "Archived New", archive: true, tags: ["legacy-only"] }
},
{
inputPath: "src/posts/2026-10-01-archived-old.md",
date: new Date("2026-10-01"),
data: { title: "Archived Old", archive: "yes", tags: ["legacy-only"] }
},
{
inputPath: "src/posts/2026-10-04-archived-draft.md",
date: new Date("2026-10-04"),
data: { title: "Archived Draft", archive: true, draft: true, tags: ["draft-tag"] }
},
{
inputPath: "src/posts/2026-10-04-archived-hidden.md",
date: new Date("2026-10-04"),
data: { title: "Archived Hidden", archive: true, hidden: true, tags: ["hidden-tag"] }
}
];
const mockCollectionApi = {
getFilteredByGlob() {
return samplePosts;
}
};
const activePosts = collections.get("posts")(mockCollectionApi);
const archivedPosts = collections.get("archivedPosts")(mockCollectionApi);
const tagList = collections.get("tagList")(mockCollectionApi);
assert.deepEqual(
activePosts.map((p) => p.data.title),
["Active New", "Active Old"],
"collections.posts must only contain non-archived, non-draft, non-hidden posts"
);
assert.deepEqual(
archivedPosts.map((p) => p.data.title),
["Archived New", "Archived Old"],
"collections.archivedPosts must only contain non-draft, non-hidden archived posts sorted chronologically"
);
assert.ok(!tagList.includes("legacy-only"), "Archived-only tags must not leak into active Dispatches tagList");
assert.ok(!tagList.includes("draft-tag"), "Draft tags must not leak into tagList");
assert.ok(!tagList.includes("hidden-tag"), "Hidden tags must not leak into tagList");
});
test("Archived Posts: Archive Telemetry sidebar contains link to /archive/ and archive.njk template omits featured section", () => {
const sidebarPath = path.resolve("src/_includes/partials/stats-sidebar.njk");
assert.ok(fs.existsSync(sidebarPath), "stats-sidebar.njk must exist");
const sidebarContent = fs.readFileSync(sidebarPath, "utf-8");
// Extract Archive Telemetry section and verify link to /archive
const telemetryMatch = sidebarContent.match(
/<section[^>]*aria-label="Archive Telemetry"[^>]*>([\s\S]*?)<\/section>/i
);
assert.ok(telemetryMatch, "Archive Telemetry section must exist in stats-sidebar.njk");
assert.match(
telemetryMatch[1],
/href="\/archive\/?"/i,
"Archive Telemetry section must contain a link to /archive/ (riverrooks.dev/archive)"
);
// Verify src/archive.njk exists and mirrors dispatches.njk minus featured section
const archivePagePath = path.resolve("src/archive.njk");
assert.ok(fs.existsSync(archivePagePath), "src/archive.njk must exist");
const archivePageContent = fs.readFileSync(archivePagePath, "utf-8");
assert.match(archivePageContent, /layout:\s*layouts\/base\.njk/, "archive.njk must use base layout");
assert.match(archivePageContent, /collections\.archivedPosts/, "archive.njk must iterate over collections.archivedPosts");
assert.match(archivePageContent, /partials\/post-card\.njk/, "archive.njk must render post cards");
assert.doesNotMatch(archivePageContent, /featured-card\.njk/, "archive.njk must NOT include the featured section");
});
test("Archived Posts: Obsidian normalization preserves archive frontmatter property", () => {
const obsidianNote = `---
title: "Legacy Architecture Notes"
created: 2026-10-03
tags: ["#archive", "#systems"]
archive: true
---
# Legacy Notes
Archived reference documentation.
`;
const normalized = normalizeObsidianMarkdown(obsidianNote);
assert.match(normalized, /archive:\s*true/, "Normalized markdown must preserve 'archive: true'");
});
test("Archived Posts: Example archived markdown post exists in src/posts/ and builds to _site/archive/index.html", () => {
const postsDir = path.resolve("src/posts");
const postFiles = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
const archivedExamples = postFiles.filter((file) => {
const content = fs.readFileSync(path.join(postsDir, file), "utf-8");
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
return fmMatch && /^archive:\s*true\s*$/m.test(fmMatch[1]);
});
assert.ok(
archivedExamples.length >= 1,
"At least one example markdown post in src/posts/ must have 'archive: true'"
);
const dispatchesHtmlPath = path.resolve("_site/dispatches/index.html");
const archiveHtmlPath = path.resolve("_site/archive/index.html");
if (!fs.existsSync(archiveHtmlPath)) {
execSync("npm run build", {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
}
assert.ok(fs.existsSync(dispatchesHtmlPath), "_site/dispatches/index.html must exist");
assert.ok(fs.existsSync(archiveHtmlPath), "_site/archive/index.html must exist");
const dispatchesHtml = fs.readFileSync(dispatchesHtmlPath, "utf-8");
const archiveHtml = fs.readFileSync(archiveHtmlPath, "utf-8");
// 1. Dispatches page has link to /archive/ in Archive Telemetry
const dispatchesTelemetry = dispatchesHtml.match(
/<section[^>]*aria-label="Archive Telemetry"[^>]*>([\s\S]*?)<\/section>/i
);
assert.ok(dispatchesTelemetry, "Compiled dispatches page must have Archive Telemetry section");
assert.match(
dispatchesTelemetry[1],
/href="\/archive\/?"/i,
"Compiled Archive Telemetry section must link to /archive/"
);
// 2. Archived example post does NOT appear on Dispatches page, but DOES appear on Archive page
assert.doesNotMatch(
dispatchesHtml,
/Archived Dispatch Demo/,
"Archived post must NOT appear in the Dispatches page list or featured card"
);
assert.match(
archiveHtml,
/Archived Dispatch Demo/,
"Archived post MUST appear on the /archive/ page"
);
// 3. Archive page does NOT have the Featured Dispatch section
assert.doesNotMatch(
archiveHtml,
/aria-label="Featured Technical Dispatch"/i,
"Archive page must not contain the Featured Dispatch section"
);
});
+62
View File
@@ -0,0 +1,62 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import configureEleventy from "../eleventy.config.js";
import { formatBuildAge } from "../src/assets/js/build-timer.js";
test("Build age: formats elapsed time as a compact live footer value", () => {
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-20T12:00:00.000Z"),
"0s ago"
);
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-20T12:01:01.000Z"),
"1m 1s ago"
);
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-21T14:03:04.000Z"),
"1d 2h ago"
);
assert.equal(
formatBuildAge("2026-10-20T12:00:00.000Z", "2026-10-20T11:59:59.000Z"),
"0s ago"
);
});
test("Build age: Eleventy exposes one ISO timestamp for each rebuild", () => {
const globalData = new Map();
const events = new Map();
configureEleventy({
setUseGitIgnore() {},
addPassthroughCopy() {},
addPlugin() {},
setLibrary() {},
addFilter() {},
addCollection() {},
addGlobalData(name, value) {
globalData.set(name, value);
},
on(name, callback) {
events.set(name, callback);
},
setServerOptions() {}
});
events.get("eleventy.before")();
assert.match(
globalData.get("buildTimestamp")(),
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
);
});
test("Build age: footer provides an accessible live timer backed by the build timestamp", () => {
const footer = fs.readFileSync(path.resolve("src/_includes/partials/footer.njk"), "utf-8");
const baseLayout = fs.readFileSync(path.resolve("src/_includes/layouts/base.njk"), "utf-8");
assert.match(
footer,
/<time id="build-age" data-build-timestamp="{{ buildTimestamp }}" aria-live="polite">REBUILT: INITIALIZING<\/time>/
);
assert.match(baseLayout, /<script type="module" src="\/assets\/js\/build-timer\.js"><\/script>/);
});
+86
View File
@@ -0,0 +1,86 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const siteDir = path.resolve("_site");
function getAllHtmlFiles(dir) {
let results = [];
const list = fs.readdirSync(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(getAllHtmlFiles(filePath));
} else if (file.endsWith(".html")) {
results.push(filePath);
}
}
return results;
}
test("Build & Link Integrity: Output inspection and link verification", () => {
// Ensure fresh build exists
if (!fs.existsSync(path.join(siteDir, "index.html"))) {
execSync("npm run build", {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
}
// 1. Core Pages Exist
assert.ok(fs.existsSync(path.join(siteDir, "index.html")), "Root _site/index.html must exist");
assert.ok(fs.existsSync(path.join(siteDir, "dispatches/index.html")), "_site/dispatches/index.html must exist");
assert.ok(fs.existsSync(path.join(siteDir, "assets/css/main.css")), "_site/assets/css/main.css must exist");
const htmlFiles = getAllHtmlFiles(siteDir);
assert.ok(htmlFiles.length >= 4, "Must compile all core pages and articles");
for (const htmlFile of htmlFiles) {
const relHtml = path.relative(siteDir, htmlFile);
const content = fs.readFileSync(htmlFile, "utf-8");
// 2. HTML Boilerplate Invariants
assert.match(content, /<!DOCTYPE html>/i, `${relHtml} must contain <!DOCTYPE html>`);
assert.match(content, /<meta\s+name="viewport"/i, `${relHtml} must have viewport meta tag`);
assert.match(content, /<link\s+rel="stylesheet"\s+href="\/assets\/css\/main\.css(?:\?[^"]*)?">/i, `${relHtml} must link to main.css`);
// 3. Internal Link Integrity (No 404s!)
const internalLinks = [...content.matchAll(/href="(\/[^"#?]*)"/g)].map((m) => m[1]);
for (const link of internalLinks) {
if (link.startsWith("/feed.xml")) continue; // dynamic RSS route
// Normalize link to file on disk: /dispatches/ -> _site/dispatches/index.html or _site/dispatches.html
let targetPath;
if (link.endsWith("/")) {
targetPath = path.join(siteDir, link, "index.html");
} else if (path.extname(link)) {
targetPath = path.join(siteDir, link);
} else {
targetPath = path.join(siteDir, `${link}.html`);
if (!fs.existsSync(targetPath)) {
targetPath = path.join(siteDir, link, "index.html");
}
}
assert.ok(
fs.existsSync(targetPath),
`Broken internal link in ${relHtml}: href="${link}" resolves to missing file: ${targetPath}`
);
}
// 4. Anchor Link Integrity (TOC anchor links match actual heading IDs)
const anchorLinks = [...content.matchAll(/href="#([^"]+)"/g)].map((m) => m[1]);
for (const anchor of anchorLinks) {
// Ignore skip-to-content or standard body anchors
if (anchor === "main-content") continue;
const expectedIdRegex = new RegExp(`id="${anchor}"`, "i");
assert.match(
content,
expectedIdRegex,
`Broken TOC anchor link in ${relHtml}: href="#${anchor}" has no corresponding id="${anchor}"`
);
}
}
});
+86
View File
@@ -0,0 +1,86 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const cssDir = path.resolve("src/assets/css");
function getAllCssFiles(dir) {
let results = [];
const list = fs.readdirSync(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(getAllCssFiles(filePath));
} else if (file.endsWith(".css")) {
results.push(filePath);
}
}
return results;
}
test("Design Specification: Strict Anti-Pattern Regression Testing", () => {
const cssFiles = getAllCssFiles(cssDir);
assert.ok(cssFiles.length >= 5, "Must inspect all modular CSS files");
for (const file of cssFiles) {
const relPath = path.relative(process.cwd(), file);
const content = fs.readFileSync(file, "utf-8");
// 1. Anti-Pattern: NO Glassmorphism (backdrop-filter)
assert.doesNotMatch(
content,
/backdrop-filter\s*:/i,
`Violation in ${relPath}: Glassmorphism (backdrop-filter) is strictly forbidden`
);
// 2. Anti-Pattern: NO Drop Shadows or Glows (box-shadow, text-shadow)
// Allowed: box-shadow: none; or text-shadow: none;
const shadowMatches = content.match(/(box-shadow|text-shadow)\s*:\s*([^;]+);/gi) || [];
for (const match of shadowMatches) {
assert.match(
match,
/(box-shadow|text-shadow)\s*:\s*none\s*;/i,
`Violation in ${relPath}: Drop shadows and glows are forbidden (found: '${match}')`
);
}
// 3. Anti-Pattern: NO Gradients (linear, radial, conic)
assert.doesNotMatch(
content,
/(linear-gradient|radial-gradient|conic-gradient)/i,
`Violation in ${relPath}: Gradients are strictly forbidden`
);
// 4. Anti-Pattern: NO Rounded Bubbles (border-radius capped at 4px max)
const radiusMatches = content.match(/border-radius\s*:\s*([^;]+);/gi) || [];
for (const match of radiusMatches) {
// Check for pixel values > 4px (e.g. 5px, 6px, 8px, 12px, 20px)
const pxMatch = match.match(/(\d+)\s*px/i);
if (pxMatch) {
const px = parseInt(pxMatch[1], 10);
assert.ok(
px <= 4,
`Violation in ${relPath}: Border radius must be 0px to 4px max, found: ${px}px in '${match}'`
);
}
// Check for percentages (e.g. 50%)
assert.doesNotMatch(
match,
/\d+%/,
`Violation in ${relPath}: Rounded pill/circular percentages forbidden in '${match}'`
);
}
// 5. Anti-Pattern: NO Float/Spring Transitions (Must be instant snaps)
const transitionMatches = content.match(/transition\s*:\s*([^;]+);/gi) || [];
for (const match of transitionMatches) {
assert.match(
match,
/transition\s*:\s*(none(\s*!important)?|0s\s*);/i,
`Violation in ${relPath}: Transitions must be instant (none or 0s), found: '${match}'`
);
}
}
});
+112
View File
@@ -0,0 +1,112 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const agentsMdPath = path.resolve("AGENTS.md");
const rulesMdPath = path.resolve(".agents/rules/blog-system.md");
test("Development Standards: AGENTS.md enforces test-first, adversarial testing, and verification gates", () => {
assert.ok(fs.existsSync(agentsMdPath), "AGENTS.md must exist in root");
const content = fs.readFileSync(agentsMdPath, "utf-8");
// 1. Must enforce writing tests BEFORE modifying functional/production code
assert.match(
content,
/before\s+(?:writing\s+or\s+)?modifying\s+(?:any\s+)?(?:functional|production)\s+code/i,
"AGENTS.md must explicitly mandate that tests are written before modifying functional/production code"
);
// 2. Must mandate adversarial testing
assert.match(
content,
/adversarial\s+test(?:ing|s)/i,
"AGENTS.md must mandate adversarial testing"
);
// 3. Must mandate that all tests pass before presenting final output
assert.match(
content,
/pass\s+before\s+presenting\s+(?:the\s+)?final\s+output/i,
"AGENTS.md must mandate that test cases pass before presenting final output"
);
});
test("Development Standards: .agents/rules/blog-system.md enforces test-first, adversarial testing, and verification gates", () => {
assert.ok(fs.existsSync(rulesMdPath), ".agents/rules/blog-system.md must exist");
const content = fs.readFileSync(rulesMdPath, "utf-8");
// 1. Must enforce writing tests BEFORE modifying functional/production code
assert.match(
content,
/before\s+(?:writing\s+or\s+)?modifying\s+(?:any\s+)?(?:functional|production)\s+code/i,
"blog-system.md must explicitly mandate that tests are written before modifying functional/production code"
);
// 2. Must mandate adversarial testing
assert.match(
content,
/adversarial\s+test(?:ing|s)/i,
"blog-system.md must mandate adversarial testing"
);
// 3. Must mandate that all tests pass before presenting final output
assert.match(
content,
/pass\s+before\s+presenting\s+(?:the\s+)?final\s+output/i,
"blog-system.md must mandate that test cases pass before presenting final output"
);
});
test("Development Standards: Adversarial checks against vague or permissive testing language", () => {
const files = [agentsMdPath, rulesMdPath];
for (const filePath of files) {
const content = fs.readFileSync(filePath, "utf-8");
const relPath = path.relative(process.cwd(), filePath);
// Adversarial: ensure no permissive "or alongside implementing" loophole exists
assert.doesNotMatch(
content,
/or\s+alongside\s+implementing/i,
`Violation in ${relPath}: Permissive loophole "or alongside implementing" must not exist; tests must strictly precede functional code`
);
}
});
test("Development Standards: README.md and skills reference TDD, adversarial testing, and verification", () => {
const readmePath = path.resolve("README.md");
assert.ok(fs.existsSync(readmePath), "README.md must exist");
const readmeContent = fs.readFileSync(readmePath, "utf-8");
assert.match(
readmeContent,
/written\s+before\s+(?:writing\s+or\s+)?modifying\s+(?:any\s+)?functional\s+code/i,
"README.md must document writing tests before modifying functional code"
);
assert.match(
readmeContent,
/adversarial\s+test(?:ing|s)/i,
"README.md must document mandatory adversarial testing"
);
assert.match(
readmeContent,
/pass\s+before\s+presenting\s+(?:the\s+)?final\s+output/i,
"README.md must document that tests must pass before presenting final output"
);
// Check eleventy-tiling-blog skill
const eleventySkillPath = path.resolve(".agents/skills/eleventy-tiling-blog/SKILL.md");
assert.ok(fs.existsSync(eleventySkillPath), "eleventy-tiling-blog SKILL.md must exist");
const eleventySkill = fs.readFileSync(eleventySkillPath, "utf-8");
assert.match(eleventySkill, /adversarial/i, "eleventy-tiling-blog SKILL.md must mention adversarial testing");
assert.match(eleventySkill, /before\s+modifying\s+functional/i, "eleventy-tiling-blog SKILL.md must mandate tests before modifying functional code");
// Check obsidian-sync skill
const obsidianSkillPath = path.resolve(".agents/skills/obsidian-sync/SKILL.md");
assert.ok(fs.existsSync(obsidianSkillPath), "obsidian-sync SKILL.md must exist");
const obsidianSkill = fs.readFileSync(obsidianSkillPath, "utf-8");
assert.match(obsidianSkill, /adversarial/i, "obsidian-sync SKILL.md must mention adversarial testing");
assert.match(obsidianSkill, /before[*\s`_]+modifying/i, "obsidian-sync SKILL.md must mandate tests before modifying functional code");
});
+64
View File
@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
test("Docker Configuration: Dockerfile enforces Node runtime, healthcheck, and entrypoint", () => {
const dockerfilePath = path.resolve(process.cwd(), "Dockerfile");
assert.ok(fs.existsSync(dockerfilePath), "Dockerfile must exist");
const content = fs.readFileSync(dockerfilePath, "utf-8");
// Must use Node 22 alpine
assert.match(content, /FROM\s+node:22-alpine/i, "Dockerfile must use node:22-alpine base image");
// Must NOT contain nginx base images or references
assert.doesNotMatch(content, /FROM\s+nginx/i, "Dockerfile must not use nginx base image");
assert.doesNotMatch(content, /nginx/i, "Dockerfile must not contain nginx directives");
// Must build static site and configure dynamic port
assert.match(content, /RUN\s+npm\s+run\s+build/, "Dockerfile must compile baseline static assets");
assert.match(content, /ARG\s+PORT=8080/, "Dockerfile must define ARG PORT with default fallback");
assert.match(content, /ENV\s+PORT=\$\{?PORT\}?/, "Dockerfile must define ENV PORT using variable");
assert.match(content, /EXPOSE\s+\$\{?PORT\}?/, "Dockerfile must expose dynamic PORT variable");
// Must define built-in HEALTHCHECK probing 127.0.0.1 loopback
assert.match(content, /HEALTHCHECK[\s\S]*?127\.0\.0\.1/, "Dockerfile must define baked-in HEALTHCHECK on 127.0.0.1");
// Must configure dynamic entrypoint script
assert.match(content, /ENTRYPOINT\s*\[\s*["']\/app\/entrypoint\.sh["']\s*\]/, "Dockerfile must use /app/entrypoint.sh entrypoint");
});
test("Docker Configuration: entrypoint.sh script exists and has valid structure", () => {
const entrypointPath = path.resolve(process.cwd(), "entrypoint.sh");
assert.ok(fs.existsSync(entrypointPath), "entrypoint.sh must exist");
const stats = fs.statSync(entrypointPath);
// Check executable permissions (at least owner executable: 0o100)
assert.ok(Boolean(stats.mode & 0o111), "entrypoint.sh must have executable permission bits");
const content = fs.readFileSync(entrypointPath, "utf-8");
assert.match(content, /^#!\/bin\/sh/, "entrypoint.sh must use POSIX /bin/sh shebang");
assert.match(content, /exec\s+"\$@"/, "entrypoint.sh must pass through custom container arguments");
assert.match(content, /npm\s+run\s+build/, "entrypoint.sh must build the static site");
assert.match(content, /exec\s+node\s+server\.js/, "entrypoint.sh must start static server with exec");
});
test("Docker Configuration: docker-compose configures Node service correctly", () => {
const composePath = fs.existsSync(path.resolve(process.cwd(), "sample-docker-compose.yml"))
? path.resolve(process.cwd(), "sample-docker-compose.yml")
: path.resolve(process.cwd(), "docker-compose.yml");
assert.ok(fs.existsSync(composePath), "Compose template file must exist");
const content = fs.readFileSync(composePath, "utf-8");
// Verifies dynamic port mapping and environment variables
assert.match(content, /PORT.*8080/, "docker-compose.yml must configure PORT fallback");
assert.match(content, /:\$\{PORT:-8080\}/, "docker-compose.yml must map dynamic container PORT");
// Verifies domain argument enforcement
assert.match(content, /SITE_DOMAIN/, "docker-compose.yml must require SITE_DOMAIN");
// Verifies markdown volume mount
assert.match(content, /\/app\/src\/posts/, "docker-compose.yml must mount external markdown posts");
});
+64
View File
@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
import nunjucks from "nunjucks";
import path from "node:path";
import { filters } from "../src/_filters/index.js";
const includesDir = path.resolve("src/_includes");
const markdownExcerpt = "Read the [deployment guide](/guides/deploy/) before continuing.";
function renderTemplate(template, context) {
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(includesDir), { autoescape: false });
for (const [name, filter] of Object.entries(filters)) {
env.addFilter(name, filter);
}
return env.render(template, context);
}
test("Excerpt Markdown: renders Markdown links as safe inline HTML", () => {
assert.equal(
filters.renderExcerptMarkdown(markdownExcerpt),
'Read the <a href="/guides/deploy/">deployment guide</a> before continuing.'
);
});
test("Excerpt Markdown: renders links in post-card previews", () => {
const output = renderTemplate("partials/post-card.njk", {
post: {
fileSlug: "example-dispatch",
url: "/dispatches/example/",
date: "2026-10-06",
data: {
title: "Example dispatch",
excerpt: markdownExcerpt,
tags: ["systems"]
},
templateContent: "A short article."
}
});
assert.match(output, /<p class="feed-excerpt">\s*Read the <a href="\/guides\/deploy\/">deployment guide<\/a> before continuing\.\s*<\/p>/);
});
test("Excerpt Markdown: renders links in featured previews and escapes raw HTML", () => {
const output = renderTemplate("partials/featured-card.njk", {
collections: {
posts: [{
url: "/dispatches/example/",
date: "2026-10-06",
data: {
featured: true,
tags: ["systems"],
title: "Example dispatch",
excerpt: `${markdownExcerpt} <script>alert('unsafe')</script>`
},
templateContent: ""
}]
}
});
assert.match(output, /<a href="\/guides\/deploy\/">deployment guide<\/a>/);
assert.match(output, /&lt;script&gt;alert\('unsafe'\)&lt;\/script&gt;/);
assert.doesNotMatch(output, /<script>/);
});
+62
View File
@@ -0,0 +1,62 @@
import test from "node:test";
import assert from "node:assert/strict";
import nunjucks from "nunjucks";
import path from "node:path";
import {
extractTerminalPreview,
filterTagList,
isoDate,
postDate,
readingTime,
renderExcerptMarkdown
} from "../src/_filters/index.js";
const includesDir = path.resolve("src/_includes");
function renderFeaturedCard(terminal) {
const env = new nunjucks.Environment(new nunjucks.FileSystemLoader(includesDir), { autoescape: false });
env.addFilter("extractTerminalPreview", extractTerminalPreview);
env.addFilter("filterTagList", filterTagList);
env.addFilter("isoDate", isoDate);
env.addFilter("postDate", postDate);
env.addFilter("readingTime", readingTime);
env.addFilter("renderExcerptMarkdown", renderExcerptMarkdown);
const featured = {
url: "/dispatches/example/",
date: "2026-10-06",
data: {
featured: true,
terminal,
tags: ["systems"],
title: "Example dispatch",
excerpt: "An example featured dispatch used to verify the terminal preview."
},
templateContent: `
<h1 id="terminal-preview">Terminal preview</h1>
<p>Inspect the first five lines:</p>
<pre><code class="language-bash">cat /dispatches/example | head -n 5</code></pre>
<h1 id="investigation">Investigation</h1>
<p>The public article begins here.</p>
`
};
return env.render("partials/featured-card.njk", {
collections: { posts: [featured] }
});
}
test("Featured terminal preview: renders Markdown output only when terminal frontmatter is true", () => {
const output = renderFeaturedCard(true);
assert.match(output, /class="[^"]*terminal-code-box[^"]*"/);
assert.match(output, /Inspect the first five lines:/);
assert.match(output, /cat \/dispatches\/example \| head -n 5/);
assert.doesNotMatch(output, /Terminal preview/);
assert.doesNotMatch(output, /init_cluster\.sh/);
});
test("Featured terminal preview: does not render when terminal frontmatter is false or absent", () => {
assert.doesNotMatch(renderFeaturedCard(false), /terminal-code-box/);
assert.doesNotMatch(renderFeaturedCard(undefined), /terminal-code-box/);
});
+107
View File
@@ -0,0 +1,107 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
postDate,
isoDate,
limit,
filterTagList,
readingTime,
extractHeadings
} from "../src/_filters/index.js";
test("Filters - postDate: formats valid dates", () => {
assert.equal(postDate("2026-07-20"), "JUL 20, 2026");
assert.equal(postDate(new Date("2026-12-31T00:00:00Z")), "DEC 31, 2026");
});
test("Filters - postDate: handles adversarial / invalid inputs gracefully", () => {
assert.equal(postDate(null), "");
assert.equal(postDate(undefined), "");
assert.equal(postDate("invalid-date-string"), "");
assert.equal(postDate(""), "");
});
test("Filters - isoDate: formats ISO strings", () => {
assert.equal(isoDate("2026-07-20"), "2026-07-20");
assert.equal(isoDate(new Date("2026-05-15T00:00:00Z")), "2026-05-15");
});
test("Filters - isoDate: handles adversarial / invalid inputs gracefully", () => {
assert.equal(isoDate(null), "");
assert.equal(isoDate(undefined), "");
assert.equal(isoDate("not-a-date"), "");
});
test("Filters - limit: slices arrays correctly", () => {
assert.deepEqual(limit([1, 2, 3, 4, 5], 3), [1, 2, 3]);
assert.deepEqual(limit([1, 2], 5), [1, 2]);
});
test("Filters - limit: handles adversarial inputs (negative, 0, non-array)", () => {
assert.deepEqual(limit([1, 2, 3], 0), []);
assert.deepEqual(limit([1, 2, 3], -5), []);
assert.deepEqual(limit(null, 3), []);
assert.deepEqual(limit("not-an-array", 3), []);
});
test("Filters - filterTagList: removes internal system tags", () => {
const input = ["firmware", "posts", "security", "all", "nav", "post"];
assert.deepEqual(filterTagList(input), ["firmware", "security"]);
});
test("Filters - filterTagList: handles adversarial inputs (# prefix, casing, non-string)", () => {
const input = ["#firmware", "#POSTS", "ALL", "#homelab", 42, null, undefined];
assert.deepEqual(filterTagList(input), ["#firmware", "#homelab"]);
assert.deepEqual(filterTagList(null), []);
assert.deepEqual(filterTagList(undefined), []);
});
test("Filters - readingTime: calculates reading time from word count", () => {
// 199 words -> 1 min
const shortText = "word ".repeat(199);
assert.equal(readingTime(shortText), "1 MIN READ");
// 201 words -> 2 min
const mediumText = "word ".repeat(201);
assert.equal(readingTime(mediumText), "2 MIN READ");
// 1000 words -> 5 min
const longText = "word ".repeat(1000);
assert.equal(readingTime(longText), "5 MIN READ");
});
test("Filters - readingTime: respects explicit frontmatter override", () => {
assert.equal(readingTime("any text", "15 MIN READ"), "15 MIN READ");
});
test("Filters - readingTime: handles adversarial HTML tags without inflating word count", () => {
// HTML tags should be stripped before counting words
const htmlText = "<div><span>word</span> <p>another</p></div>";
assert.equal(readingTime(htmlText), "1 MIN READ");
assert.equal(readingTime(""), "1 MIN READ");
assert.equal(readingTime(null), "1 MIN READ");
});
test("Filters - extractHeadings: extracts H2 and H3 with IDs and titles", () => {
const html = `
<h1>Title To Ignore</h1>
<h2 id="section-1">1.0 First Section</h2>
<p>Some text</p>
<h3 id="sub-1">1.1 Sub Section</h3>
<h4 id="h4-ignore">Ignore H4</h4>
<h2 id="section-2">2.0 Second Section with <code>code</code> and <span class="tag">span</span></h2>
`;
const headings = extractHeadings(html);
assert.equal(headings.length, 3);
assert.deepEqual(headings[0], { level: 2, id: "section-1", title: "1.0 First Section" });
assert.deepEqual(headings[1], { level: 3, id: "sub-1", title: "1.1 Sub Section" });
assert.deepEqual(headings[2], { level: 2, id: "section-2", title: "2.0 Second Section with code and span" });
});
test("Filters - extractHeadings: handles adversarial HTML inputs gracefully", () => {
assert.deepEqual(extractHeadings(""), []);
assert.deepEqual(extractHeadings(null), []);
assert.deepEqual(extractHeadings(undefined), []);
assert.deepEqual(extractHeadings("<p>No headings here</p>"), []);
});
+76
View File
@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const postsDir = path.resolve("src/posts");
test("Content Contract: All posts in src/posts/ satisfy frontmatter schema", () => {
if (!fs.existsSync(postsDir)) {
fs.mkdirSync(postsDir, { recursive: true });
}
const postFiles = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
if (postFiles.length === 0) {
// When posts are decoupled and mounted externally or ingested via API
return;
}
for (const file of postFiles) {
const filePath = path.join(postsDir, file);
const content = fs.readFileSync(filePath, "utf-8");
// 1. Filename Convention: YYYY-MM-DD-slug.md
assert.match(
file,
/^\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.md$/,
`Post filename '${file}' must follow 'YYYY-MM-DD-slug.md' naming standard`
);
// 2. YAML Frontmatter Boundary check
assert.match(
content,
/^---\n([\s\S]*?)\n---/,
`Post '${file}' must start with valid YAML frontmatter delimiters`
);
// Extract frontmatter block
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const fmText = fmMatch[1];
// 3. Title Invariant
assert.match(
fmText,
/title:\s*"[^"]{5,}"/,
`Post '${file}' must contain a valid title of at least 5 characters`
);
// 4. Date Invariant (ISO Format)
assert.match(
fmText,
/date:\s*\d{4}-\d{2}-\d{2}/,
`Post '${file}' must define an ISO date (YYYY-MM-DD)`
);
// 5. Layout Invariant
assert.match(
fmText,
/layout:\s*layouts\/post\.njk/,
`Post '${file}' must specify 'layout: layouts/post.njk'`
);
// 6. Tags Invariant
assert.match(
fmText,
/tags:\s*\n(\s+-\s+[a-z0-9-]+\n)+/,
`Post '${file}' must specify a non-empty YAML tags list`
);
// 7. Excerpt Invariant
assert.match(
fmText,
/excerpt:\s*"[^"]{20,}"/,
`Post '${file}' must provide a substantive excerpt of at least 20 characters`
);
}
});
+5
View File
@@ -0,0 +1,5 @@
const port = process.env.PORT || 8080;
fetch(`http://127.0.0.1:${port}/healthz`)
.then((res) => process.exit(res.ok ? 0 : 1))
.catch(() => process.exit(1));
+79
View File
@@ -0,0 +1,79 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import http from "node:http";
import { spawn } from "node:child_process";
test("Healthcheck: test/healthcheck.js script exists and parses cleanly", () => {
const scriptPath = path.resolve(process.cwd(), "test/healthcheck.js");
assert.ok(fs.existsSync(scriptPath), "test/healthcheck.js must exist");
const content = fs.readFileSync(scriptPath, "utf-8");
assert.match(content, /127\.0\.0\.1/, "Must target IPv4 loopback 127.0.0.1 to avoid Alpine IPv6 lookup failures");
assert.match(content, /\/healthz/, "Must target the /healthz endpoint");
});
test("Healthcheck: test/healthcheck.js succeeds when server returns 200 OK", async () => {
const scriptPath = path.resolve(process.cwd(), "test/healthcheck.js");
// Start dummy HTTP server responding 200 on /healthz
const server = http.createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
} else {
res.writeHead(404);
res.end();
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
const exitCode = await new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath], {
env: { ...process.env, PORT: String(port) },
});
child.on("close", resolve);
});
assert.strictEqual(exitCode, 0, "healthcheck.js must exit with code 0 on healthy endpoint");
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test("Healthcheck: test/healthcheck.js fails when connection is refused or server is 500", async () => {
const scriptPath = path.resolve(process.cwd(), "test/healthcheck.js");
// Pick an unused high port
const exitCodeDown = await new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath], {
env: { ...process.env, PORT: "59999" },
});
child.on("close", resolve);
});
assert.strictEqual(exitCodeDown, 1, "healthcheck.js must exit with code 1 when server is unreachable");
// Test 500 status code
const server = http.createServer((req, res) => {
res.writeHead(500);
res.end();
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
const exitCode500 = await new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath], {
env: { ...process.env, PORT: String(port) },
});
child.on("close", resolve);
});
assert.strictEqual(exitCode500, 1, "healthcheck.js must exit with code 1 on HTTP 500");
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
+293
View File
@@ -0,0 +1,293 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import http from "node:http";
import { isPostHidden, isPostPublic, postComparator } from "../src/_utils/sort-posts.js";
import { createServer, normalizeObsidianMarkdown } from "../server.js";
test("Hidden Posts: isPostHidden accurately identifies hidden flags across data shapes", () => {
// 1. Explicit boolean true
assert.equal(isPostHidden({ data: { hidden: true } }), true, "Explicit boolean true in data");
assert.equal(isPostHidden({ hidden: true }), true, "Root level hidden true");
// 2. Truthy string representations (case-insensitive & trimmed)
assert.equal(isPostHidden({ data: { hidden: "true" } }), true, "String 'true'");
assert.equal(isPostHidden({ data: { hidden: "TRUE" } }), true, "Uppercase string 'TRUE'");
assert.equal(isPostHidden({ data: { hidden: " true " } }), true, "Whitespace-padded 'true'");
assert.equal(isPostHidden({ data: { hidden: "yes" } }), true, "String 'yes'");
assert.equal(isPostHidden({ data: { hidden: "YES" } }), true, "String 'YES'");
assert.equal(isPostHidden({ data: { hidden: "1" } }), true, "String '1'");
assert.equal(isPostHidden({ data: { hidden: 1 } }), true, "Numeric 1");
// 3. Falsy representations
assert.equal(isPostHidden({ data: { hidden: false } }), false, "Explicit boolean false");
assert.equal(isPostHidden({ data: { hidden: "false" } }), false, "String 'false'");
assert.equal(isPostHidden({ data: { hidden: "no" } }), false, "String 'no'");
assert.equal(isPostHidden({ data: { hidden: "0" } }), false, "String '0'");
assert.equal(isPostHidden({ data: { hidden: 0 } }), false, "Numeric 0");
assert.equal(isPostHidden({ data: { hidden: "" } }), false, "Empty string");
assert.equal(isPostHidden({ data: { hidden: null } }), false, "Null property");
assert.equal(isPostHidden({ data: {} }), false, "Missing hidden property");
assert.equal(isPostHidden({}), false, "Empty post object");
// 4. Adversarial edge cases
assert.equal(isPostHidden(null), false, "Null input");
assert.equal(isPostHidden(undefined), false, "Undefined input");
assert.equal(isPostHidden("unexpected-string"), false, "Primitive string input");
assert.equal(isPostHidden(12345), false, "Primitive number input");
assert.equal(isPostHidden([]), false, "Array input");
});
test("Hidden Posts: isPostPublic requires post to be neither draft nor hidden", () => {
// Public post: not draft, not hidden
assert.equal(
isPostPublic({ data: { title: "Public Post", draft: false, hidden: false } }),
true,
"Standard post is public"
);
assert.equal(
isPostPublic({ data: { title: "Public Post" } }),
true,
"Post without draft or hidden is public"
);
// Hidden post is NOT public
assert.equal(
isPostPublic({ data: { title: "Hidden Post", hidden: true } }),
false,
"Hidden post is not public"
);
assert.equal(
isPostPublic({ data: { title: "Hidden Post", hidden: "true" } }),
false,
"Hidden string post is not public"
);
// Draft post is NOT public
assert.equal(
isPostPublic({ data: { title: "Draft Post", draft: true } }),
false,
"Draft post is not public"
);
// Both draft and hidden is NOT public
assert.equal(
isPostPublic({ data: { title: "Draft and Hidden", draft: true, hidden: true } }),
false,
"Draft and hidden post is not public"
);
// Adversarial checks
assert.equal(isPostPublic(null), false, "Null is not public");
assert.equal(isPostPublic(undefined), false, "Undefined is not public");
assert.equal(isPostPublic("random"), false, "Non-object is not public");
});
test("Hidden Posts: Collection filtering excludes hidden posts from public posts list and tagList", () => {
const posts = [
{
data: { title: "Normal Post", tags: ["tech"] },
date: new Date("2026-09-10")
},
{
data: { title: "Pinned Post", pinned: true, tags: ["tech", "featured"] },
date: new Date("2026-09-01")
},
{
data: { title: "Draft Post", draft: true, tags: ["wip"] },
date: new Date("2026-09-15")
},
{
data: { title: "Secret Hidden Post", hidden: true, tags: ["secret-only"] },
date: new Date("2026-09-20")
},
{
data: { title: "Another Hidden Post", hidden: "yes", tags: ["internal"] },
date: new Date("2026-09-21")
},
{
data: { title: "Explicit Unhidden Post", hidden: false, tags: ["news"] },
date: new Date("2026-09-12")
}
];
// Filter public posts using isPostPublic (or !draft && !isPostHidden)
const publicPosts = posts.filter(isPostPublic).sort(postComparator);
const titles = publicPosts.map((p) => p.data.title);
assert.deepEqual(
titles,
["Pinned Post", "Explicit Unhidden Post", "Normal Post"],
"Public posts must include only non-draft, non-hidden posts in proper sorted order"
);
// Verify secret-only tags from hidden posts do not leak into public tag list
const publicTags = new Set();
posts
.filter(isPostPublic)
.forEach((item) => {
const tags = item.data.tags || [];
for (const t of tags) publicTags.add(t);
});
assert.ok(!publicTags.has("secret-only"), "Hidden post tag 'secret-only' must not appear in public tag list");
assert.ok(!publicTags.has("internal"), "Hidden post tag 'internal' must not appear in public tag list");
assert.ok(!publicTags.has("wip"), "Draft post tag 'wip' must not appear in public tag list");
assert.ok(publicTags.has("tech"), "Public post tag 'tech' must appear in public tag list");
assert.ok(publicTags.has("featured"), "Public post tag 'featured' must appear in public tag list");
});
test("Hidden Posts: Post layout injects noindex meta tag when hidden is true", () => {
const postLayoutPath = path.resolve("src/_includes/layouts/post.njk");
assert.ok(fs.existsSync(postLayoutPath), "post.njk must exist");
const postLayout = fs.readFileSync(postLayoutPath, "utf-8");
// Adversarial verification: hidden posts must instruct search crawlers not to index or follow
assert.match(
postLayout,
/noindex,\s*nofollow/i,
"post.njk must define 'noindex, nofollow' robots meta tag when hidden"
);
assert.match(
postLayout,
/{%\s*if\s+hidden\b/i,
"post.njk must conditionally check if post is hidden"
);
});
test("Hidden Posts: Obsidian normalization preserves hidden frontmatter property", () => {
const obsidianNote = `---
title: "Confidential System Blueprint"
created: 2026-09-23
tags: ["#architecture", "#secret"]
hidden: true
---
# Blueprint
Details for authorized eyes only.
`;
const normalized = normalizeObsidianMarkdown(obsidianNote);
assert.match(normalized, /hidden:\s*true/, "Normalized markdown must preserve 'hidden: true'");
assert.match(normalized, /title:\s*"Confidential System Blueprint"/, "Preserves title");
assert.match(normalized, /date:\s*2026-09-23/, "Bridges created to date");
assert.match(normalized, /layout:\s*layouts\/post\.njk/, "Adds post layout");
});
test("Hidden Posts: End-to-end static compilation and accessibility via direct link", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
try {
// 1. Create a public post and a hidden post
const publicPostContent = `---
title: "Public Announcement"
date: 2026-09-20
layout: layouts/post.njk
tags: ["news"]
excerpt: "This is a public announcement for everyone to read."
---
Public content visible to everyone.`;
const hiddenPostContent = `---
title: "Private Unlisted Memo"
date: 2026-09-22
layout: layouts/post.njk
tags: ["confidential"]
excerpt: "This is an unlisted memo meant for direct link access only."
hidden: true
---
Secret memo content available by link only.`;
fs.writeFileSync(path.join(tmpPosts, "2026-09-20-public-post.md"), publicPostContent, "utf-8");
fs.writeFileSync(path.join(tmpPosts, "2026-09-22-secret-memo.md"), hiddenPostContent, "utf-8");
// 2. Set up server with custom directories
process.env.API_SECRET = "test-secret";
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
// Simulate static files created by build
fs.mkdirSync(path.join(tmpDir, "posts/2026-09-22-secret-memo"), { recursive: true });
fs.mkdirSync(path.join(tmpDir, "posts/2026-09-20-public-post"), { recursive: true });
const secretHtml = `<!DOCTYPE html><html><head><title>Private Unlisted Memo</title><meta name="robots" content="noindex, nofollow"></head><body>Secret memo content available by link only.</body></html>`;
const publicHtml = `<!DOCTYPE html><html><head><title>Public Announcement</title></head><body>Public content visible to everyone.</body></html>`;
const indexHtml = `<!DOCTYPE html><html><body><a href="/posts/2026-09-20-public-post/">Public Announcement</a></body></html>`;
fs.writeFileSync(path.join(tmpDir, "posts/2026-09-22-secret-memo/index.html"), secretHtml);
fs.writeFileSync(path.join(tmpDir, "posts/2026-09-20-public-post/index.html"), publicHtml);
fs.writeFileSync(path.join(tmpDir, "index.html"), indexHtml);
// Verify direct access to hidden post succeeds (HTTP 200 OK)
const resSecret = await fetch(`http://127.0.0.1:${port}/posts/2026-09-22-secret-memo/`);
assert.strictEqual(resSecret.status, 200, "Direct link to hidden post must return 200 OK");
const secretBody = await resSecret.text();
assert.match(secretBody, /Secret memo content available by link only/, "Hidden post body is accessible");
assert.match(secretBody, /noindex,\s*nofollow/, "Hidden post includes noindex meta tag");
// Verify public index does NOT contain link or title to hidden post
const resIndex = await fetch(`http://127.0.0.1:${port}/`);
assert.strictEqual(resIndex.status, 200);
const indexBody = await resIndex.text();
assert.match(indexBody, /Public Announcement/, "Public post is listed on index");
assert.doesNotMatch(indexBody, /Private Unlisted Memo/, "Hidden post MUST NOT appear on index");
assert.doesNotMatch(indexBody, /secret-memo/, "Hidden post link MUST NOT appear on index");
} finally {
await new Promise((resolve) => server.close(resolve));
}
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
test("Hidden Posts: Obsidian API ingestion correctly writes hidden frontmatter", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = "test-secret";
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
const res = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer test-secret"
},
body: JSON.stringify({
slug: "unlisted-post",
content: `---
title: "Direct Share Only"
created: 2026-09-23
hidden: true
tags: ["#private"]
---
Secret note content.`
})
});
assert.strictEqual(res.status, 200);
const json = await res.json();
assert.strictEqual(json.status, "published");
assert.strictEqual(json.path, "/posts/unlisted-post/");
const writtenFile = path.join(tmpPosts, "unlisted-post.md");
assert.ok(fs.existsSync(writtenFile), "Post file must exist");
const writtenContent = fs.readFileSync(writtenFile, "utf-8");
assert.match(writtenContent, /hidden:\s*true/);
assert.match(writtenContent, /layout:\s*layouts\/post\.njk/);
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
+303
View File
@@ -0,0 +1,303 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import http from "node:http";
import { createServer, normalizeObsidianMarkdown, triggerEleventyBuild } from "../server.js";
const TEST_SECRET = "super-secret-test-token";
test("Obsidian normalization: preserves linked note header fragments", () => {
const normalized = normalizeObsidianMarkdown(
"See [[a note#header one]] and [[another note#Second Header|the other section]]."
);
assert.match(
normalized,
/\[a note#header one\]\(\/posts\/a-note\/#header-one\)/,
"A wikilink header must become a URL fragment on the linked note"
);
assert.match(
normalized,
/\[the other section\]\(\/posts\/another-note\/#second-header\)/,
"Aliased wikilinks must retain their linked-note header fragment"
);
});
test("Ingestion API: rejects requests without proper Bearer token", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
// 1. Missing Authorization header
const resNoAuth = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "test", content: "test" })
});
assert.strictEqual(resNoAuth.status, 401, "Missing auth header must return 401");
// 2. Invalid Bearer token
const resBadAuth = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer wrong-token"
},
body: JSON.stringify({ slug: "test", content: "test" })
});
assert.strictEqual(resBadAuth.status, 401, "Invalid token must return 401");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
test("Ingestion API: validates required fields and frontmatter schema", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
// Missing slug
const resNoSlug = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({ content: "---\ntitle: 'Test'\ndate: 2026-09-14\n---\nHello" })
});
assert.strictEqual(resNoSlug.status, 400, "Missing slug must return 400");
// Missing required frontmatter title
const resNoTitle = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "no-title-post",
content: "---\ndate: 2026-09-14\n---\nHello"
})
});
assert.strictEqual(resNoTitle.status, 400, "Missing frontmatter title must return 400");
// Missing required frontmatter date or created
const resNoDate = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "no-date-post",
content: "---\ntitle: 'Valid Title'\n---\nHello"
})
});
assert.strictEqual(resNoDate.status, 400, "Missing frontmatter created/date must return 400");
// Valid with created instead of date
const resWithCreated = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "created-tag-post",
content: "---\ntitle: 'Valid Title'\ncreated: 2026-09-14\n---\nHello"
})
});
assert.strictEqual(resWithCreated.status, 200, "Valid post with created tag must return 200");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
test("Ingestion API: publishes markdown, normalizes wikilinks, saves assets, and handles delete", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
const tmpAssets = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-assets-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts, tmpAssets);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
const rawMarkdown = `---
title: "Reverse Engineering Flock Safety"
created: 2026-09-14
tags: ["#security", "#firmware"]
excerpt: "A deep dive into firmware."
---
Check out [[other-post|Other Post]] and also [[simple-link]].
Here is an image: ![[camera-board.png]]
`;
// 1x1 transparent PNG base64
const sampleBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
try {
// Publish post
const resPublish = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "flock-firmware",
content: rawMarkdown,
assets: [
{
filename: "camera-board.png",
data: sampleBase64
}
]
})
});
assert.strictEqual(resPublish.status, 200, "Valid publish must return 200");
const json = await resPublish.json();
assert.strictEqual(json.status, "published");
assert.strictEqual(json.path, "/posts/flock-firmware/");
// Verify markdown file written and wikilinks normalized
const writtenPath = path.join(tmpPosts, "flock-firmware.md");
assert.ok(fs.existsSync(writtenPath), "Post markdown file must be written to disk");
const writtenContent = fs.readFileSync(writtenPath, "utf-8");
assert.match(writtenContent, /^layout:\s*layouts\/post\.njk$/m, "Layout must be injected into frontmatter when missing");
assert.match(writtenContent, /\[Other Post\]\(\/posts\/other-post\/\)/, "Wikilink with alias must be translated");
assert.match(writtenContent, /\[simple-link\]\(\/posts\/simple-link\/\)/, "Simple wikilink must be translated");
assert.match(writtenContent, /!\[camera-board\.png\]\(\/assets\/images\/posts\/camera-board\.png\)/, "Attachment wikilink must be translated");
// Verify asset image was written
const assetPath = path.join(tmpAssets, "camera-board.png");
assert.ok(fs.existsSync(assetPath), "Asset file must be saved");
// Create simulated compiled static post files in siteDir
const compiledPostDir = path.join(tmpDir, "posts", "flock-firmware");
fs.mkdirSync(compiledPostDir, { recursive: true });
const compiledPostHtml = path.join(compiledPostDir, "index.html");
fs.writeFileSync(compiledPostHtml, "<html><body>Simulated compiled post</body></html>");
const standalonePostHtml = path.join(tmpDir, "posts", "flock-firmware.html");
fs.writeFileSync(standalonePostHtml, "<html><body>Simulated standalone post</body></html>");
assert.ok(fs.existsSync(compiledPostHtml), "Simulated compiled post must exist before delete");
assert.ok(fs.existsSync(standalonePostHtml), "Simulated standalone post must exist before delete");
// Delete post
const resDelete = await fetch(`http://127.0.0.1:${port}/api/posts?slug=flock-firmware`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${TEST_SECRET}`
}
});
assert.strictEqual(resDelete.status, 200, "Delete must return 200");
assert.ok(!fs.existsSync(writtenPath), "Post markdown file must be deleted");
assert.ok(!fs.existsSync(compiledPostDir), "Compiled post directory must be deleted from _site");
assert.ok(!fs.existsSync(standalonePostHtml), "Compiled standalone HTML must be deleted from _site");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
fs.rmSync(tmpAssets, { recursive: true, force: true });
}
});
test("Ingestion API: adversarial protection against path traversal in slug and DELETE validation", async () => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-site-"));
const tmpPosts = fs.mkdtempSync(path.join(process.cwd(), "test-tmp-posts-"));
process.env.API_SECRET = TEST_SECRET;
const server = createServer(tmpDir, tmpPosts);
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = server.address().port;
try {
// 1. Path traversal in POST
const resTraversal = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${TEST_SECRET}`
},
body: JSON.stringify({
slug: "../../etc/evil",
content: "---\ntitle: 'Evil'\ndate: 2026-09-14\n---\nEvil content"
})
});
// Should sanitize or reject slug containing path traversal
if (resTraversal.status === 200) {
const evilPath = path.resolve(tmpPosts, "../../etc/evil.md");
assert.ok(!fs.existsSync(evilPath), "Must never write outside posts directory");
} else {
assert.strictEqual(resTraversal.status, 400, "Invalid slug must return 400");
}
// 2. DELETE without auth token
const resDeleteNoAuth = await fetch(`http://127.0.0.1:${port}/api/posts?slug=any-post`, {
method: "DELETE"
});
assert.strictEqual(resDeleteNoAuth.status, 401, "DELETE without Bearer token must return 401");
// 3. DELETE with missing slug parameter
const resDeleteNoSlug = await fetch(`http://127.0.0.1:${port}/api/posts`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${TEST_SECRET}`
}
});
assert.strictEqual(resDeleteNoSlug.status, 400, "DELETE without slug parameter must return 400");
// 4. DELETE with invalid/empty slug
const resDeleteBadSlug = await fetch(`http://127.0.0.1:${port}/api/posts?slug=///`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${TEST_SECRET}`
}
});
assert.strictEqual(resDeleteBadSlug.status, 400, "DELETE with invalid slug must return 400");
// 5. DELETE non-existent post succeeds gracefully (idempotent)
const resDeleteNonExistent = await fetch(`http://127.0.0.1:${port}/api/posts?slug=non-existent-post`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${TEST_SECRET}`
}
});
assert.strictEqual(resDeleteNonExistent.status, 200, "DELETE non-existent post must return 200 idempotently");
const jsonNonExistent = await resDeleteNonExistent.json();
assert.strictEqual(jsonNonExistent.status, "deleted");
assert.strictEqual(jsonNonExistent.slug, "non-existent-post");
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(tmpDir, { recursive: true, force: true });
fs.rmSync(tmpPosts, { recursive: true, force: true });
}
});
test("Ingestion API: triggerEleventyBuild builds site and handles concurrency", async () => {
assert.strictEqual(typeof triggerEleventyBuild, "function");
// Trigger two concurrent calls to assert queuing does not crash or throw
const [b1, b2] = await Promise.all([triggerEleventyBuild(), triggerEleventyBuild()]);
assert.strictEqual(b1, undefined);
assert.strictEqual(b2, undefined);
});
+175
View File
@@ -0,0 +1,175 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { resolveLinkedArticles, validateLinkedArticles } from "../src/_utils/linked-articles.js";
import { filters } from "../src/_filters/index.js";
import configureEleventy from "../eleventy.config.js";
test("Linked articles: accepts an article's list when every name identifies another post", () => {
assert.doesNotThrow(() => {
validateLinkedArticles([
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: ["Markdown Preview Demo"]
}
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
]);
});
});
test("Linked articles: accepts Obsidian's serialized internal Markdown links", () => {
const posts = [
{
inputPath: "src/posts/project-crimson-week-7.md",
data: {
title: "Project Crimson Week 7",
linkedArticles: ["[Project Crimson Week 6](/posts/project-crimson-week-6/)"]
}
},
{
inputPath: "src/posts/project-crimson-week-6.md",
url: "/posts/project-crimson-week-6/",
data: { title: "Project Crimson Week 6" }
}
];
assert.doesNotThrow(() => validateLinkedArticles(posts));
assert.deepEqual(resolveLinkedArticles(posts, posts[0].data.linkedArticles), [posts[1]]);
});
test("Linked articles: rejects a non-list frontmatter value", () => {
assert.throws(
() => {
validateLinkedArticles([
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: "Markdown Preview Demo"
}
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
]);
},
/must define 'linkedArticles' as a YAML list/
);
});
test("Linked articles: rejects empty and unknown article names", () => {
const posts = [
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: { title: "Terminal Preview Demo", linkedArticles: [""] }
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
];
assert.throws(() => validateLinkedArticles(posts), /non-empty article titles/);
posts[0].data.linkedArticles = ["Missing Dispatch"];
assert.throws(() => validateLinkedArticles(posts), /links to unknown article 'Missing Dispatch'/);
});
test("Linked articles: rejects self-references and duplicate navigation targets", () => {
const posts = [
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: ["Terminal Preview Demo"]
}
},
{
inputPath: "src/posts/2026-10-05-markdown-preview-demo.md",
data: { title: "Markdown Preview Demo" }
}
];
assert.throws(() => validateLinkedArticles(posts), /cannot link to itself/);
posts[0].data.linkedArticles = ["Markdown Preview Demo", "Markdown Preview Demo"];
assert.throws(() => validateLinkedArticles(posts), /cannot contain duplicate article titles/);
});
test("Linked articles: the Eleventy posts collection rejects invalid frontmatter", () => {
const collections = new Map();
configureEleventy({
setUseGitIgnore() {},
addPassthroughCopy() {},
addPlugin() {},
setLibrary() {},
addFilter() {},
addCollection(name, callback) {
collections.set(name, callback);
},
addGlobalData() {},
on() {},
setServerOptions() {}
});
assert.throws(
() => {
collections.get("posts")({
getFilteredByGlob() {
return [
{
inputPath: "src/posts/2026-10-06-terminal-preview-demo.md",
data: {
title: "Terminal Preview Demo",
linkedArticles: ["Missing Dispatch"]
}
}
];
}
});
},
/links to unknown article 'Missing Dispatch'/
);
});
test("Linked articles: resolves named posts in the frontmatter order for navigation", () => {
const posts = [
{
url: "/dispatches/terminal-preview-demo/",
data: { title: "Terminal Preview Demo" }
},
{
url: "/dispatches/markdown-preview-demo/",
data: { title: "Markdown Preview Demo" }
}
];
assert.deepEqual(
resolveLinkedArticles(posts, ["Markdown Preview Demo", "Terminal Preview Demo"]),
[posts[1], posts[0]]
);
});
test("Linked articles: exposes navigation resolution to Nunjucks", () => {
assert.strictEqual(filters.resolveLinkedArticles, resolveLinkedArticles);
});
test("Linked articles: post layout renders Other Articles at the top of dispatch telemetry", () => {
const postLayout = fs.readFileSync(
path.resolve("src/_includes/layouts/post.njk"),
"utf-8"
);
assert.match(
postLayout,
/{% set linkedPosts = collections\.posts \| resolveLinkedArticles\(linkedArticles\) %}[\s\S]*?<div[^>]+class="sidebar-heading">OTHER ARTICLES<\/div>[\s\S]*?<nav[^>]+aria-label="Other Articles">[\s\S]*?href="{{ linkedPost\.url }}"/
);
});
+10
View File
@@ -0,0 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const config = fs.readFileSync(path.resolve("eleventy.config.js"), "utf-8");
test("Eleventy config: builds local ignored post fixtures", () => {
assert.match(config, /setUseGitIgnore\(false\)/);
});
+43
View File
@@ -0,0 +1,43 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
import { createServer } from "../server.js";
const siteDir = path.resolve("_site");
test("unknown routes render the themed 404 page with a real 404 status", async () => {
execFileSync("npm", ["run", "build"], {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: process.env.SITE_DOMAIN || "riverrooks.dev" }
});
const html = fs.readFileSync(path.join(siteDir, "404.html"), "utf8");
assert.match(html, /<title>Page not found/);
assert.match(html, /<h1[^>]*>[^<]*404/);
assert.match(html, /href="\/"/);
assert.match(html, /href="\/dispatches\/"/);
assert.match(html, /href="\/assets\/css\/main\.css/);
const css = fs.readFileSync(path.join(siteDir, "assets/css/main.css"), "utf8");
assert.match(css, /components\/not-found\.css/);
const pageCss = fs.readFileSync(path.join(siteDir, "assets/css/components/not-found.css"), "utf8");
assert.doesNotMatch(pageCss, /(?:backdrop-filter|box-shadow|text-shadow|linear-gradient|radial-gradient)\s*:/);
const server = createServer(siteDir, path.resolve("src/posts"), path.resolve("src/assets/images/posts"));
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const origin = `http://127.0.0.1:${server.address().port}`;
for (const route of ["/missing-page/", "/assets/missing.css"]) {
const response = await fetch(`${origin}${route}`);
assert.equal(response.status, 404);
assert.match(response.headers.get("content-type"), /text\/html/);
assert.match(await response.text(), /Page not found/);
}
const home = await fetch(origin);
assert.equal(home.status, 200);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
+162
View File
@@ -0,0 +1,162 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const pluginPath = path.resolve("/home/venus/Documents/Notes/.obsidian/plugins/blog-publisher/main.js");
// Pure helper function to be tested and exported/used by plugin
export function insertOrUpdateFrontmatterProperty(content, key, value) {
if (typeof content !== "string") return "";
const fmRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
const match = content.match(fmRegex);
if (match) {
let fmBlock = match[1];
const keyRegex = new RegExp(`^${key}:.*$`, "m");
if (value === null || value === undefined) {
// Remove property if value is null
fmBlock = fmBlock.replace(new RegExp(`^${key}:.*\\r?\\n?`, "m"), "");
} else if (keyRegex.test(fmBlock)) {
// Replace existing property
fmBlock = fmBlock.replace(keyRegex, `${key}: "${value}"`);
} else {
// Append property to frontmatter
fmBlock = `${fmBlock}\n${key}: "${value}"`;
}
return content.replace(fmRegex, `---\n${fmBlock.trim()}\n---`);
} else if (value) {
return `---\n${key}: "${value}"\n---\n\n${content}`;
}
return content;
}
export function computePublicUrl(serverUrl, postPath) {
const base = (serverUrl || "").replace(/\/+$/, "");
const p = (postPath || "").startsWith("/") ? postPath : `/${postPath || ""}`;
return `${base}${p}`;
}
test("Obsidian Plugin: computePublicUrl normalizes slashes correctly", () => {
assert.equal(
computePublicUrl("https://riverrooks.dev", "/posts/test-note/"),
"https://riverrooks.dev/posts/test-note/"
);
assert.equal(
computePublicUrl("https://riverrooks.dev/", "/posts/test-note/"),
"https://riverrooks.dev/posts/test-note/"
);
assert.equal(
computePublicUrl("https://riverrooks.dev///", "posts/test-note/"),
"https://riverrooks.dev/posts/test-note/"
);
assert.equal(
computePublicUrl("http://localhost:8080", "/posts/test-note/"),
"http://localhost:8080/posts/test-note/"
);
});
test("Obsidian Plugin: insertOrUpdateFrontmatterProperty adds public-url when missing", () => {
const original = `---
title: "Sample Note"
date: 2026-09-23
publish: true
---
# Content here`;
const updated = insertOrUpdateFrontmatterProperty(
original,
"public-url",
"https://riverrooks.dev/posts/sample-note/"
);
assert.match(
updated,
/public-url:\s*"https:\/\/riverrooks\.dev\/posts\/sample-note\/"/,
"Must insert public-url into frontmatter"
);
assert.match(updated, /title:\s*"Sample Note"/, "Preserves title");
assert.match(updated, /publish:\s*true/, "Preserves publish flag");
assert.match(updated, /# Content here/, "Preserves body content");
});
test("Obsidian Plugin: insertOrUpdateFrontmatterProperty updates existing public-url", () => {
const original = `---
title: "Updated Note"
date: 2026-09-23
public-url: "https://old.domain/posts/sample-note/"
---
Body`;
const updated = insertOrUpdateFrontmatterProperty(
original,
"public-url",
"https://riverrooks.dev/posts/updated-note/"
);
assert.match(
updated,
/public-url:\s*"https:\/\/riverrooks\.dev\/posts\/updated-note\/"/,
"Must update public-url to new URL"
);
assert.doesNotMatch(
updated,
/old\.domain/,
"Old URL must be replaced"
);
});
test("Obsidian Plugin: insertOrUpdateFrontmatterProperty removes public-url on unpublish", () => {
const original = `---
title: "Unpublished Note"
date: 2026-09-23
public-url: "https://riverrooks.dev/posts/sample-note/"
---
Body`;
const updated = insertOrUpdateFrontmatterProperty(original, "public-url", null);
assert.doesNotMatch(updated, /public-url:/, "Must remove public-url when value is null");
assert.match(updated, /title:\s*"Unpublished Note"/, "Preserves other frontmatter");
});
test("Obsidian Plugin: Adversarial edge cases for frontmatter property insertion", () => {
// 1. Empty content
assert.equal(insertOrUpdateFrontmatterProperty("", "public-url", "https://url"), "---\npublic-url: \"https://url\"\n---\n\n");
// 2. Non-string inputs
assert.equal(insertOrUpdateFrontmatterProperty(null, "public-url", "https://url"), "");
assert.equal(insertOrUpdateFrontmatterProperty(undefined, "public-url", "https://url"), "");
// 3. Note without frontmatter
const plain = "# Just a header\nSome text.";
const result = insertOrUpdateFrontmatterProperty(plain, "public-url", "https://url");
assert.match(result, /^---\npublic-url:\s*"https:\/\/url"\n---/);
});
test("Obsidian Plugin: main.js in vault includes public-url insertion and recursion guard", () => {
assert.ok(fs.existsSync(pluginPath), `Plugin main.js must exist at ${pluginPath}`);
const pluginContent = fs.readFileSync(pluginPath, "utf-8");
// 1. Must handle public-url
assert.match(pluginContent, /public-url/i, "main.js must reference 'public-url'");
// 2. Must guard against infinite loops during frontmatter updates
assert.match(
pluginContent,
/isUpdatingFrontmatter/i,
"main.js must maintain a guard to prevent metadataCache recursion loops"
);
// 3. Must use processFrontMatter or frontmatter updater
assert.match(
pluginContent,
/processFrontMatter|updateFrontmatter/i,
"main.js must implement frontmatter updating logic"
);
});
+104
View File
@@ -0,0 +1,104 @@
import test from "node:test";
import assert from "node:assert/strict";
import { postComparator, isPostPinned, getPostTimestamp } from "../src/_utils/sort-posts.js";
test("Post Sorting: isPostPinned detection", () => {
assert.equal(isPostPinned({ data: { pinned: true } }), true);
assert.equal(isPostPinned({ data: { featured: true } }), true);
assert.equal(isPostPinned({ data: { pinned: false } }), false);
assert.equal(isPostPinned({ data: { featured: false } }), false);
assert.equal(isPostPinned({ data: { pinned: null } }), false);
assert.equal(isPostPinned({ data: {} }), false);
assert.equal(isPostPinned({}), false);
assert.equal(isPostPinned(null), false);
assert.equal(isPostPinned(undefined), false);
assert.equal(isPostPinned("invalid"), false);
});
test("Post Sorting: getPostTimestamp extraction and fallback", () => {
const d = new Date("2026-09-01T00:00:00Z");
assert.equal(getPostTimestamp({ date: d }), d.getTime());
assert.equal(getPostTimestamp({ data: { date: "2026-09-01" } }), new Date("2026-09-01").getTime());
assert.equal(getPostTimestamp({ data: { created: "2026-09-01" } }), new Date("2026-09-01").getTime());
assert.equal(getPostTimestamp({ date: "not-a-date" }), 0);
assert.equal(getPostTimestamp({}), 0);
assert.equal(getPostTimestamp(null), 0);
});
test("Post Sorting: standard chronological sort (unpinned posts)", () => {
const postNew = {
data: { title: "Newer Post" },
date: new Date("2026-09-10")
};
const postOld = {
data: { title: "Older Post" },
date: new Date("2026-08-01")
};
const sorted = [postOld, postNew].sort(postComparator);
assert.deepEqual(sorted.map((p) => p.data.title), ["Newer Post", "Older Post"]);
});
test("Post Sorting: pinned post forces top position over newer unpinned post", () => {
const newerUnpinned = {
data: { title: "Newer Unpinned", pinned: false },
date: new Date("2026-09-20")
};
const olderPinned = {
data: { title: "Older Pinned", pinned: true },
date: new Date("2026-07-01")
};
const sorted = [newerUnpinned, olderPinned].sort(postComparator);
assert.equal(sorted[0].data.title, "Older Pinned");
assert.equal(sorted[1].data.title, "Newer Unpinned");
});
test("Post Sorting: featured post forces top position over newer unpinned post", () => {
const newerRegular = {
data: { title: "Fresh Article" },
date: new Date("2026-09-22")
};
const olderFeatured = {
data: { title: "Featured Pillar", featured: true },
date: new Date("2026-06-15")
};
const sorted = [newerRegular, olderFeatured].sort(postComparator);
assert.equal(sorted[0].data.title, "Featured Pillar");
assert.equal(sorted[1].data.title, "Fresh Article");
});
test("Post Sorting: multiple pinned posts sort among themselves chronologically", () => {
const olderPinned = {
data: { title: "Older Pinned", pinned: true },
date: new Date("2026-05-01")
};
const newerPinned = {
data: { title: "Newer Pinned", featured: true },
date: new Date("2026-08-15")
};
const freshUnpinned = {
data: { title: "Fresh Regular" },
date: new Date("2026-09-01")
};
const list = [olderPinned, freshUnpinned, newerPinned];
const sorted = list.sort(postComparator);
assert.deepEqual(
sorted.map((p) => p.data.title),
["Newer Pinned", "Older Pinned", "Fresh Regular"]
);
});
test("Post Sorting: adversarial robustness against missing data or equal dates", () => {
const postNoData = { date: new Date("2026-09-01") };
const postMalformedDate = { data: { title: "Malformed", date: "invalid-date" } };
const postIdenticalDate1 = { data: { title: "Post A" }, date: new Date("2026-09-01") };
const postIdenticalDate2 = { data: { title: "Post B" }, date: new Date("2026-09-01") };
assert.doesNotThrow(() => {
[postNoData, postMalformedDate, postIdenticalDate1, postIdenticalDate2].sort(postComparator);
});
});
+10
View File
@@ -0,0 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const postLayout = fs.readFileSync(path.resolve("src/_includes/layouts/post.njk"), "utf-8");
test("Post layout: omits the leading terminal preview from the public article body", () => {
assert.match(postLayout, /content\s*\|\s*removeTerminalPreview\s*\|\s*safe/);
});
+87
View File
@@ -0,0 +1,87 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import http from "node:http";
import path from "node:path";
import { execSync } from "node:child_process";
import { createServer } from "../server.js";
const TEST_PORT = 8999;
const BASE_URL = `http://127.0.0.1:${TEST_PORT}`;
// Helper to perform HTTP GET requests
function get(urlPath) {
return new Promise((resolve, reject) => {
http.get(`${BASE_URL}${urlPath}`, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: data
});
});
}).on("error", reject);
});
}
test("Node Static Server: lifecycle, routing, and adversarial security tests", async (t) => {
// Ensure _site exists before running server tests
if (!fs.existsSync(path.resolve(process.cwd(), "_site/index.html"))) {
execSync("npm run build", {
stdio: "pipe",
env: { ...process.env, SITE_DOMAIN: "riverrooks.dev" }
});
}
const server = createServer();
await new Promise((resolve) => server.listen(TEST_PORT, "127.0.0.1", resolve));
t.after(() => {
return new Promise((resolve) => server.close(resolve));
});
await t.test("serves root index.html with 200 OK and text/html", async () => {
const res = await get("/");
assert.equal(res.statusCode, 200);
assert.match(res.headers["content-type"], /text\/html/);
assert.match(res.body, /<!DOCTYPE html>/i);
// Verify security headers
assert.equal(res.headers["x-content-type-options"], "nosniff");
assert.equal(res.headers["x-frame-options"], "DENY");
});
await t.test("serves clean directory URL (/dispatches/)", async () => {
const res = await get("/dispatches/");
assert.equal(res.statusCode, 200);
assert.match(res.headers["content-type"], /text\/html/);
});
await t.test("serves static assets with appropriate Content-Type", async () => {
const res = await get("/assets/css/main.css");
assert.equal(res.statusCode, 200);
assert.match(res.headers["content-type"], /text\/css/);
});
await t.test("responds with 200 OK JSON on /healthz", async () => {
const res = await get("/healthz");
assert.equal(res.statusCode, 200);
assert.match(res.headers["content-type"], /application\/json/);
const body = JSON.parse(res.body);
assert.equal(body.status, "ok");
});
await t.test("returns 404 Not Found for non-existent routes", async () => {
const res = await get("/non-existent-file-xyz.html");
assert.equal(res.statusCode, 404);
});
await t.test("adversarial: blocks path traversal attempts", async () => {
const res = await get("/../../../../etc/passwd");
// Must either return 403 Forbidden or 404 Not Found, never 200
assert.notEqual(res.statusCode, 200);
assert.ok(res.statusCode === 403 || res.statusCode === 404);
assert.doesNotMatch(res.body, /root:x:/);
});
});
+64
View File
@@ -0,0 +1,64 @@
import test from "node:test";
import assert from "node:assert/strict";
import { execSync } from "node:child_process";
test("Site Config: throws explicit error when SITE_DOMAIN is unset", () => {
// Run isolated Node process with empty environment to verify error throw
const cmd = `node -e '
process.env.SITE_DOMAIN = "";
delete process.env.SITE_DOMAIN;
// mock empty cwd without .env
process.chdir("/tmp");
import("${process.cwd()}/src/_data/site.js")
.then(() => process.exit(0))
.catch(err => {
if (err.message.includes("FATAL CONFIGURATION ERROR: SITE_DOMAIN")) {
process.exit(42);
}
process.exit(1);
});
'`;
try {
execSync(cmd, { stdio: "pipe" });
assert.fail("Should have thrown error when SITE_DOMAIN is missing");
} catch (err) {
assert.equal(err.status, 42, "Process exited with status 42 (expected error caught)");
}
});
test("Site Config: correctly parses SITE_DOMAIN and derives service subdomains", async () => {
process.env.SITE_DOMAIN = "testcluster.internal";
delete process.env.SITE_URL;
// Import site configuration dynamically
const { default: site } = await import(`../src/_data/site.js?t=${Date.now()}`);
assert.equal(site.domain, "testcluster.internal");
assert.equal(site.url, "https://testcluster.internal");
// Verify dynamic subdomains
const gitService = site.services.find((s) => s.name === "Gitea");
assert.ok(gitService, "Gitea service exists");
assert.equal(gitService.url, "https://git.riverrooks.dev/venus");
assert.equal(gitService.tag, "git.testcluster.internal:443");
// Verify navigation links
const navHome = site.nav.find((n) => n.label === "Home");
assert.ok(navHome, "Nav Home link exists");
assert.equal(navHome.url, "/");
const navCode = site.nav.find((n) => n.label === "Code");
assert.ok(navCode, "Nav Code link exists");
assert.equal(navCode.url, "https://git.riverrooks.dev/venus");
});
test("Site Config: respects explicit SITE_URL override", async () => {
process.env.SITE_DOMAIN = "riverrooks.dev";
process.env.SITE_URL = "http://localhost:8080";
const { default: site } = await import(`../src/_data/site.js?t=${Date.now()}`);
assert.equal(site.domain, "riverrooks.dev");
assert.equal(site.url, "http://localhost:8080");
});
+92
View File
@@ -0,0 +1,92 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
const skillDir = path.resolve(".agents/skills/standards-verification");
const skillPath = path.join(skillDir, "SKILL.md");
const standardsDocPath = path.resolve(".agents/standards.md");
const skillStandardsRefPath = path.join(skillDir, "references/standards.md");
test("Standards Verification Skill: Directory, SKILL.md, and standards document exist", () => {
assert.ok(fs.existsSync(skillDir), ".agents/skills/standards-verification directory must exist");
assert.ok(fs.existsSync(skillPath), "SKILL.md must exist in standards-verification skill");
assert.ok(fs.existsSync(standardsDocPath), "Dedicated standards document .agents/standards.md must exist");
assert.ok(fs.existsSync(skillStandardsRefPath), "Skill references/standards.md must exist");
});
test("Standards Verification Skill: SKILL.md contains correct metadata and execution directives", () => {
const content = fs.readFileSync(skillPath, "utf-8");
// 1. YAML frontmatter name and description
assert.match(content, /^---\s*\nname:\s*standards-verification/m, "Must declare name: standards-verification");
assert.match(content, /description:\s*>-?[\s\S]*?---/, "Must define description in frontmatter");
// 2. Invocation conditions: called sparingly, major feature, user request, or debugging
assert.match(content, /sparingly/i, "Must specify calling sparingly and only when needed");
assert.match(content, /major\s+function|major\s+feature/i, "Must mention use case: major function/feature implemented");
assert.match(content, /user(?:\s+\w+)?\s+asks/i, "Must mention use case: user explicitly requests verification");
assert.match(content, /debug(?:ging)?/i, "Must mention use case: debugging code issues");
// 3. Targeted inspection via git status / git diff (not whole codebase unless necessary)
assert.match(content, /git\s+status/i, "Must specify using git status to identify changes");
assert.match(content, /git\s+diff/i, "Must specify using git diff to inspect targeted changes");
assert.match(content, /not\s+(?:the\s+)?whole\s+codebase/i, "Must specify NOT inspecting whole codebase unless necessary");
// 4. Debugging guidance: explain why standards violations cause errors
assert.match(content, /(?:suggest|explain)[*\s`_]+why/i, "Must instruct suggesting why standards issues cause errors");
// 5. External standards document reference
assert.match(content, /standards\.md/i, "Must refer to standards.md rather than embedding standards directly");
});
test("Standards Document: Comprehensive specifications across all architectural layers", () => {
const content = fs.readFileSync(standardsDocPath, "utf-8");
// 1. Design & CSS Anti-Patterns
assert.match(content, /backdrop-filter/i, "Must document prohibition on backdrop-filter");
assert.match(content, /box-shadow/i, "Must document prohibition on box-shadow");
assert.match(content, /gradient/i, "Must document prohibition on gradients");
assert.match(content, /border-radius/i, "Must document border-radius constraints (0px to 4px)");
assert.match(content, /instant/i, "Must document instant binary transitions");
// 2. Templating & Eleventy
assert.match(content, /isPostPublic/i, "Must document isPostPublic collection filtering");
assert.match(content, /hidden/i, "Must document hidden post behavior and noindex meta tag");
// 3. Frontmatter & Content Contract
assert.match(content, /title/i, "Must document title requirement");
assert.match(content, /date|created/i, "Must document date requirement");
assert.match(content, /tags/i, "Must document tags requirement");
assert.match(content, /excerpt/i, "Must document excerpt requirement");
// 4. Server & Security Invariants
assert.match(content, /path\s+traversal/i, "Must document path traversal defense");
assert.match(content, /healthz/i, "Must document healthcheck endpoint");
assert.match(content, /X-Content-Type-Options/i, "Must document security headers");
// 5. Strict TDD & Verification Invariants
assert.match(content, /before\s+(?:writing\s+or\s+)?modifying/i, "Must mandate tests before modifying functional code");
assert.match(content, /adversarial/i, "Must mandate adversarial testing");
assert.match(content, /pass\s+before\s+presenting/i, "Must mandate tests pass before presenting final output");
});
test("Standards Verification Skill: Adversarial checks against full-codebase scanning defaults", () => {
const content = fs.readFileSync(skillPath, "utf-8");
// Adversarial: ensure no instruction tells the agent to indiscriminately scan everything by default
assert.doesNotMatch(
content,
/always\s+scan\s+(?:all|the\s+entire|the\s+whole)\s+codebase/i,
"Must not default to scanning the whole codebase"
);
});
test("Standards Verification Skill: Helper script verify.js exists and executes safely", () => {
const scriptPath = path.join(skillDir, "scripts/verify.js");
assert.ok(fs.existsSync(scriptPath), "scripts/verify.js must exist");
const content = fs.readFileSync(scriptPath, "utf-8");
assert.match(content, /git\s+status/i, "Script must inspect git status");
});
+15
View File
@@ -0,0 +1,15 @@
import test from "node:test";
import assert from "node:assert/strict";
import { extractTerminalPreview, removeTerminalPreview } from "../src/_filters/index.js";
test("Terminal previews: stop before the first subsequent Markdown heading at any level", () => {
const renderedPost = `
<h1 id="terminal-preview">Terminal preview</h1>
<p>Plain Markdown preview text is supported.</p>
<h2 id="findings">Findings</h2>
<p>The article begins here.</p>
`;
assert.match(extractTerminalPreview(renderedPost), /Plain Markdown preview text/);
assert.match(removeTerminalPreview(renderedPost), /^\n <h2 id="findings">/);
});
+37
View File
@@ -0,0 +1,37 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
extractTerminalPreview,
removeTerminalPreview
} from "../src/_filters/index.js";
test("Terminal previews: extracts rendered Markdown between the leading preview and article headings", () => {
const renderedPost = `
<h1 id="terminal-preview">Terminal preview</h1>
<p>Use the following command:</p>
<pre class="language-bash"><code class="language-bash">cat /dispatches/example | head -n 5</code></pre>
<h1 id="investigation">Investigation</h1>
<p>The public article starts here.</p>
`;
assert.equal(
extractTerminalPreview(renderedPost),
`\n <p>Use the following command:</p>\n <pre class="language-bash"><code class="language-bash">cat /dispatches/example | head -n 5</code></pre>\n `
);
assert.equal(
removeTerminalPreview(renderedPost),
`\n <h1 id="investigation">Investigation</h1>\n <p>The public article starts here.</p>\n `
);
});
test("Terminal previews: ignores missing, misplaced, and malformed preview headings", () => {
const regularPost = "<h1 id=\"article\">Article</h1><p>Visible text.</p>";
const misplacedPreview = "<h1 id=\"article\">Article</h1><h1 id=\"terminal-preview\">Terminal preview</h1><p>Do not extract.</p>";
assert.equal(extractTerminalPreview(regularPost), "");
assert.equal(removeTerminalPreview(regularPost), regularPost);
assert.equal(extractTerminalPreview(misplacedPreview), "");
assert.equal(removeTerminalPreview(misplacedPreview), misplacedPreview);
assert.equal(extractTerminalPreview(null), "");
assert.equal(removeTerminalPreview(null), "");
});