Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04a39ca852 | ||
|
|
eddbf5470e | ||
|
|
acd47b80aa | ||
|
|
900014f506 | ||
|
|
737a6e3e51 | ||
|
|
2423546ee6 | ||
|
|
d55297f1dd | ||
|
|
d3aefa343e | ||
|
|
78b1e0b485 | ||
|
|
e733f479dd | ||
|
|
1079b956d3 | ||
|
|
125b3e8839 | ||
|
|
cedd8194f6 | ||
|
|
e4601dbbcb | ||
|
|
fc6c17b541 | ||
|
|
1491d3fa5c | ||
|
|
b13a50ccee | ||
|
|
d8fb8a3699 | ||
|
|
c0f67e75a3 |
@@ -0,0 +1,87 @@
|
||||
# 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
|
||||
```
|
||||
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. **Red-Green-Refactor Lifecycle:**
|
||||
- Write tests in `test/*.test.js` using Node's native test runner (`node:test`, `node:assert/strict`).
|
||||
- Test inputs before implementing filters, data loaders, or template macros.
|
||||
2. **Adversarial Test Coverage Required:**
|
||||
- **Data & Configuration:** Test that missing required environment variables (e.g. `SITE_DOMAIN`) throw fatal errors.
|
||||
- **Filters:** Test edge cases: empty strings, undefined values, single-word inputs, massive word counts, non-date objects.
|
||||
- **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. **Execution Invariant:**
|
||||
- Always run `npm test` before committing. All tests must pass cleanly with 0 failures.
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
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
|
||||
|
||||
Run these commands to test Eleventy builds:
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
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`.
|
||||
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 `` 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/`.
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
*.md
|
||||
.agents
|
||||
node_modules
|
||||
_site
|
||||
sample-designs
|
||||
*.log
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
_site/
|
||||
.cache/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env
|
||||
docker-compose.yml
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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
|
||||
- **Test-First Development:** For all new features, custom filters, template helpers, frontmatter schemas, or configuration changes, write automated tests asserting expected behaviors and edge cases *before* or alongside implementing the code.
|
||||
- **Adversarial Mindset:** Actively design tests to break assumptions:
|
||||
- Missing environment variables (e.g. unset `SITE_DOMAIN` must throw).
|
||||
- Malformed frontmatter (missing titles, invalid dates, empty tags).
|
||||
- Edge-case inputs to filters (empty strings, giant numbers, special characters).
|
||||
- Design rule violations (asserting 0 instances of `backdrop-filter`, `box-shadow`, gradients, or floaty animations).
|
||||
- Broken internal links and missing anchor tags in generated HTML.
|
||||
- **Verification Gate:** Every code change must be verified by running `npm test`. No feature or commit is considered complete unless 100% of the test suite passes.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
```
|
||||
- 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.
|
||||
+23
-10
@@ -1,15 +1,28 @@
|
||||
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 . .
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
# Build static production assets
|
||||
RUN npm run build
|
||||
|
||||
# Expose web server port dynamically
|
||||
EXPOSE ${PORT}
|
||||
|
||||
# Serve site with npm
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# River Rooks | Technical Publication & Systems Architecture Blog
|
||||
|
||||
> Developer-centric, high-signal, low-noise personal technical blog and homelab documentation site.
|
||||
|
||||
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. Architectural Vision & Pillars
|
||||
|
||||
### 1.1 Static Generation via Eleventy (11ty)
|
||||
- **Engine:** Fast, pre-rendered static HTML and minimal client-side runtime using **Eleventy** and **Nunjucks (`.njk`)** templates.
|
||||
- **Performance:** Instant time-to-first-byte (TTFB), zero client-side framework bloat, and pure static asset delivery via high-performance native Node runtime.
|
||||
- **Syntax Highlighting & Code Blocks:** Terminal-styled code blocks with title bars displaying filenames, language tags, and high-contrast monospace code.
|
||||
|
||||
### 1.2 Dynamic Obsidian Publishing Pipeline
|
||||
- **Seamless Authoring:** Author and edit technical dispatches, CTF write-ups, and homelab notes directly inside an **Obsidian** vault.
|
||||
- **Custom Ingestion API:** A dedicated server-side endpoint (`POST /api/posts`) accepting markdown payloads and assets, paired with a custom Obsidian plugin for one-click publishing.
|
||||
- **LiveSync Integration:** Architectural support for syncing notes from a self-hosted Obsidian LiveSync (CouchDB) instance.
|
||||
- **Content Normalization:** Translates Obsidian wikilinks (`[[slug|Title]]`), callouts (`> [!NOTE]`), and asset references into standard web formats during ingestion and build time.
|
||||
|
||||
### 1.3 Strict CSS Isolation & Reusable Templating
|
||||
- **No Monolithic Inlines:** Replaces repetitive inline `<style>` tags with a modular CSS token and component architecture.
|
||||
- **Design Tokens:** Centralized CSS custom properties (`--bg-canvas`, `--bg-surface`, `--border-color`, `--accent-cyan`, etc.) for consistent dark-mode native styling.
|
||||
- **DRY Layouts & Partials:** Reusable Nunjucks components for global navigation (`global-bar`), hero tiles (`hero-tile`), split cards, and article workspace layouts (`article-workspace-grid`).
|
||||
|
||||
### 1.4 AI Pair Architect & Pedagogical Explainer
|
||||
- **Interactive Learning:** The workspace includes customized Antigravity agent configurations (`.agents/` and `AGENTS.md`) that pair-program with the author, architecting solutions while clearly explaining *how* and *why* code is built.
|
||||
|
||||
---
|
||||
|
||||
## 2. Strict Design Specification
|
||||
|
||||
Adhering to the principles in [`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** | High-signal terminal accents used sparingly: Cyan (`#00f0ff`), Green (`#00e676`), Amber (`#ffb700`). |
|
||||
| **Borders & Radius** | Crisp 1px solid borders. Zero to 4px maximum border-radius (sharp corners preferred). |
|
||||
| **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. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Project Directory Map
|
||||
|
||||
```text
|
||||
.
|
||||
├── .agents/ # Antigravity agent configurations & customizations
|
||||
│ ├── rules/
|
||||
│ │ └── blog-system.md # Architectural constraints, CSS/template isolation & explainer mode
|
||||
│ └── skills/
|
||||
│ ├── eleventy-tiling-blog/ # Runbook & recipes for Eleventy builds and component extraction
|
||||
│ └── obsidian-sync/ # Runbook & API contracts for Obsidian publishing & plugin sync
|
||||
├── sample-designs/ # Canonical HTML/CSS prototypes & design specification
|
||||
│ ├── article-tiling-firmware.html
|
||||
│ ├── blog-tiling-lead.html
|
||||
│ ├── design-specification.md
|
||||
│ └── service-gateway-tiling.html
|
||||
├── src/ # Eleventy source directory (scaffolded in Phase 2)
|
||||
│ ├── _data/ # Global site metadata, navigation, telemetry data
|
||||
│ ├── _includes/
|
||||
│ │ ├── layouts/ # Base, post, and page templates
|
||||
│ │ └── partials/ # Global bar, hero tile, card, code box, sidebar
|
||||
│ ├── assets/
|
||||
│ │ ├── css/ # Modular CSS (tokens, reset, layout, components)
|
||||
│ │ └── fonts/ # Self-hosted monospace & prose webfonts
|
||||
│ └── posts/ # Markdown content ingested from Obsidian
|
||||
├── .eleventy.js # Eleventy configuration, markdown-it plugins, and shortcodes
|
||||
├── package.json # Project dependencies & build scripts
|
||||
├── server.js # Native Node static web server & clean-URL router
|
||||
├── Dockerfile # Node container definition (built & served via npm)
|
||||
├── docker-compose.yml # Container orchestration (web gateway)
|
||||
└── AGENTS.md # Project-level agent instructions & pair programming guide
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Development & Build Roadmap
|
||||
|
||||
- [x] **Phase 1: Foundation & Agent Customizations**
|
||||
- Establish `README.md` architectural blueprint.
|
||||
- Setup `.agents/` workspace rules, skills, and root `AGENTS.md`.
|
||||
- [ ] **Phase 2: Eleventy Scaffolding & Modularization**
|
||||
- Initialize Node/npm environment and configure `.eleventy.js`.
|
||||
- Extract design tokens, layout styles, and components into `src/assets/css/`.
|
||||
- Build reusable Nunjucks layouts and partials modeled on `sample-designs/`.
|
||||
- [ ] **Phase 3: Dynamic Obsidian Ingestion & Plugin**
|
||||
- Implement the server-side note ingestion endpoint (`POST /api/posts`).
|
||||
- Develop the custom Obsidian plugin for publishing notes and frontmatter.
|
||||
- Integrate wikilink and callout transformation filters.
|
||||
- [ ] **Phase 4: Containerization & Deployment**
|
||||
- Verify static build outputs in `_site/`.
|
||||
- Update Dockerfile and `docker-compose.yml` for unified production serving.
|
||||
|
||||
---
|
||||
|
||||
## 5. Development Setup
|
||||
|
||||
*(To be initialized during Phase 2)*
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start Eleventy local development server with hot-reload
|
||||
npm run dev
|
||||
|
||||
# Compile static assets for production
|
||||
npm run build
|
||||
|
||||
# Run via Docker Compose
|
||||
docker compose up --build
|
||||
```
|
||||
@@ -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
|
||||
@@ -0,0 +1,81 @@
|
||||
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";
|
||||
|
||||
export default function (eleventyConfig) {
|
||||
// 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
|
||||
// Chronological blog posts collection
|
||||
eleventyConfig.addCollection("posts", (collectionApi) => {
|
||||
return collectionApi
|
||||
.getFilteredByGlob("src/posts/*.md")
|
||||
.filter((post) => !post.data.draft)
|
||||
.sort((a, b) => b.date - a.date);
|
||||
});
|
||||
|
||||
// Dynamic Tag List Collection across all posts
|
||||
eleventyConfig.addCollection("tagList", (collectionApi) => {
|
||||
const tagSet = new Set();
|
||||
collectionApi.getFilteredByGlob("src/posts/*.md").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"
|
||||
};
|
||||
}
|
||||
-278
@@ -1,278 +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 & 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>
|
||||
<span class="tag">[#golang]</span>
|
||||
<span class="tag">[#rust]</span>
|
||||
<span class="tag">[#proxmox]</span>
|
||||
<span class="tag">[#truenas]</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Piped Contact Footer -->
|
||||
<footer class="footer-links">
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link">Git Forge</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@rooks.dev" class="footer-link">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="#" title="Fingerprint: 0x4A8E92F1C0B3D4E7" class="footer-link">PGP Key</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="samples/index.html" class="footer-link">Spec Samples Catalog</a>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
-34
@@ -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;
|
||||
}
|
||||
Generated
+1618
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
web:
|
||||
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}"
|
||||
image: river-rooks-blog:latest
|
||||
container_name: river_rooks_web
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "test/healthcheck.js"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
start_period: 3s
|
||||
retries: 3
|
||||
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}"
|
||||
ports:
|
||||
- "${HOST_PORT:-${PORT:-8080}}:${PORT:-8080}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,889 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CTF Write-up: Heap Exploitation in glibc 2.35 | River Rooks</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:ital,wght@0,400;0,500;0,700;1,400&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 surface */
|
||||
--border-color: #2b303b; /* Crisp 1px border */
|
||||
--text-primary: #e0e6f0; /* Light off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospace metadata */
|
||||
--accent-cyan: #00f0ff; /* Active accent */
|
||||
--accent-green: #00e676; /* Success / online */
|
||||
--accent-amber: #ffb700; /* Warning / highlight */
|
||||
--accent-red: #ff5252; /* Critical alert */
|
||||
}
|
||||
|
||||
* {
|
||||
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.7;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px 48px 16px;
|
||||
}
|
||||
|
||||
.tiling-layout {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Top Global Header Bar (Gateway 3 Standard) */
|
||||
.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;
|
||||
}
|
||||
|
||||
.bar-link:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Article Workspace: 2-Column Split (Sidebar on Left) */
|
||||
.article-workspace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 290px 1fr;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.article-workspace-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Left Sidebar */
|
||||
.article-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.sidebar-tile {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toc-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 4px 6px;
|
||||
border-radius: 2px;
|
||||
transition: none;
|
||||
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: 14px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
/* Collapsible Menu */
|
||||
.collapsible-drawer {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--bg-canvas);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsible-summary {
|
||||
background-color: var(--bg-elevated);
|
||||
padding: 8px 12px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible-summary::after {
|
||||
content: "[+]";
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.collapsible-drawer[open] .collapsible-summary::after {
|
||||
content: "[-]";
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 8px 10px;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.related-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.related-date {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.related-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.related-card:hover .related-title {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Main Article Tile */
|
||||
.article-tile {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 36px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.article-tile {
|
||||
padding: 24px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.article-header-block {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.article-breadcrumbs {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.article-main-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2.3rem;
|
||||
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.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.article-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Markdown Body */
|
||||
.markdown-prose {
|
||||
max-width: 68ch;
|
||||
font-size: 1.02rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown-prose p {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.markdown-prose h2 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 36px 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.markdown-prose h3 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 28px 0 12px 0;
|
||||
}
|
||||
|
||||
.markdown-prose code:not(pre code) {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.88rem;
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Mathematical / Algorithm Formula Box */
|
||||
.formula-card {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid var(--accent-cyan);
|
||||
border-radius: 3px;
|
||||
padding: 14px 18px;
|
||||
margin: 20px 0;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.formula-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* Code Block Containers */
|
||||
.code-block-container {
|
||||
margin: 22px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-canvas);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
background-color: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 6px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
padding: 2px 8px;
|
||||
font-family: inherit;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.code-block-container pre {
|
||||
padding: 14px 16px;
|
||||
overflow-x: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* GitHub-style Alerts */
|
||||
.markdown-callout {
|
||||
margin: 20px 0;
|
||||
padding: 14px 18px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--bg-canvas);
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.callout-caution {
|
||||
border-left: 4px solid var(--accent-red);
|
||||
}
|
||||
|
||||
.callout-note {
|
||||
border-left: 4px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.callout-caution .callout-title { color: var(--accent-red); }
|
||||
.callout-note .callout-title { color: var(--accent-cyan); }
|
||||
|
||||
/* Tables */
|
||||
.table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin: 22px 0;
|
||||
}
|
||||
|
||||
.markdown-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.84rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-table th {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.markdown-table td {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Footnotes Section */
|
||||
.footnotes-section {
|
||||
margin-top: 36px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 0.84rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.footnote-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Global Footer Tile (Gateway 3 Exact Match) */
|
||||
.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 Header Bar (Gateway 3 Standard) -->
|
||||
<header class="global-bar">
|
||||
<div class="brand-group">
|
||||
<span class="status-indicator"></span>
|
||||
<span>RIVER ROOKS // CTF EXPLOITATION ARCHIVE</span>
|
||||
</div>
|
||||
<nav class="bar-nav" aria-label="Navigation">
|
||||
<a href="service-gateway-tiling.html" class="bar-link">Gateway 3 ↗</a>
|
||||
<a href="blog-tiling-lead.html" class="bar-link">← Back to Blog Feed</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</a>
|
||||
<a href="index.html" class="bar-link">Catalog ↗</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main Workspace: Sidebar TOC + Reading Canvas -->
|
||||
<div class="article-workspace-grid">
|
||||
|
||||
<!-- Left Sidebar: Document Outline + Collapsible Series -->
|
||||
<aside class="article-sidebar" aria-label="Table of Contents">
|
||||
|
||||
<!-- Outline Tile -->
|
||||
<div class="sidebar-tile">
|
||||
<div class="sidebar-heading">
|
||||
<span>DOCUMENT OUTLINE</span>
|
||||
<span style="color: var(--accent-cyan);">INDEX</span>
|
||||
</div>
|
||||
|
||||
<nav class="toc-nav" aria-label="Table of Contents">
|
||||
<a href="#challenge-intro" class="toc-link active">> 1.0 Challenge Synopsis</a>
|
||||
<a href="#safe-linking" class="toc-link">> 2.0 Safe-Linking Pointer Mangling</a>
|
||||
<a href="#mangling-math" class="toc-link toc-sub">2.1 Mathematical Derivation</a>
|
||||
<a href="#gdb-session" class="toc-link">> 3.0 GDB Memory Inspection</a>
|
||||
<a href="#house-of-apple" class="toc-link">> 4.0 House of Apple Hijacking</a>
|
||||
<a href="#mitigation-matrix" class="toc-link">> 5.0 Mitigation History</a>
|
||||
<a href="#footnotes" class="toc-link">> 6.0 References & Notes</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible Related Dispatches Tile -->
|
||||
<div class="sidebar-tile">
|
||||
<div class="sidebar-heading">
|
||||
<span>SECURITY REPOSITORY</span>
|
||||
<span>DRAWER</span>
|
||||
</div>
|
||||
|
||||
<details class="collapsible-drawer" open>
|
||||
<summary class="collapsible-summary">
|
||||
<span>Security Dispatches (3)</span>
|
||||
</summary>
|
||||
<div class="drawer-content">
|
||||
|
||||
<a href="article-tiling-firmware.html" class="related-card">
|
||||
<span class="related-date">2026-07-20 • 8 min</span>
|
||||
<span class="related-title">Reverse Engineering IoT Firmware: SPI Flash</span>
|
||||
<span style="color: var(--text-muted); font-size: 0.7rem;">[#firmware] [#hardware]</span>
|
||||
</a>
|
||||
|
||||
<a href="article-tiling-traefik.html" class="related-card">
|
||||
<span class="related-date">2026-07-15 • 12 min</span>
|
||||
<span class="related-title">High-Availability Traefik Router with Docker</span>
|
||||
<span style="color: var(--text-muted); font-size: 0.7rem;">[#homelab] [#networking]</span>
|
||||
</a>
|
||||
|
||||
<a href="blog-tiling-lead.html" class="related-card">
|
||||
<span class="related-date">2026-07-10 • 6 min</span>
|
||||
<span class="related-title">POSIX sh vs. Bash: Portability in Modern Scripts</span>
|
||||
<span style="color: var(--text-muted); font-size: 0.7rem;">[#posix-sh] [#unix]</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Target Binary Info Tile -->
|
||||
<div class="sidebar-tile" style="font-family: 'IBM Plex Mono', monospace; font-size: 0.76rem; color: var(--text-muted);">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
||||
<span>ALLOCATOR:</span>
|
||||
<span style="color: var(--accent-cyan); font-weight: 700;">GLIBC 2.35-0UBUNTU3</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
||||
<span>EXPLOIT PRIMITIVE:</span>
|
||||
<span style="color: var(--accent-amber); font-weight: 700;">TCACHE POISONING</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>TARGET HOOK:</span>
|
||||
<span style="color: var(--accent-green);">_IO_FILE VTABLE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- Center Article Tile -->
|
||||
<article class="article-tile">
|
||||
|
||||
<!-- Article Header Block -->
|
||||
<header class="article-header-block">
|
||||
<div class="article-breadcrumbs">
|
||||
<span>~/notes/</span>
|
||||
<span>></span>
|
||||
<span style="color: var(--accent-cyan);">ctf-heap-glibc-2-35.md</span>
|
||||
</div>
|
||||
|
||||
<h1 class="article-main-title">CTF Write-up: Heap Exploitation in glibc 2.35</h1>
|
||||
|
||||
<div class="article-meta-row">
|
||||
<div><span style="color: var(--accent-cyan);">2026-06-28</span></div>
|
||||
<span>•</span>
|
||||
<div><span>15 MIN READ</span></div>
|
||||
<span>•</span>
|
||||
<div><span>AUTHOR: RIVER ROOKS</span></div>
|
||||
<span>•</span>
|
||||
<div>
|
||||
<span class="tag-chip">[#ctf]</span>
|
||||
<span class="tag-chip">[#binary-exploitation]</span>
|
||||
<span class="tag-chip">[#security]</span>
|
||||
<span class="tag-chip">[#glibc]</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Prose Content -->
|
||||
<div class="markdown-prose">
|
||||
|
||||
<h2 id="challenge-intro">1.0 Challenge Synopsis</h2>
|
||||
<p>
|
||||
During the DEFCON qualifier challenges, our team encountered a hardened 64-bit Linux binary linked against <code>glibc 2.35</code>. Standard legacy heap exploitation techniques—such as poisoning <code>__malloc_hook</code> or <code>__free_hook</code>—are rendered obsolete because upstream glibc maintainers removed all allocation hook pointer symbols entirely in version 2.34.
|
||||
</p>
|
||||
<p>
|
||||
This write-up covers breaking modern <strong>tcache safe-linking pointer mangling</strong> and redirecting execution control flow using the <strong>House of Apple 2</strong> technique against the wide character vtable pointers inside <code>_IO_FILE</code> structures.
|
||||
</p>
|
||||
|
||||
<h2 id="safe-linking">2.0 Safe-Linking Pointer Mangling</h2>
|
||||
<p>
|
||||
Introduced in glibc 2.32, safe-linking protects single-linked singly linked list pointers (such as entries inside the Thread Local Cache <code>tcache</code> and fastbins) against off-by-one overwrites and UAF corruption.
|
||||
</p>
|
||||
|
||||
<h3 id="mangling-math">2.1 Mathematical Derivation</h3>
|
||||
|
||||
<!-- Formula Callout Box Showcase -->
|
||||
<div class="formula-card">
|
||||
<div class="formula-title">POINTER MANGLING FORMULA</div>
|
||||
<div>Mangled_Pointer (P') = (Address_of_Chunk >> 12) ^ Target_Pointer (P)</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Since the lowest 12 bits of <code>Address_of_Chunk >> 12</code> are randomized by ASLR, overwriting <code>P'</code> directly without an address leak causes the kernel to dereference invalid virtual memory pages, aborting with <code>SIGSEGV</code>.
|
||||
</p>
|
||||
<p>
|
||||
However, when an arbitrary read primitive leaks the mangled value of the terminal chunk in the tcache bin (where <code>Target_Pointer = NULL</code>), the equation simplifies:
|
||||
</p>
|
||||
|
||||
<div class="formula-card">
|
||||
<div>Leaked_Mangled = (Chunk_Addr >> 12) ^ 0x0 ==> ASLR_Key = Leaked_Mangled</div>
|
||||
</div>
|
||||
|
||||
<!-- Alert Callout Showcase -->
|
||||
<div class="markdown-callout callout-caution">
|
||||
<div class="callout-title">[!CAUTION] Pointer Alignment Constraints</div>
|
||||
<p>
|
||||
In glibc 2.35, chunks inserted into tcache bins must also adhere to strict 16-byte address alignment. Forging a fake chunk unaligned to a 16-byte boundary triggers <code>malloc(): unaligned tcache chunk detected</code> aborts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="gdb-session">3.0 GDB Memory Inspection</h2>
|
||||
<p>
|
||||
Attaching GDB with the <code>pwndbg</code> extension confirms the tcache bin layout and reveals the mangled pointer representation in memory:
|
||||
</p>
|
||||
|
||||
<!-- Code Block Showcase -->
|
||||
<div class="code-block-container">
|
||||
<div class="code-block-header">
|
||||
<div class="code-title">
|
||||
<span>gdb-pwndbg stdout: heap inspection</span>
|
||||
<span>GDB</span>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="copySnippet(this)">Copy GDB</button>
|
||||
</div>
|
||||
<pre><code>pwndbg> tcachebins
|
||||
tcachebins
|
||||
0x40 [ 2]: 0x55555555a2a0 → 0x55555555a260
|
||||
pwndbg> x/4gx 0x55555555a2a0
|
||||
0x55555555a2a0: 0x0000555555550730 0x0000000000000000
|
||||
0x55555555a2b0: 0x0000000000000000 0x0000000000000041
|
||||
pwndbg> tele 0x55555555a2a0
|
||||
00:0000 │ 0x55555555a2a0 → 0x555555550730 (MANGLED POINTER: 0x55555555a260 ^ (0x55555555a >> 12))</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="house-of-apple">4.0 House of Apple Hijacking</h2>
|
||||
<p>
|
||||
With the ability to allocate chunks onto arbitrary memory addresses via tcache poisoning, we target <code>_IO_list_all</code>. When <code>exit()</code> or <code>abort()</code> is invoked by the runtime, the glibc standard I/O cleanup routine iterates through the <code>_IO_FILE</code> linked list and invokes <code>_IO_wfile_overflow()</code>:
|
||||
</p>
|
||||
|
||||
<!-- C Exploit Snippet Showcase -->
|
||||
<div class="code-block-container">
|
||||
<div class="code-block-header">
|
||||
<div class="code-title">
|
||||
<span>house_of_apple_payload.c</span>
|
||||
<span>C99</span>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="copySnippet(this)">Copy C Code</button>
|
||||
</div>
|
||||
<pre><code>// Constructing fake _IO_FILE_plus structure
|
||||
struct _IO_FILE_plus *fake_file = (struct _IO_FILE_plus *)payload;
|
||||
fake_file->file._flags = 0x6873; // "sh" in ASCII
|
||||
fake_file->file._mode = 1; // Force wide character stream mode
|
||||
fake_file->vtable = (const struct _IO_jump_t *)wide_vtable_addr;
|
||||
|
||||
// Trigger call to system("/bin/sh") on stream flush
|
||||
fake_file->file._wide_data->_wide_vtable->doallocate = (void *)system_addr;</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="mitigation-matrix">5.0 Glibc Heap Mitigation History</h2>
|
||||
<p>
|
||||
Tracking defensive hardening milestones across recent glibc releases:
|
||||
</p>
|
||||
|
||||
<!-- Table Showcase -->
|
||||
<div class="table-container">
|
||||
<table class="markdown-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>glibc Version</th>
|
||||
<th>Defensive Mitigation Added</th>
|
||||
<th>Status</th>
|
||||
<th>Impact on Attackers</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>2.26</code></td>
|
||||
<td>Thread Cache (tcache) introduced</td>
|
||||
<td>Active</td>
|
||||
<td>Initially lacked double-free and count sanity checks.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2.29</code></td>
|
||||
<td>tcache double-free check (key field)</td>
|
||||
<td>Active</td>
|
||||
<td>Stores pointer to tcache struct in chunk data.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2.32</code></td>
|
||||
<td>Safe-Linking Pointer Mangling</td>
|
||||
<td>Hardened</td>
|
||||
<td>Requires ASLR heap base leak to forge pointers.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2.34</code></td>
|
||||
<td>Removal of <code>__malloc_hook</code> / <code>__free_hook</code></td>
|
||||
<td>Enforced</td>
|
||||
<td>Forces targeting of <code>_IO_FILE</code> and stack frames.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>2.35</code></td>
|
||||
<td>Alignment verification on tcache chunks</td>
|
||||
<td>Enforced</td>
|
||||
<td>Eliminates misaligned chunk allocations entirely.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Footnotes Section Showcase -->
|
||||
<div class="footnotes-section" id="footnotes">
|
||||
<div class="footnote-item">
|
||||
<sup>[1]</sup> Safe-Linking was first proposed by Eyal Itkin for memory allocators in modern operating systems.
|
||||
</div>
|
||||
<div class="footnote-item">
|
||||
<sup>[2]</sup> House of Apple exploitation methodology authored by roderickchan, targeting wide character I/O functions.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Global Footer Tile (Gateway 3 Exact Match) -->
|
||||
<footer class="footer-tile">
|
||||
<div>River Rooks • 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 copySnippet(btn) {
|
||||
const code = btn.closest('.code-block-container').querySelector('pre').innerText;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const orig = btn.innerText;
|
||||
btn.innerText = 'Copied!';
|
||||
btn.style.color = 'var(--accent-green)';
|
||||
btn.style.borderColor = 'var(--accent-green)';
|
||||
setTimeout(() => {
|
||||
btn.innerText = orig;
|
||||
btn.style.color = '';
|
||||
btn.style.borderColor = '';
|
||||
}, 1800);
|
||||
});
|
||||
}
|
||||
|
||||
// ScrollSpy for TOC
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const links = document.querySelectorAll('.toc-link');
|
||||
const sections = [];
|
||||
|
||||
links.forEach(link => {
|
||||
const targetId = link.getAttribute('href').substring(1);
|
||||
const section = document.getElementById(targetId);
|
||||
if (section) sections.push({ link, section });
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollPos = window.scrollY + 100;
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
if (sections[i].section.offsetTop <= scrollPos) {
|
||||
links.forEach(l => l.classList.remove('active'));
|
||||
sections[i].link.classList.add('active');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,887 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Building a High-Availability Traefik Router with Docker Compose | River Rooks</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:ital,wght@0,400;0,500;0,700;1,400&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 surface */
|
||||
--border-color: #2b303b; /* Crisp 1px border */
|
||||
--text-primary: #e0e6f0; /* Light off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospace metadata */
|
||||
--accent-cyan: #00f0ff; /* Active accent */
|
||||
--accent-green: #00e676; /* Success / online */
|
||||
--accent-amber: #ffb700; /* Warning / highlight */
|
||||
}
|
||||
|
||||
* {
|
||||
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.7;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px 48px 16px;
|
||||
}
|
||||
|
||||
.tiling-layout {
|
||||
width: 100%;
|
||||
max-width: 1160px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Top Global Header Bar (Gateway 3 Standard) */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Right Sidebar Reading Workspace Grid: Content (Left) + Sidebar (Right) */
|
||||
.article-workspace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 290px;
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.article-workspace-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.article-sidebar {
|
||||
order: -1; /* Display TOC on top on mobile viewports */
|
||||
}
|
||||
}
|
||||
|
||||
/* Article Content Container Tile */
|
||||
.article-tile {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 36px 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.article-tile {
|
||||
padding: 24px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.article-header-block {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.article-breadcrumbs {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.article-main-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2.3rem;
|
||||
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.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.article-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Right Sidebar Components */
|
||||
.article-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.sidebar-tile {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sidebar-heading {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toc-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.toc-link {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 4px 6px;
|
||||
border-radius: 2px;
|
||||
transition: none;
|
||||
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: 14px;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
/* Collapsible Menu */
|
||||
.collapsible-drawer {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--bg-canvas);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsible-summary {
|
||||
background-color: var(--bg-elevated);
|
||||
padding: 8px 12px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.collapsible-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible-summary::after {
|
||||
content: "[+]";
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.collapsible-drawer[open] .collapsible-summary::after {
|
||||
content: "[-]";
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.related-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 2px;
|
||||
padding: 8px 10px;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.related-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.related-date {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.7rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.related-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.related-card:hover .related-title {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Markdown Prose Elements */
|
||||
.markdown-prose {
|
||||
max-width: 68ch;
|
||||
font-size: 1.02rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.markdown-prose p {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.markdown-prose h2 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 36px 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.markdown-prose h3 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 28px 0 12px 0;
|
||||
}
|
||||
|
||||
.markdown-prose code:not(pre code) {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.88rem;
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* ASCII Network Diagram Component */
|
||||
.ascii-diagram-container {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin: 22px 0;
|
||||
overflow-x: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--accent-cyan);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Code Block Component */
|
||||
.code-block-container {
|
||||
margin: 22px 0;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-canvas);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-block-header {
|
||||
background-color: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 6px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
padding: 2px 8px;
|
||||
font-family: inherit;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.code-block-container pre {
|
||||
padding: 14px 16px;
|
||||
overflow-x: auto;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.5;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Callout Boxes */
|
||||
.markdown-callout {
|
||||
margin: 20px 0;
|
||||
padding: 14px 18px;
|
||||
border-radius: 3px;
|
||||
border: 1px 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-tip {
|
||||
border-left: 4px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
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-tip .callout-title { color: var(--accent-green); }
|
||||
|
||||
/* Tables */
|
||||
.table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin: 22px 0;
|
||||
}
|
||||
|
||||
.markdown-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.84rem;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-table th {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.markdown-table td {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Global Footer Tile (Gateway 3 Exact Match) */
|
||||
.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 Header Bar (Gateway 3 Standard) -->
|
||||
<header class="global-bar">
|
||||
<div class="brand-group">
|
||||
<span class="status-indicator"></span>
|
||||
<span>RIVER ROOKS // INFRASTRUCTURE ARCHIVE</span>
|
||||
</div>
|
||||
<nav class="bar-nav" aria-label="Navigation">
|
||||
<a href="service-gateway-tiling.html" class="bar-link">Gateway 3 ↗</a>
|
||||
<a href="blog-tiling-lead.html" class="bar-link">← Back to Blog Feed</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</a>
|
||||
<a href="index.html" class="bar-link">Catalog ↗</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Right Sidebar Workspace Grid -->
|
||||
<div class="article-workspace-grid">
|
||||
|
||||
<!-- Center / Left: Article Content -->
|
||||
<article class="article-tile">
|
||||
|
||||
<!-- Article Header Block -->
|
||||
<header class="article-header-block">
|
||||
<div class="article-breadcrumbs">
|
||||
<span>~/notes/</span>
|
||||
<span>></span>
|
||||
<span style="color: var(--accent-cyan);">ha-traefik-docker.md</span>
|
||||
</div>
|
||||
|
||||
<h1 class="article-main-title">Building a High-Availability Traefik Router with Docker Compose</h1>
|
||||
|
||||
<div class="article-meta-row">
|
||||
<div><span style="color: var(--accent-cyan);">2026-07-15</span></div>
|
||||
<span>•</span>
|
||||
<div><span>12 MIN READ</span></div>
|
||||
<span>•</span>
|
||||
<div><span>AUTHOR: RIVER ROOKS</span></div>
|
||||
<span>•</span>
|
||||
<div>
|
||||
<span class="tag-chip">[#homelab]</span>
|
||||
<span class="tag-chip">[#docker-traefik]</span>
|
||||
<span class="tag-chip">[#networking]</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Markdown Prose Body -->
|
||||
<div class="markdown-prose">
|
||||
|
||||
<h2 id="architecture">1.0 Ingress Architecture & Topologies</h2>
|
||||
<p>
|
||||
When running multi-tier web applications across self-hosted bare-metal clusters, an edge reverse proxy is indispensable. Managing static Nginx reverse proxy virtual host configs by hand becomes unmaintainable once container fleets scale past twenty services.
|
||||
</p>
|
||||
<p>
|
||||
Traefik v3.1 solves this by inspecting the Docker socket daemon directly. When a new container is deployed, Traefik dynamically binds routing rules, attaches middlewares, and provisions automated wildcard TLS certificates without restarting the daemon.
|
||||
</p>
|
||||
|
||||
<!-- ASCII Architecture Diagram Showcase -->
|
||||
<div class="ascii-diagram-container">
|
||||
<pre>+-------------------------------------------------------------------------+
|
||||
| [INTERNET / WAN INGRESS] ==> Cloudflare DNS (DNS-01 ACME Challenge) |
|
||||
+-------------------------------------------------------------------------+
|
||||
||
|
||||
\/
|
||||
+-------------------------------------------------------------------------+
|
||||
| gw-node01.lan (OPNsense 24.7 Gateway / NAT Port 443 Forwarding) |
|
||||
+-------------------------------------------------------------------------+
|
||||
||
|
||||
\/ 10GbE VLAN 20 (Server DMZ)
|
||||
+-------------------------------------------------------------------------+
|
||||
| pve-node02.lan :: Traefik v3.1 Reverse Proxy Container (:443/:80) |
|
||||
+-------------------------------------------------------------------------+
|
||||
|| || ||
|
||||
\/ (Docker Internal) \/ (Docker Internal) \/ (WireGuard)
|
||||
+--------------------+ +--------------------+ +--------------------+
|
||||
| Forgejo Git Server | | CyberChef Security | | Uptime Kuma Status |
|
||||
+--------------------+ +--------------------+ +--------------------+
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<h2 id="dns-challenge">2.0 Automated DNS-01 ACME with Cloudflare</h2>
|
||||
<p>
|
||||
Standard HTTP-01 challenges require opening port <code>80/tcp</code> publicly to the internet, exposing unauthenticated ingress surfaces. By employing the <strong>DNS-01 challenge</strong>, Traefik proves domain ownership by creating temporary <code>_acme-challenge.rooks.dev</code> TXT records through the Cloudflare API.
|
||||
</p>
|
||||
|
||||
<!-- Callout Showcase -->
|
||||
<div class="markdown-callout callout-tip">
|
||||
<div class="callout-title">[!TIP] Principle of Least Privilege</div>
|
||||
<p>
|
||||
Do not use your Global Cloudflare API Key. Instead, generate a scoped API Token with permissions restricted strictly to: <code>Zone.Zone:Read</code> and <code>Zone.DNS:Edit</code> scoped exclusively to your primary apex zone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2 id="compose-spec">3.0 Complete Docker Compose Specification</h2>
|
||||
<p>
|
||||
Below is the production-tested <code>compose.yaml</code> definition. It mounts the Docker socket in read-only mode (<code>:ro</code>) and stores ACME certificate state inside an encrypted <code>acme.json</code> file with strict <code>0600</code> permissions:
|
||||
</p>
|
||||
|
||||
<!-- Code Block Showcase -->
|
||||
<div class="code-block-container">
|
||||
<div class="code-block-header">
|
||||
<div class="code-title">
|
||||
<span>docker-compose.traefik.yml</span>
|
||||
<span>YAML</span>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="copySnippet(this)">Copy Compose</button>
|
||||
</div>
|
||||
<pre><code>services:
|
||||
traefik:
|
||||
image: traefik:v3.1.2
|
||||
container_name: edge-traefik-proxy
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
networks:
|
||||
- proxy-tier
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
- CF_DNS_API_TOKEN_FILE=/run/secrets/cf_token
|
||||
secrets:
|
||||
- cf_token
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./data/traefik.yml:/etc/traefik/traefik.yml:ro
|
||||
- ./data/acme.json:/acme.json
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.api.rule=Host(`proxy.rooks.dev`)"
|
||||
- "traefik.http.routers.api.entrypoints=websecure"
|
||||
- "traefik.http.routers.api.tls.certresolver=cloudflare"
|
||||
- "traefik.http.routers.api.service=api@internal"
|
||||
|
||||
secrets:
|
||||
cf_token:
|
||||
file: ./secrets/cloudflare_api_token.txt
|
||||
|
||||
networks:
|
||||
proxy-tier:
|
||||
external: true</code></pre>
|
||||
</div>
|
||||
|
||||
<h2 id="configuration-matrix">4.0 Configuration Parameters Matrix</h2>
|
||||
<p>
|
||||
Key parameters governing the Traefik core engine:
|
||||
</p>
|
||||
|
||||
<!-- Markdown Table Showcase -->
|
||||
<div class="table-container">
|
||||
<table class="markdown-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Directive Parameter</th>
|
||||
<th>Value</th>
|
||||
<th>Scope</th>
|
||||
<th>Behavior Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>providers.docker.exposedbydefault</code></td>
|
||||
<td><code>false</code></td>
|
||||
<td>Core Security</td>
|
||||
<td>Prevents untagged backend containers from being routed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>entrypoints.web.http.redirections</code></td>
|
||||
<td><code>to: websecure</code></td>
|
||||
<td>HTTP → HTTPS</td>
|
||||
<td>Enforces automatic HTTP 301 redirects to port 443.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>certificatesresolvers.cf.acme.dnschallenge</code></td>
|
||||
<td><code>cloudflare</code></td>
|
||||
<td>Let's Encrypt</td>
|
||||
<td>Uses Cloudflare DNS provider for wildcard propagation.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>certificatesresolvers.cf.acme.storage</code></td>
|
||||
<td><code>/acme.json</code></td>
|
||||
<td>Persistent Storage</td>
|
||||
<td>Encrypted JSON database for RSA/ECDSA key pairs.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 id="verification">5.0 Verification & Failover Testing</h2>
|
||||
<p>
|
||||
Verify TLS certificate issuance and HTTP/3 ALPN negotiation directly via <code>curl</code>:
|
||||
</p>
|
||||
|
||||
<div class="code-block-container">
|
||||
<div class="code-block-header">
|
||||
<div class="code-title">
|
||||
<span>terminal stdout: curl -Iv</span>
|
||||
<span>Bash</span>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="copySnippet(this)">Copy Output</button>
|
||||
</div>
|
||||
<pre><code>$ curl -Iv https://proxy.rooks.dev
|
||||
* Server certificate:
|
||||
* subject: CN=*.rooks.dev
|
||||
* start date: Jul 15 00:00:00 2026 GMT
|
||||
* expire date: Oct 13 23:59:59 2026 GMT
|
||||
* issuer: C=US, O=Let's Encrypt, CN=R3
|
||||
* SSL certificate verify ok.
|
||||
< HTTP/2 200 OK
|
||||
< server: traefik
|
||||
< strict-transport-security: max-age=63072000; includeSubDomains; preload</code></pre>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Zero downtime updates are achieved through rolling container health checks configured inside Traefik dynamic labels.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</article>
|
||||
|
||||
<!-- Right Sidebar: TOC & Collapsible Related Articles -->
|
||||
<aside class="article-sidebar" aria-label="Table of Contents and Series">
|
||||
|
||||
<!-- Document Outline Tile -->
|
||||
<div class="sidebar-tile">
|
||||
<div class="sidebar-heading">
|
||||
<span>DOCUMENT OUTLINE</span>
|
||||
<span style="color: var(--accent-cyan);">INDEX</span>
|
||||
</div>
|
||||
|
||||
<nav class="toc-nav" aria-label="Table of Contents">
|
||||
<a href="#architecture" class="toc-link active">> 1.0 Ingress Architecture</a>
|
||||
<a href="#dns-challenge" class="toc-link">> 2.0 DNS-01 ACME Setup</a>
|
||||
<a href="#compose-spec" class="toc-link">> 3.0 Compose Specification</a>
|
||||
<a href="#configuration-matrix" class="toc-link">> 4.0 Configuration Matrix</a>
|
||||
<a href="#verification" class="toc-link">> 5.0 Verification & Testing</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Collapsible Related Dispatches Tile -->
|
||||
<div class="sidebar-tile">
|
||||
<div class="sidebar-heading">
|
||||
<span>RELATED DISPATCHES</span>
|
||||
<span>DRAWER</span>
|
||||
</div>
|
||||
|
||||
<details class="collapsible-drawer" open>
|
||||
<summary class="collapsible-summary">
|
||||
<span>Homelab Series (3)</span>
|
||||
</summary>
|
||||
<div class="drawer-content">
|
||||
|
||||
<a href="article-tiling-firmware.html" class="related-card">
|
||||
<span class="related-date">2026-07-20 • 8 min</span>
|
||||
<span class="related-title">Reverse Engineering IoT Firmware: SPI Flash</span>
|
||||
<span style="color: var(--text-muted); font-size: 0.7rem;">[#firmware] [#hardware]</span>
|
||||
</a>
|
||||
|
||||
<a href="article-tiling-heap.html" class="related-card">
|
||||
<span class="related-date">2026-06-28 • 15 min</span>
|
||||
<span class="related-title">CTF Write-up: Heap Exploitation in glibc 2.35</span>
|
||||
<span style="color: var(--text-muted); font-size: 0.7rem;">[#ctf] [#security]</span>
|
||||
</a>
|
||||
|
||||
<a href="blog-tiling-lead.html" class="related-card">
|
||||
<span class="related-date">2026-06-14 • 10 min</span>
|
||||
<span class="related-title">Zero-Trust Network Isolation with WireGuard</span>
|
||||
<span style="color: var(--text-muted); font-size: 0.7rem;">[#networking] [#wireguard]</span>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Runtime Metadata Tile -->
|
||||
<div class="sidebar-tile" style="font-family: 'IBM Plex Mono', monospace; font-size: 0.76rem; color: var(--text-muted);">
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
||||
<span>TRAEFIK ENGINE:</span>
|
||||
<span style="color: var(--accent-cyan); font-weight: 700;">v3.1.2 DOCKER</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 6px;">
|
||||
<span>CERT PROVIDER:</span>
|
||||
<span style="color: var(--accent-green); font-weight: 700;">ACME DNS-01</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span>NODE TARGET:</span>
|
||||
<span style="color: var(--text-primary);">pve-node02.lan</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Global Footer Tile (Gateway 3 Exact Match) -->
|
||||
<footer class="footer-tile">
|
||||
<div>River Rooks • 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 copySnippet(btn) {
|
||||
const code = btn.closest('.code-block-container').querySelector('pre').innerText;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const orig = btn.innerText;
|
||||
btn.innerText = 'Copied!';
|
||||
btn.style.color = 'var(--accent-green)';
|
||||
btn.style.borderColor = 'var(--accent-green)';
|
||||
setTimeout(() => {
|
||||
btn.innerText = orig;
|
||||
btn.style.color = '';
|
||||
btn.style.borderColor = '';
|
||||
}, 1800);
|
||||
});
|
||||
}
|
||||
|
||||
// ScrollSpy for Right TOC
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const links = document.querySelectorAll('.toc-link');
|
||||
const sections = [];
|
||||
|
||||
links.forEach(link => {
|
||||
const targetId = link.getAttribute('href').substring(1);
|
||||
const section = document.getElementById(targetId);
|
||||
if (section) sections.push({ link, section });
|
||||
});
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
const scrollPos = window.scrollY + 100;
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
if (sections[i].section.offsetTop <= scrollPos) {
|
||||
links.forEach(l => l.classList.remove('active'));
|
||||
sections[i].link.classList.add('active');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,629 +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 | Field Notes & Dispatches (Cards Lander)</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; /* Card background surface */
|
||||
--bg-elevated: #1c1f26; /* Hovered card surface */
|
||||
--border-color: #2b303b; /* Low-contrast 1px border */
|
||||
--text-primary: #e0e6f0; /* Light off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospace metadata */
|
||||
--accent-cyan: #00f0ff; /* Primary terminal accent */
|
||||
--accent-green: #00e676; /* Operational status indicator */
|
||||
--accent-amber: #ffb700; /* Pinned badge 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;
|
||||
align-items: center;
|
||||
padding: 0 0 64px 0;
|
||||
}
|
||||
|
||||
/* Top Sticky Header */
|
||||
.top-navbar {
|
||||
width: 100%;
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.nav-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--accent-green);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
padding: 3px 6px;
|
||||
transition: none; /* Instant snap */
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Page Container */
|
||||
.page-container {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
padding: 40px 20px 0 20px;
|
||||
}
|
||||
|
||||
/* Hero Identity Header */
|
||||
.hero-header {
|
||||
margin-bottom: 36px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-cyan);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.hero-bio {
|
||||
font-size: 0.98rem;
|
||||
color: var(--text-primary);
|
||||
max-width: 70ch;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.stack-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.stack-tag {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Featured Lead Card */
|
||||
.featured-section {
|
||||
margin-bottom: 44px;
|
||||
}
|
||||
|
||||
.section-label {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.featured-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.featured-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.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-date {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.featured-title-link {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.45rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
line-height: 1.3;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.featured-title-link:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.featured-title-link:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.featured-abstract {
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Code Snippet in Lead Card */
|
||||
.code-wrapper {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--bg-canvas);
|
||||
overflow: hidden;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.code-header {
|
||||
background-color: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 6px 12px;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.code-body {
|
||||
padding: 12px;
|
||||
font-size: 0.8rem;
|
||||
overflow-x: auto;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.featured-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.cta-button {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
padding: 6px 14px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-weight: 700;
|
||||
font-size: 0.82rem;
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--accent-cyan);
|
||||
transition: none; /* Instant snap */
|
||||
}
|
||||
|
||||
.cta-button:hover {
|
||||
background-color: transparent;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.cta-button:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Recent Dispatches Grid */
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.cards-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.dispatch-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;
|
||||
gap: 12px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.dispatch-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.card-meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-date {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.card-heading-link {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
line-height: 1.35;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.dispatch-card:hover .card-heading-link {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.card-heading-link:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.card-bottom-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.card-tag-cluster {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.read-more-link {
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.read-more-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Bottom Site Footer */
|
||||
.site-footer {
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 24px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
padding: 2px 4px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.footer-link:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.pipe {
|
||||
color: var(--border-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Sticky Top Navigation -->
|
||||
<header class="top-navbar">
|
||||
<a href="#" class="nav-brand">
|
||||
<span class="status-dot"></span>
|
||||
<span>ROOKS.DEV // NOTES</span>
|
||||
</a>
|
||||
<nav class="nav-links" aria-label="Site Navigation">
|
||||
<a href="#" class="nav-item active">[Dispatches]</a>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="nav-item">Git</a>
|
||||
<a href="feed.xml" class="nav-item">RSS</a>
|
||||
<a href="index.html" class="nav-item">Catalog ↗</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main class="page-container">
|
||||
|
||||
<!-- Hero Identity Block -->
|
||||
<section class="hero-header" aria-label="Author Profile">
|
||||
<h1 class="hero-title">River Rooks</h1>
|
||||
<div class="hero-subtitle">SYSTEMS ARCHITECTURE & CYBERSECURITY • ALABAMA, USA</div>
|
||||
<p class="hero-bio">
|
||||
Cybersecurity student at the University of Alabama College of Engineering. Architecting high-throughput homelab networks, reverse engineering IoT firmware, and writing zero-allocation Unix utilities.
|
||||
</p>
|
||||
<div class="stack-tags">
|
||||
<span class="stack-tag">[#network-forensics]</span>
|
||||
<span class="stack-tag">[#docker-traefik]</span>
|
||||
<span class="stack-tag">[#arch-linux]</span>
|
||||
<span class="stack-tag">[#neovim]</span>
|
||||
<span class="stack-tag">[#wireguard]</span>
|
||||
<span class="stack-tag">[#posix-sh]</span>
|
||||
<span class="stack-tag">[#python]</span>
|
||||
<span class="stack-tag">[#rust]</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Lead Story -->
|
||||
<section class="featured-section" aria-label="Featured Technical Article">
|
||||
<div class="section-label">
|
||||
<span>// Pinned Technical Write-Up</span>
|
||||
</div>
|
||||
|
||||
<article class="featured-card">
|
||||
<div class="featured-badge-row">
|
||||
<span class="badge-pinned">[PINNED WRITE-UP]</span>
|
||||
<span class="featured-date">2026-07-20 • 8 min read</span>
|
||||
</div>
|
||||
|
||||
<h2 class="featured-title">
|
||||
<a href="#" class="featured-title-link">Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
</h2>
|
||||
|
||||
<p class="featured-abstract">
|
||||
In security audits of embedded systems, retrieving the 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>
|
||||
|
||||
<!-- Code Snippet -->
|
||||
<div class="code-wrapper">
|
||||
<div class="code-header">
|
||||
<span>extract_firmware.sh</span>
|
||||
<span>POSIX sh</span>
|
||||
</div>
|
||||
<div class="code-body">
|
||||
<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-footer">
|
||||
<div class="stack-tags">
|
||||
<span class="stack-tag">[#firmware]</span>
|
||||
<span class="stack-tag">[#hardware]</span>
|
||||
<span class="stack-tag">[#security]</span>
|
||||
<span class="stack-tag">[#bus-pirate]</span>
|
||||
</div>
|
||||
<a href="#" class="cta-button">READ FULL WRITEUP [8 MIN] →</a>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- Recent Dispatches Grid -->
|
||||
<section class="recent-section" aria-label="Recent Technical Dispatches">
|
||||
<div class="section-label">
|
||||
<span>// Recent Dispatches & Architecture Notes</span>
|
||||
</div>
|
||||
|
||||
<div class="cards-grid">
|
||||
|
||||
<!-- Card 1: Traefik -->
|
||||
<article class="dispatch-card">
|
||||
<div>
|
||||
<div class="card-meta-row">
|
||||
<time datetime="2026-07-15" class="card-date">2026-07-15</time>
|
||||
<span>12 min read</span>
|
||||
</div>
|
||||
<h3 style="margin: 10px 0 8px 0;">
|
||||
<a href="#" class="card-heading-link">Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
</h3>
|
||||
<p class="card-summary">
|
||||
Architecting automated TLS wildcard certificate propagation, Docker provider label routing, zero-downtime container updates, and backend health-check failover routes.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-bottom-row">
|
||||
<div class="card-tag-cluster">
|
||||
<span>[#homelab]</span>
|
||||
<span>[#docker-traefik]</span>
|
||||
</div>
|
||||
<a href="#" class="read-more-link">Read →</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Card 2: POSIX sh -->
|
||||
<article class="dispatch-card">
|
||||
<div>
|
||||
<div class="card-meta-row">
|
||||
<time datetime="2026-07-10" class="card-date">2026-07-10</time>
|
||||
<span>6 min read</span>
|
||||
</div>
|
||||
<h3 style="margin: 10px 0 8px 0;">
|
||||
<a href="#" class="card-heading-link">POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
</h3>
|
||||
<p class="card-summary">
|
||||
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>
|
||||
<div class="card-bottom-row">
|
||||
<div class="card-tag-cluster">
|
||||
<span>[#posix-sh]</span>
|
||||
<span>[#unix]</span>
|
||||
</div>
|
||||
<a href="#" class="read-more-link">Read →</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Card 3: Heap Exploitation -->
|
||||
<article class="dispatch-card">
|
||||
<div>
|
||||
<div class="card-meta-row">
|
||||
<time datetime="2026-06-28" class="card-date">2026-06-28</time>
|
||||
<span>15 min read</span>
|
||||
</div>
|
||||
<h3 style="margin: 10px 0 8px 0;">
|
||||
<a href="#" class="card-heading-link">CTF Write-up: Heap Exploitation in glibc 2.35</a>
|
||||
</h3>
|
||||
<p class="card-summary">
|
||||
Deconstructing modern glibc memory allocator defenses: tcache safe-linking pointer mangling, bypass primitives, and House of Apple file structure hijacking.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-bottom-row">
|
||||
<div class="card-tag-cluster">
|
||||
<span>[#ctf]</span>
|
||||
<span>[#security]</span>
|
||||
</div>
|
||||
<a href="#" class="read-more-link">Read →</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- Card 4: WireGuard OPNsense -->
|
||||
<article class="dispatch-card">
|
||||
<div>
|
||||
<div class="card-meta-row">
|
||||
<time datetime="2026-06-14" class="card-date">2026-06-14</time>
|
||||
<span>10 min read</span>
|
||||
</div>
|
||||
<h3 style="margin: 10px 0 8px 0;">
|
||||
<a href="#" class="card-heading-link">Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a>
|
||||
</h3>
|
||||
<p class="card-summary">
|
||||
Designing strict zone-based firewall rules across isolated VLANs (Management, Server DMZ, IoT, Trusted Clients) with point-to-point WireGuard cryptographic routing.
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-bottom-row">
|
||||
<div class="card-tag-cluster">
|
||||
<span>[#networking]</span>
|
||||
<span>[#wireguard]</span>
|
||||
</div>
|
||||
<a href="#" class="read-more-link">Read →</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Site Footer -->
|
||||
<footer class="site-footer">
|
||||
<div>River Rooks © 2026 • Node RACK-01</div>
|
||||
<nav class="footer-links" aria-label="Footer Links">
|
||||
<a href="feed.xml" class="footer-link">Feed</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link">Git Forge</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@rooks.dev" class="footer-link">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="#" title="PGP: 0x4A8E92F1C0B3D4E7" class="footer-link">PGP</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="index.html" class="footer-link">Samples ↗</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,483 +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 | Technical Notes & Blog (Stream Lander)</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; /* Card surface background */
|
||||
--bg-elevated: #1c1f26; /* Hovered / elevated state */
|
||||
--border-color: #2b303b; /* Low-contrast 1px border */
|
||||
--text-primary: #e0e6f0; /* Off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospaced metadata gray */
|
||||
--accent-cyan: #00f0ff; /* Primary terminal accent */
|
||||
--accent-green: #00e676; /* Operational 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;
|
||||
align-items: center;
|
||||
padding: 48px 20px 64px 20px;
|
||||
}
|
||||
|
||||
/* Reading container strictly capped at 65ch */
|
||||
.stream-container {
|
||||
width: 100%;
|
||||
max-width: 65ch;
|
||||
}
|
||||
|
||||
/* POSIX Path Top Banner */
|
||||
.top-path-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.path-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--accent-green);
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
color: var(--accent-green);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Identity Section */
|
||||
.author-name {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.author-role {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-cyan);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.author-bio {
|
||||
font-size: 0.98rem;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* Topic Filter Pills */
|
||||
.filter-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
color: var(--text-muted);
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
padding: 3px 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: none; /* Instant snap */
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.filter-pill:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.filter-pill.active {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
border-color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Section Divider */
|
||||
.section-divider {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 28px 0;
|
||||
}
|
||||
|
||||
/* Article Stream Entries */
|
||||
.articles-stream {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 36px;
|
||||
}
|
||||
|
||||
.article-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.article-entry.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.article-date {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.article-title-link {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.22rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
line-height: 1.35;
|
||||
display: inline-block;
|
||||
transition: none; /* Instant snap */
|
||||
}
|
||||
|
||||
.article-title-link::before {
|
||||
content: '> ';
|
||||
color: var(--border-color);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.article-title-link:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.article-title-link:hover::before {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.article-title-link:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.article-abstract {
|
||||
font-size: 0.94rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.article-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.article-tag {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.article-read-cta {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.article-read-cta:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Piped Footer */
|
||||
.stream-footer {
|
||||
margin-top: 48px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
padding: 2px 4px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.footer-link:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.pipe {
|
||||
color: var(--border-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="stream-container">
|
||||
|
||||
<!-- POSIX Path Top Banner -->
|
||||
<header class="top-path-bar">
|
||||
<div class="path-display">
|
||||
<span class="status-dot" title="Cluster state: operational"></span>
|
||||
<span>~/blog/notes/</span>
|
||||
</div>
|
||||
<div class="status-badge">[5 POSTS • FEED ONLINE]</div>
|
||||
</header>
|
||||
|
||||
<!-- Identity & Bio -->
|
||||
<section class="identity-section" aria-label="Author Identity">
|
||||
<h1 class="author-name">River Rooks</h1>
|
||||
<div class="author-role">Systems Architecture & Cybersecurity</div>
|
||||
<p class="author-bio">
|
||||
Cybersecurity student at the University of Alabama College of Engineering, focusing on network forensics, self-hosted infrastructure, and bare-metal systems. Field notes, architecture specs, and low-level security research.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Quick Topic Filters -->
|
||||
<nav class="filter-section" aria-label="Filter Articles by Topic">
|
||||
<span class="filter-label">Filter:</span>
|
||||
<button class="filter-pill active" data-filter="all" onclick="filterArticles('all', this)">[All (5)]</button>
|
||||
<button class="filter-pill" data-filter="security" onclick="filterArticles('security', this)">[#security (3)]</button>
|
||||
<button class="filter-pill" data-filter="homelab" onclick="filterArticles('homelab', this)">[#homelab (2)]</button>
|
||||
<button class="filter-pill" data-filter="networking" onclick="filterArticles('networking', this)">[#networking (2)]</button>
|
||||
<button class="filter-pill" data-filter="unix" onclick="filterArticles('unix', this)">[#unix (1)]</button>
|
||||
</nav>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Articles Stream (Reverse Chronological) -->
|
||||
<section class="articles-stream" id="articlesStream" aria-label="Article Feed">
|
||||
|
||||
<!-- Post 1: Firmware -->
|
||||
<article class="article-entry" data-category="security firmware">
|
||||
<div class="article-meta">
|
||||
<time datetime="2026-07-20" class="article-date">2026-07-20</time>
|
||||
<span>•</span>
|
||||
<span>8 min read</span>
|
||||
<span>•</span>
|
||||
<span>~/notes/iot-firmware-spi-extraction.md</span>
|
||||
</div>
|
||||
<h2 class="article-title">
|
||||
<a href="#" class="article-title-link">Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
</h2>
|
||||
<p class="article-abstract">
|
||||
In security audits of embedded systems, retrieving the 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>
|
||||
<div class="article-tags">
|
||||
<span class="article-tag">[#firmware]</span>
|
||||
<span class="article-tag">[#hardware]</span>
|
||||
<span class="article-tag">[#security]</span>
|
||||
<span class="article-tag">[#bus-pirate]</span>
|
||||
</div>
|
||||
<a href="#" class="article-read-cta">Read Writeup [8 min] →</a>
|
||||
</article>
|
||||
|
||||
<!-- Post 2: Traefik Docker -->
|
||||
<article class="article-entry" data-category="homelab networking">
|
||||
<div class="article-meta">
|
||||
<time datetime="2026-07-15" class="article-date">2026-07-15</time>
|
||||
<span>•</span>
|
||||
<span>12 min read</span>
|
||||
<span>•</span>
|
||||
<span>~/notes/ha-traefik-docker.md</span>
|
||||
</div>
|
||||
<h2 class="article-title">
|
||||
<a href="#" class="article-title-link">Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
</h2>
|
||||
<p class="article-abstract">
|
||||
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="article-tags">
|
||||
<span class="article-tag">[#homelab]</span>
|
||||
<span class="article-tag">[#docker-traefik]</span>
|
||||
<span class="article-tag">[#networking]</span>
|
||||
</div>
|
||||
<a href="#" class="article-read-cta">Read Writeup [12 min] →</a>
|
||||
</article>
|
||||
|
||||
<!-- Post 3: POSIX sh -->
|
||||
<article class="article-entry" data-category="unix">
|
||||
<div class="article-meta">
|
||||
<time datetime="2026-07-10" class="article-date">2026-07-10</time>
|
||||
<span>•</span>
|
||||
<span>6 min read</span>
|
||||
<span>•</span>
|
||||
<span>~/notes/posix-sh-vs-bash.md</span>
|
||||
</div>
|
||||
<h2 class="article-title">
|
||||
<a href="#" class="article-title-link">POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
</h2>
|
||||
<p class="article-abstract">
|
||||
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="article-tags">
|
||||
<span class="article-tag">[#posix-sh]</span>
|
||||
<span class="article-tag">[#shell]</span>
|
||||
<span class="article-tag">[#unix]</span>
|
||||
</div>
|
||||
<a href="#" class="article-read-cta">Read Writeup [6 min] →</a>
|
||||
</article>
|
||||
|
||||
<!-- Post 4: Heap Exploitation -->
|
||||
<article class="article-entry" data-category="security">
|
||||
<div class="article-meta">
|
||||
<time datetime="2026-06-28" class="article-date">2026-06-28</time>
|
||||
<span>•</span>
|
||||
<span>15 min read</span>
|
||||
<span>•</span>
|
||||
<span>~/notes/ctf-heap-glibc-2-35.md</span>
|
||||
</div>
|
||||
<h2 class="article-title">
|
||||
<a href="#" class="article-title-link">CTF Write-up: Heap Exploitation in glibc 2.35</a>
|
||||
</h2>
|
||||
<p class="article-abstract">
|
||||
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="article-tags">
|
||||
<span class="article-tag">[#ctf]</span>
|
||||
<span class="article-tag">[#binary-exploitation]</span>
|
||||
<span class="article-tag">[#security]</span>
|
||||
</div>
|
||||
<a href="#" class="article-read-cta">Read Writeup [15 min] →</a>
|
||||
</article>
|
||||
|
||||
<!-- Post 5: Zero Trust Network -->
|
||||
<article class="article-entry" data-category="networking security homelab">
|
||||
<div class="article-meta">
|
||||
<time datetime="2026-06-14" class="article-date">2026-06-14</time>
|
||||
<span>•</span>
|
||||
<span>10 min read</span>
|
||||
<span>•</span>
|
||||
<span>~/notes/zero-trust-wireguard-opnsense.md</span>
|
||||
</div>
|
||||
<h2 class="article-title">
|
||||
<a href="#" class="article-title-link">Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a>
|
||||
</h2>
|
||||
<p class="article-abstract">
|
||||
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="article-tags">
|
||||
<span class="article-tag">[#networking]</span>
|
||||
<span class="article-tag">[#wireguard]</span>
|
||||
<span class="article-tag">[#security]</span>
|
||||
</div>
|
||||
<a href="#" class="article-read-cta">Read Writeup [10 min] →</a>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Piped Footer -->
|
||||
<footer class="stream-footer">
|
||||
<div>River Rooks © 2026</div>
|
||||
<nav class="footer-links" aria-label="Footer Links">
|
||||
<a href="feed.xml" class="footer-link">RSS Feed</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link">Git Forge</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@rooks.dev" class="footer-link">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="#" title="PGP: 0x4A8E92F1C0B3D4E7" class="footer-link">PGP Key</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="index.html" class="footer-link">Catalog ↗</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
function filterArticles(category, btn) {
|
||||
// Toggle active pill
|
||||
document.querySelectorAll('.filter-pill').forEach(el => el.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
const entries = document.querySelectorAll('.article-entry');
|
||||
entries.forEach(entry => {
|
||||
if (category === 'all') {
|
||||
entry.classList.remove('hidden');
|
||||
} else {
|
||||
const cats = entry.getAttribute('data-category') || '';
|
||||
if (cats.includes(category)) {
|
||||
entry.classList.remove('hidden');
|
||||
} else {
|
||||
entry.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,617 +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 Index Blog Lander</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; /* Table & card surface */
|
||||
--bg-elevated: #1c1f26; /* Selected row / elevated state */
|
||||
--border-color: #2b303b; /* 1px solid low-contrast border */
|
||||
--text-primary: #e0e6f0; /* Light off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospace metadata */
|
||||
--accent-cyan: #00f0ff; /* Primary terminal accent */
|
||||
--accent-green: #00e676; /* Operational indicator */
|
||||
--accent-amber: #ffb700; /* Warning / highlight */
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-canvas);
|
||||
color: var(--text-primary);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 36px 16px 48px 16px;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
/* Terminal Window Frame */
|
||||
.terminal-window {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Window Titlebar */
|
||||
.terminal-titlebar {
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.window-controls {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.window-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--border-color);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.window-title {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Terminal Body */
|
||||
.terminal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* System Header Info */
|
||||
.system-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.system-identity {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.author-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.author-role {
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.system-bio {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
max-width: 75ch;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Command Prompt & Live Grep Filter */
|
||||
.prompt-bar {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.prompt-text {
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grep-label {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.grep-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.grep-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.grep-count {
|
||||
color: var(--accent-green);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* Tabular Directory View */
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.directory-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.directory-table th {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
background-color: var(--bg-canvas);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.directory-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.directory-row {
|
||||
cursor: pointer;
|
||||
transition: none; /* Instant snap */
|
||||
}
|
||||
|
||||
.directory-row:hover {
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.directory-row.active {
|
||||
background-color: var(--bg-elevated);
|
||||
border-left: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.col-title {
|
||||
white-space: normal !important;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.entry-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.directory-row:hover .entry-link,
|
||||
.directory-row.active .entry-link {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.tag-badge {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.time-badge {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.preview-indicator {
|
||||
color: var(--accent-cyan);
|
||||
font-size: 0.75rem;
|
||||
text-decoration: none;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.directory-row:hover .preview-indicator {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Live Inspection / Preview Pane */
|
||||
.preview-pane {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 8px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.preview-filename {
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.preview-abstract {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.92rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview-code-block {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
overflow-x: auto;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.read-btn {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
border: 1px solid var(--accent-cyan);
|
||||
padding: 4px 10px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.read-btn:hover {
|
||||
background-color: transparent;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Terminal Status Bar at Bottom */
|
||||
.terminal-statusbar {
|
||||
background-color: var(--bg-canvas);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
padding: 1px 4px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.footer-link:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.pipe {
|
||||
color: var(--border-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="app-wrapper">
|
||||
|
||||
<div class="terminal-window">
|
||||
|
||||
<!-- Titlebar -->
|
||||
<div class="terminal-titlebar">
|
||||
<div class="window-controls">
|
||||
<span class="window-dot"></span>
|
||||
<span class="window-dot"></span>
|
||||
<span class="window-dot"></span>
|
||||
<span class="window-title" style="margin-left: 6px;">rooks@sys: ~/articles</span>
|
||||
</div>
|
||||
<div>POSIX SH • UTF-8 • [READ-ONLY]</div>
|
||||
</div>
|
||||
|
||||
<div class="terminal-body">
|
||||
|
||||
<!-- System Identity Header -->
|
||||
<header class="system-header">
|
||||
<div class="system-identity">
|
||||
<h1 class="author-title">River Rooks // Articles Directory</h1>
|
||||
<span class="author-role">[SYSTEMS ARCHITECTURE & CYBERSECURITY]</span>
|
||||
</div>
|
||||
<p class="system-bio">
|
||||
Field notes and technical write-ups covering embedded firmware reverse engineering, network forensics, low-level memory exploitation, and self-hosted infrastructure.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Command Prompt with Live Filter -->
|
||||
<div class="prompt-bar" role="search">
|
||||
<span class="prompt-text">dev-blog@system:~$</span>
|
||||
<span class="grep-label">grep -i</span>
|
||||
<input
|
||||
type="text"
|
||||
id="searchInput"
|
||||
class="grep-input"
|
||||
placeholder="filter by keyword, tag, or title..."
|
||||
oninput="handleSearch(this.value)"
|
||||
aria-label="Filter articles"
|
||||
>
|
||||
<span class="grep-count" id="matchCounter">5/5 matches</span>
|
||||
</div>
|
||||
|
||||
<!-- Directory Listing Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="directory-table" id="directoryTable" aria-label="Articles directory table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">PERMS</th>
|
||||
<th scope="col">DATE</th>
|
||||
<th scope="col">PRIMARY TAG</th>
|
||||
<th scope="col">ARTICLE TITLE / FILE</th>
|
||||
<th scope="col">EST. TIME</th>
|
||||
<th scope="col">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBody">
|
||||
|
||||
<tr class="directory-row active" onclick="selectRow(0)" data-index="0" data-keywords="firmware hardware security bus-pirate spi flash extraction iot">
|
||||
<td><span style="color: var(--text-muted);">-rw-r--r--</span></td>
|
||||
<td><time datetime="2026-07-20" style="color: var(--accent-cyan);">2026-07-20</time></td>
|
||||
<td><span class="tag-badge">[#firmware]</span></td>
|
||||
<td class="col-title">
|
||||
<a href="#" class="entry-link">> Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
</td>
|
||||
<td><span class="time-badge">8 mins</span></td>
|
||||
<td><span class="preview-indicator">[INSPECT]</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="directory-row" onclick="selectRow(1)" data-index="1" data-keywords="homelab docker traefik networking proxy compose">
|
||||
<td><span style="color: var(--text-muted);">-rw-r--r--</span></td>
|
||||
<td><time datetime="2026-07-15" style="color: var(--accent-cyan);">2026-07-15</time></td>
|
||||
<td><span class="tag-badge">[#homelab]</span></td>
|
||||
<td class="col-title">
|
||||
<a href="#" class="entry-link">> Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
</td>
|
||||
<td><span class="time-badge">12 mins</span></td>
|
||||
<td><span class="preview-indicator">[INSPECT]</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="directory-row" onclick="selectRow(2)" data-index="2" data-keywords="posix-sh shell unix portability scripts bashisms">
|
||||
<td><span style="color: var(--text-muted);">-rw-r--r--</span></td>
|
||||
<td><time datetime="2026-07-10" style="color: var(--accent-cyan);">2026-07-10</time></td>
|
||||
<td><span class="tag-badge">[#posix-sh]</span></td>
|
||||
<td class="col-title">
|
||||
<a href="#" class="entry-link">> POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
</td>
|
||||
<td><span class="time-badge">6 mins</span></td>
|
||||
<td><span class="preview-indicator">[INSPECT]</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="directory-row" onclick="selectRow(3)" data-index="3" data-keywords="ctf binary exploitation heap glibc 2.35 memory security">
|
||||
<td><span style="color: var(--text-muted);">-rw-r--r--</span></td>
|
||||
<td><time datetime="2026-06-28" style="color: var(--accent-cyan);">2026-06-28</time></td>
|
||||
<td><span class="tag-badge">[#ctf]</span></td>
|
||||
<td class="col-title">
|
||||
<a href="#" class="entry-link">> CTF Write-up: Heap Exploitation in glibc 2.35</a>
|
||||
</td>
|
||||
<td><span class="time-badge">15 mins</span></td>
|
||||
<td><span class="preview-indicator">[INSPECT]</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="directory-row" onclick="selectRow(4)" data-index="4" data-keywords="networking wireguard security zerotrust opnsense vlans">
|
||||
<td><span style="color: var(--text-muted);">-rw-r--r--</span></td>
|
||||
<td><time datetime="2026-06-14" style="color: var(--accent-cyan);">2026-06-14</time></td>
|
||||
<td><span class="tag-badge">[#networking]</span></td>
|
||||
<td class="col-title">
|
||||
<a href="#" class="entry-link">> Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a>
|
||||
</td>
|
||||
<td><span class="time-badge">10 mins</span></td>
|
||||
<td><span class="preview-indicator">[INSPECT]</span></td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Live Inspection / Details Panel -->
|
||||
<section class="preview-pane" id="previewPane" aria-label="Article Details Preview">
|
||||
<div class="preview-header">
|
||||
<span class="preview-filename" id="previewFilePath">~/notes/iot-firmware-spi-extraction.md</span>
|
||||
<span id="previewMeta">2026-07-20 • 8 mins read • [#firmware] [#hardware] [#security]</span>
|
||||
</div>
|
||||
<p class="preview-abstract" id="previewAbstract">
|
||||
In security audits of embedded systems, retrieving the 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>
|
||||
<div class="preview-code-block" id="previewCode">
|
||||
<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 class="preview-actions">
|
||||
<span style="font-size: 0.78rem; color: var(--text-muted);">Format: IEEE Std 1003.1 Markdown</span>
|
||||
<a href="#" class="read-btn" id="previewReadBtn">READ COMPLETE WRITEUP →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom Statusbar -->
|
||||
<footer class="terminal-statusbar">
|
||||
<div>5 FILES INDEXED • 51m READING DURATION • STATUS: 0 (OK)</div>
|
||||
<nav class="footer-links" aria-label="Terminal Footer Links">
|
||||
<a href="feed.xml" class="footer-link">feed.xml</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link">git.rooks.dev</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="index.html" class="footer-link">catalog ↗</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
const articlesData = [
|
||||
{
|
||||
path: '~/notes/iot-firmware-spi-extraction.md',
|
||||
date: '2026-07-20',
|
||||
time: '8 mins read',
|
||||
tags: '[#firmware] [#hardware] [#security] [#bus-pirate]',
|
||||
abstract: 'In security audits of embedded systems, retrieving the 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.',
|
||||
code: `# Query the SPI flash details using flashrom via Bus Pirate\nflashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump.bin\n\n# Verify the integrity of the binary dump by comparing hashes\nif [ $? -eq 0 ]; then\n echo "Dump successful. Running md5sum..."\n md5sum firmware_dump.bin\nfi`
|
||||
},
|
||||
{
|
||||
path: '~/notes/ha-traefik-docker.md',
|
||||
date: '2026-07-15',
|
||||
time: '12 mins read',
|
||||
tags: '[#homelab] [#docker-traefik] [#networking]',
|
||||
abstract: 'Architecting automated TLS wildcard certificate propagation, Docker provider label routing, zero-downtime container updates, and backend health-check failover routes on bare-metal infrastructure.',
|
||||
code: `services:\n traefik:\n image: traefik:v3.1\n command:\n - "--providers.docker=true"\n - "--entrypoints.websecure.address=:443"\n - "--certificatesresolvers.cf.acme.dnschallenge=true"`
|
||||
},
|
||||
{
|
||||
path: '~/notes/posix-sh-vs-bash.md',
|
||||
date: '2026-07-10',
|
||||
time: '6 mins read',
|
||||
tags: '[#posix-sh] [#shell] [#unix]',
|
||||
abstract: '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.',
|
||||
code: `#!/bin/sh\n# Zero-allocation POSIX argument validation\n[ "$#" -ge 1 ] || { echo "Usage: $0 <target>" >&2; exit 1; }\ntarget="$1"\nprintf 'Configuring target node: %s\\n' "$target"`
|
||||
},
|
||||
{
|
||||
path: '~/notes/ctf-heap-glibc-2-35.md',
|
||||
date: '2026-06-28',
|
||||
time: '15 mins read',
|
||||
tags: '[#ctf] [#binary-exploitation] [#security]',
|
||||
abstract: 'Deconstructing modern glibc memory allocator defenses: tcache safe-linking pointer mangling, bypass primitives, and House of Apple file structure hijacking for arbitrary code execution.',
|
||||
code: `// glibc 2.35 tcache safe-linking pointer de-mangling\nuint64_t reveal_ptr(uint64_t mangled_ptr, uint64_t addr) {\n return mangled_ptr ^ (addr >> 12);\n}`
|
||||
},
|
||||
{
|
||||
path: '~/notes/zero-trust-wireguard-opnsense.md',
|
||||
date: '2026-06-14',
|
||||
time: '10 mins read',
|
||||
tags: '[#networking] [#wireguard] [#security]',
|
||||
abstract: 'Designing strict zone-based firewall rules across isolated VLANs (Management, Server DMZ, IoT, Trusted Clients) with point-to-point WireGuard cryptographic routing.',
|
||||
code: `[Interface]\nPrivateKey = aGlkZGVuX2tleV9kYXRhX2hlcmU=\nAddress = 10.100.0.5/32\nDNS = 10.100.0.1\n\n[Peer]\nPublicKey = cGVlcl9wdWJsaWNfa2V5X2RhdGFfaGVyZQ==\nEndpoint = vpn.rooks.dev:51820\nAllowedIPs = 10.100.0.0/24`
|
||||
}
|
||||
];
|
||||
|
||||
function selectRow(index) {
|
||||
document.querySelectorAll('.directory-row').forEach(row => row.classList.remove('active'));
|
||||
const activeRow = document.querySelector(`.directory-row[data-index="${index}"]`);
|
||||
if (activeRow) activeRow.classList.add('active');
|
||||
|
||||
const data = articlesData[index];
|
||||
if (!data) return;
|
||||
|
||||
document.getElementById('previewFilePath').innerText = data.path;
|
||||
document.getElementById('previewMeta').innerText = `${data.date} • ${data.time} • ${data.tags}`;
|
||||
document.getElementById('previewAbstract').innerText = data.abstract;
|
||||
document.getElementById('previewCode').innerHTML = `<pre><code>${escapeHtml(data.code)}</code></pre>`;
|
||||
}
|
||||
|
||||
function handleSearch(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
const rows = document.querySelectorAll('.directory-row');
|
||||
let visibleCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const text = (row.innerText + ' ' + (row.getAttribute('data-keywords') || '')).toLowerCase();
|
||||
if (!q || text.includes(q)) {
|
||||
row.style.display = '';
|
||||
visibleCount++;
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('matchCounter').innerText = `${visibleCount}/5 matches`;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,661 +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 | Blog Lander (Dual-Lead Grid & 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 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; /* 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 Header Bar (Gateway 3 Match) */
|
||||
.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 Section Tile */
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.hero-landing-text {
|
||||
font-size: 0.98rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.65;
|
||||
max-width: 84ch;
|
||||
}
|
||||
|
||||
.hero-metadata-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.meta-badge {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 3px 8px;
|
||||
border-radius: 2px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Dual-Lead Grid: 2 Side-by-Side Featured Cards */
|
||||
.dual-lead-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
.dual-lead-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;
|
||||
justify-content: space-between;
|
||||
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.88rem;
|
||||
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);
|
||||
}
|
||||
|
||||
.lead-badge-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
.lead-heading {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
line-height: 1.35;
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.lead-heading:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.lead-excerpt {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* Terminal Code Snippet */
|
||||
.code-preview {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.code-preview-header {
|
||||
background-color: var(--bg-elevated);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 4px 10px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.code-preview-body {
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.lead-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.read-lead-btn {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
padding: 5px 12px;
|
||||
font-weight: 700;
|
||||
border-radius: 2px;
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.read-lead-btn:hover {
|
||||
background-color: transparent;
|
||||
color: var(--accent-cyan);
|
||||
box-shadow: inset 0 0 0 1px var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Archive Dispatches Feed Table */
|
||||
.archive-feed-tile {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.archive-table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.archive-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.archive-table th {
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
}
|
||||
|
||||
.archive-table td {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.archive-row {
|
||||
cursor: pointer;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.archive-row:hover {
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.archive-row.active {
|
||||
background-color: var(--bg-elevated);
|
||||
border-left: 2px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.archive-title-link {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.archive-row:hover .archive-title-link {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Interactive Inline Inspection Pane */
|
||||
.inspect-box {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.inspect-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.inspect-prose {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Global Footer Tile (Gateway 3 Match) */
|
||||
.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 Bar (Gateway 3 Match) -->
|
||||
<header class="global-bar">
|
||||
<div class="brand-group">
|
||||
<span class="status-indicator"></span>
|
||||
<span>RIVER ROOKS // DUAL-LEAD DISPATCH CATALOG</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">> Catalog</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</a>
|
||||
<a href="index.html" class="bar-link">Samples ↗</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Hero Tile: Mission Statement & Metadata Badges -->
|
||||
<section class="hero-tile" aria-label="Blog Declaration">
|
||||
<div class="hero-pretitle">RESEARCH • DISPATCHES • SPECIFICATIONS</div>
|
||||
<h1 class="hero-main-title">Systems Research & Security Engineering Feed</h1>
|
||||
<p class="hero-landing-text">
|
||||
This catalog organizes field reports across two primary research vectors: hardware firmware reverse engineering and bare-metal self-hosted cloud infrastructure. Full source code, shell bootstrapping scripts, and network topology manifests are included.
|
||||
</p>
|
||||
|
||||
<div class="hero-metadata-badges">
|
||||
<span class="meta-badge">[NODE: gw-node01.lan]</span>
|
||||
<span class="meta-badge">[5 CANONICAL DISPATCHES]</span>
|
||||
<span class="meta-badge">[FORMAT: GHOSTTY / ZED MINIMALISM]</span>
|
||||
<span class="meta-badge">[ENCRYPTED SYNC: WIREGUARD]</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Dual-Lead Grid: 2 Featured Stories Side-by-Side -->
|
||||
<div class="dual-lead-grid">
|
||||
|
||||
<!-- Lead 1: Firmware Reversing -->
|
||||
<section class="tile" aria-label="Lead Article 1">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// LEAD: HARDWARE FORENSICS</span>
|
||||
</div>
|
||||
<span class="tile-meta">2026-07-20 • 8 MIN</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div class="lead-badge-row">
|
||||
<span style="color: var(--accent-amber); font-weight: 700; font-family: 'IBM Plex Mono', monospace;">[PINNED LEAD]</span>
|
||||
<span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">~/notes/iot-firmware-spi-extraction.md</span>
|
||||
</div>
|
||||
|
||||
<h2 style="margin: 0;">
|
||||
<a href="#" class="lead-heading">Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
</h2>
|
||||
|
||||
<p class="lead-excerpt">
|
||||
Extracting binary firmware from an 8-pin SOP SPI flash chip using a Bus Pirate and flashrom to bypass locked-down UART/JTAG interfaces on production microcontrollers.
|
||||
</p>
|
||||
|
||||
<div class="code-preview">
|
||||
<div class="code-preview-header">
|
||||
<span>extract_firmware.sh</span>
|
||||
<span>POSIX sh</span>
|
||||
</div>
|
||||
<div class="code-preview-body">
|
||||
<pre><code>flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M \
|
||||
-r firmware_dump.bin && md5sum firmware_dump.bin</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lead-footer">
|
||||
<span style="color: var(--text-muted);">[#firmware] [#hardware]</span>
|
||||
<a href="#" class="read-lead-btn">READ WRITEUP [8 MIN] →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Lead 2: Traefik Infrastructure -->
|
||||
<section class="tile" aria-label="Lead Article 2">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// LEAD: INFRASTRUCTURE ROUTING</span>
|
||||
</div>
|
||||
<span class="tile-meta">2026-07-15 • 12 MIN</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<div class="lead-badge-row">
|
||||
<span style="color: var(--accent-cyan); font-weight: 700; font-family: 'IBM Plex Mono', monospace;">[PINNED LEAD]</span>
|
||||
<span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">~/notes/ha-traefik-docker.md</span>
|
||||
</div>
|
||||
|
||||
<h2 style="margin: 0;">
|
||||
<a href="#" class="lead-heading">High-Availability Traefik Router with Docker Compose</a>
|
||||
</h2>
|
||||
|
||||
<p class="lead-excerpt">
|
||||
Architecting automated TLS wildcard certificate propagation, Docker provider label routing, zero-downtime container updates, and backend health-check failover.
|
||||
</p>
|
||||
|
||||
<div class="code-preview">
|
||||
<div class="code-preview-header">
|
||||
<span>docker-compose.traefik.yml</span>
|
||||
<span>YAML</span>
|
||||
</div>
|
||||
<div class="code-preview-body">
|
||||
<pre><code>services:
|
||||
traefik:
|
||||
image: traefik:v3.1
|
||||
command: ["--providers.docker=true", "--entrypoints.websecure.address=:443"]</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lead-footer">
|
||||
<span style="color: var(--text-muted);">[#homelab] [#docker-traefik]</span>
|
||||
<a href="#" class="read-lead-btn">READ WRITEUP [12 MIN] →</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Archive Dispatches Feed Tile with Interactive Inspector -->
|
||||
<section class="archive-feed-tile" aria-label="Chronological Dispatches Archive">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// CHRONOLOGICAL DISPATCHES ARCHIVE</span>
|
||||
</div>
|
||||
<span class="tile-meta">SELECT ROW TO INSPECT NOTES</span>
|
||||
</div>
|
||||
|
||||
<div class="archive-table-wrapper">
|
||||
<table class="archive-table" aria-label="Archive table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">DATE</th>
|
||||
<th scope="col">CATEGORY</th>
|
||||
<th scope="col">TITLE / PATH</th>
|
||||
<th scope="col">READ TIME</th>
|
||||
<th scope="col">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr class="archive-row active" onclick="inspectRow(0)">
|
||||
<td><span style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace;">2026-07-10</span></td>
|
||||
<td><span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">[#posix-sh]</span></td>
|
||||
<td><a href="#" class="archive-title-link">> POSIX sh vs. Bash: Portability in Modern Scripts</a></td>
|
||||
<td><span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">6 min</span></td>
|
||||
<td><span style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace;">[INSPECT ↓]</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="archive-row" onclick="inspectRow(1)">
|
||||
<td><span style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace;">2026-06-28</span></td>
|
||||
<td><span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">[#ctf]</span></td>
|
||||
<td><a href="#" class="archive-title-link">> CTF Write-up: Heap Exploitation in glibc 2.35</a></td>
|
||||
<td><span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">15 min</span></td>
|
||||
<td><span style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace;">[INSPECT ↓]</span></td>
|
||||
</tr>
|
||||
|
||||
<tr class="archive-row" onclick="inspectRow(2)">
|
||||
<td><span style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace;">2026-06-14</span></td>
|
||||
<td><span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">[#networking]</span></td>
|
||||
<td><a href="#" class="archive-title-link">> Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a></td>
|
||||
<td><span style="color: var(--text-muted); font-family: 'IBM Plex Mono', monospace;">10 min</span></td>
|
||||
<td><span style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace;">[INSPECT ↓]</span></td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Inspection Details Box -->
|
||||
<div class="inspect-box" id="inspectBox">
|
||||
<div class="inspect-head">
|
||||
<span id="inspectPath" style="color: var(--accent-cyan); font-weight: 700;">~/notes/posix-sh-vs-bash.md</span>
|
||||
<span id="inspectMeta">2026-07-10 • 6 MIN READ • [#posix-sh] [#shell] [#unix]</span>
|
||||
</div>
|
||||
<p class="inspect-prose" id="inspectProse">
|
||||
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 style="display: flex; justify-content: flex-end;">
|
||||
<a href="#" id="inspectReadBtn" class="read-lead-btn" style="padding: 4px 10px; font-size: 0.76rem;">OPEN ENTRY →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Global Footer Tile (Matching Gateway 3 Exactly) -->
|
||||
<footer class="footer-tile">
|
||||
<div>River Rooks • 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>
|
||||
const archiveData = [
|
||||
{
|
||||
path: '~/notes/posix-sh-vs-bash.md',
|
||||
meta: '2026-07-10 • 6 MIN READ • [#posix-sh] [#shell] [#unix]',
|
||||
prose: '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.'
|
||||
},
|
||||
{
|
||||
path: '~/notes/ctf-heap-glibc-2-35.md',
|
||||
meta: '2026-06-28 • 15 MIN READ • [#ctf] [#binary-exploitation] [#security]',
|
||||
prose: 'Deconstructing modern glibc memory allocator defenses: tcache safe-linking pointer mangling, bypass primitives, and House of Apple file structure hijacking for arbitrary code execution.'
|
||||
},
|
||||
{
|
||||
path: '~/notes/zero-trust-wireguard-opnsense.md',
|
||||
meta: '2026-06-14 • 10 MIN READ • [#networking] [#wireguard] [#security]',
|
||||
prose: 'Designing strict zone-based firewall rules across isolated VLANs (Management, Server DMZ, IoT, Trusted Clients) with point-to-point WireGuard cryptographic routing.'
|
||||
}
|
||||
];
|
||||
|
||||
function inspectRow(idx) {
|
||||
document.querySelectorAll('.archive-row').forEach(row => row.classList.remove('active'));
|
||||
const activeRow = document.querySelectorAll('.archive-row')[idx];
|
||||
if (activeRow) activeRow.classList.add('active');
|
||||
|
||||
const data = archiveData[idx];
|
||||
if (data) {
|
||||
document.getElementById('inspectPath').innerText = data.path;
|
||||
document.getElementById('inspectMeta').innerText = data.meta;
|
||||
document.getElementById('inspectProse').innerText = data.prose;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,780 +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 | Blog Lander (Tiling Hero & 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;
|
||||
}
|
||||
|
||||
.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 & 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">> 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 • ~/NOTES/INDEX.MD</div>
|
||||
<h1 class="hero-main-title">Engineering Notes, CTF Write-Ups & 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 • 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 • ~/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 →</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">> Embedded & 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">> Network Routing & 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">> 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 • 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">> 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 →</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 • 6 min read</time>
|
||||
<span>~/notes/posix-sh-vs-bash.md</span>
|
||||
</div>
|
||||
<h3 style="margin: 0;">
|
||||
<a href="#" class="feed-heading">> 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 →</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 • 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">> 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 →</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 • 10 min read</time>
|
||||
<span>~/notes/zero-trust-wireguard-opnsense.md</span>
|
||||
</div>
|
||||
<h3 style="margin: 0;">
|
||||
<a href="#" class="feed-heading">> Zero-Trust Network Isolation with WireGuard & 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 →</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Global Footer Tile (Matching Gateway 3 Exactly) -->
|
||||
<footer class="footer-tile">
|
||||
<div>River Rooks • 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>
|
||||
@@ -1,711 +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 | Blog Lander (Asymmetric 3-Tile 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 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; /* Pinned 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 Header Bar (Gateway 3 Standard) */
|
||||
.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 Manifesto Tile */
|
||||
.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;
|
||||
}
|
||||
|
||||
.hero-pretitle {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-cyan);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hero-prose {
|
||||
font-size: 0.98rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.65;
|
||||
max-width: 84ch;
|
||||
}
|
||||
|
||||
/* Interactive Grep Bar */
|
||||
.grep-bar {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 8px 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.grep-prompt {
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.grep-input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.82rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Main 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);
|
||||
}
|
||||
|
||||
.tile-meta {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Featured Entry Card inside Primary Column */
|
||||
.featured-entry {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.featured-entry:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.entry-badge-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
line-height: 1.35;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.entry-title:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.entry-abstract {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Code Preview Box */
|
||||
.snippet-box {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snippet-head {
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 4px 10px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.snippet-body {
|
||||
padding: 10px;
|
||||
overflow-x: auto;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.entry-footer-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.read-pill {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
padding: 4px 10px;
|
||||
font-weight: 700;
|
||||
border-radius: 2px;
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.read-pill:hover {
|
||||
background-color: transparent;
|
||||
color: var(--accent-cyan);
|
||||
box-shadow: inset 0 0 0 1px var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Right Column Component: Track Cards */
|
||||
.track-tile-item {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.track-tile-item:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.track-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.track-tile-item:hover .track-title {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.track-meta {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.72rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.track-desc {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* Right Column: Mini Feed Items */
|
||||
.compact-feed-item {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.compact-feed-item:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.compact-date {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.compact-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.compact-title:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Global Footer Tile (Gateway 3 Match) */
|
||||
.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 Bar (Matching Gateway 3 Exactly) -->
|
||||
<header class="global-bar">
|
||||
<div class="brand-group">
|
||||
<span class="status-indicator"></span>
|
||||
<span>RIVER ROOKS // TECHNICAL NOTES WORKSPACE</span>
|
||||
</div>
|
||||
<nav class="bar-nav" aria-label="Top Navigation">
|
||||
<a href="service-gateway-tiling.html" class="bar-link">Gateway 3 ↗</a>
|
||||
<a href="#" class="bar-link active">> Field Notes</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: Mission Statement & Live Filter -->
|
||||
<section class="hero-tile" aria-label="Notes Mission">
|
||||
<div class="hero-pretitle">SYSTEM DISPATCHES • KNOWLEDGE BASE</div>
|
||||
<h1 class="hero-title">Technical Notes & Systems Architecture</h1>
|
||||
<p class="hero-prose">
|
||||
Deep technical documentation on network forensics, self-hosted hypervisor infrastructure, embedded hardware exploitation, and minimal Unix tool design.
|
||||
</p>
|
||||
|
||||
<div class="grep-bar">
|
||||
<span class="grep-prompt">dev-blog@system:~$</span>
|
||||
<span style="color: var(--text-muted);">grep -i</span>
|
||||
<input
|
||||
type="text"
|
||||
id="liveFilterInput"
|
||||
class="grep-input"
|
||||
placeholder="filter dispatches by keyword, hardware, or ctf..."
|
||||
oninput="filterArticles(this.value)"
|
||||
>
|
||||
<span id="matchStats" style="color: var(--accent-green); font-size: 0.74rem;">5 dispatches loaded</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Asymmetric Split Grid -->
|
||||
<div class="split-grid">
|
||||
|
||||
<!-- Left Column: Featured Articles & Primary Feed -->
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
|
||||
<!-- Tile: Featured Article 1 (Hardware Lead) -->
|
||||
<section class="tile article-container" data-keywords="firmware hardware security spi bus-pirate flash extraction" aria-label="Lead Article 1">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// FEATURED WRITE-UP 01</span>
|
||||
</div>
|
||||
<span class="tile-meta">2026-07-20 • 8 MIN READ</span>
|
||||
</div>
|
||||
|
||||
<div class="featured-entry">
|
||||
<div class="entry-badge-row">
|
||||
<span style="color: var(--accent-amber); font-weight: 700;">[PINNED RESEARCH]</span>
|
||||
<span style="color: var(--text-muted);">~/notes/iot-firmware-spi-extraction.md</span>
|
||||
</div>
|
||||
|
||||
<h2 style="margin: 0;">
|
||||
<a href="#" class="entry-title">> Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
</h2>
|
||||
|
||||
<p class="entry-abstract">
|
||||
In security audits of embedded systems, retrieving the 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.
|
||||
</p>
|
||||
|
||||
<div class="snippet-box">
|
||||
<div class="snippet-head">
|
||||
<span>extract_firmware.sh</span>
|
||||
<span>POSIX sh</span>
|
||||
</div>
|
||||
<div class="snippet-body">
|
||||
<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="entry-footer-row">
|
||||
<span style="color: var(--text-muted);">[#firmware] [#hardware] [#security] [#bus-pirate]</span>
|
||||
<a href="#" class="read-pill">READ WRITEUP [8 MIN] →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tile: Featured Article 2 (Infrastructure Lead) -->
|
||||
<section class="tile article-container" data-keywords="homelab docker traefik networking compose proxy acme" aria-label="Lead Article 2">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// FEATURED WRITE-UP 02</span>
|
||||
</div>
|
||||
<span class="tile-meta">2026-07-15 • 12 MIN READ</span>
|
||||
</div>
|
||||
|
||||
<div class="featured-entry">
|
||||
<div class="entry-badge-row">
|
||||
<span style="color: var(--accent-cyan); font-weight: 700;">[INFRASTRUCTURE SPEC]</span>
|
||||
<span style="color: var(--text-muted);">~/notes/ha-traefik-docker.md</span>
|
||||
</div>
|
||||
|
||||
<h2 style="margin: 0;">
|
||||
<a href="#" class="entry-title">> Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
</h2>
|
||||
|
||||
<p class="entry-abstract">
|
||||
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="snippet-box">
|
||||
<div class="snippet-head">
|
||||
<span>traefik-compose.yml</span>
|
||||
<span>YAML</span>
|
||||
</div>
|
||||
<div class="snippet-body">
|
||||
<pre><code>services:
|
||||
traefik:
|
||||
image: traefik:v3.1
|
||||
command:
|
||||
- "--providers.docker=true"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--certificatesresolvers.cf.acme.dnschallenge=true"</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entry-footer-row">
|
||||
<span style="color: var(--text-muted);">[#homelab] [#docker-traefik] [#networking]</span>
|
||||
<a href="#" class="read-pill">READ WRITEUP [12 MIN] →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tile: Featured Article 3 (Unix Systems) -->
|
||||
<section class="tile article-container" data-keywords="posix-sh shell unix portability alpine scripts bashisms" aria-label="Lead Article 3">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// SYSTEMS & SCRIPTING DISPATCH</span>
|
||||
</div>
|
||||
<span class="tile-meta">2026-07-10 • 6 MIN READ</span>
|
||||
</div>
|
||||
|
||||
<div class="featured-entry">
|
||||
<div class="entry-badge-row">
|
||||
<span style="color: var(--accent-green); font-weight: 700;">[STANDARDS & COMPLIANCE]</span>
|
||||
<span style="color: var(--text-muted);">~/notes/posix-sh-vs-bash.md</span>
|
||||
</div>
|
||||
|
||||
<h2 style="margin: 0;">
|
||||
<a href="#" class="entry-title">> POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
</h2>
|
||||
|
||||
<p class="entry-abstract">
|
||||
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="entry-footer-row">
|
||||
<span style="color: var(--text-muted);">[#posix-sh] [#shell] [#unix]</span>
|
||||
<a href="#" class="read-pill">READ WRITEUP [6 MIN] →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Curated Tracks & Additional Dispatches -->
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
|
||||
<!-- Curated Topic Tracks Tile -->
|
||||
<section class="tile" aria-label="Curated Topic Tracks">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// CURATED TRACKS</span>
|
||||
</div>
|
||||
<span class="tile-meta">SPECIALIZATIONS</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
|
||||
<a href="#" class="track-tile-item">
|
||||
<div class="track-title">Embedded Silicon & Hardware</div>
|
||||
<div class="track-meta">[1 ARTICLE • BUS PIRATE]</div>
|
||||
<p class="track-desc">Physical chip extraction, UART/JTAG probing, SPI dump validation.</p>
|
||||
</a>
|
||||
|
||||
<a href="#" class="track-tile-item">
|
||||
<div class="track-title">Self-Hosted Infrastructure</div>
|
||||
<div class="track-meta">[2 ARTICLES • TRAEFIK & WIREGUARD]</div>
|
||||
<p class="track-desc">Automated edge routing, zero-trust overlay networks, VLAN firewalls.</p>
|
||||
</a>
|
||||
|
||||
<a href="#" class="track-tile-item">
|
||||
<div class="track-title">Low-Level Binary Exploitation</div>
|
||||
<div class="track-meta">[1 ARTICLE • GLIBC HEAP]</div>
|
||||
<p class="track-desc">Memory allocator internals, tcache safe-linking mangling bypass.</p>
|
||||
</a>
|
||||
|
||||
<a href="#" class="track-tile-item">
|
||||
<div class="track-title">Zero-Allocation Tooling</div>
|
||||
<div class="track-meta">[1 ARTICLE • POSIX SH]</div>
|
||||
<p class="track-desc">High-throughput logging, shell parser ASTs, minimalist runtimes.</p>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- More Articles in Archive Tile -->
|
||||
<section class="tile" aria-label="Additional Dispatches">
|
||||
<div class="tile-header">
|
||||
<div class="tile-title">
|
||||
<span>// CTF & SECURITY WIRE</span>
|
||||
</div>
|
||||
<span class="tile-meta">REPORTS</span>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
|
||||
<!-- Post 4: Heap Exploitation -->
|
||||
<div class="compact-feed-item article-container" data-keywords="ctf binary exploitation heap glibc 2.35 memory security">
|
||||
<span class="compact-date">2026-06-28 • 15 min read</span>
|
||||
<a href="#" class="compact-title">> CTF Write-up: Heap Exploitation in glibc 2.35</a>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); line-height: 1.45;">
|
||||
Deconstructing tcache safe-linking pointer mangling, bypass primitives, and House of Apple file structure hijacking.
|
||||
</p>
|
||||
<div style="font-family: 'IBM Plex Mono', monospace; font-size: 0.72rem; color: var(--text-muted); margin-top: 4px;">
|
||||
[#ctf] [#binary-exploitation]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post 5: Zero-Trust WireGuard -->
|
||||
<div class="compact-feed-item article-container" data-keywords="networking wireguard security zerotrust opnsense vlans">
|
||||
<span class="compact-date">2026-06-14 • 10 min read</span>
|
||||
<a href="#" class="compact-title">> Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); line-height: 1.45;">
|
||||
Designing strict zone-based firewall rules across isolated VLANs with point-to-point WireGuard cryptographic routing.
|
||||
</p>
|
||||
<div style="font-family: 'IBM Plex Mono', monospace; font-size: 0.72rem; color: var(--text-muted); margin-top: 4px;">
|
||||
[#networking] [#wireguard]
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Global Footer Tile (Matching Gateway 3 Exactly) -->
|
||||
<footer class="footer-tile">
|
||||
<div>River Rooks • 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 filterArticles(query) {
|
||||
const q = query.toLowerCase().trim();
|
||||
const items = document.querySelectorAll('.article-container');
|
||||
let matches = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const text = (item.innerText + ' ' + (item.getAttribute('data-keywords') || '')).toLowerCase();
|
||||
if (!q || text.includes(q)) {
|
||||
item.style.display = '';
|
||||
matches++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('matchStats').innerText = `${matches} dispatches visible`;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,288 +0,0 @@
|
||||
# Canonical Content Dataset & Single Source of Truth for Design Samples
|
||||
|
||||
**Document Purpose:** Standardized dataset containing canonical profile data, active services, technical blog notes, engineering projects, hardware nodes, and interactive ASCII daemon assets. All design sample layouts (`homepage.html`, `split-panel.html`, `companion.html`, `document-flow.html`, `portfolio.html`, `homelab.html`, `public-services.html`, `sample-dashboard.html`, `sample-command-center.html`, `sample-dual-pane-explorer.html`) pull from this common material.
|
||||
|
||||
---
|
||||
|
||||
## 1. Persona & Identity (`whoami`)
|
||||
|
||||
| Field | Canonical Value |
|
||||
| :--- | :--- |
|
||||
| **Full Name** | `River Rooks` |
|
||||
| **Primary Title** | `Systems Architecture & Cybersecurity` |
|
||||
| **Academic / Institutional Role** | Cybersecurity student at the University of Alabama College of Engineering |
|
||||
| **Specialization Areas** | Network forensics, self-hosted infrastructure, systems architecture, low-level tooling, bare-metal virtualization |
|
||||
| **Physical Node Location** | Alabama, USA / `RACK-01` |
|
||||
| **System Hostname Prompt** | `dev-blog@system:~$` / `agy --node-01` |
|
||||
| **PGP Key Fingerprint** | `0x4A8E92F1C0B3D4E7` |
|
||||
| **SSH Public Key Digest** | `SHA256:7uK+N04wP5rL/9z3Q8s1vX4dF6mK2pT8yB1wN0qZ8` |
|
||||
| **Email Address** | `river@rooks.dev` (or `river@example.com`) |
|
||||
| **Public Git Forge** | `https://git.rooks.dev` / `https://github.com` |
|
||||
| **Matrix Chat Endpoint** | `@river:rooks.dev` |
|
||||
|
||||
### Biography Snippets
|
||||
* **One-Line Meta:** `Cybersecurity student at UA College of Engineering focusing on network forensics, self-hosted infrastructure, and systems architecture.`
|
||||
* **Extended Bio:** `Cybersecurity student and systems practitioner. Spends time architecting high-throughput homelab networks, reverse engineering IoT firmware, hardening FreeBSD/Linux hypervisors, and writing zero-allocation Unix utilities.`
|
||||
|
||||
### Active Tech Stack Tags
|
||||
```text
|
||||
[#network-forensics] [#docker-traefik] [#arch-linux] [#neovim] [#wireguard]
|
||||
[#posix-sh] [#python] [#golang] [#rust] [#proxmox] [#truenas]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Services Directory & Infrastructure Catalog
|
||||
|
||||
### A. Core & Network Infrastructure
|
||||
| Service Name | Category | Port / Protocol | Domain / Routing | Access | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Traefik Proxy (v3.1)** | `#core` | `443/tcp` (TLS) | `https://proxy.rooks.dev` | `INTERNAL / ONLINE` | Edge HTTP reverse proxy and load balancer with dynamic Docker provider discovery and ACME Let's Encrypt TLS wildcard certificates. |
|
||||
| **OPNsense Firewall (v24.7)** | `#core` | `VLAN 10 / Mgmt` | Gateway IP | `PROTECTED / ACTIVE` | Hardened FreeBSD firewall routing 10GbE traffic with VLAN isolation, Suricata IDS/IPS, and Unbound DNS resolvers. |
|
||||
| **AdGuard Home** | `#dns` | `53/udp`, `853/tcp` | `https://dns.rooks.dev` | `INTERNAL / PROTECTED` | Network-wide DNS sinkhole filtering trackers and ad domains across all VLANs with DNS-over-HTTPS upstream security. |
|
||||
| **WireGuard Mesh** | `#vpn` | `51820/udp` | Peer Mesh | `PUBLIC / 14 PEERS` | High-performance encrypted point-to-point VPN tunnel connecting remote laptops and mobile endpoints to homelab subnet. |
|
||||
|
||||
### B. Compute, Virtualization & Storage
|
||||
| Service Name | Category | Port / Protocol | Domain / Routing | Access | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Proxmox VE (v8.2)** | `#compute` | `8006/tcp` | `https://pve.rooks.dev` | `INTERNAL / ONLINE` | Enterprise hypervisor cluster hosting Debian LXC containers and KVM virtual machines with Ceph & ZFS backend. |
|
||||
| **TrueNAS SCALE** | `#storage` | `NFS / SMB / 443` | `https://nas.rooks.dev` | `INTERNAL / ONLINE` | Primary network attached storage managing 48TB ZFS RaidZ2 array, dataset snapshots, NFS exports, and scrub jobs. |
|
||||
| **Forgejo Git Server** | `#dev-git` | `3000/tcp`, `2222/ssh` | `https://git.rooks.dev` | `PUBLIC / OPEN` | Lightweight self-hosted Git forge hosting infrastructure-as-code repos, Ansible playbooks, dotfiles, and CTF archives. |
|
||||
|
||||
### C. Public Tools & Developer Utilities
|
||||
| Service Name | Category | Port / Protocol | Domain / Routing | Access | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **CyberChef Workbench** | `#security-tool` | `443/tcp` | `https://chef.rooks.dev` | `PUBLIC / OPEN` | Cyber Swiss Army Knife for hex decoding, Base64 operations, regex extraction, cryptographic hashing, and CTF payload analysis. |
|
||||
| **Stirling PDF Studio** | `#pdf-tools` | `443/tcp` | `https://pdf.rooks.dev` | `PUBLIC / OPEN` | Self-hosted document processing suite for merging, splitting, OCR text extraction, password encryption, and PDF page manipulation. |
|
||||
| **HedgeDoc Collaborative** | `#markdown` | `443/tcp` | `https://pad.rooks.dev` | `PUBLIC / OPEN` | Real-time collaborative Markdown editor for technical specs, RFC drafts, and collaborative meeting notes. |
|
||||
|
||||
### D. Media, Reading & Content Automation
|
||||
| Service Name | Category | Port / Protocol | Domain / Routing | Access | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Jellyfin Media Portal** | `#media-video` | `8096/tcp` | `https://watch.rooks.dev` | `AUTH REQUIRED` | FOSS media streaming engine leveraging Intel QuickSync GPU hardware acceleration for 4K HDR transcoding. |
|
||||
| **Arr Automation Suite** | `#automation` | `8989 / 7878 / 9696` | `https://arr.rooks.dev` | `INTERNAL / RUNNING` | Automated content indexing, Prowlarr indexer management, Sonarr/Radarr pipeline, and SABnzbd queue downloader. |
|
||||
| **Audiobookshelf** | `#media-audio` | `443/tcp` | `https://listen.rooks.dev` | `INVITE ONLY` | Self-hosted audiobook and podcast streaming server with continuous playback position synchronization across mobile clients. |
|
||||
| **Calibre-Web Library** | `#ebooks` | `443/tcp` | `https://read.rooks.dev` | `PUBLIC READ` | E-book library catalog with e-reader OPDS feed integration and in-browser reading support. |
|
||||
|
||||
### E. Security, Telemetry & Communications
|
||||
| Service Name | Category | Port / Protocol | Domain / Routing | Access | Description |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **Vaultwarden** | `#secrets` | `443/tcp` (E2EE) | `https://vault.rooks.dev` | `2FA / AUTH` | Lightweight Rust implementation of the Bitwarden API hosting encrypted credentials, SSH keys, and TOTP tokens. |
|
||||
| **Prometheus & Grafana** | `#telemetry` | `3000 / 9090` | `https://mon.rooks.dev` | `INTERNAL / SCRAPING` | Time-series metric collection with Node Exporter, Telegraf agents, and live cluster performance dashboards. |
|
||||
| **Uptime Kuma** | `#status` | `3001/tcp` | `https://status.rooks.dev` | `PUBLIC / 100% HEALTH`| Synthetic HTTP, Ping, and TCP health monitor broadcasting real-time service uptime alerts to Discord webhooks. |
|
||||
| **Matrix Synapse** | `#messaging` | `8448/tcp` | `https://matrix.rooks.dev` | `FEDERATED / OPEN` | Federated Matrix homeserver for encrypted messaging, public security channels, and cross-server communication. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Technical Notes & Blog Articles
|
||||
|
||||
### Post 1: Featured Article (Full Writeup)
|
||||
* **Title:** `Reverse Engineering IoT Firmware: SPI Flash Extraction`
|
||||
* **File / Path:** `~/notes/iot-firmware-spi-extraction.md`
|
||||
* **Publication Date:** `2026-07-20`
|
||||
* **Reading Time:** `8 mins`
|
||||
* **Tags:** `[#firmware]`, `[#hardware]`, `[#security]`, `[#bus-pirate]`
|
||||
* **Abstract:** In security audits of embedded systems, retrieving the 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.
|
||||
* **Code Snippet (`extract_firmware.sh`):**
|
||||
```bash
|
||||
#!/bin/sh
|
||||
# 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
|
||||
else
|
||||
echo "Failed to read flash memory." >&2
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Post 2: Traefik Docker Architecture
|
||||
* **Title:** `Building a High-Availability Traefik Router with Docker Compose`
|
||||
* **File / Path:** `~/notes/ha-traefik-docker.md`
|
||||
* **Publication Date:** `2026-07-15`
|
||||
* **Reading Time:** `12 mins`
|
||||
* **Tags:** `[#homelab]`, `[#docker-traefik]`, `[#networking]`
|
||||
* **Abstract:** Architecting automated TLS wildcard certificate propagation, Docker provider label routing, zero-downtime container updates, and backend health-check failover routes.
|
||||
|
||||
### Post 3: Shell Portability
|
||||
* **Title:** `POSIX sh vs. Bash: Portability in Modern Scripts`
|
||||
* **File / Path:** `~/notes/posix-sh-vs-bash.md`
|
||||
* **Publication Date:** `2026-07-10`
|
||||
* **Reading Time:** `6 mins`
|
||||
* **Tags:** `[#posix-sh]`, `[#shell]`, `[#unix]`
|
||||
* **Abstract:** 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.
|
||||
|
||||
### Post 4: Low-Level CTF Exploitation
|
||||
* **Title:** `CTF Write-up: Heap Exploitation in glibc 2.35`
|
||||
* **File / Path:** `~/notes/ctf-heap-glibc-2-35.md`
|
||||
* **Publication Date:** `2026-06-28`
|
||||
* **Reading Time:** `15 mins`
|
||||
* **Tags:** `[#ctf]`, `[#binary-exploitation]`, `[#security]`
|
||||
* **Abstract:** Deconstructing modern glibc memory allocator defenses: tcache safe-linking pointer mangling, bypass primitives, and House of Apple file structure hijacking for arbitrary code execution.
|
||||
|
||||
### Post 5: Network Isolation
|
||||
* **Title:** `Zero-Trust Network Isolation with WireGuard & OPNsense VLANs`
|
||||
* **File / Path:** `~/notes/zero-trust-wireguard-opnsense.md`
|
||||
* **Publication Date:** `2026-06-14`
|
||||
* **Reading Time:** `10 mins`
|
||||
* **Tags:** `[#networking]`, `[#wireguard]`, `[#security]`
|
||||
* **Abstract:** Designing strict zone-based firewall rules across isolated VLANs (Management, Server DMZ, IoT, Trusted Clients) with point-to-point WireGuard cryptographic routing.
|
||||
|
||||
---
|
||||
|
||||
## 4. Engineering Projects & System Modules
|
||||
|
||||
### Module 1: Custom POSIX Shell Parser
|
||||
* **Path / Identifier:** `/dev/posix-shell-parser`
|
||||
* **Version:** `v1.2.0`
|
||||
* **Primary Language:** `Lua`
|
||||
* **Tags:** `[#lua]`, `[#ast-parser]`, `[#compilers]`
|
||||
* **Summary:** A lightweight Lexer and AST parser written in Lua for parsing POSIX-compliant shell scripts. Optimized to avoid runtime heap allocations during tokenization, enabling real-time syntax coloring inside terminal buffers.
|
||||
* **Code Snippet (`lexer.lua`):**
|
||||
```lua
|
||||
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
|
||||
```
|
||||
|
||||
### Module 2: WireGuard Mesh Topology Builder
|
||||
* **Path / Identifier:** `/dev/wireguard-mesh`
|
||||
* **Version:** `v0.8.4`
|
||||
* **Primary Language:** `Python` / `POSIX sh`
|
||||
* **Tags:** `[#networking]`, `[#wireguard]`, `[#automation]`
|
||||
* **Summary:** Automated configuration generator that computes public/private key matrices and generates atomic configuration files for full-mesh WireGuard overlay networks spanning cloud VMs and homelab servers.
|
||||
|
||||
### Module 3: Zero-Allocation Logger (Go)
|
||||
* **Path / Identifier:** `/dev/zero-alloc-logger`
|
||||
* **Version:** `v2.1.1`
|
||||
* **Primary Language:** `Go`
|
||||
* **Tags:** `[#go]`, `[#performance]`, `[#systems]`
|
||||
* **Summary:** A high-throughput structured JSON logger utilizing `sync.Pool` memory recycling to eliminate GC pause overhead during disk I/O (>500,000 log events/second).
|
||||
|
||||
### Module 4: Embedded Kernel Boot Strap
|
||||
* **Path / Identifier:** `/dev/kernel-bootstrap`
|
||||
* **Version:** `v0.1.0`
|
||||
* **Primary Language:** `ARM Cortex Assembly` / `C`
|
||||
* **Tags:** `[#arm-cortex]`, `[#assembly]`, `[#embedded]`
|
||||
* **Summary:** Bare-metal boot sector and initial C runtime setup for ARM Cortex-M microcontrollers. Configures vector table offset registers, performs memory section copying, and initializes early UART debugging output.
|
||||
|
||||
### Module 5: Traefik Cloudflare Dynamic DNS Daemon
|
||||
* **Path / Identifier:** `/dev/ddns-daemon`
|
||||
* **Version:** `v1.0.2`
|
||||
* **Primary Language:** `Python` / `Docker SDK`
|
||||
* **Tags:** `[#docker]`, `[#traefik]`, `[#cloudflare]`
|
||||
* **Summary:** Background controller daemon monitoring Docker container state transitions and synchronizing container label subdomains with Cloudflare DNS A/AAAA records.
|
||||
|
||||
---
|
||||
|
||||
## 5. Physical Hardware Nodes & Telemetry
|
||||
|
||||
```text
|
||||
+-------------------+ 10GbE SFP+ +-------------------+
|
||||
| gw-node01.lan | <==================> | pve-node02.lan |
|
||||
| OPNsense 24.7 | | Proxmox VE 8.2 |
|
||||
| Intel N100 (4C/4T)| | AMD 5900X (12C/24T|
|
||||
+-------------------+ +-------------------+
|
||||
|| ||
|
||||
|| 10GbE Trunk || 10GbE NFS/SMB
|
||||
\/ \/
|
||||
+--------------------------------------------------------------+
|
||||
| nas-node03.lan |
|
||||
| TrueNAS SCALE (48TB RaidZ2 Pool) |
|
||||
| Intel Core i5-12400 (6C/12T) / 64GB ECC |
|
||||
+--------------------------------------------------------------+
|
||||
```
|
||||
|
||||
| Hostname | Role / Operating System | Hardware Specifications | Nominal Load |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`gw-node01.lan`** | Edge Gateway / OPNsense 24.7 | Intel N100 (4C/4T), 16GB DDR5, 4x 2.5GbE I226-V | CPU: 18% \| RAM: 32% |
|
||||
| **`pve-node02.lan`** | Hypervisor / Proxmox VE 8.2 | AMD Ryzen 9 5900X (12C/24T), 128GB ECC DDR4, 2x 2TB NVMe | CPU: 42% \| RAM: 68% |
|
||||
| **`nas-node03.lan`** | Storage / TrueNAS SCALE | Intel Core i5-12400 (6C/12T), 64GB ECC, 6x 16TB Enterprise SAS | CPU: 14% \| RAM: 82% (ZFS ARC) |
|
||||
|
||||
---
|
||||
|
||||
## 6. ASCII Mascot & Daemon Artframes (`sys-daemon`)
|
||||
|
||||
### Frame 1: Idle / Scanning
|
||||
```text
|
||||
.-----------------.
|
||||
/ ^ ^ ^ /|
|
||||
/ (_) (_) (_) / |
|
||||
/__________________/ |
|
||||
| ___ ___ | |
|
||||
| [o_o] [o_o] | |
|
||||
| `-' `-' | |
|
||||
| ______ | |
|
||||
| / \ | /
|
||||
| | -- | | /
|
||||
| \____/ |/
|
||||
`------------------'
|
||||
```
|
||||
|
||||
### Frame 2: Active / Alert
|
||||
```text
|
||||
.-----------------.
|
||||
/ ^ ^ ^ /|
|
||||
/ (_) (_) (_) / |
|
||||
/__________________/ |
|
||||
| ___ ___ | |
|
||||
| [-_-] [-_-] | |
|
||||
| `-' `-' | |
|
||||
| ______ | |
|
||||
| / \ | /
|
||||
| | O | | /
|
||||
| \____/ |/
|
||||
`------------------'
|
||||
```
|
||||
|
||||
### Frame 3: Scroll-Tracker Cat Daemon
|
||||
```text
|
||||
/\___/\
|
||||
( o o ) sys-daemon
|
||||
( =^= ) [TRACKING]
|
||||
(---)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Sample Pages Material Mapping Matrix
|
||||
|
||||
This matrix specifies which components and data entries from this canonical document are presented in each sample layout:
|
||||
|
||||
| Sample File | Layout Style | Persona / Bio | Services Included | Notes / Articles | Project Modules | Nodes & Telemetry | Mascot / Daemon |
|
||||
| :--- | :--- | :---: | :---: | :---: | :---: | :---: | :---: |
|
||||
| [`index.html`](file:///home/venus/code/blog/index.html) | Root Minimalist Business Card | Full whoami | — | — | Active Stack Tags | Terminal stdout | — |
|
||||
| [`samples/homepage.html`](file:///home/venus/code/blog/samples/homepage.html) | Sample 1: Developer Homepage | Profile header | — | Post 1 (Full) + Archive Index (2,3,4) | — | — | — |
|
||||
| [`samples/split-panel.html`](file:///home/venus/code/blog/samples/split-panel.html) | Sample 2: Split Panel Feed | — | — | — | Modules 1–5 | Status row | Frame 1/2 Animated |
|
||||
| [`samples/companion.html`](file:///home/venus/code/blog/samples/companion.html) | Sample 3: Companion Layout | — | — | — | Modules 1–5 | Terminal card | Frame 3 Cat Daemon |
|
||||
| [`samples/document-flow.html`](file:///home/venus/code/blog/samples/document-flow.html) | Sample 4: Continuous Document Flow | Header banner | — | Post 1 Snippets | Modules 1–5 in detail | — | Frame 3 Scroll Tracker |
|
||||
| [`samples/portfolio.html`](file:///home/venus/code/blog/samples/portfolio.html) | Sample 5: Portfolio Card | Full whoami | — | — | Active Stack Tags | Terminal stdout | — |
|
||||
| [`samples/homelab.html`](file:///home/venus/code/blog/samples/homelab.html) | Sample 6: Homelab Lander | Header banner | All Core & Compute (12 services) | — | — | Nodes 1–3 + ASCII Topology | Terminal logs |
|
||||
| [`samples/public-services.html`](file:///home/venus/code/blog/samples/public-services.html) | Sample 7: Public Portal | Header banner | All Public Utilities (9 services) | — | — | Keys & PGP box | — |
|
||||
| [`samples/sample-dashboard.html`](file:///home/venus/code/blog/samples/sample-dashboard.html) | Sample 8: 3-Tile Workspace | Center tile bio | Left tile services | Right tile archive | Center tile projects | Status bar | Vim command bar |
|
||||
| [`samples/sample-command-center.html`](file:///home/venus/code/blog/samples/sample-command-center.html) | Sample 9: Tabbed Command Center | Tab 3 (Resume) | Tab 1 (Services) | Tab 2 (Blog Notes) | Tab 3 (Projects) | Tab 1 (Nodes) | Status dots |
|
||||
| [`samples/sample-dual-pane-explorer.html`](file:///home/venus/code/blog/samples/sample-dual-pane-explorer.html) | Sample 10: Helix Tree Explorer | Sidebar links | Services subtree | Notes subtree | Modules subtree | Status bar | Sidebar companion |
|
||||
| [`samples/blog-lander-stream.html`](file:///home/venus/code/blog/samples/blog-lander-stream.html) | Blog Lander 1: Minimalist Stream | Header & Bio | — | Posts 1–5 (Linear Feed & Topic Filter) | — | — | — |
|
||||
| [`samples/blog-lander-terminal.html`](file:///home/venus/code/blog/samples/blog-lander-terminal.html) | Blog Lander 2: Terminal Directory Index | Header & Subtitle | — | Posts 1–5 (Dense Tabular Index & Code Inspection) | — | Terminal Status Bar | — |
|
||||
| [`samples/blog-lander-cards.html`](file:///home/venus/code/blog/samples/blog-lander-cards.html) | Blog Lander 3: Curated Dispatch & Cards | Profile & Bio | — | Post 1 (Pinned Lead Card) + Posts 2–5 (Cards Grid) | — | — | — |
|
||||
| [`samples/service-gateway-manifesto.html`](file:///home/venus/code/blog/samples/service-gateway-manifesto.html) | Gateway Lander 1: Manifesto & System Hub | Hero Manifesto & Bio | 6 Core/Public Services | Posts 1–3 Excerpts | — | Status banner | — |
|
||||
| [`samples/service-gateway-terminal.html`](file:///home/venus/code/blog/samples/service-gateway-terminal.html) | Gateway Lander 2: Host Console & MOTD Router | MOTD Narrative & Specs | 6 Tabular Services | Posts 1–5 Log Lines | — | Hardware summary | — |
|
||||
| [`samples/service-gateway-tiling.html`](file:///home/venus/code/blog/samples/service-gateway-tiling.html) | Gateway Lander 3: Tiling Workspace Hub | Hero Landing Tile | 5 Grid Services (Blog Core Tile) | Posts 1–4 Sublinks | — | Nodes 1–3 Live Telemetry | — |
|
||||
| [`samples/blog-tiling-lead.html`](file:///home/venus/code/blog/samples/blog-tiling-lead.html) | Tiling Blog 1: Hero Dispatch & Dual Stream | Hero Tile & Topic Filter | — | Post 1 (Lead with Code) + Posts 2–5 (Feed) | — | Archive Telemetry | — |
|
||||
| [`samples/blog-tiling-split.html`](file:///home/venus/code/blog/samples/blog-tiling-split.html) | Tiling Blog 2: Asymmetric 3-Tile Workspace | Hero Tile & Grep Bar | — | Posts 1–3 (Featured) + Posts 4–5 (Wire) | — | Curated Tracks | — |
|
||||
| [`samples/blog-tiling-catalog.html`](file:///home/venus/code/blog/samples/blog-tiling-catalog.html) | Tiling Blog 3: Dual-Lead Grid & Inspection Feed | Hero Tile & Badges | — | Posts 1–2 (Dual Leads) + Posts 3–5 (Table) | — | Inline Note Inspector | — |
|
||||
| [`samples/article-tiling-firmware.html`](file:///home/venus/code/blog/samples/article-tiling-firmware.html) | Article 1: IoT Firmware Extraction | Header & Meta | — | Post 1 (Full Markdown, Code, Tables, Alerts) | — | Winbond 25Q64 specs | — |
|
||||
| [`samples/article-tiling-traefik.html`](file:///home/venus/code/blog/samples/article-tiling-traefik.html) | Article 2: HA Traefik Router | Header & Meta | — | Post 2 (Full Markdown, Compose YAML, ASCII Diagram) | — | pve-node02.lan specs | — |
|
||||
| [`samples/article-tiling-heap.html`](file:///home/venus/code/blog/samples/article-tiling-heap.html) | Article 3: CTF Heap Exploitation | Header & Meta | — | Post 4 (Full Markdown, GDB session, C payload, Formula) | — | glibc 2.35 runtime | — |
|
||||
|
||||
|
||||
|
||||
@@ -1,475 +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>
|
||||
<a href="homelab.html" class="nav-link">Sample 6</a>
|
||||
<a href="public-services.html" class="nav-link">Sample 7</a>
|
||||
<a href="sample-dashboard.html" class="nav-link">Sample 8</a>
|
||||
<a href="sample-command-center.html" class="nav-link">Sample 9</a>
|
||||
<a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10</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">River Rooks // 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 <span style="font-size: 0.8rem; font-weight: 400; color: var(--text-muted);">[v1.2.0 • #lua]</span></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 without runtime heap allocations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 2 -->
|
||||
<div class="project-card" tabindex="0" data-target="/dev/wireguard-mesh">
|
||||
<h3 class="card-title">WireGuard Mesh Topology Builder <span style="font-size: 0.8rem; font-weight: 400; color: var(--text-muted);">[v0.8.4 • #wireguard]</span></h3>
|
||||
<p class="card-description">
|
||||
Automated configuration generator for computing key matrices and 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) <span style="font-size: 0.8rem; font-weight: 400; color: var(--text-muted);">[v2.1.1 • #golang]</span></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 (>500k ops/sec).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Card 4 -->
|
||||
<div class="project-card" tabindex="0" data-target="/dev/kernel-bootstrap">
|
||||
<h3 class="card-title">Embedded Kernel Boot Strap <span style="font-size: 0.8rem; font-weight: 400; color: var(--text-muted);">[v0.1.0 • #arm-cortex]</span></h3>
|
||||
<p class="card-description">
|
||||
Minimalist boot sector and initial kernel bootstrap for ARM Cortex-M microcontrollers to handle vector table offset mapping 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 <span style="font-size: 0.8rem; font-weight: 400; color: var(--text-muted);">[v1.0.2 • #docker-traefik]</span></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 dynamically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="pane-footer">
|
||||
<span>Sample 3: Companion Layout</span>
|
||||
<span>© 2026 River Rooks</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>
|
||||
@@ -1,81 +0,0 @@
|
||||
# Project Brief: Technical Web Architecture & Blog
|
||||
|
||||
**Project Type:** Personal Blog & Technical Documentation Site
|
||||
**Design Philosophy:** High-signal, low-noise. Developer-centric minimalism.
|
||||
**Vibe/Comparisons:** Zed Editor, Ghostty, Noctalia.dev.
|
||||
**Strictly Avoid:** Raycast, Astro, Vercel-style "SaaS" aesthetics.
|
||||
|
||||
## 1. Executive Summary
|
||||
This project requires a web interface that feels like a native development environment or terminal emulator, not a marketing page. The design must prioritize raw performance, keyboard navigability, and information density. It should evoke the craftsmanship of tiling window managers and modal text editors.
|
||||
|
||||
The content will heavily feature deep technical writing, including CTF write-ups, network routing diagrams, Docker/Traefik architectures, and raw code snippets (Lua, POSIX `sh`, and Python). The design must accommodate high-density information without visual clutter.
|
||||
|
||||
---
|
||||
|
||||
## 2. Global Rules & Anti-Patterns
|
||||
|
||||
To achieve the correct aesthetic, the designer must adhere to strict constraints.
|
||||
|
||||
> **The "Do Not Use" List**
|
||||
> * **NO Glassmorphism:** Zero use of `backdrop-filter: blur()`, frosted glass, or translucent overlays.
|
||||
> * **NO Glows or Drop Shadows:** Elements should not float, glow, or cast shadows. Depth is created through crisp 1px borders and distinct surface colors.
|
||||
> * **NO Gradients:** Avoid sweeping linear or radial gradients. Stick to solid colors.
|
||||
> * **NO Spring Animations:** Avoid floaty, delayed transitions.
|
||||
> * **NO Rounded Blobs:** Keep border-radii minimal (0px to 4px max). Sharp corners are preferred.
|
||||
|
||||
---
|
||||
|
||||
## 3. Typography System
|
||||
|
||||
The typographical hierarchy is a hybrid system utilizing monospace fonts for structural elements and a clean sans-serif for long-form reading.
|
||||
|
||||
| Usage | Font Family Preference | Weight | Role |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Structural / UI** | IBM Plex Mono, JetBrains Mono | Regular (400), Medium (500) | Navigation links, metadata, dates, tags, timestamps, buttons. |
|
||||
| **Code Blocks** | JetBrains Mono, Fira Code | Regular (400) | Inline code snippets, terminal command blocks, scripts. |
|
||||
| **Body Prose** | Inter, Geist Sans, Roboto | Regular (400) | Standard article paragraphs, lists. |
|
||||
| **Headings (H1-H3)**| IBM Plex Mono (or Sans variant) | Bold (700) | Article titles, section headers. |
|
||||
|
||||
**Typographical Constraints:**
|
||||
* **Line Height:** `1.6` to `1.7` for prose.
|
||||
* **Max Width:** Cap article text at `65ch` (characters) for optimal readability.
|
||||
|
||||
---
|
||||
|
||||
## 4. Color Palette (Dark Mode Native)
|
||||
|
||||
The site does not need a light mode. It should be built as a dark-mode native experience using a muted, low-saturation charcoal palette, mimicking a modern terminal or IDE.
|
||||
|
||||
* **Base Canvas (`#121417` or `#0d0f12`):** A deep slate/charcoal. Not pure black (`#000000`), which causes eye strain.
|
||||
* **Surface / Card (`#1c1f26`):** Slightly lighter than the canvas for article blocks or terminal windows.
|
||||
* **Borders & Dividers (`#2b303b`):** Subtle, low-contrast 1px solid lines used to define grids and separate content.
|
||||
* **Primary Text (`#e0e6f0`):** Off-white/light ash for prose.
|
||||
* **Muted Text (`#8a95a5`):** For timestamps, metadata, and non-essential UI text.
|
||||
* **Accent Color (Designer's Choice):** One single, vibrant terminal color (e.g., ANSI Green, Amber, or Cyan) used sparingly for active states, link hovers, or terminal prompts (like `>`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Layout & Component Architecture
|
||||
|
||||
The layout should be heavily grid-based, feeling structural and deliberate.
|
||||
|
||||
### Core Components Needed
|
||||
1. **Terminal-Style Code Blocks:** Must support syntax highlighting. Should include a simple header (e.g., showing the filename like `config.lua` or `docker-compose.yml`) with a 1px border.
|
||||
2. **Metadata Tags:** Small, monospaced inline blocks for categorizing posts (e.g., `[#homelab]`, `[#ctf]`, `[#hyprland]`).
|
||||
3. **Keyboard Shortcut Hints:** Visual styling for `<kbd>` elements, rendering as crisp, bordered boxes (e.g., `Ctrl` + `K`).
|
||||
4. **Index/Archive List:** A dense, tabular list for older posts, prioritized by date, title, and tags, rather than large image thumbnails.
|
||||
|
||||
### Spacing & Grid
|
||||
* Use a rigid 4pt or 8pt baseline grid.
|
||||
* Padding inside code blocks and cards should be dense but mathematically consistent.
|
||||
* Full-bleed horizontal dividers (`<hr>`) should be used to separate major sections instead of large margins.
|
||||
|
||||
---
|
||||
|
||||
## 6. Interaction & Motion
|
||||
|
||||
Interactivity should mirror command-line interfaces: fast, binary, and unembellished.
|
||||
|
||||
* **Hover States:** Immediate state changes. Instead of fading a button's background over `300ms`, snap the background color instantly or invert the text/background colors.
|
||||
* **Focus States:** Explicit, high-visibility 1px or 2px solid outlines (using the accent color) when navigating via the `Tab` key. Accessibility and keyboard navigation are paramount.
|
||||
* **Active Elements:** Use structural indicators (like a `>` prepended to an active navigation link, or a solid left-border block) rather than changing font sizes or scaling elements up.
|
||||
@@ -1,609 +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>
|
||||
<a href="homelab.html" class="nav-link">Sample 6</a>
|
||||
<a href="public-services.html" class="nav-link">Sample 7</a>
|
||||
<a href="sample-dashboard.html" class="nav-link">Sample 8</a>
|
||||
<a href="sample-command-center.html" class="nav-link">Sample 9</a>
|
||||
<a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10</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">River Rooks // Systems & Architecture</h1>
|
||||
<p class="doc-subtitle">
|
||||
A high-density technical reading flow. Pinned left pane contains an automated terminal tracker daemon observing scroll depth across system modules.
|
||||
</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 River Rooks</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>
|
||||
@@ -1,871 +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>
|
||||
<a href="public-services.html" class="nav-link">Sample 7</a>
|
||||
<a href="sample-dashboard.html" class="nav-link">Sample 8</a>
|
||||
<a href="sample-command-center.html" class="nav-link">Sample 9</a>
|
||||
<a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10</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 & 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 & Network</button>
|
||||
<button class="filter-btn" data-filter="compute" role="tab">Compute & Storage</button>
|
||||
<button class="filter-btn" data-filter="media" role="tab">Media & Automation</button>
|
||||
<button class="filter-btn" data-filter="monitoring" role="tab">Monitoring & 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 & 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 & 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 & 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 Core i5-12400 (6C/12T) | 64GB ECC | 6x 16TB RaidZ2</td>
|
||||
<td><span class="metric-bar">[██░░░░░░░░]</span> 14%</td>
|
||||
<td><span class="metric-bar">[████████░░]</span> 82%</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 & 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 / Dynamic Docker)
|
||||
└───────────┬─────────────┘
|
||||
│
|
||||
┌─────────┴───────────────────────┬─────────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────────────────┐ ┌───────────────────────┐ ┌─────────────────────────┐
|
||||
│ VLAN 10: Management │ │ VLAN 20: Services │ │ VLAN 30: Storage Pool │
|
||||
│ • Proxmox VE (8006) │ │ • Grafana (3000) │ │ • TrueNAS ZFS (SMB/NFS) │
|
||||
│ • AdGuard Home (53) │ │ • Jellyfin Media │ │ • Forgejo Git (3000) │
|
||||
└─────────────────────────┘ └───────────────────────┘ └─────────────────────────┘
|
||||
</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://git.rooks.dev" class="footer-link">Git Forge</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="mailto:river@rooks.dev" class="footer-link">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<span style="font-size:0.78rem; color:var(--text-muted);">CLUSTER ONLINE • 48TB ZFS POOL • 10GbE SFP+ BACKBONE</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>
|
||||
@@ -1,497 +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">River Rooks</div>
|
||||
<div class="profile-subtitle">dev-blog@system:~$</div>
|
||||
|
||||
<nav>
|
||||
<ul class="nav-list">
|
||||
<li class="nav-item"><a href="index.html" class="nav-link">Catalog</a></li>
|
||||
<li class="nav-item"><a href="homepage.html" class="nav-link active">Sample 1 (Home)</a></li>
|
||||
<li class="nav-item"><a href="split-panel.html" class="nav-link">Sample 2 (Split)</a></li>
|
||||
<li class="nav-item"><a href="companion.html" class="nav-link">Sample 3 (Daemon)</a></li>
|
||||
<li class="nav-item"><a href="document-flow.html" class="nav-link">Sample 4 (Doc Flow)</a></li>
|
||||
<li class="nav-item"><a href="portfolio.html" class="nav-link">Sample 5 (Portfolio)</a></li>
|
||||
<li class="nav-item"><a href="homelab.html" class="nav-link">Sample 6 (Homelab)</a></li>
|
||||
<li class="nav-item"><a href="public-services.html" class="nav-link">Sample 7 (Services)</a></li>
|
||||
<li class="nav-item"><a href="sample-dashboard.html" class="nav-link">Sample 8 (Dashboard)</a></li>
|
||||
<li class="nav-item"><a href="sample-command-center.html" class="nav-link">Sample 9 (Command)</a></li>
|
||||
<li class="nav-item"><a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10 (Explorer)</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Keybindings Panel Widget -->
|
||||
<div class="keybindings-panel">
|
||||
<div class="keybindings-title">Active Stack</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted); line-height: 1.6;">
|
||||
[#network-forensics]<br>
|
||||
[#docker-traefik]<br>
|
||||
[#arch-linux] [#neovim]<br>
|
||||
[#wireguard] [#posix-sh]
|
||||
</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>
|
||||
<a href="#" class="tag">[#bus-pirate]</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);">[#posix-sh]</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>
|
||||
<tr onclick="location.href='#'">
|
||||
<td class="post-date">2026-06-14</td>
|
||||
<td><a href="#">Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a></td>
|
||||
<td><span style="color: var(--accent);">[#networking]</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer>
|
||||
<span>© 2026 River Rooks</span>
|
||||
<span>Aligned with Canonical-Sample-Content.md</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,910 +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 | Design Spec Samples Framework</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;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Top Master Navigation Bar */
|
||||
.master-header {
|
||||
background-color: var(--bg-surface);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.header-top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--accent-cyan);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.header-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Samples Selector Pill Bar */
|
||||
.samples-nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border-color) transparent;
|
||||
}
|
||||
|
||||
.sample-btn {
|
||||
background-color: var(--bg-canvas);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 6px 12px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
transition: none; /* Instant snap */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.sample-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.sample-btn.active {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
border-color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-badge {
|
||||
font-size: 0.68rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Sub-bar / Viewport Controls */
|
||||
.viewport-bar {
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 6px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.current-url-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.prompt-tag {
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.frame-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ctrl-btn {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
padding: 3px 8px;
|
||||
font-family: inherit;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: none;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.ctrl-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.ctrl-btn.active {
|
||||
color: var(--accent-cyan);
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
/* Main Workspace / Viewport */
|
||||
.workspace-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background-color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.frame-container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: calc(100vh - 165px);
|
||||
min-height: 560px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.frame-header {
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 6px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.window-dots {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.window-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background-color: #0d0f12;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Markdown Reader Mode Fallback Container */
|
||||
.markdown-viewer {
|
||||
display: none;
|
||||
padding: 32px 24px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
background-color: var(--bg-surface);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.markdown-prose {
|
||||
max-width: 75ch;
|
||||
margin: 0 auto;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown-prose h1, .markdown-prose h2, .markdown-prose h3 {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
color: var(--text-primary);
|
||||
margin: 24px 0 12px 0;
|
||||
}
|
||||
|
||||
.markdown-prose pre {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 14px;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.82rem;
|
||||
overflow-x: auto;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.markdown-prose code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.markdown-prose table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 20px 0;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.markdown-prose th, .markdown-prose td {
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown-prose th {
|
||||
background-color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
/* Spec Catalog Grid Drawer */
|
||||
.catalog-drawer {
|
||||
display: none;
|
||||
padding: 24px 16px 40px 16px;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card:hover .card-title {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
font-family: 'JetBrains 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;
|
||||
}
|
||||
|
||||
/* Bottom Hotkey Help Footer */
|
||||
.master-footer {
|
||||
background-color: var(--bg-surface);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 8px 20px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
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 4px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Top Master Navigation Bar with Full Sample Options -->
|
||||
<header class="master-header">
|
||||
<div class="header-top-row">
|
||||
<a href="index.html" class="brand-title">
|
||||
<span class="status-dot"></span>
|
||||
<span>RIVER ROOKS // SAMPLES VIEWER</span>
|
||||
</a>
|
||||
|
||||
<div class="header-tools">
|
||||
<span>Hotkeys: <kbd>1</kbd>-<kbd>9</kbd>, <kbd>0</kbd></span>
|
||||
<button class="ctrl-btn" id="btn-toggle-mode" onclick="toggleViewMode()">View: Frame Preview</button>
|
||||
<a href="../index.html" class="ctrl-btn">Root Lander ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options Bar to Select Each Sample -->
|
||||
<nav class="samples-nav-bar" id="sampleOptionsBar" aria-label="Sample Selection Options">
|
||||
<button class="sample-btn active" data-file="homepage.html" onclick="loadSample('homepage.html', this)">
|
||||
<span>[1] Homepage</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="split-panel.html" onclick="loadSample('split-panel.html', this)">
|
||||
<span>[2] Split Panel</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="companion.html" onclick="loadSample('companion.html', this)">
|
||||
<span>[3] Companion</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="document-flow.html" onclick="loadSample('document-flow.html', this)">
|
||||
<span>[4] Doc Flow</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="portfolio.html" onclick="loadSample('portfolio.html', this)">
|
||||
<span>[5] Portfolio</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="homelab.html" onclick="loadSample('homelab.html', this)">
|
||||
<span>[6] Homelab</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="public-services.html" onclick="loadSample('public-services.html', this)">
|
||||
<span>[7] Services</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="sample-dashboard.html" onclick="loadSample('sample-dashboard.html', this)">
|
||||
<span>[8] Dashboard</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="sample-command-center.html" onclick="loadSample('sample-command-center.html', this)">
|
||||
<span>[9] Command</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="sample-dual-pane-explorer.html" onclick="loadSample('sample-dual-pane-explorer.html', this)">
|
||||
<span>[0] Explorer</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="blog-lander-stream.html" onclick="loadSample('blog-lander-stream.html', this)">
|
||||
<span>[Lander 1] Stream</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="blog-lander-terminal.html" onclick="loadSample('blog-lander-terminal.html', this)">
|
||||
<span>[Lander 2] Terminal</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="blog-lander-cards.html" onclick="loadSample('blog-lander-cards.html', this)">
|
||||
<span>[Lander 3] Cards</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="service-gateway-manifesto.html" onclick="loadSample('service-gateway-manifesto.html', this)">
|
||||
<span>[Gateway 1] Manifesto</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="service-gateway-terminal.html" onclick="loadSample('service-gateway-terminal.html', this)">
|
||||
<span>[Gateway 2] Host Console</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="service-gateway-tiling.html" onclick="loadSample('service-gateway-tiling.html', this)">
|
||||
<span>[Gateway 3] Tiling Hub</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="blog-tiling-lead.html" onclick="loadSample('blog-tiling-lead.html', this)">
|
||||
<span>[Tiling Blog 1] Lead</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="blog-tiling-split.html" onclick="loadSample('blog-tiling-split.html', this)">
|
||||
<span>[Tiling Blog 2] Workspace</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="blog-tiling-catalog.html" onclick="loadSample('blog-tiling-catalog.html', this)">
|
||||
<span>[Tiling Blog 3] Dual-Lead</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="article-tiling-firmware.html" onclick="loadSample('article-tiling-firmware.html', this)">
|
||||
<span>[Article 1] Firmware</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="article-tiling-traefik.html" onclick="loadSample('article-tiling-traefik.html', this)">
|
||||
<span>[Article 2] Traefik</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="article-tiling-heap.html" onclick="loadSample('article-tiling-heap.html', this)">
|
||||
<span>[Article 3] Heap CTF</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="canonical-sample-content.md" onclick="loadSample('canonical-sample-content.md', this)">
|
||||
<span>[Dataset] Content Spec</span>
|
||||
</button>
|
||||
<button class="sample-btn" data-file="unified-design-specification.md" onclick="loadSample('unified-design-specification.md', this)">
|
||||
<span>[Spec] Master Brief</span>
|
||||
</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Viewport & Iframe Tool Control Bar -->
|
||||
<div class="viewport-bar">
|
||||
<div class="current-url-display">
|
||||
<span class="prompt-tag">$ agy --view=</span>
|
||||
<span id="activeFilePath">homepage.html</span>
|
||||
<span id="activeFileDesc" style="color: var(--text-muted); font-size: 0.72rem; margin-left: 6px;">[Sample 1: Developer Homepage]</span>
|
||||
</div>
|
||||
|
||||
<div class="frame-controls">
|
||||
<!-- Responsive Width Selector -->
|
||||
<span style="color: var(--text-muted); margin-right: 4px;">Viewport:</span>
|
||||
<button class="ctrl-btn active" id="vp-full" onclick="setViewport('100%', this)">100%</button>
|
||||
<button class="ctrl-btn" id="vp-tablet" onclick="setViewport('768px', this)">768px</button>
|
||||
<button class="ctrl-btn" id="vp-mobile" onclick="setViewport('380px', this)">380px</button>
|
||||
<span style="color: var(--border-color);">|</span>
|
||||
<!-- Direct Link Actions -->
|
||||
<button class="ctrl-btn" onclick="reloadIframe()" title="Reload current sample">⟳ Reload</button>
|
||||
<a href="homepage.html" id="openTabBtn" target="_blank" class="ctrl-btn" title="Open directly in new tab">Open Tab ↗</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workspace: Live Iframe Container -->
|
||||
<main class="workspace-wrapper" id="frameWrapper">
|
||||
<div class="frame-container" id="frameContainer">
|
||||
<div class="frame-header">
|
||||
<div class="window-dots">
|
||||
<span class="window-dot"></span>
|
||||
<span class="window-dot"></span>
|
||||
<span class="window-dot"></span>
|
||||
</div>
|
||||
<div id="frameStatusText">SANDBOX IFRAME READY • ISOLATED WORKSPACE</div>
|
||||
<div>STDOUT [200 OK]</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Interactive Iframe -->
|
||||
<iframe src="homepage.html" id="sampleIframe" class="preview-iframe" title="Sample Live Viewport"></iframe>
|
||||
|
||||
<!-- Markdown Viewer Fallback (for .md files) -->
|
||||
<div id="markdownViewer" class="markdown-viewer">
|
||||
<div class="markdown-prose" id="markdownContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Spec Catalog Grid Drawer (Alternate View Mode) -->
|
||||
<section class="catalog-drawer" id="catalogDrawer" aria-label="Catalog Grid View">
|
||||
<div style="display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 12px;">
|
||||
<h2 style="font-family: 'IBM Plex Mono', monospace; font-size: 1.2rem; color: var(--text-primary);">Design Spec Samples Catalog</h2>
|
||||
<span style="font-family: 'IBM Plex Mono', monospace; font-size: 0.8rem; color: var(--text-muted);">27 Artifacts Registered</span>
|
||||
</div>
|
||||
<div class="drawer-grid" id="drawerGrid"></div>
|
||||
</section>
|
||||
|
||||
<!-- Bottom Global Status Footer -->
|
||||
<footer class="master-footer">
|
||||
<div>Unified Design Language • Dark Slate Native (#0d0f12)</div>
|
||||
<div>Press <kbd>1</kbd>-<kbd>0</kbd> to quick-switch • Canonical Dataset Compliant</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const catalogData = [
|
||||
{
|
||||
key: '1',
|
||||
file: 'homepage.html',
|
||||
title: 'Sample 1: Developer Homepage',
|
||||
type: 'html',
|
||||
desc: 'Dark-mode developer homepage with sidebar navigation, active stack cluster, featured firmware SPI extraction write-up, and dense archive table.'
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
file: 'split-panel.html',
|
||||
title: 'Sample 2: Split Panel Layout',
|
||||
type: 'html',
|
||||
desc: 'Fixed left terminal mascot with binary animation frames, and scrollable right panel showcasing the 5 canonical system modules.'
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
file: 'companion.html',
|
||||
title: 'Sample 3: Companion Layout',
|
||||
type: 'html',
|
||||
desc: '50/50 vertical split layout with system module cards on the left and a reactive ASCII companion daemon (sys-daemon) on the right.'
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
file: 'document-flow.html',
|
||||
title: 'Sample 4: Continuous Document Flow',
|
||||
type: 'html',
|
||||
desc: 'Continuous reading stream across system modules with pinned IntersectionObserver scroll-tracker daemon observing scroll depth.'
|
||||
},
|
||||
{
|
||||
key: '5',
|
||||
file: 'portfolio.html',
|
||||
title: 'Sample 5: River Rooks Portfolio',
|
||||
type: 'html',
|
||||
desc: 'Single-column digital business card landing page with stdout terminal status block, 11-item active stack tags, and cryptographic links.'
|
||||
},
|
||||
{
|
||||
key: '6',
|
||||
file: 'homelab.html',
|
||||
title: 'Sample 6: Homelab Lander',
|
||||
type: 'html',
|
||||
desc: 'Infrastructure catalog with category filter tabs, 12 microservices, 3 bare-metal server specs, and ASCII network topology routing map.'
|
||||
},
|
||||
{
|
||||
key: '7',
|
||||
file: 'public-services.html',
|
||||
title: 'Sample 7: Public Services Portal',
|
||||
type: 'html',
|
||||
desc: 'Launcher portal for self-hosted web applications with real-time keyword filter, category pills, access levels, and PGP/SSH identity verification box.'
|
||||
},
|
||||
{
|
||||
key: '8',
|
||||
file: 'sample-dashboard.html',
|
||||
title: 'Sample 8: 3-Tile Workspace Dashboard',
|
||||
type: 'html',
|
||||
desc: 'Neovim/Tmux 3-tile modal workspace featuring live services in left tile, River Rooks resume/bio in center, and 5-post blog archive in right tile.'
|
||||
},
|
||||
{
|
||||
key: '9',
|
||||
file: 'sample-command-center.html',
|
||||
title: 'Sample 9: Tabbed Command Center',
|
||||
type: 'html',
|
||||
desc: 'Zed Editor / Ghostty tabbed utility layout with keyboard shortcuts [1], [2], [3] switching between Services, Blog Archive, and Resume Config.'
|
||||
},
|
||||
{
|
||||
key: '0',
|
||||
file: 'sample-dual-pane-explorer.html',
|
||||
title: 'Sample 10: Dual-Pane Helix Tree Explorer',
|
||||
type: 'html',
|
||||
desc: 'Split-screen file-tree explorer with interactive document viewer and sys-daemon mascot reacting to folder item selection.'
|
||||
},
|
||||
{
|
||||
key: 'l1',
|
||||
file: 'blog-lander-stream.html',
|
||||
title: 'Blog Lander 1: Minimalist Stream',
|
||||
type: 'html',
|
||||
desc: 'Single-column 65ch linear reading stream with topic filter pills, canonical article abstracts, tag clusters, and PGP/RSS links.'
|
||||
},
|
||||
{
|
||||
key: 'l2',
|
||||
file: 'blog-lander-terminal.html',
|
||||
title: 'Blog Lander 2: Terminal Directory Index',
|
||||
type: 'html',
|
||||
desc: 'High-density POSIX tabular directory index mimicking ls -la with real-time grep filter and inline code inspection drawer.'
|
||||
},
|
||||
{
|
||||
key: 'l3',
|
||||
file: 'blog-lander-cards.html',
|
||||
title: 'Blog Lander 3: Curated Dispatch & Cards',
|
||||
type: 'html',
|
||||
desc: 'Structured blog lander with a pinned lead article card featuring code snippet preview, and a 2-column grid of recent technical dispatches.'
|
||||
},
|
||||
{
|
||||
key: 'g1',
|
||||
file: 'service-gateway-manifesto.html',
|
||||
title: 'Gateway Lander 1: Manifesto & System Hub',
|
||||
type: 'html',
|
||||
desc: 'Gateway portal where the homepage manifesto is the central focus, routing visitors to homelab services (with the blog highlighted) and recent dispatches.'
|
||||
},
|
||||
{
|
||||
key: 'g2',
|
||||
file: 'service-gateway-terminal.html',
|
||||
title: 'Gateway Lander 2: Host Console & MOTD Router',
|
||||
type: 'html',
|
||||
desc: 'Terminal-style MOTD gateway centering on system landing prose, an interactive service routing table featuring the blog, and live dispatches log.'
|
||||
},
|
||||
{
|
||||
key: 'g3',
|
||||
file: 'service-gateway-tiling.html',
|
||||
title: 'Gateway Lander 3: Tiling Workspace Hub',
|
||||
type: 'html',
|
||||
desc: 'Tiling window manager aesthetic with a prominent hero landing tile, a dedicated core blog feature tile, homelab services grid, and node telemetry.'
|
||||
},
|
||||
{
|
||||
key: 'tb1',
|
||||
file: 'blog-tiling-lead.html',
|
||||
title: 'Tiling Blog 1: Hero Dispatch & Dual Stream',
|
||||
type: 'html',
|
||||
desc: 'Gateway 3 tiling aesthetic applied to the blog, featuring a pinned lead write-up with code preview, research tracks, and full-width chronological feed.'
|
||||
},
|
||||
{
|
||||
key: 'tb2',
|
||||
file: 'blog-tiling-split.html',
|
||||
title: 'Tiling Blog 2: Asymmetric 3-Tile Workspace',
|
||||
type: 'html',
|
||||
desc: 'Asymmetric 3-tile blog workspace with interactive live grep filter, embedded code blocks, curated tracks, and security wire.'
|
||||
},
|
||||
{
|
||||
key: 'tb3',
|
||||
file: 'blog-tiling-catalog.html',
|
||||
title: 'Tiling Blog 3: Dual-Lead Grid & Inspection Feed',
|
||||
type: 'html',
|
||||
desc: 'Dual-lead side-by-side featured article tiles with code snippets, plus an archive feed table with an interactive inline note inspector.'
|
||||
},
|
||||
{
|
||||
key: 'a1',
|
||||
file: 'article-tiling-firmware.html',
|
||||
title: 'Article 1: IoT Firmware Reverse Engineering',
|
||||
type: 'html',
|
||||
desc: 'Article reading template featuring left sidebar TOC, collapsible related dispatches drawer, SOP8 pinout matrix, flashrom code blocks with copy utility, alerts, and task lists.'
|
||||
},
|
||||
{
|
||||
key: 'a2',
|
||||
file: 'article-tiling-traefik.html',
|
||||
title: 'Article 2: High-Availability Traefik Router',
|
||||
type: 'html',
|
||||
desc: 'Article reading template featuring right sidebar TOC, collapsible homelab series drawer, ASCII network routing diagram, YAML compose code blocks, and configuration matrix.'
|
||||
},
|
||||
{
|
||||
key: 'a3',
|
||||
file: 'article-tiling-heap.html',
|
||||
title: 'Article 3: CTF Heap Exploitation in glibc 2.35',
|
||||
type: 'html',
|
||||
desc: 'Article reading template featuring left sidebar TOC, collapsible security drawer, safe-linking formula box, GDB stdout terminal blocks, C structs, and mitigation history table.'
|
||||
},
|
||||
{
|
||||
key: 'd',
|
||||
file: 'canonical-sample-content.md',
|
||||
title: 'Canonical Content Dataset (Single Source of Truth)',
|
||||
type: 'md',
|
||||
desc: 'Master markdown dataset specifying canonical persona profile, services catalog, blog articles, engineering modules, and telemetry nodes.'
|
||||
},
|
||||
{
|
||||
key: 'u',
|
||||
file: 'unified-design-specification.md',
|
||||
title: 'Unified Design Specification (Master Brief)',
|
||||
type: 'md',
|
||||
desc: 'Master specification uniting design language across homelab, technical notes, and portfolio.'
|
||||
},
|
||||
{
|
||||
key: 's',
|
||||
file: 'design-specification.md',
|
||||
title: 'Design Specification (Primary Technical Brief)',
|
||||
type: 'md',
|
||||
desc: 'Primary specification document detailing technical web architecture guidelines, typography rules, color tokens, and anti-patterns.'
|
||||
},
|
||||
{
|
||||
key: 'p',
|
||||
file: 'portfolio-spec.md',
|
||||
title: 'Portfolio Specification (Landing Page Brief)',
|
||||
type: 'md',
|
||||
desc: 'Specification sheet for the single-page under-construction portfolio card layout.'
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
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 currentFile = 'homepage.html';
|
||||
let currentMode = 'frame'; // 'frame' or 'grid'
|
||||
|
||||
function loadSample(fileName, btnElement) {
|
||||
currentFile = fileName;
|
||||
|
||||
// Update active state in top options bar
|
||||
document.querySelectorAll('#sampleOptionsBar .sample-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.getAttribute('data-file') === fileName) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update active file label and direct open link
|
||||
document.getElementById('activeFilePath').innerText = fileName;
|
||||
document.getElementById('openTabBtn').href = fileName;
|
||||
|
||||
const itemInfo = catalogData.find(item => item.file === fileName);
|
||||
if (itemInfo) {
|
||||
document.getElementById('activeFileDesc').innerText = `[${itemInfo.title}]`;
|
||||
}
|
||||
|
||||
const iframe = document.getElementById('sampleIframe');
|
||||
const mdViewer = document.getElementById('markdownViewer');
|
||||
|
||||
if (fileName.endsWith('.html')) {
|
||||
mdViewer.style.display = 'none';
|
||||
iframe.style.display = 'block';
|
||||
iframe.src = fileName;
|
||||
} else {
|
||||
// Render Markdown file via simple fetch & format
|
||||
iframe.style.display = 'none';
|
||||
mdViewer.style.display = 'block';
|
||||
renderMarkdownFile(fileName);
|
||||
}
|
||||
|
||||
// If currently in grid mode, switch back to frame preview mode
|
||||
if (currentMode === 'grid') {
|
||||
toggleViewMode();
|
||||
}
|
||||
}
|
||||
|
||||
async function renderMarkdownFile(fileName) {
|
||||
const mdContent = document.getElementById('markdownContent');
|
||||
mdContent.innerHTML = `<div style="font-family: monospace; color: var(--text-muted);">Fetching and parsing ${fileName}...</div>`;
|
||||
|
||||
try {
|
||||
const response = await fetch(fileName);
|
||||
if (!response.ok) throw new Error('File fetch failed');
|
||||
const text = await response.text();
|
||||
|
||||
// Simple high-readability markdown format converter
|
||||
let html = text
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^\> (.*$)/gim, '<blockquote>$1</blockquote>')
|
||||
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
|
||||
.replace(/\*(.*)\*/gim, '<em>$1</em>')
|
||||
.replace(/```([\s\S]*?)```/gim, '<pre><code>$1</code></pre>')
|
||||
.replace(/`([^`]+)`/gim, '<code>$1</code>')
|
||||
.replace(/\n\n/gim, '<br><br>');
|
||||
|
||||
mdContent.innerHTML = html;
|
||||
} catch (e) {
|
||||
mdContent.innerHTML = `<div style="color: var(--accent-amber); font-family: monospace;">Failed to load markdown directly in browser. <a href="${fileName}" target="_blank" style="color: var(--accent-cyan);">Click here to view raw file</a></div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function setViewport(width, btnElement) {
|
||||
const container = document.getElementById('frameContainer');
|
||||
container.style.maxWidth = width;
|
||||
|
||||
document.querySelectorAll('.frame-controls .ctrl-btn').forEach(btn => {
|
||||
if (btn.id && btn.id.startsWith('vp-')) {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
if (btnElement) btnElement.classList.add('active');
|
||||
}
|
||||
|
||||
function reloadIframe() {
|
||||
const iframe = document.getElementById('sampleIframe');
|
||||
if (iframe.style.display !== 'none') {
|
||||
iframe.src = iframe.src;
|
||||
} else {
|
||||
renderMarkdownFile(currentFile);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleViewMode() {
|
||||
const frameWrapper = document.getElementById('frameWrapper');
|
||||
const catalogDrawer = document.getElementById('catalogDrawer');
|
||||
const modeBtn = document.getElementById('btn-toggle-mode');
|
||||
|
||||
if (currentMode === 'frame') {
|
||||
currentMode = 'grid';
|
||||
frameWrapper.style.display = 'none';
|
||||
catalogDrawer.style.display = 'block';
|
||||
modeBtn.innerText = 'View: Grid Catalog';
|
||||
renderDrawerGrid();
|
||||
} else {
|
||||
currentMode = 'frame';
|
||||
frameWrapper.style.display = 'flex';
|
||||
catalogDrawer.style.display = 'none';
|
||||
modeBtn.innerText = 'View: Frame Preview';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDrawerGrid() {
|
||||
const grid = document.getElementById('drawerGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
catalogData.forEach(item => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.onclick = () => loadSample(item.file, null);
|
||||
|
||||
card.innerHTML = `
|
||||
<div>
|
||||
<div class="card-title">${item.title}</div>
|
||||
<div class="card-desc">${item.desc}</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<span>${item.file}</span>
|
||||
<span style="color: var(--accent-cyan);">[LOAD FRAME ↗]</span>
|
||||
</div>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts (1-9, 0)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// If user is not typing in an input
|
||||
if (['input', 'textarea'].includes(document.activeElement.tagName.toLowerCase())) return;
|
||||
|
||||
const keyMap = {
|
||||
'1': 'homepage.html',
|
||||
'2': 'split-panel.html',
|
||||
'3': 'companion.html',
|
||||
'4': 'document-flow.html',
|
||||
'5': 'portfolio.html',
|
||||
'6': 'homelab.html',
|
||||
'7': 'public-services.html',
|
||||
'8': 'sample-dashboard.html',
|
||||
'9': 'sample-command-center.html',
|
||||
'0': 'sample-dual-pane-explorer.html',
|
||||
'b': 'blog-lander-stream.html',
|
||||
't': 'blog-lander-terminal.html',
|
||||
'c': 'blog-lander-cards.html'
|
||||
};
|
||||
|
||||
if (keyMap[e.key]) {
|
||||
loadSample(keyMap[e.key], null);
|
||||
}
|
||||
});
|
||||
|
||||
// Check URL parameters on initialization
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const requestedSample = params.get('sample') || params.get('view');
|
||||
|
||||
if (requestedSample) {
|
||||
const match = catalogData.find(item => item.file === requestedSample || item.key === requestedSample);
|
||||
if (match) {
|
||||
loadSample(match.file, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Default load Sample 1
|
||||
loadSample('homepage.html', null);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -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.
|
||||
@@ -1,369 +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>
|
||||
<a href="homelab.html" class="nav-link">Sample 6</a>
|
||||
<a href="public-services.html" class="nav-link">Sample 7</a>
|
||||
<a href="sample-dashboard.html" class="nav-link">Sample 8</a>
|
||||
<a href="sample-command-center.html" class="nav-link">Sample 9</a>
|
||||
<a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10</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 & 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">
|
||||
Provisioning 10GbE homelab cluster. Routing traffic through Traefik v3 edge proxy...<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, systems architecture, and low-level Unix tooling.
|
||||
</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>
|
||||
<span class="tag">[#golang]</span>
|
||||
<span class="tag">[#rust]</span>
|
||||
<span class="tag">[#proxmox]</span>
|
||||
<span class="tag">[#truenas]</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- E. Footer (Contact & Links) -->
|
||||
<footer class="footer-links">
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link">Git Forge</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@rooks.dev" class="footer-link">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="#" title="Fingerprint: 0x4A8E92F1C0B3D4E7" class="footer-link">PGP: 0x4A8E92F1...</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="index.html" class="footer-link">Spec Catalog</a>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,902 +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>
|
||||
<a href="sample-dashboard.html" class="nav-link">Sample 8</a>
|
||||
<a href="sample-command-center.html" class="nav-link">Sample 9</a>
|
||||
<a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10</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 & Tools</button>
|
||||
<button class="filter-btn" data-filter="media" role="tab">Media & Content</button>
|
||||
<button class="filter-btn" data-filter="security" role="tab">Identity & 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_&_verification</h2>
|
||||
<span class="section-meta">Cryptographic Identity</span>
|
||||
</div>
|
||||
|
||||
<div class="identity-box">
|
||||
<div class="identity-title">// Public Endpoints & Cryptographic Fingerprints</div>
|
||||
|
||||
<div class="key-row">
|
||||
<div>
|
||||
<span class="key-label">PGP Key ID / Fingerprint:</span><br>
|
||||
<span class="key-value">0x4A8E92F1C0B3D4E7 • 3F8A B29C 7D1E 4056 9912 A8B0 C4D3 E2F1 8091 22A4</span>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('0x4A8E92F1C0B3D4E7')">Copy Key ID</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 Digest:</span><br>
|
||||
<span class="key-value">SHA256:7uK+N04wP5rL/9z3Q8s1vX4dF6mK2pT8yB1wN0qZ8 (river@rooks.dev)</span>
|
||||
</div>
|
||||
<button class="copy-btn" onclick="navigator.clipboard.writeText('SHA256:7uK+N04wP5rL/9z3Q8s1vX4dF6mK2pT8yB1wN0qZ8')">Copy Digest</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://git.rooks.dev" class="footer-link">Git Forge</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="mailto:river@rooks.dev" class="footer-link">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<span style="font-size:0.78rem; color:var(--text-muted);">PUBLIC SERVICES PORTAL • RIVER ROOKS</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>
|
||||
@@ -1,521 +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>
|
||||
<a href="index.html" style="color: var(--text-primary); text-decoration: none;">RIVER ROOKS // COMMAND CENTER</a>
|
||||
</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>6 CANONICAL SERVICES</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="services-grid">
|
||||
|
||||
<div class="service-box">
|
||||
<div class="service-title">
|
||||
<a href="https://proxy.rooks.dev" target="_blank">Traefik Proxy (v3.1)</a>
|
||||
<span class="badge-status badge-online">ONLINE</span>
|
||||
</div>
|
||||
<p class="service-desc">Edge HTTP reverse proxy with dynamic ACME Let's Encrypt TLS wildcard certificates.</p>
|
||||
<div class="service-stats">
|
||||
<span>PORT: 443/tcp</span>
|
||||
<span>#core</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-box">
|
||||
<div class="service-title">
|
||||
<a href="https://nas.rooks.dev" target="_blank">TrueNAS SCALE</a>
|
||||
<span class="badge-status badge-online">ONLINE</span>
|
||||
</div>
|
||||
<p class="service-desc">Primary storage pool managing 48TB ZFS RaidZ2 array with daily scrub jobs.</p>
|
||||
<div class="service-stats">
|
||||
<span>NFS / SMB</span>
|
||||
<span>#storage</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-box">
|
||||
<div class="service-title">
|
||||
<a href="https://vault.rooks.dev" target="_blank">Vaultwarden Secrets</a>
|
||||
<span class="badge-status badge-online">E2EE</span>
|
||||
</div>
|
||||
<p class="service-desc">Rust implementation of Bitwarden API hosting credentials, SSH keys, & 2FA.</p>
|
||||
<div class="service-stats">
|
||||
<span>2FA / AUTH</span>
|
||||
<span>#secrets</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-box">
|
||||
<div class="service-title">
|
||||
<a href="https://git.rooks.dev" target="_blank">Forgejo Git Forge</a>
|
||||
<span class="badge-status badge-online">PUBLIC</span>
|
||||
</div>
|
||||
<p class="service-desc">Self-hosted Git platform for dotfiles, playbooks, and CTF binary writeups.</p>
|
||||
<div class="service-stats">
|
||||
<span>PORT: 3000</span>
|
||||
<span>#dev-git</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-box">
|
||||
<div class="service-title">
|
||||
<a href="https://chef.rooks.dev" target="_blank">CyberChef Workbench</a>
|
||||
<span class="badge-status badge-online">PUBLIC</span>
|
||||
</div>
|
||||
<p class="service-desc">Hex decoding, Base64 operations, regex extraction, & payload analysis.</p>
|
||||
<div class="service-stats">
|
||||
<span>OPEN WEB</span>
|
||||
<span>#security-tool</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-box">
|
||||
<div class="service-title">
|
||||
<a href="https://pve.rooks.dev" target="_blank">Proxmox VE (v8.2)</a>
|
||||
<span class="badge-status badge-online">ONLINE</span>
|
||||
</div>
|
||||
<p class="service-desc">Hypervisor cluster hosting Debian LXC containers & KVM virtual machines.</p>
|
||||
<div class="service-stats">
|
||||
<span>PORT: 8006</span>
|
||||
<span>#compute</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: 5</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-07-20</td>
|
||||
<td><a href="#" class="post-link">Reverse Engineering IoT Firmware: SPI Flash Extraction</a></td>
|
||||
<td><span class="tag-badge">[#firmware] [#hardware]</span></td>
|
||||
<td>8 min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2026-07-15</td>
|
||||
<td><a href="#" class="post-link">Building a High-Availability Traefik Router with Docker Compose</a></td>
|
||||
<td><span class="tag-badge">[#homelab] [#traefik]</span></td>
|
||||
<td>12 min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2026-07-10</td>
|
||||
<td><a href="#" class="post-link">POSIX sh vs. Bash: Portability in Modern Scripts</a></td>
|
||||
<td><span class="tag-badge">[#posix-sh] [#unix]</span></td>
|
||||
<td>6 min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2026-06-28</td>
|
||||
<td><a href="#" class="post-link">CTF Write-up: Heap Exploitation in glibc 2.35</a></td>
|
||||
<td><span class="tag-badge">[#ctf] [#binary]</span></td>
|
||||
<td>15 min</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2026-06-14</td>
|
||||
<td><a href="#" class="post-link">Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a></td>
|
||||
<td><span class="tag-badge">[#networking] [#wireguard]</span></td>
|
||||
<td>10 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 & Systems Architecture</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 Architecture & Cybersecurity"</span><br>
|
||||
<span class="conf-key">AFFILIATION</span> = <span class="conf-val">"University of Alabama College of Engineering"</span><br>
|
||||
<span class="conf-key">PGP_KEY</span> = <span class="conf-val">"0x4A8E92F1C0B3D4E7"</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">"Bare-Metal Virtualization"</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">"Go"</span>, <span class="conf-val">"Lua"</span>, <span class="conf-val">"ARM Assembly"</span>]<br>
|
||||
<span class="conf-key">STACK</span> = [<span class="conf-val">"Traefik v3"</span>, <span class="conf-val">"WireGuard"</span>, <span class="conf-val">"Proxmox VE"</span>, <span class="conf-val">"TrueNAS"</span>, <span class="conf-val">"Neovim"</span>]
|
||||
</div>
|
||||
|
||||
<h3 style="font-family: 'IBM Plex Mono', monospace; font-size: 0.95rem; margin-bottom: 12px;">System Modules:</h3>
|
||||
<ul style="font-size: 0.88rem; color: var(--text-muted); line-height: 1.8; margin-bottom: 20px; list-style-position: inside;">
|
||||
<li><strong style="color: var(--accent-cyan);">/dev/posix-shell-parser</strong> (v1.2.0 • Lua) — Zero-allocation AST lexer</li>
|
||||
<li><strong style="color: var(--accent-cyan);">/dev/wireguard-mesh</strong> (v0.8.4 • Python) — Full-mesh overlay provisioner</li>
|
||||
<li><strong style="color: var(--accent-cyan);">/dev/zero-alloc-logger</strong> (v2.1.1 • Go) — High-throughput sync.Pool logger</li>
|
||||
<li><strong style="color: var(--accent-cyan);">/dev/kernel-bootstrap</strong> (v0.1.0 • ARM ASM) — Bare-metal boot setup</li>
|
||||
<li><strong style="color: var(--accent-cyan);">/dev/ddns-daemon</strong> (v1.0.2 • Docker API) — Dynamic Cloudflare DNS sync</li>
|
||||
</ul>
|
||||
|
||||
<div style="display: flex; gap: 12px;">
|
||||
<a href="mailto:river@rooks.dev" class="btn-action">Email: river@rooks.dev</a>
|
||||
<a href="https://git.rooks.dev" class="btn-action">Git Repositories ↗</a>
|
||||
<a href="index.html" class="btn-action">Spec Catalog ↗</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>
|
||||
@@ -1,474 +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 style="display: flex; align-items: center; gap: 12px;">
|
||||
<span class="mode-tag">NORMAL</span>
|
||||
<span class="filepath">/home/river/dashboard.workspace</span>
|
||||
<span style="color: var(--border-color);">|</span>
|
||||
<nav style="display: flex; gap: 8px; font-size: 0.78rem;">
|
||||
<a href="index.html" style="color: var(--text-primary); text-decoration: none;">Catalog</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="homelab.html" style="color: var(--text-muted); text-decoration: none;">Homelab</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="public-services.html" style="color: var(--text-muted); text-decoration: none;">Services</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="sample-command-center.html" style="color: var(--text-muted); text-decoration: none;">Command</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="sample-dual-pane-explorer.html" style="color: var(--text-muted); text-decoration: none;">Explorer</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="status-group">
|
||||
<span><span class="status-dot"></span> TRAEFIK: V3.1 OK</span>
|
||||
<span>CLUSTER: 10GbE SFP+</span>
|
||||
<span>UPTIME: 99.98%</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main 3-Tile Dashboard -->
|
||||
<main class="dashboard-grid">
|
||||
|
||||
<!-- Left Tile: Homelab & Public Services Directory -->
|
||||
<section class="tile">
|
||||
<div class="tile-header">
|
||||
<span>/etc/services</span>
|
||||
<span>[CANONICAL SERVICES]</span>
|
||||
</div>
|
||||
<div class="tile-body">
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-head">
|
||||
<a href="https://proxy.rooks.dev" target="_blank">Traefik Proxy (v3.1)</a>
|
||||
<span class="badge-online">[ONLINE]</span>
|
||||
</div>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Edge HTTP reverse proxy with dynamic ACME wildcard TLS.</p>
|
||||
<div class="service-meta">
|
||||
<span>PORT: 443/tcp</span>
|
||||
<span>#core</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-head">
|
||||
<a href="https://nas.rooks.dev" target="_blank">TrueNAS SCALE</a>
|
||||
<span class="badge-online">[ONLINE]</span>
|
||||
</div>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Primary storage pool managing 48TB ZFS RaidZ2 array.</p>
|
||||
<div class="service-meta">
|
||||
<span>NFS / SMB</span>
|
||||
<span>#storage</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-head">
|
||||
<a href="https://vault.rooks.dev" target="_blank">Vaultwarden Secrets</a>
|
||||
<span class="badge-online">[E2EE]</span>
|
||||
</div>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Lightweight Rust Bitwarden API hosting credentials & 2FA.</p>
|
||||
<div class="service-meta">
|
||||
<span>2FA / AUTH</span>
|
||||
<span>#secrets</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-head">
|
||||
<a href="https://git.rooks.dev" target="_blank">Forgejo Git Forge</a>
|
||||
<span class="badge-online">[PUBLIC]</span>
|
||||
</div>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Self-hosted Git platform for dotfiles, playbooks & CTF code.</p>
|
||||
<div class="service-meta">
|
||||
<span>PORT: 3000</span>
|
||||
<span>#dev-git</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card">
|
||||
<div class="service-head">
|
||||
<a href="https://chef.rooks.dev" target="_blank">CyberChef Workbench</a>
|
||||
<span class="badge-online">[PUBLIC]</span>
|
||||
</div>
|
||||
<p style="font-size: 0.8rem; color: var(--text-muted); margin-bottom: 6px;">Hex decoding, Base64, hashing & CTF payload analysis.</p>
|
||||
<div class="service-meta">
|
||||
<span>OPEN WEB</span>
|
||||
<span>#security-tool</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 ARCHITECTURE & CYBERSECURITY</div>
|
||||
|
||||
<p class="bio-prose">
|
||||
Cybersecurity student at the University of Alabama College of Engineering.
|
||||
Focusing on network forensics, self-hosted infrastructure resilience, bare-metal virtualization, and low-level Unix tooling.
|
||||
</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>[#golang]</span>
|
||||
<span>[#rust]</span>
|
||||
<span>[#proxmox]</span>
|
||||
<span>[#truenas]</span>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">featured_modules</h2>
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-title">/dev/posix-shell-parser <span style="font-size: 0.75rem; color: var(--text-muted);">[v1.2.0 • #lua]</span></div>
|
||||
<p style="font-size: 0.85rem; color: var(--text-primary);">
|
||||
Zero-allocation Lexer and AST parser written in Lua for parsing POSIX shell scripts inside modal terminal buffers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-title">/dev/wireguard-mesh <span style="font-size: 0.75rem; color: var(--text-muted);">[v0.8.4 • #wireguard]</span></div>
|
||||
<p style="font-size: 0.85rem; color: var(--text-primary);">
|
||||
Automated configuration generator computing key matrices and provisioning full-mesh WireGuard private overlays across cloud and homelab nodes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="project-card">
|
||||
<div class="project-title">/dev/zero-alloc-logger <span style="font-size: 0.75rem; color: var(--text-muted);">[v2.1.1 • #golang]</span></div>
|
||||
<p style="font-size: 0.85rem; color: var(--text-primary);">
|
||||
High-throughput structured JSON logger utilizing sync.Pool byte buffers to eliminate heap allocation GC overhead (>500k ops/sec).
|
||||
</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-07-20 | #FIRMWARE</div>
|
||||
<a href="#" class="article-link">Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
<p style="font-size: 0.78rem; color: var(--text-muted);">Dumping SPI flash chips via Bus Pirate & flashrom to bypass locked bootloaders.</p>
|
||||
</div>
|
||||
|
||||
<div class="article-item">
|
||||
<div class="article-date">2026-07-15 | #HOMELAB</div>
|
||||
<a href="#" class="article-link">Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
<p style="font-size: 0.78rem; color: var(--text-muted);">Dynamic Docker provider discovery & Let's Encrypt automated TLS wildcard routing.</p>
|
||||
</div>
|
||||
|
||||
<div class="article-item">
|
||||
<div class="article-date">2026-07-10 | #POSIX-SH</div>
|
||||
<a href="#" class="article-link">POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
<p style="font-size: 0.78rem; color: var(--text-muted);">Eliminating bashisms and adhering to IEEE Std 1003.1 for high-speed bootstrapping.</p>
|
||||
</div>
|
||||
|
||||
<div class="article-item">
|
||||
<div class="article-date">2026-06-28 | #CTF</div>
|
||||
<a href="#" class="article-link">CTF Write-up: Heap Exploitation in glibc 2.35</a>
|
||||
<p style="font-size: 0.78rem; color: var(--text-muted);">Tcache safe-linking bypass and House of Apple file structure hijacking primitives.</p>
|
||||
</div>
|
||||
|
||||
<div class="article-item">
|
||||
<div class="article-date">2026-06-14 | #NETWORKING</div>
|
||||
<a href="#" class="article-link">Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a>
|
||||
<p style="font-size: 0.78rem; color: var(--text-muted);">Segregating untrusted IoT devices, homelab servers, and management networks.</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="River Rooks Workspace • Type :help or click links to navigate..." readonly>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,464 +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 style="display: flex; align-items: center; gap: 12px;">
|
||||
<span class="status-dot"></span>
|
||||
<span style="font-weight: 700;">RIVER ROOKS // TREE EXPLORER</span>
|
||||
<span style="color: var(--border-color);">|</span>
|
||||
<nav style="display: flex; gap: 8px; font-size: 0.78rem;">
|
||||
<a href="index.html" style="color: var(--text-primary); text-decoration: none;">Catalog</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="homepage.html" style="color: var(--text-muted); text-decoration: none;">Sample 1</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="homelab.html" style="color: var(--text-muted); text-decoration: none;">Homelab</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="public-services.html" style="color: var(--text-muted); text-decoration: none;">Services</a>
|
||||
<span style="color: var(--border-color);">/</span>
|
||||
<a href="sample-dashboard.html" style="color: var(--text-muted); text-decoration: none;">Dashboard</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: var(--text-muted);">HOST: pve-node02.lan [10GbE]</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, 'traefik')">traefik.md <span>[SVC]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'truenas')">truenas.md <span>[SVC]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'vaultwarden')">vaultwarden.md <span>[SVC]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'forgejo')">forgejo.md <span>[SVC]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'cyberchef')">cyberchef.md <span>[SVC]</span></div>
|
||||
|
||||
<div class="tree-folder">▾ /notes/</div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'spi-flash')">spi-flash.md <span>[BLOG]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'ha-traefik')">ha-traefik.md <span>[BLOG]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'posix-sh')">posix-sh.md <span>[BLOG]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'ctf-heap')">ctf-heap.md <span>[BLOG]</span></div>
|
||||
|
||||
<div class="tree-folder">▾ /modules/</div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'shell-parser')">shell-parser.lua <span>[DEV]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'mesh-builder')">wireguard-mesh.py <span>[DEV]</span></div>
|
||||
<div class="tree-item" onclick="selectItem(this, 'zero-logger')">zero-logger.go <span>[DEV]</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: Traefik Proxy -->
|
||||
<div class="doc-path">file:///home/river/services/traefik.md</div>
|
||||
<h1 class="doc-title">Service: Traefik Proxy (v3.1)</h1>
|
||||
|
||||
<p>
|
||||
Edge HTTP reverse proxy and load balancer with dynamic Docker provider discovery and ACME Let's Encrypt TLS wildcard certificate management.
|
||||
</p>
|
||||
|
||||
<div class="code-block">
|
||||
<div class="code-header">traefik.yml</div>
|
||||
<div class="code-content">
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
certificatesResolvers:
|
||||
cloudflare:
|
||||
acme:
|
||||
dnsChallenge:
|
||||
provider: cloudflare
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tag-cluster">
|
||||
<span>[#core]</span>
|
||||
<span>[#docker-traefik]</span>
|
||||
<span>[PORT: 443/tcp]</span>
|
||||
<span>[STATUS: ONLINE]</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Bottom Status Bar -->
|
||||
<footer class="status-bar">
|
||||
<div>UTF-8 | LF | POSIX Shell</div>
|
||||
<div>River Rooks • Canonical Content Dataset Compliant</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const docs = {
|
||||
'traefik': `
|
||||
<div class="doc-path">file:///home/river/services/traefik.md</div>
|
||||
<h1 class="doc-title">Service: Traefik Proxy (v3.1)</h1>
|
||||
<p>Edge HTTP reverse proxy and load balancer with dynamic Docker provider discovery and ACME Let's Encrypt TLS wildcard certificate management.</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">traefik.yml</div>
|
||||
<div class="code-content">
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":443"
|
||||
certificatesResolvers:
|
||||
cloudflare:
|
||||
acme:
|
||||
dnsChallenge:
|
||||
provider: cloudflare
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-cluster"><span>[#core]</span><span>[#docker-traefik]</span><span>[PORT: 443/tcp]</span><span>[STATUS: ONLINE]</span></div>
|
||||
`,
|
||||
'truenas': `
|
||||
<div class="doc-path">file:///home/river/services/truenas.md</div>
|
||||
<h1 class="doc-title">Service: TrueNAS SCALE</h1>
|
||||
<p>Primary storage pool managing 48TB ZFS RaidZ2 array across 6x 16TB enterprise SAS drives with daily snapshots and NFS/SMB exports.</p>
|
||||
<div class="tag-cluster"><span>[#storage]</span><span>[#zfs]</span><span>[PORT: NFS / SMB]</span><span>[STATUS: ONLINE]</span></div>
|
||||
`,
|
||||
'vaultwarden': `
|
||||
<div class="doc-path">file:///home/river/services/vaultwarden.md</div>
|
||||
<h1 class="doc-title">Service: Vaultwarden Secrets</h1>
|
||||
<p>Lightweight Rust implementation of the Bitwarden API hosting encrypted credentials, SSH keys, and TOTP authentication tokens.</p>
|
||||
<div class="tag-cluster"><span>[#secrets]</span><span>[#security]</span><span>[PORT: 443/tcp]</span><span>[STATUS: 2FA/AUTH]</span></div>
|
||||
`,
|
||||
'forgejo': `
|
||||
<div class="doc-path">file:///home/river/services/forgejo.md</div>
|
||||
<h1 class="doc-title">Service: Forgejo Git Forge</h1>
|
||||
<p>Self-hosted Git forge hosting homelab infrastructure-as-code repos, Ansible playbooks, dotfiles, and CTF binary writeup scripts.</p>
|
||||
<div class="tag-cluster"><span>[#dev-git]</span><span>[#git]</span><span>[PORT: 3000/tcp]</span><span>[STATUS: PUBLIC]</span></div>
|
||||
`,
|
||||
'cyberchef': `
|
||||
<div class="doc-path">file:///home/river/services/cyberchef.md</div>
|
||||
<h1 class="doc-title">Service: CyberChef Workbench</h1>
|
||||
<p>Web-based data transformation Swiss Army knife for hex decoding, Base64 operations, regex extraction, and CTF payload analysis.</p>
|
||||
<div class="tag-cluster"><span>[#security-tool]</span><span>[#crypto]</span><span>[STATUS: PUBLIC]</span></div>
|
||||
`,
|
||||
'spi-flash': `
|
||||
<div class="doc-path">file:///home/river/notes/iot-firmware-spi-extraction.md</div>
|
||||
<h1 class="doc-title">Reverse Engineering IoT Firmware: SPI Flash Extraction</h1>
|
||||
<p>Physical hardware-level dumping of SPI flash memory chips using Bus Pirate and flashrom to bypass locked bootloaders.</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">extract_firmware.sh</div>
|
||||
<div class="code-content">
|
||||
flashrom -p buspirate_spi:dev=/dev/ttyUSB0,spispeed=1M -r firmware_dump.bin
|
||||
md5sum firmware_dump.bin
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-cluster"><span>[#firmware]</span><span>[#hardware]</span><span>[#security]</span><span>[DATE: 2026-07-20]</span></div>
|
||||
`,
|
||||
'ha-traefik': `
|
||||
<div class="doc-path">file:///home/river/notes/ha-traefik-docker.md</div>
|
||||
<h1 class="doc-title">Building a High-Availability Traefik Router</h1>
|
||||
<p>Architecting automated TLS wildcard certificate propagation and dynamic Docker container discovery failover routing.</p>
|
||||
<div class="tag-cluster"><span>[#homelab]</span><span>[#docker-traefik]</span><span>[DATE: 2026-07-15]</span></div>
|
||||
`,
|
||||
'posix-sh': `
|
||||
<div class="doc-path">file:///home/river/notes/posix-sh-vs-bash.md</div>
|
||||
<h1 class="doc-title">POSIX sh vs. Bash: Portability in Modern Scripts</h1>
|
||||
<p>Eliminating bashisms and adhering to IEEE Std 1003.1 for high-speed bootstrapping in Alpine and embedded targets.</p>
|
||||
<div class="tag-cluster"><span>[#posix-sh]</span><span>[#unix]</span><span>[DATE: 2026-07-10]</span></div>
|
||||
`,
|
||||
'ctf-heap': `
|
||||
<div class="doc-path">file:///home/river/notes/ctf-heap-glibc-2-35.md</div>
|
||||
<h1 class="doc-title">CTF Write-up: Heap Exploitation in glibc 2.35</h1>
|
||||
<p>Tcache safe-linking bypass and House of Apple file structure hijacking primitives for arbitrary code execution.</p>
|
||||
<div class="tag-cluster"><span>[#ctf]</span><span>[#binary-exploitation]</span><span>[DATE: 2026-06-28]</span></div>
|
||||
`,
|
||||
'shell-parser': `
|
||||
<div class="doc-path">file:///home/river/modules/posix-shell-parser.lua</div>
|
||||
<h1 class="doc-title">Module: Custom POSIX Shell Parser (v1.2.0)</h1>
|
||||
<p>Zero-allocation Lexer and AST parser written in Lua for real-time syntax coloring in modal editors.</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">lexer.lua</div>
|
||||
<div class="code-content">
|
||||
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
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-cluster"><span>[#lua]</span><span>[#ast-parser]</span><span>[VERSION: v1.2.0]</span></div>
|
||||
`,
|
||||
'mesh-builder': `
|
||||
<div class="doc-path">file:///home/river/modules/wireguard-mesh.py</div>
|
||||
<h1 class="doc-title">Module: WireGuard Mesh Topology Builder (v0.8.4)</h1>
|
||||
<p>Matrix topology generator creating atomic configuration files and key pairings across multi-cloud and homelab nodes.</p>
|
||||
<div class="tag-cluster"><span>[#networking]</span><span>[#wireguard]</span><span>[VERSION: v0.8.4]</span></div>
|
||||
`,
|
||||
'zero-logger': `
|
||||
<div class="doc-path">file:///home/river/modules/zero-alloc-logger.go</div>
|
||||
<h1 class="doc-title">Module: Zero-Allocation Logger (Go v2.1.1)</h1>
|
||||
<p>High-throughput structured JSON logger utilizing sync.Pool byte buffers to eliminate heap allocations (>500k ops/sec).</p>
|
||||
<div class="tag-cluster"><span>[#golang]</span><span>[#performance]</span><span>[VERSION: v2.1.1]</span></div>
|
||||
`,
|
||||
'whoami': `
|
||||
<div class="doc-path">file:///home/river/resume/whoami.md</div>
|
||||
<h1 class="doc-title">River Rooks - Systems Architecture & Cybersecurity</h1>
|
||||
<p>Cybersecurity student at the University of Alabama College of Engineering, focusing on network forensics, self-hosted infrastructure, and bare-metal virtualization.</p>
|
||||
<div class="tag-cluster"><span>[#cybersecurity]</span><span>[#systems]</span><span>[UA COLLEGE OF ENGINEERING]</span></div>
|
||||
`,
|
||||
'stack': `
|
||||
<div class="doc-path">file:///home/river/resume/active_stack.conf</div>
|
||||
<h1 class="doc-title">Active Engineering Stack</h1>
|
||||
<div class="code-block">
|
||||
<div class="code-header">stack.conf</div>
|
||||
<div class="code-content">
|
||||
DOMAINS="Network Forensics, Self-Hosted Infrastructure, Linux Kernel Hardening"
|
||||
LANGUAGES="POSIX sh, Python, Go, Rust, Lua, ARM Assembly"
|
||||
STACK="Traefik v3, WireGuard, Proxmox VE, TrueNAS SCALE, Neovim"
|
||||
</div>
|
||||
</div>
|
||||
<div class="tag-cluster"><span>[#arch-linux]</span><span>[#neovim]</span><span>[#wireguard]</span></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>
|
||||
@@ -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.
|
||||
@@ -1,587 +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 | Node Gateway & System Hub</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; /* Card surface background */
|
||||
--bg-elevated: #1c1f26; /* Hovered surface */
|
||||
--border-color: #2b303b; /* Low-contrast 1px border */
|
||||
--text-primary: #e0e6f0; /* Off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospace metadata */
|
||||
--accent-cyan: #00f0ff; /* Primary terminal accent */
|
||||
--accent-green: #00e676; /* Operational indicator */
|
||||
--accent-amber: #ffb700; /* Internal / protected warning */
|
||||
}
|
||||
|
||||
* {
|
||||
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;
|
||||
align-items: center;
|
||||
padding: 40px 20px 64px 20px;
|
||||
}
|
||||
|
||||
.hub-container {
|
||||
width: 100%;
|
||||
max-width: 780px;
|
||||
}
|
||||
|
||||
/* Top Breadcrumb & Status Bar */
|
||||
.top-status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 28px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.node-identifier {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--accent-green);
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
color: var(--accent-green);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Main Landing Narrative Section (Primary Focus) */
|
||||
.landing-hero {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.author-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.author-tagline {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--accent-cyan);
|
||||
margin-bottom: 20px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.manifesto-prose {
|
||||
font-size: 1.02rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.manifesto-prose p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.callout-block {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid var(--accent-cyan);
|
||||
border-radius: 3px;
|
||||
padding: 14px 18px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 24px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.callout-block strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 1px Solid Section Divider */
|
||||
.section-divider {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: 36px 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.section-meta {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Direction to Services Router */
|
||||
.services-router {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: none; /* Instant snap */
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.service-card:focus {
|
||||
outline: 2px solid var(--accent-cyan);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
/* Highlighted Blog Service Card */
|
||||
.service-card.service-primary {
|
||||
border-left: 3px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.service-top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.service-name-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.service-name {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.service-card:hover .service-name {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.service-badge {
|
||||
font-size: 0.72rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-blog {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-public {
|
||||
border: 1px solid var(--accent-green);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.badge-internal {
|
||||
border: 1px solid var(--accent-amber);
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
|
||||
.service-endpoint {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.service-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Recent Articles Feed Section */
|
||||
.articles-preview-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.article-item {
|
||||
background-color: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.article-item:hover {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.article-meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.article-date {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.article-heading {
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.article-heading:hover {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.article-summary-text {
|
||||
font-size: 0.88rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.hub-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;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
padding: 2px 4px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.pipe {
|
||||
color: var(--border-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="hub-container">
|
||||
|
||||
<!-- Top Status Line -->
|
||||
<header class="top-status-bar">
|
||||
<div class="node-identifier">
|
||||
<span class="status-dot"></span>
|
||||
<span>node: gw-node01.lan [RACK-01 / ALABAMA]</span>
|
||||
</div>
|
||||
<div class="status-pill">[SYSTEMS OPERATIONAL • 10GbE]</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Landing Narrative (Core Focus) -->
|
||||
<section class="landing-hero" aria-label="System Introduction & Landing Manifesto">
|
||||
<h1 class="author-title">River Rooks</h1>
|
||||
<div class="author-tagline">SYSTEMS ARCHITECTURE • CYBERSECURITY • SELF-HOSTED CLUSTER</div>
|
||||
|
||||
<div class="manifesto-prose">
|
||||
<p>
|
||||
Welcome to my personal computing node and technical gateway. I am an engineering student at the University of Alabama specializing in network forensics, bare-metal hypervisors, and security systems.
|
||||
</p>
|
||||
<p>
|
||||
This server is not a commercial landing page or marketing platform. It is a sovereign, self-hosted operational command hub operating from a dedicated 10GbE home rack. All services below run on hardened local hypervisors (Proxmox VE, OPNsense, TrueNAS) routed through an automated Traefik reverse proxy with WireGuard zero-trust mesh peering.
|
||||
</p>
|
||||
<p>
|
||||
Use this landing portal to navigate the ecosystem. The <strong>Technical Blog</strong> contains deep-dive research notes and CTF post-mortems. The <strong>Public Services</strong> are live, free-to-use developer and cryptographic utilities available to the public, while the <strong>Internal Infrastructure</strong> links route directly to cluster monitoring and orchestration consoles.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="callout-block">
|
||||
<strong>ROUTING NOTICE:</strong> External ingress is authenticated and monitored via OPNsense Suricata IDS/IPS. Public utilities are open for interactive use without registration.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Direction to Services (With Blog as Primary Endpoint) -->
|
||||
<section aria-label="Homelab Services Directory">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Directory of Hosted Services & Tools</h2>
|
||||
<span class="section-meta">Select a destination →</span>
|
||||
</div>
|
||||
|
||||
<div class="services-router">
|
||||
|
||||
<!-- Service 1: The Blog (Primary Service) -->
|
||||
<a href="blog-lander-stream.html" class="service-card service-primary" title="Go to Technical Blog & Dispatches">
|
||||
<div class="service-top-row">
|
||||
<div class="service-name-group">
|
||||
<span class="service-name">> Technical Notes & Engineering Blog</span>
|
||||
<span class="service-badge badge-blog">[BLOG / DISPATCHES]</span>
|
||||
</div>
|
||||
<span class="service-endpoint">~/blog/notes/ • rooks.dev/notes</span>
|
||||
</div>
|
||||
<p class="service-desc">
|
||||
The core writing repository. Reverse-chronological feed of technical articles covering IoT SPI flash extraction, glibc 2.35 heap exploitation, high-availability Traefik routing, and POSIX shell engineering.
|
||||
</p>
|
||||
<div class="service-tags">
|
||||
<span>[#ctf-writeups]</span>
|
||||
<span>[#firmware-reversing]</span>
|
||||
<span>[#traefik-docker]</span>
|
||||
<span>[#posix-sh]</span>
|
||||
<span style="color: var(--accent-cyan); font-weight: 500;">[OPEN BLOG FEED →]</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Service 2: Forgejo Git Server -->
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="service-card" title="Open Self-Hosted Git Forge">
|
||||
<div class="service-top-row">
|
||||
<div class="service-name-group">
|
||||
<span class="service-name">Forgejo Git Server</span>
|
||||
<span class="service-badge badge-public">[PUBLIC FORGE]</span>
|
||||
</div>
|
||||
<span class="service-endpoint">https://git.rooks.dev • :2222/ssh</span>
|
||||
</div>
|
||||
<p class="service-desc">
|
||||
Lightweight, independent Git forge hosting infrastructure-as-code playbooks, dotfiles, POSIX shell parser modules, and bare-metal kernel bootstrap code.
|
||||
</p>
|
||||
<div class="service-tags">
|
||||
<span>[#iac]</span>
|
||||
<span>[#ansible]</span>
|
||||
<span>[#cortex-m]</span>
|
||||
<span>[#git]</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Service 3: CyberChef Workbench -->
|
||||
<a href="https://chef.rooks.dev" target="_blank" rel="noopener" class="service-card" title="Open CyberChef Workbench">
|
||||
<div class="service-top-row">
|
||||
<div class="service-name-group">
|
||||
<span class="service-name">CyberChef Security Workbench</span>
|
||||
<span class="service-badge badge-public">[PUBLIC TOOL]</span>
|
||||
</div>
|
||||
<span class="service-endpoint">https://chef.rooks.dev • :443</span>
|
||||
</div>
|
||||
<p class="service-desc">
|
||||
Client-side cryptographic Swiss Army knife for hex decoding, Base64 operations, XOR decryption, hashing algorithms, and CTF payload deconstruction.
|
||||
</p>
|
||||
<div class="service-tags">
|
||||
<span>[#cryptography]</span>
|
||||
<span>[#forensics]</span>
|
||||
<span>[#ctf-toolkit]</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Service 4: Stirling PDF Studio -->
|
||||
<a href="https://pdf.rooks.dev" target="_blank" rel="noopener" class="service-card" title="Open Stirling PDF Suite">
|
||||
<div class="service-top-row">
|
||||
<div class="service-name-group">
|
||||
<span class="service-name">Stirling PDF Studio</span>
|
||||
<span class="service-badge badge-public">[PUBLIC TOOL]</span>
|
||||
</div>
|
||||
<span class="service-endpoint">https://pdf.rooks.dev • :443</span>
|
||||
</div>
|
||||
<p class="service-desc">
|
||||
Locally hosted private document toolkit for OCR text extraction, PDF merging, splitting, sanitization, and password encryption without external cloud telemetry.
|
||||
</p>
|
||||
<div class="service-tags">
|
||||
<span>[#pdf-tools]</span>
|
||||
<span>[#ocr]</span>
|
||||
<span>[#privacy-first]</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Service 5: Proxmox VE & TrueNAS Cluster Management -->
|
||||
<a href="homelab.html" class="service-card" title="Inspect Homelab Infrastructure">
|
||||
<div class="service-top-row">
|
||||
<div class="service-name-group">
|
||||
<span class="service-name">Homelab Cluster & Storage</span>
|
||||
<span class="service-badge badge-internal">[INTERNAL / HYPERVISOR]</span>
|
||||
</div>
|
||||
<span class="service-endpoint">https://pve.rooks.dev • 48TB ZFS</span>
|
||||
</div>
|
||||
<p class="service-desc">
|
||||
Hypervisor cluster with AMD Ryzen 9 5900X (12C/24T), 128GB ECC RAM, and TrueNAS SCALE RaidZ2 storage pool. View node topology and live resource utilization telemetry.
|
||||
</p>
|
||||
<div class="service-tags">
|
||||
<span>[#proxmox-ve]</span>
|
||||
<span>[#truenas]</span>
|
||||
<span>[#10gbe-mesh]</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Service 6: Uptime Kuma System Telemetry -->
|
||||
<a href="https://status.rooks.dev" target="_blank" rel="noopener" class="service-card" title="Open Status Dashboard">
|
||||
<div class="service-top-row">
|
||||
<div class="service-name-group">
|
||||
<span class="service-name">Uptime Kuma Health Monitor</span>
|
||||
<span class="service-badge badge-public">[STATUS / TELEMETRY]</span>
|
||||
</div>
|
||||
<span class="service-endpoint">https://status.rooks.dev • :3001</span>
|
||||
</div>
|
||||
<p class="service-desc">
|
||||
Continuous synthetic HTTP, TCP, and ICMP health checks broadcasting real-time cluster uptime, DNS resolution latencies, and service availability.
|
||||
</p>
|
||||
<div class="service-tags">
|
||||
<span>[#uptime-100%]</span>
|
||||
<span>[#telemetry]</span>
|
||||
<span>[#prometheus]</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Recent Technical Writing (Direct Excerpts from the Blog) -->
|
||||
<section aria-label="Latest Blog Dispatches">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Latest Technical Dispatches From the Blog</h2>
|
||||
<a href="blog-lander-stream.html" style="color: var(--accent-cyan); font-family: 'IBM Plex Mono', monospace; font-size: 0.78rem; text-decoration: none;">View all 5 entries →</a>
|
||||
</div>
|
||||
|
||||
<div class="articles-preview-list">
|
||||
|
||||
<article class="article-item">
|
||||
<div class="article-meta-row">
|
||||
<time datetime="2026-07-20" class="article-date">2026-07-20 • 8 min read</time>
|
||||
<span>[#firmware] [#hardware]</span>
|
||||
</div>
|
||||
<a href="blog-lander-stream.html" class="article-heading">> Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
<p class="article-summary-text">
|
||||
Bypassing bootloader lockdowns by dumping physical SPI Flash memory from an 8-pin SOP chip using a Bus Pirate and flashrom.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="article-item">
|
||||
<div class="article-meta-row">
|
||||
<time datetime="2026-07-15" class="article-date">2026-07-15 • 12 min read</time>
|
||||
<span>[#homelab] [#docker-traefik]</span>
|
||||
</div>
|
||||
<a href="blog-lander-stream.html" class="article-heading">> Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
<p class="article-summary-text">
|
||||
Architecting automated TLS wildcard certificate propagation, Docker provider label routing, zero-downtime container updates, and health checks.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="article-item">
|
||||
<div class="article-meta-row">
|
||||
<time datetime="2026-07-10" class="article-date">2026-07-10 • 6 min read</time>
|
||||
<span>[#posix-sh] [#unix]</span>
|
||||
</div>
|
||||
<a href="blog-lander-stream.html" class="article-heading">> POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
<p class="article-summary-text">
|
||||
Eliminating bashisms and writing bulletproof, zero-allocation bootstrapping scripts for minimal Alpine and embedded Linux environments.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="hub-footer">
|
||||
<div>River Rooks • Cluster gw-node01.lan</div>
|
||||
<nav class="footer-links" aria-label="Portal Footer Navigation">
|
||||
<a href="blog-lander-stream.html" class="footer-link">Blog</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="feed.xml" class="footer-link">RSS</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link">Git Forge</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="mailto:river@rooks.dev" class="footer-link">Email</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="#" title="Fingerprint: 0x4A8E92F1C0B3D4E7" class="footer-link">PGP Key</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="index.html" class="footer-link">Samples Catalog →</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,609 +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 Host Console & Service Router</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; /* Table & card surface */
|
||||
--bg-elevated: #1c1f26; /* Selected row / elevated state */
|
||||
--border-color: #2b303b; /* Low-contrast 1px border */
|
||||
--text-primary: #e0e6f0; /* Light off-white prose */
|
||||
--text-muted: #8a95a5; /* Monospace metadata */
|
||||
--accent-cyan: #00f0ff; /* Primary terminal accent */
|
||||
--accent-green: #00e676; /* Operational status indicator */
|
||||
--accent-amber: #ffb700; /* Internal / protected warning */
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-canvas);
|
||||
color: var(--text-primary);
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32px 16px 56px 16px;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
/* Terminal Window Shell */
|
||||
.terminal-shell {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background-color: var(--bg-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shell-titlebar {
|
||||
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.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.window-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background-color: var(--border-color);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.shell-body {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* MOTD / Welcome Landing Narrative (Primary Focus) */
|
||||
.motd-banner {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.motd-header {
|
||||
font-size: 0.8rem;
|
||||
color: var(--accent-cyan);
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.motd-title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.motd-subtitle {
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.motd-prose {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.94rem;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.motd-prose p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.motd-prose p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.motd-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 0.76rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Command Prompt Separator */
|
||||
.cmd-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.cmd-prompt {
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cmd-text {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Services Routing Table */
|
||||
.services-table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.services-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.82rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.services-table th {
|
||||
padding: 8px 12px;
|
||||
background-color: var(--bg-canvas);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.services-table td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.service-row {
|
||||
transition: none; /* Instant snap */
|
||||
}
|
||||
|
||||
.service-row:hover {
|
||||
background-color: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.service-row.highlight-blog {
|
||||
border-left: 2px solid var(--accent-cyan);
|
||||
background-color: rgba(0, 240, 255, 0.03);
|
||||
}
|
||||
|
||||
.service-link {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.service-row:hover .service-link {
|
||||
color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
.badge-status {
|
||||
font-size: 0.72rem;
|
||||
padding: 1px 6px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.badge-online {
|
||||
border: 1px solid var(--accent-green);
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.badge-blog-highlight {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
font-weight: 700;
|
||||
border: 1px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.badge-protected {
|
||||
border: 1px solid var(--accent-amber);
|
||||
color: var(--accent-amber);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: none;
|
||||
font-size: 0.76rem;
|
||||
padding: 2px 6px;
|
||||
border: 1px solid transparent;
|
||||
display: inline-block;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.service-row:hover .action-btn {
|
||||
border-color: var(--accent-cyan);
|
||||
}
|
||||
|
||||
/* Articles Stream Section */
|
||||
.articles-section {
|
||||
background-color: var(--bg-canvas);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.articles-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.articles-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.articles-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.article-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(43, 48, 59, 0.4);
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.article-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.article-row-left {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.article-date-mono {
|
||||
color: var(--accent-cyan);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.article-title-click {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.article-title-click:hover {
|
||||
color: var(--accent-cyan);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.article-tags-mono {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
/* Status Bar Footer */
|
||||
.shell-footer {
|
||||
background-color: var(--bg-canvas);
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-link-item {
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.footer-link-item:hover {
|
||||
background-color: var(--accent-cyan);
|
||||
color: var(--bg-canvas);
|
||||
}
|
||||
|
||||
.pipe {
|
||||
color: var(--border-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<main class="terminal-container">
|
||||
|
||||
<div class="terminal-shell">
|
||||
|
||||
<!-- Shell Window Bar -->
|
||||
<div class="shell-titlebar">
|
||||
<div class="window-dots">
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span class="dot"></span>
|
||||
<span style="color: var(--text-primary); font-weight: 700; margin-left: 6px;">dev-blog@system: ~/gateway</span>
|
||||
</div>
|
||||
<div>SESSION: TTY1 • 10GbE TRUNK • UPTIME: 99.98%</div>
|
||||
</div>
|
||||
|
||||
<div class="shell-body">
|
||||
|
||||
<!-- MOTD / Landing Narrative (Main Focus of Homepage) -->
|
||||
<section class="motd-banner" aria-label="System Message Of The Day and Landing Introduction">
|
||||
<div class="motd-header">
|
||||
<span>/etc/motd • BROADCAST MESSAGE</span>
|
||||
<span>NODE: gw-node01.lan [RACK-01]</span>
|
||||
</div>
|
||||
|
||||
<h1 class="motd-title">River Rooks // Homelab Gateway & Service Router</h1>
|
||||
<div class="motd-subtitle">Systems Architecture • Network Forensics • Bare-Metal Infrastructure</div>
|
||||
|
||||
<div class="motd-prose">
|
||||
<p>
|
||||
Welcome to the entrypoint for my self-hosted environment. I am a cybersecurity and systems student at the University of Alabama College of Engineering. This portal acts as the root router for both my external public services and internal virtualization infrastructure.
|
||||
</p>
|
||||
<p>
|
||||
Rather than relying on commercial cloud silos, every application listed below runs directly on physical hardware housed in my home lab. Network traffic is segmented across dedicated VLANs (Management, Server DMZ, IoT) and shielded behind an OPNsense firewall with Suricata intrusion prevention and an automated Traefik reverse proxy handling Let's Encrypt TLS wildcard certificates.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Looking for technical writing?</strong> The <a href="blog-lander-terminal.html" style="color: var(--accent-cyan); text-decoration: underline;">Technical Notes & Blog</a> service is where I document firmware reverse engineering, CTF binary exploitation, and zero-trust routing architectures. All public utilities (CyberChef, Stirling PDF, Git Forge) are open and ready for immediate use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="motd-stats-grid">
|
||||
<div>GATEWAY: <span class="stat-val">OPNsense 24.7 (Intel N100)</span></div>
|
||||
<div>COMPUTE: <span class="stat-val">Proxmox VE 8.2 (AMD 5900X)</span></div>
|
||||
<div>STORAGE: <span class="stat-val">TrueNAS SCALE (48TB ZFS)</span></div>
|
||||
<div>SECURITY: <span class="stat-val">WireGuard Full-Mesh</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Command Execution Bar -->
|
||||
<div class="cmd-bar">
|
||||
<span class="cmd-prompt">dev-blog@system:~$</span>
|
||||
<span class="cmd-text">route-service --list-all --highlight=blog</span>
|
||||
</div>
|
||||
|
||||
<!-- Directory of Hosted Services (Routing) -->
|
||||
<div class="services-table-wrapper">
|
||||
<table class="services-table" aria-label="Homelab Services Routing Table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">SERVICE IDENTIFIER</th>
|
||||
<th scope="col">ROUTING DOMAIN / PATH</th>
|
||||
<th scope="col">ACCESS LEVEL</th>
|
||||
<th scope="col">DESCRIPTION</th>
|
||||
<th scope="col">DISPATCH</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<!-- Blog Service (Highlighted) -->
|
||||
<tr class="service-row highlight-blog">
|
||||
<td>
|
||||
<a href="blog-lander-terminal.html" class="service-link">
|
||||
<span>> Technical Notes & Blog</span>
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--accent-cyan);">~/notes/ • rooks.dev/notes</td>
|
||||
<td><span class="badge-status badge-blog-highlight">[FEATURED BLOG]</span></td>
|
||||
<td style="color: var(--text-primary); font-family: 'Inter', sans-serif;">
|
||||
Technical writing on firmware reversing, heap exploitation, and networking.
|
||||
</td>
|
||||
<td><a href="blog-lander-terminal.html" class="action-btn">[OPEN BLOG →]</a></td>
|
||||
</tr>
|
||||
|
||||
<!-- Forgejo Git -->
|
||||
<tr class="service-row">
|
||||
<td>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="service-link">
|
||||
<span>Forgejo Git Forge</span>
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--text-muted);">git.rooks.dev:2222</td>
|
||||
<td><span class="badge-status badge-online">[PUBLIC OPEN]</span></td>
|
||||
<td style="color: var(--text-primary); font-family: 'Inter', sans-serif;">
|
||||
Self-hosted repositories: Ansible IaC, dotfiles, and POSIX parser modules.
|
||||
</td>
|
||||
<td><a href="https://git.rooks.dev" target="_blank" rel="noopener" class="action-btn">[VISIT FORGE]</a></td>
|
||||
</tr>
|
||||
|
||||
<!-- CyberChef -->
|
||||
<tr class="service-row">
|
||||
<td>
|
||||
<a href="https://chef.rooks.dev" target="_blank" rel="noopener" class="service-link">
|
||||
<span>CyberChef Suite</span>
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--text-muted);">chef.rooks.dev:443</td>
|
||||
<td><span class="badge-status badge-online">[PUBLIC TOOL]</span></td>
|
||||
<td style="color: var(--text-primary); font-family: 'Inter', sans-serif;">
|
||||
Client-side decoding, hex analysis, hashing, and CTF payload processing.
|
||||
</td>
|
||||
<td><a href="https://chef.rooks.dev" target="_blank" rel="noopener" class="action-btn">[OPEN CHEF]</a></td>
|
||||
</tr>
|
||||
|
||||
<!-- Stirling PDF -->
|
||||
<tr class="service-row">
|
||||
<td>
|
||||
<a href="https://pdf.rooks.dev" target="_blank" rel="noopener" class="service-link">
|
||||
<span>Stirling PDF Studio</span>
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--text-muted);">pdf.rooks.dev:443</td>
|
||||
<td><span class="badge-status badge-online">[PUBLIC TOOL]</span></td>
|
||||
<td style="color: var(--text-primary); font-family: 'Inter', sans-serif;">
|
||||
Local document manipulation: OCR, PDF merge, split, and sanitize.
|
||||
</td>
|
||||
<td><a href="https://pdf.rooks.dev" target="_blank" rel="noopener" class="action-btn">[LAUNCH PDF]</a></td>
|
||||
</tr>
|
||||
|
||||
<!-- Uptime Kuma -->
|
||||
<tr class="service-row">
|
||||
<td>
|
||||
<a href="https://status.rooks.dev" target="_blank" rel="noopener" class="service-link">
|
||||
<span>Uptime Kuma Telemetry</span>
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--text-muted);">status.rooks.dev:3001</td>
|
||||
<td><span class="badge-status badge-online">[PUBLIC METRICS]</span></td>
|
||||
<td style="color: var(--text-primary); font-family: 'Inter', sans-serif;">
|
||||
Live latency metrics, cluster ping monitors, and real-time SLA status.
|
||||
</td>
|
||||
<td><a href="https://status.rooks.dev" target="_blank" rel="noopener" class="action-btn">[VIEW STATS]</a></td>
|
||||
</tr>
|
||||
|
||||
<!-- Proxmox VE -->
|
||||
<tr class="service-row">
|
||||
<td>
|
||||
<a href="homelab.html" class="service-link">
|
||||
<span>Proxmox Hypervisor</span>
|
||||
</a>
|
||||
</td>
|
||||
<td style="color: var(--text-muted);">pve.rooks.dev:8006</td>
|
||||
<td><span class="badge-status badge-protected">[INTERNAL AUTH]</span></td>
|
||||
<td style="color: var(--text-primary); font-family: 'Inter', sans-serif;">
|
||||
KVM VMs and LXC container cluster on AMD Ryzen 9 5900X (12C/24T).
|
||||
</td>
|
||||
<td><a href="homelab.html" class="action-btn">[CLUSTER VIEW]</a></td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Command Execution Bar for Articles -->
|
||||
<div class="cmd-bar">
|
||||
<span class="cmd-prompt">dev-blog@system:~$</span>
|
||||
<span class="cmd-text">tail -n 5 ~/notes/dispatches.log</span>
|
||||
</div>
|
||||
|
||||
<!-- Articles Digest Section -->
|
||||
<section class="articles-section" aria-label="Articles Feed">
|
||||
<div class="articles-header">
|
||||
<div class="articles-title">~/notes/dispatches.log • RECENT TECHNICAL WRITING</div>
|
||||
<a href="blog-lander-terminal.html" style="color: var(--accent-cyan); text-decoration: none; font-size: 0.78rem;">Full Archive →</a>
|
||||
</div>
|
||||
|
||||
<div class="articles-list">
|
||||
|
||||
<div class="article-row">
|
||||
<div class="article-row-left">
|
||||
<span class="article-date-mono">2026-07-20</span>
|
||||
<a href="blog-lander-terminal.html" class="article-title-click">Reverse Engineering IoT Firmware: SPI Flash Extraction</a>
|
||||
</div>
|
||||
<span class="article-tags-mono">[#firmware] [#hardware] • 8m</span>
|
||||
</div>
|
||||
|
||||
<div class="article-row">
|
||||
<div class="article-row-left">
|
||||
<span class="article-date-mono">2026-07-15</span>
|
||||
<a href="blog-lander-terminal.html" class="article-title-click">Building a High-Availability Traefik Router with Docker Compose</a>
|
||||
</div>
|
||||
<span class="article-tags-mono">[#homelab] [#docker-traefik] • 12m</span>
|
||||
</div>
|
||||
|
||||
<div class="article-row">
|
||||
<div class="article-row-left">
|
||||
<span class="article-date-mono">2026-07-10</span>
|
||||
<a href="blog-lander-terminal.html" class="article-title-click">POSIX sh vs. Bash: Portability in Modern Scripts</a>
|
||||
</div>
|
||||
<span class="article-tags-mono">[#posix-sh] [#unix] • 6m</span>
|
||||
</div>
|
||||
|
||||
<div class="article-row">
|
||||
<div class="article-row-left">
|
||||
<span class="article-date-mono">2026-06-28</span>
|
||||
<a href="blog-lander-terminal.html" class="article-title-click">CTF Write-up: Heap Exploitation in glibc 2.35</a>
|
||||
</div>
|
||||
<span class="article-tags-mono">[#ctf] [#binary-exploitation] • 15m</span>
|
||||
</div>
|
||||
|
||||
<div class="article-row">
|
||||
<div class="article-row-left">
|
||||
<span class="article-date-mono">2026-06-14</span>
|
||||
<a href="blog-lander-terminal.html" class="article-title-click">Zero-Trust Network Isolation with WireGuard & OPNsense VLANs</a>
|
||||
</div>
|
||||
<span class="article-tags-mono">[#networking] [#wireguard] • 10m</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="shell-footer">
|
||||
<div>STATUS: 0 (OK) • TLS: ACME WILDCARD • NODE RACK-01</div>
|
||||
<nav class="footer-nav" aria-label="Terminal Navigation Footer">
|
||||
<a href="blog-lander-terminal.html" class="footer-link-item">Blog Lander</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="https://git.rooks.dev" target="_blank" rel="noopener" class="footer-link-item">Forgejo Git</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="mailto:river@rooks.dev" class="footer-link-item">river@rooks.dev</a>
|
||||
<span class="pipe">|</span>
|
||||
<a href="index.html" class="footer-link-item">Catalog ↗</a>
|
||||
</nav>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,592 +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 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">> Blog & 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 • RACK-01 NODE GATEWAY</div>
|
||||
<h1 class="hero-main-title">Engineering Sovereign Infrastructure & Technical Notes</h1>
|
||||
|
||||
<div class="hero-landing-text">
|
||||
<p>
|
||||
I am River Rooks, a cybersecurity engineering student at the University of Alabama. My work centers on network forensics, self-hosted system architecture, low-level firmware exploitation, and zero-allocation Unix utilities.
|
||||
</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> •
|
||||
<span>[2] Forgejo Git Repos</span> •
|
||||
<span>[3] Public Cryptographic & PDF Tools</span> •
|
||||
<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/ • 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">> River Rooks Technical Notes & 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] →</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] →</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] →</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] →</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 & 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 & 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 & 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 • 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>
|
||||
@@ -1,478 +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>
|
||||
<a href="homelab.html" class="nav-link">Sample 6</a>
|
||||
<a href="public-services.html" class="nav-link">Sample 7</a>
|
||||
<a href="sample-dashboard.html" class="nav-link">Sample 8</a>
|
||||
<a href="sample-command-center.html" class="nav-link">Sample 9</a>
|
||||
<a href="sample-dual-pane-explorer.html" class="nav-link">Sample 10</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">pve-node02.lan</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">Cluster:</span>
|
||||
<span class="status-value">10GbE SFP+</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right Pane - Scrollable List of Cards -->
|
||||
<main class="right-pane">
|
||||
<div class="scroll-content">
|
||||
<h2 class="section-title">River Rooks // System Modules</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 • [#lua]</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 without runtime heap allocations.
|
||||
</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 • [#wireguard]</span>
|
||||
</div>
|
||||
<p class="tile-description">
|
||||
Automated configuration generator for computing key matrices and 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 • [#golang]</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 (>500k ops/sec).
|
||||
</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 • [#arm-cortex]</span>
|
||||
</div>
|
||||
<p class="tile-description">
|
||||
Minimalist boot sector and initial kernel bootstrap for ARM Cortex-M microcontrollers to handle vector table offset mapping 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 • [#docker-traefik]</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 dynamically.
|
||||
</p>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<footer class="pane-footer">
|
||||
<span>Sample 2: Split Panel Layout</span>
|
||||
<span>© 2026 River Rooks</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>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,136 @@
|
||||
import http from "node:http";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
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"
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates the HTTP server instance for serving static files from _site
|
||||
* @param {string} [customSiteDir] Optional custom directory to serve from
|
||||
* @returns {http.Server}
|
||||
*/
|
||||
export function createServer(customSiteDir) {
|
||||
const siteDir = path.resolve(customSiteDir || path.join(process.cwd(), "_site"));
|
||||
|
||||
return http.createServer((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;
|
||||
}
|
||||
|
||||
// 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: assets get long-term caching, html gets short
|
||||
if (decodedPath.startsWith("/assets/")) {
|
||||
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"));
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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: "Gitea",
|
||||
url: `https://git.${siteDomain}`,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
label: "RSS Feed",
|
||||
url: "/feed.xml"
|
||||
}
|
||||
],
|
||||
services: [
|
||||
{
|
||||
name: "Gitea",
|
||||
url: `https://git.${siteDomain}`,
|
||||
tag: `git.${siteDomain}:2222`,
|
||||
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://library.${siteDomain}`,
|
||||
tag: `books.${siteDomain}:443`,
|
||||
desc: `Calibre web frontend for managing and reading eBooks.`
|
||||
}
|
||||
],
|
||||
topics: [
|
||||
{
|
||||
title: "Systems Engineering",
|
||||
slug: "systems",
|
||||
desc: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
badge: "TRACK: SYSTEMS"
|
||||
},
|
||||
{
|
||||
title: "Security Research",
|
||||
slug: "security",
|
||||
desc: "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
badge: "TRACK: SECURITY"
|
||||
},
|
||||
{
|
||||
title: "Homelab Infrastructure",
|
||||
slug: "homelab",
|
||||
desc: "Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
|
||||
badge: "TRACK: INFRA"
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
/**
|
||||
* Custom Eleventy Filters for River Rooks Blog
|
||||
*/
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const filters = {
|
||||
postDate,
|
||||
isoDate,
|
||||
limit,
|
||||
filterTagList,
|
||||
readingTime,
|
||||
extractHeadings
|
||||
};
|
||||
|
||||
export default filters;
|
||||
@@ -0,0 +1,34 @@
|
||||
<!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">
|
||||
|
||||
{% 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>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
{% extends "layouts/base.njk" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-container">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,93 @@
|
||||
{% extends "layouts/base.njk" %}
|
||||
|
||||
{% 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 | 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>
|
||||
<div class="sidebar-stats-box" style="padding: 0; background: transparent; border: none;">
|
||||
<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 | 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;">
|
||||
← 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>•</span>
|
||||
<span class="meta-item">{{ content | readingTime(readingTime) }}</span>
|
||||
{% if difficulty %}
|
||||
<span>•</span>
|
||||
<span class="meta-item" style="color: var(--accent-amber);">DIFFICULTY: {{ difficulty }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="markdown-prose">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,61 @@
|
||||
{% 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 }} • {{ 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 }}
|
||||
</p>
|
||||
|
||||
<!-- Terminal Code Preview Snippet -->
|
||||
<div class="terminal-code-box">
|
||||
<div class="code-titlebar">
|
||||
<span class="code-title">
|
||||
<span style="color: var(--accent-cyan);">></span>
|
||||
<span>init_cluster.sh</span>
|
||||
</span>
|
||||
<span>POSIX sh</span>
|
||||
</div>
|
||||
<div class="code-content">
|
||||
<pre><code class="language-bash"># System initialization and node health check
|
||||
./cluster.sh --verify-nodes
|
||||
curl -s https://api.local/healthz | jq .status</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 →</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,13 @@
|
||||
<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>•</span>
|
||||
<span>THEME: NATIVE TERMINAL DARK</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-right">
|
||||
<span>© {{ currentYear or "2026" }} {{ site.author }}</span>
|
||||
<a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" class="footer-link">Source Forge ↗</a>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -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 %}> {% endif %}{{ item.label }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
</header>
|
||||
@@ -0,0 +1,24 @@
|
||||
<section class="hero-tile" aria-label="Blog Mission & Overview">
|
||||
<div class="hero-pretitle">TECHNICAL PUBLICATION REPOSITORY • ~/NOTES/INDEX.MD</div>
|
||||
<h1 class="hero-main-title">{{ heroTitle or site.description }}</h1>
|
||||
<p class="hero-landing-text">
|
||||
{{ heroText or "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation." }}
|
||||
</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>
|
||||
@@ -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 }}</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>
|
||||
@@ -0,0 +1,54 @@
|
||||
<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>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>
|
||||
</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>
|
||||
@@ -0,0 +1,298 @@
|
||||
/* ==========================================================================
|
||||
Article Workspace & Markdown Prose (components/article.css)
|
||||
========================================================================== */
|
||||
|
||||
/* Left Sticky Sidebar */
|
||||
.article-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--tile-gap);
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/* ==========================================================================
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* ==========================================================================
|
||||
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;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/* ==========================================================================
|
||||
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";
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 • 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>
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
---
|
||||
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 • NODE GATEWAY</div>
|
||||
<h1 class="hero-main-title">Engineering Sovereign Infrastructure & Technical Notes</h1>
|
||||
|
||||
<div class="hero-landing-text">
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum.
|
||||
</p>
|
||||
<p>
|
||||
Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||
</p>
|
||||
</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> •
|
||||
<a href="https://git.{{ site.domain }}" target="_blank" rel="noopener noreferrer" style="color: inherit; text-decoration: none;">[2] Gitea Repos</a> •
|
||||
<span>[3] Public Tools</span> •
|
||||
<span>[4] Cluster Telemetry</span>
|
||||
</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/ • {{ 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">> Technical Notes & Engineering Dispatches</a>
|
||||
<p class="blog-feature-desc">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.
|
||||
</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) }}] →</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 • LOAD: 0.18</span>
|
||||
</div>
|
||||
<div class="node-stat-row">
|
||||
<span class="node-name">NODE-02 (Storage Array)</span>
|
||||
<span class="node-load">ONLINE • POOL: 98% HEALTH</span>
|
||||
</div>
|
||||
<div class="node-stat-row">
|
||||
<span class="node-name">GATEWAY-01 (Edge Ingress)</span>
|
||||
<span class="node-load">ONLINE • ROUTING ACTIVE</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
layout: layouts/post.njk
|
||||
title: "Infrastructure Notes & Systems Deployment Guide"
|
||||
date: 2026-07-15
|
||||
tags:
|
||||
- homelab
|
||||
- networking
|
||||
- templates
|
||||
excerpt: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident."
|
||||
readingTime: "4 MIN READ"
|
||||
featured: false
|
||||
difficulty: "BEGINNER"
|
||||
---
|
||||
|
||||
## 1.0 Topology Overview
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua:
|
||||
|
||||
```text
|
||||
[Gateway Ingress]
|
||||
|
|
||||
[Edge Router]
|
||||
|
|
||||
+-----+-----+
|
||||
| |
|
||||
[Node 01] [Node 02]
|
||||
```
|
||||
|
||||
## 2.0 Deployment Configuration
|
||||
|
||||
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
image: service:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:80"
|
||||
```
|
||||
|
||||
## 3.0 Verification Testing
|
||||
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur:
|
||||
|
||||
```sh
|
||||
curl -s http://localhost:8080/health
|
||||
```
|
||||
|
||||
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
layout: layouts/post.njk
|
||||
title: "Sample Technical Dispatch: Systems Architecture & Implementation"
|
||||
date: 2026-07-20
|
||||
tags:
|
||||
- systems
|
||||
- architecture
|
||||
- templates
|
||||
excerpt: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam."
|
||||
readingTime: "6 MIN READ"
|
||||
featured: true
|
||||
difficulty: "INTERMEDIATE"
|
||||
---
|
||||
|
||||
## 1.0 Executive Summary
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla.
|
||||
|
||||
Curabitur sodales ligula in libero. Sed dignissim lacinia nunc. Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis, luctus non, massa.
|
||||
|
||||
<div class="markdown-callout callout-note">
|
||||
<div class="callout-title">[!NOTE] Operational Directives</div>
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam.</p>
|
||||
</div>
|
||||
|
||||
## 2.0 System Parameters & Specifications
|
||||
|
||||
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam nec ante. Sed lacinia, urna non tincidunt mattis, tortor neque adipiscing diam, a cursus ipsum ante quis turpis:
|
||||
|
||||
### 2.1 Interface Configuration Matrix
|
||||
|
||||
| Parameter ID | Signal Name | Protocol Bus | Expected Value |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **0x01** | `SYS_CLOCK` | SPI Primary | 100 MHz |
|
||||
| **0x02** | `DATA_IN` | Serial Bus | Bidirectional |
|
||||
| **0x03** | `CONTROL_REG` | I2C Header | 0x3F Active |
|
||||
| **0x04** | `GROUND_REF` | Chassis GND | 0.0V Nominal |
|
||||
|
||||
<div class="markdown-callout callout-warning">
|
||||
<div class="callout-title">[!WARNING] Configuration Voltage Boundary</div>
|
||||
<p>Sed nisi. Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta.</p>
|
||||
</div>
|
||||
|
||||
## 3.0 Execution Workflow
|
||||
|
||||
Nulla quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta:
|
||||
|
||||
```sh
|
||||
#!/bin/sh
|
||||
# Sample deployment and verification routine
|
||||
set -eu
|
||||
|
||||
echo "[+] Initializing subsystem..."
|
||||
check_environment() {
|
||||
test -f /etc/config.json && echo "Config verified."
|
||||
}
|
||||
|
||||
check_environment
|
||||
```
|
||||
|
||||
Press <kbd>Ctrl</kbd> + <kbd>C</kbd> to interrupt operations safely at any point.
|
||||
|
||||
## 4.0 Telemetry & Verification
|
||||
|
||||
Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum lacinia arcu eget nulla:
|
||||
|
||||
- **Metric A:** `1,048,576 ops/sec`
|
||||
- **Metric B:** `SHA256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||
- **Latency:** `< 2.5ms` nominal
|
||||
|
||||
## 5.0 Audit Checklist
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit:
|
||||
|
||||
- [x] Baseline architectural parameters verified
|
||||
- [x] Initial configuration and unit tests passed
|
||||
- [x] System telemetry within expected boundaries
|
||||
- [ ] Production deployment and final review
|
||||
@@ -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}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
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 and excludes Nginx", () => {
|
||||
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 serve with npm
|
||||
assert.match(content, /RUN\s+npm\s+run\s+build/, "Dockerfile must run npm build");
|
||||
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");
|
||||
assert.match(content, /CMD\s*\[\s*["']npm["']\s*,\s*["']start["']\s*\]/, "Dockerfile must start via npm start");
|
||||
});
|
||||
|
||||
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:\s*"?\$\{PORT:-8080\}"?/, "docker-compose.yml must pass dynamic PORT build arg and env");
|
||||
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");
|
||||
});
|
||||
@@ -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>"), []);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
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", () => {
|
||||
assert.ok(fs.existsSync(postsDir), "src/posts directory must exist");
|
||||
const postFiles = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
|
||||
|
||||
assert.ok(postFiles.length > 0, "There must be at least one published post");
|
||||
|
||||
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`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
@@ -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:/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
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.testcluster.internal");
|
||||
assert.equal(gitService.tag, "git.testcluster.internal:2222");
|
||||
|
||||
const chefService = site.services.find((s) => s.name === "CyberChef");
|
||||
assert.ok(chefService, "CyberChef service exists");
|
||||
assert.equal(chefService.url, "https://chef.testcluster.internal");
|
||||
|
||||
// Verify navigation links
|
||||
const navHome = site.nav.find((n) => n.label === "Home");
|
||||
assert.ok(navHome, "Nav Home link exists");
|
||||
assert.equal(navHome.url, "/");
|
||||
|
||||
const navGit = site.nav.find((n) => n.label === "Gitea");
|
||||
assert.ok(navGit, "Nav Gitea link exists");
|
||||
assert.equal(navGit.url, "https://git.testcluster.internal");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user