forked from colin/resume
2
0
Fork 0

Compare commits

...

92 Commits
main ... main

Author SHA1 Message Date
Leopere bd7aca54ee Avoid CORP block by serving matomo.js same-origin
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Fetch matomo.js to repo and serve as /matomo.js
- Update header include to load /matomo.js while keeping trackerUrl to metrics.nixc.us
2025-08-07 17:36:56 -04:00
Leopere 8909ece16c Disable CSP injection logic in update-csp-hashes.sh (no-op)
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-08-07 17:28:21 -04:00
Leopere d785776966 Remove CSP entirely for Matomo compatibility
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Remove CSP header from Caddyfile.local
- Strip CSP meta tags from all HTML
- Stop update-csp-hashes.sh from injecting CSP meta tags
2025-08-07 17:17:00 -04:00
Leopere 1f88b2c1b3 fixup
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-08-07 17:07:56 -04:00
Leopere f882ab7d3b Execute script tags in included HTML so Matomo runs
ci/woodpecker/push/woodpecker Pipeline failed Details
- Recreate and execute <script> tags from header/footer includes and head includes
- Ensures Matomo inline snippet runs when header is injected via includes.js
2025-08-07 17:04:31 -04:00
Leopere 494e4a568e Fix Matomo CSP by adding inline script hash
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added SHA256 hash for Matomo inline script to CSP
- Hash: sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM=
- This should allow the Matomo tracking script to execute properly
- Updated all HTML files with new CSP headers
2025-07-30 14:51:34 -04:00
Leopere d7eea4853f Add Matomo analytics tracking code to all pages
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added Matomo tracking script to header.html include
- Updated CSP to allow metrics.nixc.us domain for script-src, img-src, and connect-src
- Modified update-csp-hashes.sh to include metrics.nixc.us in CSP directives
- Updated all HTML files with new CSP headers
- Tracking code will now work across all portfolio pages
2025-07-30 14:14:24 -04:00
Leopere 67a51b14e4 Add story placeholders and QR tool navigation - Add home infrastructure and nuclear DNS story placeholders - Add QR code tool to navigation dropdown - Update stories index with new entries - Rebuild and test local environment
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-25 10:48:45 -04:00
Leopere 60d1208d9d Update latest changes
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-23 10:56:48 -04:00
colin 39d5d2c6aa Add markdown tool link to navigation dropdown
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-07 11:25:16 -04:00
colin 008af14d12 Update OhMyForm timeline to reflect official archival in October 2024
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 20:05:00 -04:00
colin 8757dd1c37 Update OhMyForm references to indicate it's a sunset project succeeded by Formbricks
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 19:51:37 -04:00
colin 35b7a11faf Update sitemap timestamps
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 19:37:52 -04:00
colin 681b195b24 Fix story link contrast for better accessibility 2025-07-06 19:33:07 -04:00
colin 6d974577ad Fix story link contrast for better accessibility
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 19:12:28 -04:00
colin 066703bd42 Fix accessibility issues and update sitemap domain for local testing
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 18:58:20 -04:00
colin 9ff754b262 Fix navigation contrast in dark mode for better accessibility
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 18:39:26 -04:00
colin 2c43fe7784 Fix duplicate footer issues and remove npm dependency
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 18:36:59 -04:00
colin f0d296f108 Add WCAG 2.1 AAA accessibility testing framework 2025-07-06 16:14:32 -04:00
colin 869b08ec0e Integrate CSP hash update process into test framework 2025-07-06 16:05:03 -04:00
colin 2b37907c27 Fix URL for Improving MI Practices website
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 15:25:28 -04:00
colin 6c1e85c0e5 Update favicon with professional resume design
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 15:08:42 -04:00
colin ea3a2b95e4 Add placeholder story pages for all sections on the main page 2025-07-06 14:46:41 -04:00
colin 7071c08a30 Add procedurally generated favicon with multiple formats and update includes.js to load favicon links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 14:16:30 -04:00
colin 83e02bab67 Add cross-linking from main page to story pages with styled read-more links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 14:09:09 -04:00
colin f1da69a7f9 Fix theme toggle button by moving initialization to includes.js
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 12:20:39 -04:00
colin 26136fef61 Fix CSV tool by adding papaparse.min.js to Dockerfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:52:50 -04:00
colin b1e4cbaa4e Fix viperwire.html to use standard header and footer structure
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:49:56 -04:00
colin 633302d1e6 Add placeholder story pages with 'Coming Soon' notices
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:46:56 -04:00
colin f62ca2219e Fix production site: Copy includes and stories directories in Dockerfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:40:58 -04:00
colin 69bd8f2bb4 Fix production issue: Add missing includes.js file to Dockerfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:34:08 -04:00
colin f6f9aeda99 Fix Docker build: Remove invalid COPY syntax and use volume mounts for optional directories
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:27:37 -04:00
colin 2d50f99b65 Fix Docker build issues: Replace shasum with sha256sum and handle Caddyfile path correctly
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 11:24:00 -04:00
colin c4a45ef8fd Fix navigation menu closing too quickly with transition delay and keyboard support
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 11:20:32 -04:00
colin a1e2afabb5 Save all current changes to resume project
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 11:16:43 -04:00
Your Name be50c5de9c fixing deploy step
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-06-05 13:11:42 -04:00
colin 34f659be3f Update stack.staging.yml
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-06-05 13:04:40 -04:00
colin 7aa1337538 Update stack.staging.yml
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-06-05 13:04:10 -04:00
colin aa3afc9b4c Update stack.production.yml
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-06-05 13:03:46 -04:00
Leopere 77517079a7 update to rules and tests
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-05-10 17:14:38 -04:00
Leopere 5ac1c24481 Fix CSP for PDF download button by moving to external script
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-05-10 17:04:31 -04:00
Leopere 911842dc06 Fix CSP for PDF download button by moving to external script
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-05-10 16:27:15 -04:00
Leopere 04e5a9fa34 Add download as PDF button and update test configurations
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-05-10 16:12:46 -04:00
Leopere 4f9596bbee fix: update Cal.com meeting URL to correct username
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-22 21:39:33 -04:00
Leopere ab493a89f8 fix: remove 60-minute meeting option due to URL issues
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-22 21:06:25 -04:00
Leopere ff0e765b31 fix: update 60-minute meeting URL to correct format
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-22 20:53:41 -04:00
Your Name 10e340c341 Add Cal.com calendar meeting links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-21 22:14:57 -04:00
Leopere cd94db9c03 temporarily disable HSTS to resolve certificate provisioning issues
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-15 16:33:47 -04:00
colin 0b693d7d2b Update docker/resume/index.html
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-03 11:37:33 -04:00
Your Name 7a5666ffda Add Subresource Integrity (SRI) hashes to script and style tags
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:55:44 -04:00
Your Name d2e6cc7db8 Update CSP to use hash for styles.css and remove unsafe-inline
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:54:33 -04:00
Your Name 3853b6ba6f Remove unsupported protocols directive (HTTP/2 enabled by default in Caddy 2)
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:49:32 -04:00
Your Name b93b8564de Update Dockerfile.production with multi-stage build and security hardening
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:41:26 -04:00
Your Name e97ef265ff Update Docker configuration with production build and security hardening
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:40:52 -04:00
Your Name ad26460c92 Add production Dockerfile with multi-stage build and security hardening
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:36:21 -04:00
Your Name 2cdd7341c0 Remove Brotli compression (not available in default Caddy image)
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:35:43 -04:00
Your Name ab5f8e774e Update CSP to use script hashes instead of unsafe-inline
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:31:57 -04:00
Your Name a5583c3afe Remove test results from git tracking and update .gitignore
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:30:32 -04:00
Your Name 3a9068b883 Remove TLS directives (handled by reverse proxy)
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:24:43 -04:00
Your Name 3d0dd2c361 Optimize Caddyfile for better performance
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:24:24 -04:00
Your Name 3598c99b9f Update resume with Oh My Form Docker pulls and comprehensive experience
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:18:30 -04:00
Your Name 4434650aac Add Oh My Form Docker pulls achievement
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:00:48 -04:00
Your Name 0f81e0318e Add utils.js to Docker build and update CSP with hash
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 05:05:59 -04:00
Your Name 630ef90df1 Add utils.js with SHA-256 hash in CSP
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 05:01:34 -04:00
Your Name cc0142f000 Add local header testing infrastructure
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:58:51 -04:00
Your Name 0c3c133431 Add comprehensive README with testing instructions
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:43:41 -04:00
Your Name 8c35ab5296 Add Playwright configuration file
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:43:14 -04:00
Your Name a10eea979f Add Playwright and Lighthouse testing infrastructure
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:42:57 -04:00
Your Name 39caf88782 Enhance theme toggle accessibility and confirm auto mode default
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:39:44 -04:00
Your Name 015f8ce76f Improve link accessibility with underlines and better contrast
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:36:03 -04:00
Your Name 3e2a32c1cf Revert CSP configuration to stable version without unsafe-inline
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:32:25 -04:00
Your Name 90b9d2dd1b Fix theme toggle CSS and add data-theme attribute handling
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:29:22 -04:00
Your Name 7b80d9dfa0 Comment out wait-for-deploy-production step in Woodpecker CI config
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:27:35 -04:00
Your Name 71e142b82e Fix Docker configuration to include theme.js and styles.css files
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:23:32 -04:00
Your Name 1c328df0c7 Switch from SRI to nonce-based CSP approach for better script handling
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:20:58 -04:00
Your Name 885914812d Debug CSP: temporarily allow inline scripts and remove SRI requirement
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:16:50 -04:00
Your Name 905b480a2e Fix SRI hash for theme.js to match deployed content
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:14:39 -04:00
Your Name f739edc7eb Add form-action 'none' to CSP for additional security
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:11:35 -04:00
Your Name 0b46750148 Enhance CSP with default-src 'none' for maximum security
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:08:37 -04:00
Your Name ac3d30d597 Fix CSP issues by moving inline script to external file and adding SRI
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:04:47 -04:00
colin 0fbd77f073 Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 00:23:16 -04:00
colin a62baf40e1 Update docker/resume/index.html
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 00:22:58 -04:00
colin ece9887a5b Add docker/resume/styles.css
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 00:22:44 -04:00
colin bfae2029b8 Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 00:16:14 -04:00
colin 2e9c196d8a Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 00:14:49 -04:00
colin 12f3ca9a3b Update docker/resume/index.html
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-30 23:41:58 -04:00
colin 18ece0205f Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 22:46:05 -04:00
colin 759dfa290e Update docker/resume/resume.html
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 22:45:41 -04:00
colin 1e46b4cc50 Update .woodpecker.yml
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:30:20 -04:00
colin 79c5a6d935 Update docker/resume/nginx.conf
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:28:01 -04:00
colin de1f9d3364 Update docker/resume/Dockerfile
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:27:41 -04:00
colin f963abe2ed Update docker/resume/resume.html
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:22:47 -04:00
119 changed files with 12882 additions and 346 deletions

View File

@ -0,0 +1,17 @@
# HTML Standards
## Accessibility Requirements
- All images must have alt text
- Proper heading structure (h1, h2, etc.) must be maintained
- ARIA attributes must be used where appropriate
- Color contrast must meet WCAG 2.1 Level AAA compliance
## Security
- All script and style tags must include integrity attributes
- External links must include rel="noopener noreferrer" attributes
- No inline scripts or styles without proper CSP nonce/hash
## Structure
- Use includes.js for template components when possible
- Reference includes from [docker/resume/includes/](mdc:docker/resume/includes/)
- Follow the pattern in [docker/resume/index-with-includes.html](mdc:docker/resume/index-with-includes.html)

View File

@ -0,0 +1,15 @@
# JavaScript and CSS Standards
## Security Requirements
- All JS and CSS files must be hashed for integrity checks
- Hashes must be updated in both HTML and Caddyfile CSP headers
- Use `shasum -a 256` followed by base64 encoding for hash generation
## File References
- JavaScript files should be referenced in HTML with integrity attributes
- CSS files should be referenced with integrity attributes
- The CSP in [docker/resume/Caddyfile](mdc:docker/resume/Caddyfile) must include these hashes
## Automation
- Run `docker/resume/update-csp-hashes.sh` after modifying any JS or CSS file
- Verify hashes match between HTML and Caddyfile before deployment

View File

@ -0,0 +1,23 @@
# Project Guidelines
## Core Principles
- Use the `./build-test-deploy.sh` script for all build, test, and deployment operations
- Never use interactive CLI prompts in any scripts or processes
- Avoid creating duplicate files - use git for version control instead
- Don't modify Docker files unless absolutely necessary, especially production Dockerfiles
## File Structure
- Main resume content is in `docker/resume/index.html`
- Styles are in `docker/resume/styles.css`
- Server configuration is in `docker/resume/Caddyfile`
## Security Requirements
- All JS and CSS assets must have integrity hashes in HTML and CSP headers
- The Caddyfile must include proper Content-Security-Policy headers
- Use `update-csp-hashes.sh` to update security hashes
## Testing Standards
- All changes require passing tests before deployment
- Tests must pass for both mobile and desktop viewports
- Maintain Lighthouse scores: 100/100 for accessibility and SEO
- Tests must be meaningful with actual assertions, not placeholders

49
.cursorignore Normal file
View File

@ -0,0 +1,49 @@
# Ignore all files and directories in the root except for the docker directory.
/build-test-deploy.sh
/docker-compose.production.yml
/docker-compose.staging.yml
/package-lock.json
/package.json
/playwright.config.js
/README.md
/stack.production.yml
/stack.staging.yml
# /tests/
Dockerfile.production
# Infrastructure and deployment files
/docker-compose*.yml
/stack.*.yml
/build-test-deploy.sh
/package*.json
/playwright*.js
/playwright-report/
/test-results/
/test_output.log
# Docker infrastructure
/docker/resume/Dockerfile*
/docker/resume/Caddyfile
/docker/resume/package.json
# Testing infrastructure
/tests/
/playwright-report/
/test-results/
/test_output.log
# Documentation and config
/README.md
/.gitignore
# Additional infrastructure files found in root
/.woodpecker.yml
/node_modules/
/.cursor/
/.git/
/package-lock.json
/playwright.config.js
/stack.production.yml
/stack.staging.yml
/docker-compose.production.yml
/docker-compose.staging.yml
/build-test-deploy.sh

15
.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
# Test results and reports
tests/reports/
playwright-report/
test-results/
# Node modules
node_modules/
# IDE files
.vscode/
.idea/
# OS files
.DS_Store
Thumbs.db

View File

@ -121,22 +121,22 @@ steps:
event: [push, cron]
# Wait for Deploy Completion
wait-for-deploy-production:
name: wait-for-deploy-production
image: woodpeckerci/plugin-git
commands:
- echo "Waiting for deploy step to complete rollout."
- sleep 60
when:
branch: main
event: push
# wait-for-deploy-production:
# name: wait-for-deploy-production
# image: woodpeckerci/plugin-git
# commands:
# - echo "Waiting for deploy step to complete rollout."
# - sleep 60
# when:
# branch: main
# event: push
# Post-Deployment Smoke Tests
post-deploy-smoke-tests-git-nixc-us:
name: run-post-deploy-smoke-tests-git-nixc-us
image: codeberg.org/nixius/playwright:latest
environment:
BASE_URL: "https://git.nixc.us"
when:
branch: main
event: push
# post-deploy-smoke-tests-git-nixc-us:
# name: run-post-deploy-smoke-tests-git-nixc-us
# image: codeberg.org/nixius/playwright:latest
# environment:
# BASE_URL: "https://git.nixc.us"
# when:
# branch: main
# event: push

96
README.md Normal file
View File

@ -0,0 +1,96 @@
# Colin Knapp Portfolio Resume
A professional portfolio website with accessibility features and automated testing.
## Features
- Responsive design for all device sizes
- Light/dark mode toggle with system preference detection
- High contrast accessible design
- Keyboard navigable interface
- WCAG 2.1 Level AA+ compliant
## Development
### Prerequisites
- Node.js (v14 or higher)
- npm (v6 or higher)
### Setup
1. Clone the repository:
```bash
git clone git@git.nixc.us:colin/resume.git
cd resume
```
2. Install dependencies:
```bash
npm install
```
3. Set up Playwright browsers:
```bash
npm run setup
```
### Local Development
To serve the site locally for development:
```bash
npm run serve
```
This will start a local development server at http://localhost:8080.
### Testing
The project includes automated testing using Playwright for functional tests and Lighthouse for performance and accessibility audits.
#### Running all tests
```bash
npm test
```
#### Running only Playwright tests
```bash
npm run test:playwright
```
#### Running only Lighthouse tests
```bash
npm run test:lighthouse
```
### Docker Deployment
The site is deployed using Docker and Caddy. The deployment configuration is in the `docker` directory.
To build and run the Docker container locally:
```bash
cd docker
docker build -t resume:latest ./resume/
docker run -p 8080:8080 resume:latest
```
## Accessibility
This site is designed to meet WCAG 2.1 Level AA+ standards. Key accessibility features include:
- Proper heading hierarchy
- Keyboard navigable interface with visible focus states
- Color contrast ratios that exceed WCAG AA requirements
- Semantic HTML structure
- Accessible form controls and ARIA attributes
- Light/dark mode support with system preference detection
- Responsive design for all device sizes
## License
ISC © Colin Knapp

75
build-test-deploy.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/bash
set -e
IMAGE_NAME="resume:latest"
CONTAINER_NAME="resume_test_container"
DOCKER_DIR="docker"
RESUME_DIR="$DOCKER_DIR/resume"
# Note: We no longer need to manually update the CSS hash here
# as it's handled by the update-csp-hashes.sh script during Docker build
# Build Docker image
cd "$DOCKER_DIR"
echo "Building Docker image..."
docker build -t $IMAGE_NAME ./resume/
# Stop and remove any previous container
if [ $(docker ps -aq -f name=$CONTAINER_NAME) ]; then
echo "Removing previous test container..."
docker rm -f $CONTAINER_NAME || true
fi
# Ensure port 8080 is free
echo "Ensuring port 8080 is free..."
lsof -i :8080 | grep LISTEN | awk '{print $2}' | xargs kill -9 || true
# Run Docker container in the background
echo "Starting Docker container..."
docker run -d --name $CONTAINER_NAME -p 8080:8080 $IMAGE_NAME
# Wait for the server to be ready
MAX_TRIES=20
TRIES=0
until curl -s http://localhost:8080/ > /dev/null; do
TRIES=$((TRIES+1))
if [ $TRIES -ge $MAX_TRIES ]; then
echo "Server did not start in time."
docker rm -f $CONTAINER_NAME
exit 1
fi
echo "Waiting for server... ($TRIES/$MAX_TRIES)"
sleep 2
done
echo "Server is up. Running tests..."
cd ..
npm install
npm run setup
# Run tests and save output for AI parsing
if npm test > test_output.log 2>&1; then
echo "Tests passed. Committing and pushing changes."
git add .
git commit -m "Automated build, test, and deploy"
git push
else
echo "Tests failed. Not deploying."
cat test_output.log
docker rm -f $CONTAINER_NAME
exit 1
fi
# Optionally open report in browser if available, but don't require interaction
echo "Test output saved to test_output.log for AI parsing."
if command -v open >/dev/null 2>&1; then
echo "Opening HTML report in browser (if available)..."
open http://localhost:9323 || echo "Could not open browser automatically. Please visit http://localhost:9323 to view the report."
else
echo "Browser opening not supported. Report available at http://localhost:9323 if a server is running."
fi
echo "Cleaning up Docker container..."
docker rm -f $CONTAINER_NAME
echo "Done."

View File

@ -1,8 +1,6 @@
version: '3.8'
services:
resume:
build:
context: ./docker/resume/
dockerfile: Dockerfile
dockerfile: Dockerfile.production
image: git.nixc.us/colin/resume:production

View File

@ -1,8 +1,8 @@
version: '3.8'
services:
resume:
build:
context: ./docker/resume/
dockerfile: Dockerfile
image: git.nixc.us/colin/resume:staging
ports:
- "8080:8080"

25
docker/package-lock.json generated Normal file
View File

@ -0,0 +1,25 @@
{
"name": "docker",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "docker",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"xmldom": "^0.6.0"
}
},
"node_modules/xmldom": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
}
}
}

12
docker/package.json Normal file
View File

@ -0,0 +1,12 @@
{
"name": "docker",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

101
docker/resume/Caddyfile Normal file
View File

@ -0,0 +1,101 @@
colinknapp.com {
root * .
file_server
encode gzip
# Performance optimizations
header {
# Remove default Caddy headers
-Server
-X-Powered-By
# HSTS
Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Basic security headers
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
# Cross-origin isolation headers
Cross-Origin-Embedder-Policy "require-corp"
Cross-Origin-Resource-Policy "same-origin"
Cross-Origin-Opener-Policy "same-origin"
# Cache control for static assets
Cache-Control "public, max-age=31536000, immutable"
# CSP with hashes for scripts and styles
Content-Security-Policy "default-src 'none'; script-src 'self' https://metrics.nixc.us 'sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM=' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=' 'sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA=' 'sha256-kdaXPEOwTw3zyiuCzGv1vpohcW9SqOWq8k6gy2OWgtI='; img-src 'self' https://metrics.nixc.us data:; font-src 'self' data:; connect-src 'self' https://metrics.nixc.us; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
}
# Handle 404s
handle_errors {
respond "{err.status_code} {err.status_text}"
}
# Logging
log {
output stdout
format json
}
# Enable static file serving with caching
file_server {
precompressed
browse
}
}
# Local development server
:8080 {
root * .
file_server
encode gzip
# Performance optimizations
header {
# Remove default Caddy headers
-Server
-X-Powered-By
# Basic security headers
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
# Cross-origin isolation headers
Cross-Origin-Embedder-Policy "require-corp"
Cross-Origin-Resource-Policy "same-origin"
Cross-Origin-Opener-Policy "same-origin"
# Cache control for static assets
Cache-Control "public, max-age=31536000, immutable"
# CSP with hashes for scripts and styles
Content-Security-Policy "default-src 'none'; script-src 'self' https://metrics.nixc.us 'sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM=' 'sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-uTJNJlctGr5GxR5DKnz1Ex31vH0TR93OFGloxbHe65g=' 'sha256-fOEWMJmrMxKbP5wElIXmDNUlfs6BSn+E9zt81T0Rysg=' 'sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=' 'sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=' 'sha256-1ZUvhca3M/N6hch4NrdPufDPLTnANOpJ4hfsZgRykgg=' 'sha256-UUDFHb6NI63nBRS2EmyJq4giwjTQGYPq7uSTB4UQnPc=' 'sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=' 'sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM=' 'sha256-8CNR2aPoRsO94LHwXXZzxijfMf15BfwUewt8hvVbPcE='; style-src 'self' 'sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=' 'sha256-807UZmWvd6eLc8xVckZkNX6CRP9WV8MzHURc5BgtRWo=' 'sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=' 'sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=' 'sha256-2EA12+9d+s6rrc0rkdIjfmjbh6p2o0ZSXs4wbZuk/tA=' 'sha256-kdaXPEOwTw3zyiuCzGv1vpohcW9SqOWq8k6gy2OWgtI='; img-src 'self' https://metrics.nixc.us data:; font-src 'self' data:; connect-src 'self' https://metrics.nixc.us; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"
}
# Handle 404s
handle_errors {
respond "{err.status_code} {err.status_text}"
}
# Logging
log {
output stdout
format json
}
# Enable static file serving with caching
file_server {
precompressed
browse
}
}

View File

@ -0,0 +1,43 @@
# =====================================================================
# Caddyfile.local - Local development server configuration
# =====================================================================
# To manage the server, use the unified script:
# ./caddy.sh start # Start the server
# ./caddy.sh stop # Stop the server
# ./caddy.sh restart # Restart the server
# ./caddy.sh status # Check server status
#
# DO NOT run caddy directly from other directories as it may not find
# this configuration file correctly.
# =====================================================================
:8080 {
root * .
file_server
encode gzip
# Performance optimizations
header {
# Remove default Caddy headers
-Server
-X-Powered-By
# Basic security headers
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy
Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"
# Cross-origin isolation headers
Cross-Origin-Embedder-Policy "require-corp"
Cross-Origin-Resource-Policy "same-origin"
Cross-Origin-Opener-Policy "same-origin"
# Cache control for static assets
Cache-Control "public, max-age=31536000, immutable"
# CSP with hashes for scripts and styles
}
}

View File

@ -1,5 +1,21 @@
FROM nginx:alpine
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/
COPY resume.html /usr/share/nginx/html/
FROM caddy:2.7-alpine
# Install dependencies
RUN apk add --no-cache nodejs bash
# Set working directory
WORKDIR /srv
# Copy website files
COPY . /srv
# Run all update scripts (sitemap, navigation, stories, CSP hashes, accessibility fixes)
RUN cd /srv && \
chmod +x update-all.sh && \
./update-all.sh
# Expose port
EXPOSE 8080
# Start Caddy with the local Caddyfile
CMD ["caddy", "run", "--config", "/srv/Caddyfile.local"]

View File

@ -0,0 +1 @@
FROM git.nixc.us/colin/resume:staging

37
docker/resume/README.md Normal file
View File

@ -0,0 +1,37 @@
# Resume Website
## Local Development
To run the local development server:
1. Navigate to the `docker/resume` directory
2. Use the unified Caddy management script:
```
./caddy.sh start # Start the server
./caddy.sh stop # Stop the server
./caddy.sh restart # Restart the server
./caddy.sh status # Check server status
```
3. Open http://localhost:8080 in your browser
### Important Notes
- Always use the `caddy.sh` script to manage the server
- Do not run Caddy directly from other directories
## Content Security Policy (CSP)
When adding new scripts or styles, you need to update the CSP hashes:
1. Make your changes to JS/CSS files
2. Run the update script:
```
./update-csp-hashes.sh
```
3. Restart the server using `./caddy.sh restart`
## Tools
- **CSV Viewer**: A tool for viewing CSV data in tabular format
- Available at http://localhost:8080/one-pager-tools/csv-tool.html
- Simply paste CSV data to view it as a formatted table

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

109
docker/resume/caddy.log Normal file
View File

@ -0,0 +1,109 @@
{"level":"info","ts":1751739278.708652,"msg":"maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined"}
{"level":"info","ts":1751739278.70898,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":7730941132,"previous":9223372036854775807}
{"level":"info","ts":1751739278.709314,"msg":"using config from file","file":"Caddyfile"}
{"level":"info","ts":1751739278.7104902,"msg":"adapted config to JSON","adapter":"caddyfile"}
{"level":"warn","ts":1751739278.710495,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"Caddyfile","line":2}
{"level":"info","ts":1751739278.7137249,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1751739278.713872,"logger":"http.auto_https","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
{"level":"info","ts":1751739278.7138822,"logger":"http.auto_https","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
{"level":"info","ts":1751739278.714001,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0x140005ff300"}
{"level":"info","ts":1751739278.714232,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
{"level":"info","ts":1751739278.714817,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
{"level":"warn","ts":1751739278.714853,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":8080"}
{"level":"warn","ts":1751739278.7148569,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":8080"}
{"level":"info","ts":1751739278.714859,"logger":"http.log","msg":"server running","name":"srv1","protocols":["h1","h2","h3"]}
{"level":"warn","ts":1751739278.7148852,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":80"}
{"level":"warn","ts":1751739278.7148879,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":80"}
{"level":"info","ts":1751739278.714891,"logger":"http.log","msg":"server running","name":"remaining_auto_https_redirects","protocols":["h1","h2","h3"]}
{"level":"info","ts":1751739278.714894,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["colinknapp.com"]}
{"level":"info","ts":1751739278.715245,"msg":"autosaved config (load with --resume flag)","file":"/Users/computerpro/Library/Application Support/Caddy/autosave.json"}
{"level":"info","ts":1751739278.7152488,"msg":"serving initial configuration"}
{"level":"info","ts":1751739278.722911,"logger":"tls.obtain","msg":"acquiring lock","identifier":"colinknapp.com"}
{"level":"info","ts":1751739278.7230961,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/Users/computerpro/Library/Application Support/Caddy","instance":"bb1987a6-f2f6-4230-a2aa-e3b16b9f988e","try_again":1751825678.7230961,"try_again_in":86399.999999833}
{"level":"info","ts":1751739278.723574,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"info","ts":1751739278.727818,"logger":"tls.obtain","msg":"lock acquired","identifier":"colinknapp.com"}
{"level":"info","ts":1751739278.72785,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"colinknapp.com"}
{"level":"info","ts":1751739278.729119,"logger":"http","msg":"waiting on internal rate limiter","identifiers":["colinknapp.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}
{"level":"info","ts":1751739278.729126,"logger":"http","msg":"done waiting on internal rate limiter","identifiers":["colinknapp.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}
{"level":"info","ts":1751739278.729134,"logger":"http","msg":"using ACME account","account_id":"https://acme-v02.api.letsencrypt.org/acme/acct/2509915601","account_contact":[]}
{"level":"info","ts":1751739279.476387,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739280.142849,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739280.142943,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"order":"https://acme-v02.api.letsencrypt.org/acme/order/2509915601/403011757641","attempt":1,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739281.218956,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"colinknapp.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"HTTP 429 urn:ietf:params:acme:error:rateLimited - too many failed authorizations (5) for \"colinknapp.com\" in the last 1h0m0s, retry after 2025-07-05 18:26:22 UTC: see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-hostname-per-account"}
{"level":"error","ts":1751739281.2190888,"logger":"tls.obtain","msg":"will retry","error":"[colinknapp.com] Obtain: [colinknapp.com] creating new order: attempt 1: https://acme-v02.api.letsencrypt.org/acme/new-order: HTTP 429 urn:ietf:params:acme:error:rateLimited - too many failed authorizations (5) for \"colinknapp.com\" in the last 1h0m0s, retry after 2025-07-05 18:26:22 UTC: see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-hostname-per-account (ca=https://acme-v02.api.letsencrypt.org/directory)","attempt":1,"retrying_in":60,"elapsed":2.491265416,"max_duration":2592000}
{"level":"info","ts":1751739292.2583349,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57078","client_ip":"::1","proto":"HTTP/1.1","method":"HEAD","host":"localhost:8080","uri":"/stories/open-source-success.html","headers":{"Accept":["*/*"],"User-Agent":["curl/8.7.1"]}},"bytes_read":0,"user_id":"","duration":0.003503292,"size":0,"status":200,"resp_headers":{"Referrer-Policy":["strict-origin-when-cross-origin"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Accept-Ranges":["bytes"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Etag":["\"db4bqupxyvmr9pd\""],"Cross-Origin-Resource-Policy":["same-origin"],"Content-Length":["12577"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"Vary":["Accept-Encoding"],"Content-Type":["text/html; charset=utf-8"]}}
{"level":"info","ts":1751739341.2217758,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"colinknapp.com"}
{"level":"info","ts":1751739341.2275689,"logger":"http","msg":"using ACME account","account_id":"https://acme-staging-v02.api.letsencrypt.org/acme/acct/210713203","account_contact":[]}
{"level":"info","ts":1751739341.618192,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"http-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739342.312726,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"http-01","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/9OE0nikzS9fkyPjN-_qTWq8gGI4tAL5YNSht0zwiHWU: 404","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739342.3132029,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/9OE0nikzS9fkyPjN-_qTWq8gGI4tAL5YNSht0zwiHWU: 404","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840385953","attempt":1,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"info","ts":1751739343.446597,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739344.154269,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739344.1546292,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840386333","attempt":2,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739344.155127,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"colinknapp.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name"}
{"level":"error","ts":1751739344.155275,"logger":"tls.obtain","msg":"will retry","error":"[colinknapp.com] Obtain: [colinknapp.com] solving challenge: colinknapp.com: [colinknapp.com] authorization failed: HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name (ca=https://acme-staging-v02.api.letsencrypt.org/directory)","attempt":2,"retrying_in":120,"elapsed":65.42754925,"max_duration":2592000}
{"level":"info","ts":1751739464.158327,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"colinknapp.com"}
{"level":"info","ts":1751739464.163527,"logger":"http","msg":"using ACME account","account_id":"https://acme-staging-v02.api.letsencrypt.org/acme/acct/210713203","account_contact":[]}
{"level":"info","ts":1751739464.347884,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"http-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739465.0466619,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"http-01","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/dTh_lQhbTgg9J_PW4LPviVSbTIv0f_Rb4rwIU9woF5A: 404","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739465.046904,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/dTh_lQhbTgg9J_PW4LPviVSbTIv0f_Rb4rwIU9woF5A: 404","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840417013","attempt":1,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"info","ts":1751739466.273396,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739466.958394,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739466.9587898,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840417363","attempt":2,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739466.9591172,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"colinknapp.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name"}
{"level":"error","ts":1751739466.959261,"logger":"tls.obtain","msg":"will retry","error":"[colinknapp.com] Obtain: [colinknapp.com] solving challenge: colinknapp.com: [colinknapp.com] authorization failed: HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name (ca=https://acme-staging-v02.api.letsencrypt.org/directory)","attempt":3,"retrying_in":120,"elapsed":188.231714583,"max_duration":2592000}
{"level":"info","ts":1751739586.962073,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"colinknapp.com"}
{"level":"info","ts":1751739586.96677,"logger":"http","msg":"using ACME account","account_id":"https://acme-staging-v02.api.letsencrypt.org/acme/acct/210713203","account_contact":[]}
{"level":"info","ts":1751739587.159206,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"http-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739587.860424,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"http-01","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/hKt6trbDmfbC05PqWuSSWcIDbCiNKb7eDfiiolzfohQ: 404","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739587.860965,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/hKt6trbDmfbC05PqWuSSWcIDbCiNKb7eDfiiolzfohQ: 404","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840446493","attempt":1,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"info","ts":1751739588.98678,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739589.68759,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739589.688067,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840447093","attempt":2,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739589.688373,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"colinknapp.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name"}
{"level":"error","ts":1751739589.68851,"logger":"tls.obtain","msg":"will retry","error":"[colinknapp.com] Obtain: [colinknapp.com] solving challenge: colinknapp.com: [colinknapp.com] authorization failed: HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name (ca=https://acme-staging-v02.api.letsencrypt.org/directory)","attempt":4,"retrying_in":300,"elapsed":310.961173125,"max_duration":2592000}
{"level":"info","ts":1751739673.5404959,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57250","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.html","headers":{"Accept-Encoding":["gzip, deflate, br, zstd"],"If-None-Match":["\"db4bbbwr4oyk2n1-gzip\""],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Accept-Language":["en-US,en;q=0.5"],"Priority":["u=0, i"],"Sec-Fetch-Site":["same-origin"],"If-Modified-Since":["Sat, 05 Jul 2025 17:54:12 GMT"],"Referer":["http://localhost:8080/"],"Sec-Fetch-User":["?1"],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-Dest":["document"],"Sec-Fetch-Mode":["navigate"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Dnt":["1"],"Connection":["keep-alive"]}},"bytes_read":0,"user_id":"","duration":0.015525791,"size":1665,"status":200,"resp_headers":{"Cross-Origin-Resource-Policy":["same-origin"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Vary":["Accept-Encoding"],"Content-Type":["text/html; charset=utf-8"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Etag":["\"db4bqunxd6yo3gd-gzip\""],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Content-Encoding":["gzip"]}}
{"level":"info","ts":1751739675.868175,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57250","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.html","headers":{"Sec-Fetch-Site":["same-origin"],"If-Modified-Since":["Sat, 05 Jul 2025 18:14:29 GMT"],"Sec-Fetch-Dest":["document"],"Connection":["keep-alive"],"If-None-Match":["\"db4bqunxd6yo3gd-gzip\""],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Mode":["navigate"],"Accept-Language":["en-US,en;q=0.5"],"Priority":["u=0, i"],"Sec-Fetch-User":["?1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Upgrade-Insecure-Requests":["1"],"Referer":["http://localhost:8080/"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"]}},"bytes_read":0,"user_id":"","duration":0.000579666,"size":0,"status":304,"resp_headers":{"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Frame-Options":["DENY"],"Vary":["Accept-Encoding"],"Etag":["\"db4bqunxd6yo3gd\""],"Cross-Origin-Embedder-Policy":["require-corp"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"],"Cross-Origin-Opener-Policy":["same-origin"]}}
{"level":"info","ts":1751739677.53351,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57250","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.html","headers":{"Connection":["keep-alive"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-Dest":["document"],"Sec-Fetch-User":["?1"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept-Encoding":["gzip, deflate, br, zstd"],"If-None-Match":["\"db4bqunxd6yo3gd-gzip\""],"Referer":["http://localhost:8080/"],"Dnt":["1"],"Upgrade-Insecure-Requests":["1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Priority":["u=0, i"],"Accept-Language":["en-US,en;q=0.5"],"Sec-Fetch-Site":["same-origin"],"If-Modified-Since":["Sat, 05 Jul 2025 18:14:29 GMT"]}},"bytes_read":0,"user_id":"","duration":0.001523959,"size":0,"status":304,"resp_headers":{"Vary":["Accept-Encoding"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"],"Etag":["\"db4bqunxd6yo3gd\""],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"]}}
{"level":"info","ts":1751739678.708853,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57251","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.html","headers":{"Connection":["keep-alive"],"Upgrade-Insecure-Requests":["1"],"Priority":["u=0, i"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Sec-Fetch-Dest":["document"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-User":["?1"],"Cache-Control":["no-cache"],"Referer":["http://localhost:8080/"],"Dnt":["1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Accept-Language":["en-US,en;q=0.5"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Site":["same-origin"],"Pragma":["no-cache"]}},"bytes_read":0,"user_id":"","duration":0.004137583,"size":1665,"status":200,"resp_headers":{"Content-Encoding":["gzip"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"X-Content-Type-Options":["nosniff"],"Etag":["\"db4bqunxd6yo3gd-gzip\""],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Cross-Origin-Resource-Policy":["same-origin"],"Content-Type":["text/html; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Frame-Options":["DENY"],"Vary":["Accept-Encoding"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"]}}
{"level":"info","ts":1751739678.819917,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57251","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/styles.css","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept":["text/css,*/*;q=0.1"],"Priority":["u=2"],"Pragma":["no-cache"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Sec-Fetch-Mode":["no-cors"],"Cache-Control":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Dnt":["1"],"Accept-Language":["en-US,en;q=0.5"],"Connection":["keep-alive"],"Sec-Fetch-Dest":["style"],"Sec-Fetch-Site":["same-origin"]}},"bytes_read":0,"user_id":"","duration":0.003274375,"size":1594,"status":200,"resp_headers":{"Cross-Origin-Embedder-Policy":["require-corp"],"Vary":["Accept-Encoding"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Content-Type-Options":["nosniff"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Content-Encoding":["gzip"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Frame-Options":["DENY"],"Cross-Origin-Opener-Policy":["same-origin"],"Last-Modified":["Sat, 05 Jul 2025 18:02:46 GMT"],"Cache-Control":["public, max-age=31536000, immutable"],"Etag":["\"db4bhvy0bbjy4iu-gzip\""],"Content-Type":["text/css; charset=utf-8"],"Referrer-Policy":["strict-origin-when-cross-origin"]}}
{"level":"info","ts":1751739678.821028,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57253","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/theme.js","headers":{"Sec-Fetch-Site":["same-origin"],"Pragma":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Dnt":["1"],"Priority":["u=2"],"Sec-Fetch-Mode":["no-cors"],"Accept-Language":["en-US,en;q=0.5"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Sec-Fetch-Dest":["script"],"Cache-Control":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept":["*/*"],"Connection":["keep-alive"]}},"bytes_read":0,"user_id":"","duration":0.003416792,"size":678,"status":200,"resp_headers":{"Content-Type":["text/javascript; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Vary":["Accept-Encoding"],"Etag":["\"daerkqziy4dn1is-gzip\""],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Content-Encoding":["gzip"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"],"Cross-Origin-Opener-Policy":["same-origin"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739678.821409,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57255","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.js","headers":{"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Dest":["script"],"Pragma":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept":["*/*"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Sec-Fetch-Mode":["no-cors"],"Dnt":["1"],"Connection":["keep-alive"],"Sec-Fetch-Site":["same-origin"],"Cache-Control":["no-cache"],"Accept-Language":["en-US,en;q=0.5"]}},"bytes_read":0,"user_id":"","duration":0.00490175,"size":1984,"status":200,"resp_headers":{"X-Content-Type-Options":["nosniff"],"Content-Type":["text/javascript; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Vary":["Accept-Encoding"],"Last-Modified":["Sat, 05 Jul 2025 17:31:10 GMT"],"Content-Encoding":["gzip"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Frame-Options":["DENY"],"Etag":["\"db4atop72u2i4ye-gzip\""],"Referrer-Policy":["strict-origin-when-cross-origin"]}}
{"level":"info","ts":1751739678.824568,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57252","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/tool-styles.css","headers":{"Priority":["u=2"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Dnt":["1"],"Accept":["text/css,*/*;q=0.1"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Pragma":["no-cache"],"Cache-Control":["no-cache"],"Connection":["keep-alive"],"Sec-Fetch-Mode":["no-cors"],"Sec-Fetch-Site":["same-origin"],"Accept-Language":["en-US,en;q=0.5"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Sec-Fetch-Dest":["style"]}},"bytes_read":0,"user_id":"","duration":0.003773125,"size":1469,"status":200,"resp_headers":{"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Cross-Origin-Opener-Policy":["same-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Etag":["\"db4b2xr4l0jf44o-gzip\""],"Last-Modified":["Sat, 05 Jul 2025 17:43:15 GMT"],"Cross-Origin-Resource-Policy":["same-origin"],"Cross-Origin-Embedder-Policy":["require-corp"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Vary":["Accept-Encoding"],"Content-Type":["text/css; charset=utf-8"],"Content-Encoding":["gzip"]}}
{"level":"info","ts":1751739678.8256042,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57254","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/utils.js","headers":{"Pragma":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Dnt":["1"],"Accept-Language":["en-US,en;q=0.5"],"Sec-Fetch-Dest":["script"],"Priority":["u=2"],"Cache-Control":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Connection":["keep-alive"],"Sec-Fetch-Mode":["no-cors"],"Sec-Fetch-Site":["same-origin"],"Accept":["*/*"]}},"bytes_read":0,"user_id":"","duration":0.005408458,"size":775,"status":200,"resp_headers":{"Content-Type":["text/javascript; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Resource-Policy":["same-origin"],"Etag":["\"daerkqzizlwo1eq-gzip\""],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Vary":["Accept-Encoding"],"X-Frame-Options":["DENY"],"Content-Encoding":["gzip"],"Cross-Origin-Opener-Policy":["same-origin"],"Referrer-Policy":["strict-origin-when-cross-origin"],"X-Content-Type-Options":["nosniff"]}}
{"level":"info","ts":1751739678.857478,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57255","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/favicon.ico","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Accept":["image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"],"Accept-Language":["en-US,en;q=0.5"],"Dnt":["1"],"Sec-Fetch-Site":["same-origin"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Mode":["no-cors"],"Priority":["u=6"],"Connection":["keep-alive"],"Sec-Fetch-Dest":["image"],"Pragma":["no-cache"],"Cache-Control":["no-cache"]}},"bytes_read":0,"user_id":"","duration":0.000408458,"size":1,"status":200,"resp_headers":{"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Type":["image/x-icon"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Length":["1"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Vary":["Accept-Encoding"],"Etag":["\"daerkqzimk0j1\""],"Accept-Ranges":["bytes"],"Cross-Origin-Embedder-Policy":["require-corp"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739679.9043121,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57256","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.html","headers":{"Cache-Control":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Dnt":["1"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-User":["?1"],"Sec-Fetch-Dest":["document"],"Referer":["http://localhost:8080/"],"Priority":["u=0, i"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],"Accept-Language":["en-US,en;q=0.5"],"Connection":["keep-alive"],"Upgrade-Insecure-Requests":["1"],"Pragma":["no-cache"],"Sec-Fetch-Site":["same-origin"]}},"bytes_read":0,"user_id":"","duration":0.000926833,"size":1665,"status":200,"resp_headers":{"Content-Encoding":["gzip"],"Cross-Origin-Opener-Policy":["same-origin"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"],"Vary":["Accept-Encoding"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Embedder-Policy":["require-corp"],"Etag":["\"db4bqunxd6yo3gd-gzip\""],"Content-Type":["text/html; charset=utf-8"],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739679.972051,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57256","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/styles.css","headers":{"Dnt":["1"],"Sec-Fetch-Dest":["style"],"Sec-Fetch-Mode":["no-cors"],"Pragma":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Cache-Control":["no-cache"],"Accept-Language":["en-US,en;q=0.5"],"Sec-Fetch-Site":["same-origin"],"Accept":["text/css,*/*;q=0.1"],"Connection":["keep-alive"],"Priority":["u=2"]}},"bytes_read":0,"user_id":"","duration":0.000524958,"size":1594,"status":200,"resp_headers":{"Etag":["\"db4bhvy0bbjy4iu-gzip\""],"Cross-Origin-Resource-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Last-Modified":["Sat, 05 Jul 2025 18:02:46 GMT"],"X-Frame-Options":["DENY"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"Vary":["Accept-Encoding"],"Content-Type":["text/css; charset=utf-8"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Encoding":["gzip"],"Cross-Origin-Embedder-Policy":["require-corp"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"]}}
{"level":"info","ts":1751739679.972645,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57257","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/tool-styles.css","headers":{"Pragma":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept-Language":["en-US,en;q=0.5"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Dnt":["1"],"Sec-Fetch-Dest":["style"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Connection":["keep-alive"],"Priority":["u=2"],"Cache-Control":["no-cache"],"Accept":["text/css,*/*;q=0.1"],"Sec-Fetch-Mode":["no-cors"],"Sec-Fetch-Site":["same-origin"]}},"bytes_read":0,"user_id":"","duration":0.000355708,"size":1469,"status":200,"resp_headers":{"Vary":["Accept-Encoding"],"Content-Encoding":["gzip"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"X-Content-Type-Options":["nosniff"],"Content-Type":["text/css; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Etag":["\"db4b2xr4l0jf44o-gzip\""],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Cross-Origin-Resource-Policy":["same-origin"],"Last-Modified":["Sat, 05 Jul 2025 17:43:15 GMT"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739679.972853,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57258","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/theme.js","headers":{"Accept-Language":["en-US,en;q=0.5"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Dnt":["1"],"Sec-Fetch-Dest":["script"],"Cache-Control":["no-cache"],"Connection":["keep-alive"],"Sec-Fetch-Mode":["no-cors"],"Sec-Fetch-Site":["same-origin"],"Priority":["u=2"],"Pragma":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept":["*/*"]}},"bytes_read":0,"user_id":"","duration":0.000476167,"size":678,"status":200,"resp_headers":{"X-Frame-Options":["DENY"],"Cache-Control":["public, max-age=31536000, immutable"],"Etag":["\"daerkqziy4dn1is-gzip\""],"Cross-Origin-Resource-Policy":["same-origin"],"Cross-Origin-Embedder-Policy":["require-corp"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Content-Encoding":["gzip"],"X-Content-Type-Options":["nosniff"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Vary":["Accept-Encoding"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Type":["text/javascript; charset=utf-8"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"]}}
{"level":"info","ts":1751739679.973341,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57260","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/one-pager-tools/csv-tool.js","headers":{"Accept-Language":["en-US,en;q=0.5"],"Dnt":["1"],"Pragma":["no-cache"],"Accept":["*/*"],"Sec-Fetch-Dest":["script"],"Sec-Fetch-Mode":["no-cors"],"Sec-Fetch-Site":["same-origin"],"Cache-Control":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Connection":["keep-alive"]}},"bytes_read":0,"user_id":"","duration":0.000869166,"size":1984,"status":200,"resp_headers":{"Cross-Origin-Embedder-Policy":["require-corp"],"Content-Type":["text/javascript; charset=utf-8"],"Last-Modified":["Sat, 05 Jul 2025 17:31:10 GMT"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Encoding":["gzip"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"],"Cross-Origin-Opener-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Vary":["Accept-Encoding"],"Etag":["\"db4atop72u2i4ye-gzip\""],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739679.973473,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57259","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/utils.js","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept-Language":["en-US,en;q=0.5"],"Dnt":["1"],"Sec-Fetch-Dest":["script"],"Priority":["u=2"],"Accept":["*/*"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Connection":["keep-alive"],"Sec-Fetch-Site":["same-origin"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Mode":["no-cors"],"Pragma":["no-cache"],"Cache-Control":["no-cache"]}},"bytes_read":0,"user_id":"","duration":0.000989667,"size":775,"status":200,"resp_headers":{"Vary":["Accept-Encoding"],"X-Frame-Options":["DENY"],"Content-Encoding":["gzip"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Etag":["\"daerkqzizlwo1eq-gzip\""],"Cross-Origin-Opener-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"],"Content-Type":["text/javascript; charset=utf-8"],"Referrer-Policy":["strict-origin-when-cross-origin"]}}
{"level":"info","ts":1751739679.999177,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"127.0.0.1","remote_port":"57260","client_ip":"127.0.0.1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/favicon.ico","headers":{"Connection":["keep-alive"],"Sec-Fetch-Dest":["image"],"Priority":["u=6"],"Accept-Language":["en-US,en;q=0.5"],"Referer":["http://localhost:8080/one-pager-tools/csv-tool.html"],"Sec-Fetch-Site":["same-origin"],"Pragma":["no-cache"],"Cache-Control":["no-cache"],"Accept":["image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"],"Sec-Fetch-Mode":["no-cors"],"Dnt":["1"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:142.0) Gecko/20100101 Firefox/142.0"],"Accept-Encoding":["gzip, deflate, br, zstd"]}},"bytes_read":0,"user_id":"","duration":0.000280666,"size":1,"status":200,"resp_headers":{"Content-Type":["image/x-icon"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"Vary":["Accept-Encoding"],"Accept-Ranges":["bytes"],"Content-Length":["1"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Etag":["\"daerkqzimk0j1\""],"Cross-Origin-Opener-Policy":["same-origin"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cross-Origin-Resource-Policy":["same-origin"],"Cross-Origin-Embedder-Policy":["require-corp"]}}
{"level":"info","ts":1751739720.884835,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57287","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"If-None-Match":["\"db4b159qb1xccqw-gzip\""],"Accept-Language":["en-US,en;q=0.9"],"Sec-Fetch-Dest":["document"],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-Site":["none"],"Sec-Ch-Ua-Mobile":["?0"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Accept-Encoding":["gzip, deflate, br, zstd"],"If-Modified-Since":["Sat, 05 Jul 2025 17:40:54 GMT"],"Cache-Control":["max-age=0"],"Connection":["keep-alive"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-User":["?1"]}},"bytes_read":0,"user_id":"","duration":0.006238541,"size":5186,"status":200,"resp_headers":{"X-Content-Type-Options":["nosniff"],"Cross-Origin-Opener-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Etag":["\"db4bquo9k394d1r-gzip\""],"Content-Encoding":["gzip"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Frame-Options":["DENY"],"Cross-Origin-Embedder-Policy":["require-corp"],"Vary":["Accept-Encoding"],"Content-Type":["text/html; charset=utf-8"]}}
{"level":"info","ts":1751739721.273846,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57287","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"Upgrade-Insecure-Requests":["1"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-User":["?1"],"Connection":["keep-alive"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Cache-Control":["no-cache"],"Accept-Language":["en-US,en;q=0.9"],"Sec-Fetch-Mode":["navigate"],"Sec-Fetch-Dest":["document"],"Pragma":["no-cache"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Sec-Fetch-Site":["none"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Sec-Ch-Ua-Mobile":["?0"],"Accept-Encoding":["gzip, deflate, br, zstd"]}},"bytes_read":0,"user_id":"","duration":0.000861208,"size":5186,"status":200,"resp_headers":{"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Content-Encoding":["gzip"],"Cross-Origin-Embedder-Policy":["require-corp"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Vary":["Accept-Encoding"],"Etag":["\"db4bquo9k394d1r-gzip\""],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Opener-Policy":["same-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"Content-Type":["text/html; charset=utf-8"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739721.283733,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57287","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/styles.css","headers":{"Sec-Fetch-Site":["same-origin"],"Connection":["keep-alive"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Accept":["text/css,*/*;q=0.1"],"Pragma":["no-cache"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Accept-Language":["en-US,en;q=0.9"],"Cache-Control":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Fetch-Dest":["style"],"Referer":["http://localhost:8080/"],"Sec-Fetch-Mode":["no-cors"],"Sec-Ch-Ua-Mobile":["?0"]}},"bytes_read":0,"user_id":"","duration":0.002707125,"size":1594,"status":200,"resp_headers":{"Cross-Origin-Embedder-Policy":["require-corp"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Content-Type-Options":["nosniff"],"Vary":["Accept-Encoding"],"Etag":["\"db4bhvy0bbjy4iu-gzip\""],"Content-Type":["text/css; charset=utf-8"],"Last-Modified":["Sat, 05 Jul 2025 18:02:46 GMT"],"Cross-Origin-Opener-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Frame-Options":["DENY"],"Content-Encoding":["gzip"],"Cross-Origin-Resource-Policy":["same-origin"]}}
{"level":"info","ts":1751739721.2837389,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57288","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/theme.js","headers":{"Accept":["*/*"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Sec-Fetch-Mode":["no-cors"],"Referer":["http://localhost:8080/"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Accept-Language":["en-US,en;q=0.9"],"Pragma":["no-cache"],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Fetch-Dest":["script"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Fetch-Site":["same-origin"],"Cache-Control":["no-cache"],"Connection":["keep-alive"]}},"bytes_read":0,"user_id":"","duration":0.002064334,"size":678,"status":200,"resp_headers":{"Cross-Origin-Resource-Policy":["same-origin"],"X-Frame-Options":["DENY"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Content-Type":["text/javascript; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Etag":["\"daerkqziy4dn1is-gzip\""],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Content-Type-Options":["nosniff"],"Vary":["Accept-Encoding"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Content-Encoding":["gzip"],"Cross-Origin-Opener-Policy":["same-origin"],"Referrer-Policy":["strict-origin-when-cross-origin"]}}
{"level":"info","ts":1751739721.32471,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57288","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/favicon.ico","headers":{"Pragma":["no-cache"],"Sec-Fetch-Site":["same-origin"],"Referer":["http://localhost:8080/"],"Cache-Control":["no-cache"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Connection":["keep-alive"],"Accept-Language":["en-US,en;q=0.9"],"Accept-Encoding":["gzip, deflate, br, zstd"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Fetch-Mode":["no-cors"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Sec-Ch-Ua-Mobile":["?0"],"Accept":["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"],"Sec-Fetch-Dest":["image"]}},"bytes_read":0,"user_id":"","duration":0.000416791,"size":1,"status":200,"resp_headers":{"Accept-Ranges":["bytes"],"X-Content-Type-Options":["nosniff"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Vary":["Accept-Encoding"],"Etag":["\"daerkqzimk0j1\""],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"X-Frame-Options":["DENY"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Length":["1"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cache-Control":["public, max-age=31536000, immutable"],"Cross-Origin-Resource-Policy":["same-origin"],"Content-Type":["image/x-icon"]}}
{"level":"info","ts":1751739723.5364192,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57291","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/","headers":{"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Mode":["navigate"],"Pragma":["no-cache"],"Cache-Control":["no-cache"],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Sec-Fetch-User":["?1"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Fetch-Dest":["document"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-Site":["none"]}},"bytes_read":0,"user_id":"","duration":0.006212709,"size":5186,"status":200,"resp_headers":{"Etag":["\"db4bquo9k394d1r-gzip\""],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Cache-Control":["public, max-age=31536000, immutable"],"Vary":["Accept-Encoding"],"Content-Type":["text/html; charset=utf-8"],"Cross-Origin-Resource-Policy":["same-origin"],"Content-Encoding":["gzip"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"]}}
{"level":"info","ts":1751739723.549154,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57292","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/theme.js","headers":{"Sec-Ch-Ua-Platform":["\"macOS\""],"Sec-Fetch-Site":["same-origin"],"Referer":["http://localhost:8080/"],"Cache-Control":["no-cache"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Accept":["*/*"],"Sec-Fetch-Mode":["no-cors"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Fetch-Dest":["script"],"Pragma":["no-cache"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Sec-Ch-Ua-Mobile":["?0"]}},"bytes_read":0,"user_id":"","duration":0.000694583,"size":678,"status":200,"resp_headers":{"Cross-Origin-Embedder-Policy":["require-corp"],"Vary":["Accept-Encoding"],"Cache-Control":["public, max-age=31536000, immutable"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Content-Encoding":["gzip"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Frame-Options":["DENY"],"Etag":["\"daerkqziy4dn1is-gzip\""],"Content-Type":["text/javascript; charset=utf-8"],"Cross-Origin-Opener-Policy":["same-origin"],"X-Content-Type-Options":["nosniff"]}}
{"level":"info","ts":1751739723.549674,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57291","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/styles.css","headers":{"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Sec-Fetch-Mode":["no-cors"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Accept":["text/css,*/*;q=0.1"],"Referer":["http://localhost:8080/"],"Cache-Control":["no-cache"],"Sec-Fetch-Dest":["style"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Sec-Ch-Ua-Mobile":["?0"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Pragma":["no-cache"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Fetch-Site":["same-origin"]}},"bytes_read":0,"user_id":"","duration":0.000732458,"size":1594,"status":200,"resp_headers":{"Cross-Origin-Embedder-Policy":["require-corp"],"Content-Type":["text/css; charset=utf-8"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Frame-Options":["DENY"],"Etag":["\"db4bhvy0bbjy4iu-gzip\""],"Content-Encoding":["gzip"],"X-Content-Type-Options":["nosniff"],"Vary":["Accept-Encoding"],"Last-Modified":["Sat, 05 Jul 2025 18:02:46 GMT"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"]}}
{"level":"info","ts":1751739723.565779,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57291","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/favicon.ico","headers":{"Referer":["http://localhost:8080/"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept":["image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"],"Sec-Fetch-Site":["same-origin"],"Sec-Fetch-Dest":["image"],"Sec-Ch-Ua-Mobile":["?0"],"Connection":["keep-alive"],"Pragma":["no-cache"],"Sec-Fetch-Mode":["no-cors"],"Cache-Control":["no-cache"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Accept-Language":["en-US,en;q=0.9"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""]}},"bytes_read":0,"user_id":"","duration":0.000751042,"size":1,"status":200,"resp_headers":{"Content-Length":["1"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"Cross-Origin-Resource-Policy":["same-origin"],"X-Frame-Options":["DENY"],"Content-Type":["image/x-icon"],"Referrer-Policy":["strict-origin-when-cross-origin"],"Cross-Origin-Opener-Policy":["same-origin"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Content-Type-Options":["nosniff"],"Cross-Origin-Embedder-Policy":["require-corp"],"Etag":["\"daerkqzimk0j1\""],"Vary":["Accept-Encoding"],"Last-Modified":["Thu, 05 Jun 2025 17:09:29 GMT"],"Accept-Ranges":["bytes"]}}
{"level":"info","ts":1751739726.127846,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57291","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/stories/","headers":{"Sec-Fetch-Mode":["navigate"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Sec-Ch-Ua-Platform":["\"macOS\""],"Upgrade-Insecure-Requests":["1"],"Sec-Fetch-Site":["same-origin"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"],"Sec-Fetch-Dest":["document"],"Referer":["http://localhost:8080/"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Sec-Ch-Ua-Mobile":["?0"],"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Fetch-User":["?1"]}},"bytes_read":0,"user_id":"","duration":0.00319225,"size":1987,"status":200,"resp_headers":{"Referrer-Policy":["strict-origin-when-cross-origin"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Content-Type-Options":["nosniff"],"Vary":["Accept-Encoding"],"Etag":["\"db4bqupjqm554sq-gzip\""],"Content-Type":["text/html; charset=utf-8"],"Cross-Origin-Embedder-Policy":["require-corp"],"Cross-Origin-Opener-Policy":["same-origin"],"Content-Encoding":["gzip"],"Cross-Origin-Resource-Policy":["same-origin"],"Last-Modified":["Sat, 05 Jul 2025 18:14:29 GMT"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Cache-Control":["public, max-age=31536000, immutable"],"X-Frame-Options":["DENY"]}}
{"level":"info","ts":1751739726.1533198,"logger":"http.log.access.log1","msg":"handled request","request":{"remote_ip":"::1","remote_port":"57291","client_ip":"::1","proto":"HTTP/1.1","method":"GET","host":"localhost:8080","uri":"/stories/stories.css","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"],"Sec-Ch-Ua-Mobile":["?0"],"Sec-Fetch-Mode":["no-cors"],"Sec-Fetch-Dest":["style"],"Referer":["http://localhost:8080/stories/"],"Accept-Encoding":["gzip, deflate, br, zstd"],"Sec-Ch-Ua":["\"Google Chrome\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\""],"Accept":["text/css,*/*;q=0.1"],"Sec-Fetch-Site":["same-origin"],"Accept-Language":["en-US,en;q=0.9"],"Connection":["keep-alive"],"Sec-Ch-Ua-Platform":["\"macOS\""]}},"bytes_read":0,"user_id":"","duration":0.0008405,"size":1226,"status":200,"resp_headers":{"Content-Encoding":["gzip"],"Cross-Origin-Opener-Policy":["same-origin"],"Cross-Origin-Embedder-Policy":["require-corp"],"Vary":["Accept-Encoding"],"Content-Type":["text/css; charset=utf-8"],"Content-Security-Policy":["default-src 'none'; script-src 'self' 'sha256-nyj4rRbjhJDz0uLB5qgDfrvwLYUJSg9YWnHihpFA8rk=' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; style-src 'self' 'sha256-/v9rQU2KOOA3o+QIcyY9sJYiY6V+pBRixwWCntlYMz8=' 'sha256-Y37imeLroP7YzkjRmiLxoP6J3M6m1v6nek3IpWObGZI=' 'sha256-9SeZBz89VCv28LEByMlQjCC8UalqDwwjouZGacpHeJk='; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';"],"X-Content-Type-Options":["nosniff"],"X-Frame-Options":["DENY"],"Cross-Origin-Resource-Policy":["same-origin"],"Permissions-Policy":["accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"],"Last-Modified":["Sat, 05 Jul 2025 18:14:20 GMT"],"Cache-Control":["public, max-age=31536000, immutable"],"Etag":["\"db4bqqjbe6x53bg-gzip\""],"Referrer-Policy":["strict-origin-when-cross-origin"]}}
{"level":"info","ts":1751739889.693784,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"colinknapp.com"}
{"level":"info","ts":1751739889.7038321,"logger":"http","msg":"using ACME account","account_id":"https://acme-staging-v02.api.letsencrypt.org/acme/acct/210713203","account_contact":[]}
{"level":"info","ts":1751739890.085554,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"http-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739890.770806,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"http-01","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/DWUN2FubCp8-Utbvg1U3sQzEN9ohjNM7v505ji-DDLI: 404","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739890.773333,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:unauthorized","title":"","detail":"2604:a880:cad:d0::ef5:1001: Invalid response from http://colinknapp.com/.well-known/acme-challenge/DWUN2FubCp8-Utbvg1U3sQzEN9ohjNM7v505ji-DDLI: 404","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840531423","attempt":1,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"info","ts":1751739891.900807,"msg":"trying to solve challenge","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","ca":"https://acme-staging-v02.api.letsencrypt.org/directory"}
{"level":"error","ts":1751739892.895112,"msg":"challenge failed","identifier":"colinknapp.com","challenge_type":"tls-alpn-01","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"stacktrace":"github.com/mholt/acmez/v3.(*Client).pollAuthorization\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:557\ngithub.com/mholt/acmez/v3.(*Client).solveChallenges\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:378\ngithub.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:136\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739892.895329,"msg":"validating authorization","identifier":"colinknapp.com","problem":{"type":"urn:ietf:params:acme:error:tls","title":"","detail":"138.197.167.216: remote error: tls: unrecognized name","instance":"","subproblems":null},"order":"https://acme-staging-v02.api.letsencrypt.org/acme/order/210713203/25840531943","attempt":2,"max_attempts":3,"stacktrace":"github.com/mholt/acmez/v3.(*Client).ObtainCertificate\n\tgithub.com/mholt/acmez/v3@v3.1.2/client.go:152\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).doIssue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:489\ngithub.com/caddyserver/certmagic.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/certmagic@v0.23.0/acmeissuer.go:382\ngithub.com/caddyserver/caddy/v2/modules/caddytls.(*ACMEIssuer).Issue\n\tgithub.com/caddyserver/caddy/v2@v2.10.0/modules/caddytls/acmeissuer.go:288\ngithub.com/caddyserver/certmagic.(*Config).obtainCert.func2\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:626\ngithub.com/caddyserver/certmagic.doWithRetry\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:104\ngithub.com/caddyserver/certmagic.(*Config).obtainCert\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:700\ngithub.com/caddyserver/certmagic.(*Config).ObtainCertAsync\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:505\ngithub.com/caddyserver/certmagic.(*Config).manageOne.func1\n\tgithub.com/caddyserver/certmagic@v0.23.0/config.go:415\ngithub.com/caddyserver/certmagic.(*jobManager).worker\n\tgithub.com/caddyserver/certmagic@v0.23.0/async.go:73"}
{"level":"error","ts":1751739892.895437,"logger":"tls.obtain","msg":"could not get certificate from issuer","identifier":"colinknapp.com","issuer":"acme-v02.api.letsencrypt.org-directory","error":"HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name"}
{"level":"error","ts":1751739892.895788,"logger":"tls.obtain","msg":"will retry","error":"[colinknapp.com] Obtain: [colinknapp.com] solving challenge: colinknapp.com: [colinknapp.com] authorization failed: HTTP 400 urn:ietf:params:acme:error:tls - 138.197.167.216: remote error: tls: unrecognized name (ca=https://acme-staging-v02.api.letsencrypt.org/directory)","attempt":5,"retrying_in":600,"elapsed":614.168914,"max_duration":2592000}
{"level":"info","ts":1751739910.3035798,"msg":"shutting down apps, then terminating","signal":"SIGTERM"}
{"level":"warn","ts":1751739910.3039591,"msg":"exiting; byeee!! 👋","signal":"SIGTERM"}
{"level":"info","ts":1751739910.3044071,"logger":"http","msg":"servers shutting down with eternal grace period"}
{"level":"info","ts":1751739910.3074849,"logger":"tls.obtain","msg":"releasing lock","identifier":"colinknapp.com"}
{"level":"error","ts":1751739910.310326,"msg":"unable to clean up lock in storage backend","signal":"SIGTERM","storage":"FileStorage:/Users/computerpro/Library/Application Support/Caddy","lock_key":"issue_cert_colinknapp.com","error":"remove /Users/computerpro/Library/Application Support/Caddy/locks/issue_cert_colinknapp.com.lock: no such file or directory"}
{"level":"info","ts":1751739910.310804,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"}
{"level":"info","ts":1751739910.310834,"msg":"shutdown complete","signal":"SIGTERM","exit_code":0}

58
docker/resume/caddy.sh Executable file
View File

@ -0,0 +1,58 @@
#!/bin/bash
# =====================================================================
# caddy.sh - Simple script to start Caddy server
# =====================================================================
# Usage:
# ./caddy.sh - Start/restart the Caddy server
#
# This script handles all Caddy server operations from the correct directory
# =====================================================================
set -e
# Ensure we're in the correct directory (where this script is located)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
cd "$SCRIPT_DIR"
# Stop any existing Caddy processes
echo "=== Stopping Caddy Server ==="
killall caddy 2>/dev/null || true
pkill -f "caddy run" 2>/dev/null || true
sleep 1
echo "All Caddy processes stopped."
# Update CSP hashes
echo "=== Updating CSP Hashes ==="
# Run the update-csp-hashes.sh script
if [ -f "./update-csp-hashes.sh" ]; then
./update-csp-hashes.sh
else
echo "WARNING: update-csp-hashes.sh not found. Skipping CSP hash updates."
fi
# Start Caddy
echo "=== Starting Caddy Server ==="
echo "Working directory: $(pwd)"
# Check if Caddyfile.local exists
if [ ! -f "Caddyfile.local" ]; then
echo "ERROR: Caddyfile.local not found in $(pwd)"
echo "Please ensure you're running this script from the directory containing Caddyfile.local"
exit 1
fi
# Launch Caddy
echo "Starting Caddy with Caddyfile.local..."
caddy run --config Caddyfile.local &
# Wait a moment to check if Caddy started successfully
sleep 2
if ! pgrep -f "caddy run" > /dev/null; then
echo "ERROR: Caddy failed to start. Check the error messages above."
exit 1
fi
echo "=== Caddy Server Started Successfully ==="
echo "Local server running at: http://localhost:8080"
exit 0

View File

@ -0,0 +1,51 @@
#!/bin/bash
# Generate the SVG favicon
echo "Generating SVG favicon..."
node generate-favicon.js
# Check if the SVG was generated
if [ ! -f "favicon.svg" ]; then
echo "Error: favicon.svg was not generated."
exit 1
fi
# Create a temporary directory for favicon files
mkdir -p favicon_tmp
# Convert SVG to PNG files of different sizes
echo "Converting SVG to PNG files of various sizes..."
for size in 16 32 48 64 128 256; do
# Using ImageMagick's convert command if available
if command -v convert > /dev/null; then
convert -background none -size ${size}x${size} favicon.svg favicon_tmp/favicon-${size}.png
else
echo "ImageMagick not found. Please install it to convert SVG to PNG."
exit 1
fi
done
# Create favicon.ico from the PNG files
echo "Creating favicon.ico..."
if command -v convert > /dev/null; then
convert favicon_tmp/favicon-16.png favicon_tmp/favicon-32.png favicon_tmp/favicon-48.png favicon.ico
else
echo "ImageMagick not found. Please install it to create favicon.ico."
exit 1
fi
# Create apple-touch-icon.png (for iOS)
echo "Creating apple-touch-icon.png..."
cp favicon_tmp/favicon-128.png apple-touch-icon.png
# Create favicon-32x32.png and favicon-16x16.png (for modern browsers)
echo "Creating standard browser favicons..."
cp favicon_tmp/favicon-32.png favicon-32x32.png
cp favicon_tmp/favicon-16.png favicon-16x16.png
# Clean up temporary directory
echo "Cleaning up temporary files..."
rm -rf favicon_tmp
echo "Favicon generation complete!"
echo "Created: favicon.ico, favicon-16x16.png, favicon-32x32.png, apple-touch-icon.png"

View File

@ -0,0 +1,71 @@
/**
* Script to help convert existing HTML files to use the includes system
*
* Usage:
* 1. Include this script in your HTML file temporarily
* 2. Open the page in a browser
* 3. Open the browser console
* 4. The script will output the modified HTML content that uses includes
*/
document.addEventListener('DOMContentLoaded', function() {
// Extract the head content
const headContent = document.head.innerHTML;
// Extract the navigation content
const navContent = document.querySelector('.main-nav')?.outerHTML || '';
// Extract the theme switcher content
const themeSwitcherContent = document.querySelector('.theme-switch')?.outerHTML || '';
// Extract the footer content (accessibility notice)
const footerContent = document.querySelector('.accessibility-notice')?.parentElement?.innerHTML || '';
// Extract the main content
const mainContentElement = document.querySelector('.container-fluid');
let mainContent = '';
if (mainContentElement) {
// Clone the main content to avoid modifying the original
const mainContentClone = mainContentElement.cloneNode(true);
// Remove the accessibility notice from the clone if it exists
const accessibilityNotice = mainContentClone.querySelector('.accessibility-notice');
if (accessibilityNotice && accessibilityNotice.parentElement) {
accessibilityNotice.parentElement.removeChild(accessibilityNotice);
}
mainContent = mainContentClone.innerHTML;
}
// Create the modified HTML content
const modifiedHTML = `<!DOCTYPE html>
<html lang="en">
<head>
${headContent}
<script src="${getRelativePath()}includes.js"></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<!-- Main Content -->
${mainContent}
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>`;
// Output the modified HTML content to the console
console.log('=== MODIFIED HTML CONTENT ===');
console.log(modifiedHTML);
console.log('=== END MODIFIED HTML CONTENT ===');
// Helper function to get the relative path to the root
function getRelativePath() {
const path = window.location.pathname;
const depth = (path.match(/\//g) || []).length - 1;
return depth > 0 ? '../'.repeat(depth) : '';
}
});

View File

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - CSV Processing Tool">
<title>CSV Viewer - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css">
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<link rel="stylesheet" href="csv-tool-fix.css?v=2" integrity="sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
<script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544="></script>
<style>
/* Additional inline styles to fix layout */
.container-fluid {
max-width: 100%;
padding: 0 15px;
}
.tool-container {
width: 100%;
max-width: 100%;
}
.form-group.full-width {
width: 100%;
max-width: 100%;
}
#csvInput {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* More aggressive fixes for textarea */
textarea#csvInput {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 12px !important;
font-family: 'Courier New', monospace !important;
}
/* Fix container width */
body {
max-width: 100% !important;
padding: 20px !important;
box-sizing: border-box !important;
}
</style>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<h1>CSV Viewer</h1>
<p>Simply paste CSV data below to view it as a formatted table.</p>
<div class="tool-container">
<div class="tool-controls">
<h3>Paste CSV Data</h3>
<div class="form-group full-width">
<textarea id="csvInput" class="form-control" rows="15" placeholder="Paste your CSV data here to automatically view it as a table..."></textarea>
</div>
<div class="form-group">
<label for="delimiter">Delimiter:</label>
<select id="delimiter" class="form-control">
<option value="," selected>Comma (,)</option>
<option value=";">Semicolon (;)</option>
<option value="\t">Tab</option>
<option value="|">Pipe (|)</option>
</select>
</div>
<div class="form-group">
<label for="hasHeader">First row is header:</label>
<input type="checkbox" id="hasHeader" checked>
</div>
</div>
<div class="tool-output" id="output">
<h3>Output</h3>
<p class="alert alert-info">Paste CSV data above to view it as a table.</p>
</div>
</div>
<hr>
<h2>About This Tool</h2>
<p>This CSV Viewer allows you to:</p>
<ul>
<li>Paste and preview CSV data directly in your browser</li>
<li>Automatically view your data in a table format</li>
<li>Sort columns by clicking on column headers</li>
</ul>
<p>The tool processes everything in your browser - no data is sent to any server.</p>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
<!-- Load PapaParse first (local version) -->
<script src="../papaparse.min.js" integrity="sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc="></script>
<!-- Then load our script -->
<script src="csv-tool.js?v=3" integrity="sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI="></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

BIN
docker/resume/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

31
docker/resume/favicon.svg Normal file
View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#2c3e50" />
<stop offset="100%" stop-color="#1a2530" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="64" height="64" rx="8" fill="url(#gradient)" />
<!-- Document/Resume icon -->
<rect x="18" y="14" width="28" height="36" rx="2" fill="#ecf0f1" />
<rect x="22" y="20" width="20" height="2" rx="1" fill="#2c3e50" />
<rect x="22" y="24" width="16" height="2" rx="1" fill="#2c3e50" />
<rect x="22" y="28" width="20" height="2" rx="1" fill="#2c3e50" />
<rect x="22" y="32" width="12" height="2" rx="1" fill="#2c3e50" />
<!-- Accent corner fold -->
<path d="M46,14 L46,22 L38,14 Z" fill="#3498db" />
<!-- Initials as paths instead of text for better compatibility -->
<g transform="translate(20, 42)">
<!-- C letter -->
<path d="M8,2 C5,2 3,3.5 3,6 C3,8.5 5,10 8,10 C10,10 11.5,9 12,7.5"
stroke="#3498db" stroke-width="2" fill="none" stroke-linecap="round" />
<!-- K letter -->
<path d="M16,2 L16,10 M16,6 L20,2 M16,6 L20,10"
stroke="#3498db" stroke-width="2" fill="none" stroke-linecap="round" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,52 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Create an SVG favicon with a professional design for a resume site
function generateFavicon() {
// Define colors
const backgroundColor = '#2c3e50'; // Dark blue-gray
const accentColor = '#3498db'; // Bright blue
const textColor = '#ecf0f1'; // Light gray/white
// Create SVG content with a professional design
const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${backgroundColor}" />
<stop offset="100%" stop-color="#1a2530" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="64" height="64" rx="8" fill="url(#gradient)" />
<!-- Document/Resume icon -->
<rect x="18" y="14" width="28" height="36" rx="2" fill="${textColor}" />
<rect x="22" y="20" width="20" height="2" rx="1" fill="${backgroundColor}" />
<rect x="22" y="24" width="16" height="2" rx="1" fill="${backgroundColor}" />
<rect x="22" y="28" width="20" height="2" rx="1" fill="${backgroundColor}" />
<rect x="22" y="32" width="12" height="2" rx="1" fill="${backgroundColor}" />
<!-- Accent corner fold -->
<path d="M46,14 L46,22 L38,14 Z" fill="${accentColor}" />
<!-- Initials as paths instead of text for better compatibility -->
<g transform="translate(20, 42)">
<!-- C letter -->
<path d="M8,2 C5,2 3,3.5 3,6 C3,8.5 5,10 8,10 C10,10 11.5,9 12,7.5"
stroke="${accentColor}" stroke-width="2" fill="none" stroke-linecap="round" />
<!-- K letter -->
<path d="M16,2 L16,10 M16,6 L20,2 M16,6 L20,10"
stroke="${accentColor}" stroke-width="2" fill="none" stroke-linecap="round" />
</g>
</svg>`;
// Write SVG file
fs.writeFileSync(path.join(__dirname, 'favicon.svg'), svgContent);
console.log('Generated favicon.svg');
}
// Run the generator
generateFavicon();

View File

@ -0,0 +1,61 @@
#!/bin/bash
# =====================================================================
# generate-sitemap.sh - Generate sitemap.xml for the website
# =====================================================================
# This script generates a sitemap.xml file for the website
# It should be run after any content updates
# =====================================================================
set -e
echo "Generating sitemap.xml..."
# Directory containing the files
BASE_DIR="$(pwd)"
DOMAIN="http://localhost:8080" # Use localhost for local development and testing
# Get current date in ISO 8601 format
CURRENT_DATE=$(date -u +"%Y-%m-%dT%H:%M:%S+00:00")
# Create sitemap header
cat > "$BASE_DIR/sitemap.xml" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
EOF
# Find all HTML files and add them to sitemap
find "$BASE_DIR" -name "*.html" -type f | sort | while read -r html_file; do
# Skip files in includes directory and template files
if [[ "$html_file" == *"/includes/"* ]] || [[ "$html_file" == *"-with-includes.html" ]] || [[ "$html_file" == *"template"* ]]; then
continue
fi
# Get relative path from base directory
rel_path="${html_file#$BASE_DIR/}"
# Skip csv-tool-output.html as it's dynamically generated
if [[ "$rel_path" == "csv-tool-output.html" ]]; then
continue
fi
# Create URL
url="$DOMAIN/$rel_path"
# Add URL to sitemap
cat >> "$BASE_DIR/sitemap.xml" << EOF
<url>
<loc>$url</loc>
<lastmod>$CURRENT_DATE</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
EOF
done
# Close sitemap
cat >> "$BASE_DIR/sitemap.xml" << EOF
</urlset>
EOF
echo "Sitemap generated at $BASE_DIR/sitemap.xml"
echo "Total URLs: $(grep -c "<url>" "$BASE_DIR/sitemap.xml")"

View File

@ -0,0 +1 @@
<!-- This is a placeholder for the Discord community screenshot. Replace with the actual image. -->

View File

@ -0,0 +1 @@
<!-- This is a placeholder for the Docker Hub stats screenshot. Replace with the actual image. -->

313
docker/resume/includes.js Normal file
View File

@ -0,0 +1,313 @@
/**
* Includes.js - Handles the inclusion of header and footer files
* and applies the correct active states to navigation items
*/
document.addEventListener('DOMContentLoaded', function() {
// Helper: recreate and execute a <script> element
function recreateAndExecuteScript(originalScriptElement, targetParent) {
const executableScript = document.createElement('script');
// Copy attributes (e.g., src, async, defer)
for (const { name, value } of Array.from(originalScriptElement.attributes)) {
executableScript.setAttribute(name, value);
}
// Inline script content
if (!originalScriptElement.src) {
executableScript.textContent = originalScriptElement.textContent;
}
// Append to target to trigger execution
(targetParent || document.head || document.body).appendChild(executableScript);
}
// Helper: find and execute all scripts within a container element
function executeScriptsInContainer(containerElement, targetParent) {
if (!containerElement) return;
const scriptElements = Array.from(containerElement.querySelectorAll('script'));
scriptElements.forEach((scriptEl) => {
recreateAndExecuteScript(scriptEl, targetParent);
});
}
// Function to include HTML content
async function includeHTML(elementId, filePath, callback) {
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`Failed to load ${filePath}: ${response.status} ${response.statusText}`);
}
const content = await response.text();
const targetElement = document.getElementById(elementId);
targetElement.innerHTML = content;
// Ensure any scripts inside included content (e.g., Matomo) are executed
executeScriptsInContainer(targetElement, document.head);
if (callback) callback();
} catch (error) {
console.error('Error including HTML:', error);
}
}
// Function to include HTML content in the head
async function includeInHead(filePath) {
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`Failed to load ${filePath}: ${response.status} ${response.statusText}`);
}
const content = await response.text();
const headElement = document.getElementsByTagName('head')[0];
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
// Append each child from the loaded content to the head
while (tempDiv.firstChild) {
const node = tempDiv.firstChild;
if (node.tagName && node.tagName.toLowerCase() === 'script') {
recreateAndExecuteScript(node, headElement);
node.remove();
} else {
headElement.appendChild(node);
}
}
} catch (error) {
console.error('Error including HTML in head:', error);
}
}
// Function to set active navigation item
function setActiveNavItem() {
const currentPath = window.location.pathname;
// Wait for the navigation to be loaded
setTimeout(() => {
// Remove all active classes first
document.querySelectorAll('.main-nav a').forEach(link => {
link.classList.remove('active');
});
// Set active class based on current path
if (currentPath === '/' || currentPath === '/index.html') {
const portfolioLink = document.getElementById('nav-portfolio');
if (portfolioLink) portfolioLink.classList.add('active');
} else if (currentPath.includes('/stories/')) {
const storiesLink = document.getElementById('nav-stories');
if (storiesLink) storiesLink.classList.add('active');
// Check for specific story pages
if (currentPath.includes('viperwire.html')) {
const link = document.getElementById('nav-viperwire');
if (link) link.classList.add('active');
} else if (currentPath.includes('fawe-plotsquared.html')) {
const link = document.getElementById('nav-fawe');
if (link) link.classList.add('active');
} else if (currentPath.includes('healthcare-platform.html')) {
const link = document.getElementById('nav-healthcare');
if (link) link.classList.add('active');
} else if (currentPath.includes('wordpress-security.html')) {
const link = document.getElementById('nav-wordpress');
if (link) link.classList.add('active');
} else if (currentPath.includes('airport-dns.html')) {
const link = document.getElementById('nav-airport');
if (link) link.classList.add('active');
} else if (currentPath.includes('nitric-leadership.html')) {
const link = document.getElementById('nav-nitric');
if (link) link.classList.add('active');
} else if (currentPath.includes('open-source-success.html')) {
const link = document.getElementById('nav-opensource');
if (link) link.classList.add('active');
}
} else if (currentPath.includes('/one-pager-tools/')) {
const toolsLink = document.getElementById('nav-tools');
if (toolsLink) toolsLink.classList.add('active');
// Check for specific tool pages
if (currentPath.includes('csv-tool.html')) {
const link = document.getElementById('nav-csv');
if (link) link.classList.add('active');
}
}
}, 100); // Small delay to ensure the DOM is updated
}
// Function to initialize theme toggle
function initThemeToggle() {
const themeToggle = document.getElementById('themeToggle');
if (!themeToggle) {
console.log('Theme toggle button not found on this page');
return;
}
// Check for saved theme preference, default to auto
const savedTheme = localStorage.getItem('theme') || 'auto';
// Set initial value for aria-checked attribute
themeToggle.setAttribute('aria-checked', savedTheme !== 'auto');
updateTheme(savedTheme);
function updateTheme(theme) {
// Update button state and labels
const themeLabels = {
light: 'Theme mode: Light',
dark: 'Theme mode: Dark',
auto: 'Theme mode: Auto'
};
themeToggle.setAttribute('aria-label', themeLabels[theme]);
themeToggle.setAttribute('aria-checked', theme !== 'auto');
// Update button icon
const themeIcons = {
light: '🌞',
dark: '🌙',
auto: '🌓'
};
themeToggle.textContent = themeIcons[theme];
const html = document.documentElement;
if (theme === 'auto') {
html.removeAttribute('data-theme');
} else {
html.setAttribute('data-theme', theme);
}
}
themeToggle.addEventListener('click', () => {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme') || 'auto';
let newTheme;
switch(currentTheme) {
case 'light':
newTheme = 'dark';
break;
case 'dark':
newTheme = 'auto';
break;
default:
newTheme = 'light';
}
updateTheme(newTheme);
localStorage.setItem('theme', newTheme);
});
// Handle keyboard navigation
themeToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
themeToggle.click();
}
});
}
// Include favicon links
includeInHead('/includes/favicon-links.html');
// Process header and footer placeholders
const headerElement = document.getElementById('header-include');
const footerElement = document.getElementById('footer-include');
if (headerElement) {
includeHTML('header-include', '/includes/header.html', () => {
setActiveNavItem();
setupNavDropdowns();
initThemeToggle();
});
}
if (footerElement) {
includeHTML('footer-include', '/includes/footer.html');
}
// Setup dropdown behavior with delay
function setupNavDropdowns() {
const dropdowns = document.querySelectorAll('.main-nav .dropdown');
let timeoutId;
dropdowns.forEach(dropdown => {
// Mouse interactions
dropdown.addEventListener('mouseenter', () => {
clearTimeout(timeoutId);
dropdowns.forEach(d => {
if (d !== dropdown) {
d.querySelector('.dropdown-content').style.display = 'none';
d.querySelector('.dropdown-content').style.opacity = '0';
d.querySelector('.dropdown-content').style.visibility = 'hidden';
}
});
const dropdownContent = dropdown.querySelector('.dropdown-content');
dropdownContent.style.display = 'block';
// Small delay to allow CSS transition to work properly
setTimeout(() => {
dropdownContent.style.opacity = '1';
dropdownContent.style.visibility = 'visible';
}, 10);
});
dropdown.addEventListener('mouseleave', () => {
const dropdownContent = dropdown.querySelector('.dropdown-content');
// Add delay before hiding the dropdown
timeoutId = setTimeout(() => {
dropdownContent.style.opacity = '0';
dropdownContent.style.visibility = 'hidden';
// Wait for transition to complete before changing display
setTimeout(() => {
if (dropdownContent.style.opacity === '0') {
dropdownContent.style.display = 'none';
}
}, 200);
}, 300); // 300ms delay before starting to close
});
// Keyboard interactions
const dropdownLink = dropdown.querySelector('a');
dropdownLink.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
const dropdownContent = dropdown.querySelector('.dropdown-content');
dropdownContent.style.display = 'block';
setTimeout(() => {
dropdownContent.style.opacity = '1';
dropdownContent.style.visibility = 'visible';
// Focus the first link in the dropdown
const firstLink = dropdownContent.querySelector('a');
if (firstLink) firstLink.focus();
}, 10);
}
});
// Add keyboard navigation for dropdown items
const dropdownLinks = dropdown.querySelectorAll('.dropdown-content a');
dropdownLinks.forEach((link, index) => {
link.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const nextLink = dropdownLinks[index + 1] || dropdownLinks[0];
nextLink.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prevLink = dropdownLinks[index - 1] || dropdownLinks[dropdownLinks.length - 1];
prevLink.focus();
} else if (e.key === 'Escape') {
e.preventDefault();
dropdown.querySelector('a').focus();
const dropdownContent = dropdown.querySelector('.dropdown-content');
dropdownContent.style.opacity = '0';
dropdownContent.style.visibility = 'hidden';
setTimeout(() => {
dropdownContent.style.display = 'none';
}, 200);
}
});
});
});
}
});

View File

@ -0,0 +1,83 @@
# Implementation Guide: Converting to the Includes System
This guide explains how to convert the existing portfolio website to use the includes system for headers and footers.
## What We've Created
1. **Header and Footer Templates**
- `includes/header.html`: Contains the common header elements
- `includes/footer.html`: Contains the common footer elements
2. **JavaScript for Includes**
- `includes.js`: Handles the inclusion of header and footer files and applies the correct active states to navigation items
3. **Example Files**
- `template-with-includes.html`: Basic template
- `index-with-includes.html`: Example of the index page
- `stories/story-with-includes.html`: Example of a story page
- `one-pager-tools/tool-with-includes.html`: Example of a tool page
4. **Conversion Helper**
- `convert-to-includes.js`: A script to help convert existing HTML files to use the includes system
## Implementation Steps
### 1. Update the CSP in Caddyfile and Caddyfile.local
Add the includes.js script hash to the Content-Security-Policy:
```
Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-HASH_FOR_INCLUDES_JS' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; ...
```
### 2. Convert Existing Pages
For each HTML page:
1. Add the includes.js script to the head section:
```html
<script src="../includes.js"></script>
```
(Adjust the path as needed based on the location of the HTML file)
2. Replace the header content (everything from the opening `<body>` tag to the opening `<div class="container-fluid">` tag) with:
```html
<!-- Header Include -->
<div id="header-include"></div>
```
3. Replace the footer content (everything from the closing `</div>` of the main container to the closing `</body>` tag) with:
```html
<!-- Footer Include -->
<div id="footer-include"></div>
```
You can use the `convert-to-includes.js` script to help with this process:
1. Temporarily add this script to an existing page:
```html
<script src="convert-to-includes.js"></script>
```
2. Open the page in a browser and check the console output
3. Use the generated HTML as a starting point for your conversion
### 3. Testing
After converting each page:
1. Test the page in a browser to ensure it loads correctly
2. Verify that the navigation active states work as expected
3. Check that all CSS and JavaScript files are loaded correctly
## Benefits
- **Easier Maintenance**: Changes to the header or footer only need to be made in one place
- **Consistency**: All pages will have the same header and footer structure
- **Reduced File Size**: Each HTML file will be smaller since the common elements are externalized
- **Improved Developer Experience**: Easier to focus on the unique content of each page
## Future Enhancements
- **Dynamic Meta Tags**: Enhance the includes system to support dynamic meta tags and titles
- **Page-Specific CSS/JS**: Add support for page-specific CSS and JavaScript files
- **Breadcrumbs**: Implement a breadcrumb system that works with the includes system

View File

@ -0,0 +1,81 @@
# HTML Includes System
This system allows for separating headers and footers into external HTML files that can be included in all individual pages, making maintenance easier and ensuring consistency across the site.
## Files
- `header.html`: Contains the common header elements for all pages
- `footer.html`: Contains the common footer elements for all pages
- `includes.js`: JavaScript file that handles the inclusion of header and footer files and applies the correct active states to navigation items
## How to Use
### 1. Include the JavaScript
Add the `includes.js` script to your HTML file:
```html
<script src="../includes.js"></script>
```
(Adjust the path as needed based on the location of your HTML file)
### 2. Add Include Placeholders
Add placeholder divs where you want the header and footer to be included:
```html
<!-- Header Include -->
<div id="header-include"></div>
<!-- Your page content here -->
<!-- Footer Include -->
<div id="footer-include"></div>
```
### 3. Example Structure
Here's a basic template for a page using includes:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Your description here">
<title>Your Title - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css">
<!-- Additional CSS files as needed -->
<script src="../theme.js"></script>
<script src="../includes.js"></script>
<!-- Additional JS files as needed -->
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<!-- Main Content -->
<h1>Your Page Title</h1>
<p>Your page content...</p>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>
```
## Navigation Active States
The `includes.js` file automatically sets the active state for navigation items based on the current page. The navigation items in `header.html` have IDs that are used to identify which item should be active.
## Example Files
See the following example files that demonstrate how to use the includes system:
- `/template-with-includes.html`: Basic template
- `/index-with-includes.html`: Example of the index page
- `/stories/story-with-includes.html`: Example of a story page
- `/one-pager-tools/tool-with-includes.html`: Example of a tool page

View File

@ -0,0 +1,5 @@
<!-- Favicon links for modern browsers -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">

View File

@ -0,0 +1,6 @@
<hr>
<p class="accessibility-notice"><strong>Accessibility:</strong> This website is designed and developed to meet WCAG 2.1 Level AAA standards, ensuring the highest level of accessibility for all users. Features include high contrast ratios, keyboard navigation, screen reader compatibility, and responsive design. The site supports both light and dark modes with automatic system preference detection.</p>
</div>
</body>
</html>

View File

@ -0,0 +1,67 @@
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.colinknapp.com"]);
_paq.push(["setDomains", ["*.colinknapp.com"]]);
_paq.push(["disableCookies"]);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//metrics.nixc.us/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '3']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src='/matomo.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img referrerpolicy="no-referrer-when-downgrade" src="//metrics.nixc.us/matomo.php?idsite=3&amp;rec=1" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->
<div class="theme-switch">
<button
id="themeToggle"
aria-label="Theme mode: Auto"
role="switch"
aria-checked="false"
title="Toggle between light, dark, and auto theme modes"
tabindex="0"
>🌓</button>
</div>
<nav class="main-nav">
<ul>
<li><a href="/" id="nav-portfolio">Portfolio</a></li>
<li class="dropdown">
<a href="/stories/" id="nav-stories">Stories</a>
<div class="dropdown-content">
<a href="/stories/airport-dns.html" id="nav-airportdns">Airport Dns</a>
<a href="/stories/app-development.html" id="nav-appdevelopment">App Development</a>
<a href="/stories/athion-turnaround.html" id="nav-athionturnaround">Athion Turnaround</a>
<a href="/stories/fawe-plotsquared.html" id="nav-faweplotsquared">Fawe Plotsquared</a>
<a href="/stories/healthcare-platform.html" id="nav-healthcareplatform">Healthcare Platform</a>
<a href="/stories/home-infrastructure.html" id="nav-homeinfrastructure">Home Infrastructure</a>
<a href="/stories/motherboard-repair.html" id="nav-motherboardrepair">Motherboard Repair</a>
<a href="/stories/nitric-leadership.html" id="nav-nitricleadership">Nitric Leadership</a>
<a href="/stories/nuclear-dns.html" id="nav-nucleardns">Nuclear DNS</a>
<a href="/stories/open-source-success.html" id="nav-opensourcesuccess">Open Source Success</a>
<a href="/stories/showerloop.html" id="nav-showerloop">Showerloop</a>
<a href="/stories/viperwire.html" id="nav-viperwire">Viperwire</a>
<a href="/stories/web-design-java.html" id="nav-webdesignjava">Web Design Java</a>
<a href="/stories/wordpress-security.html" id="nav-wordpresssecurity">Wordpress Security</a>
<a href="/stories/youtube-game-dev.html" id="nav-youtubegamedev">Youtube Game Dev</a>
</div>
</li>
<li class="dropdown">
<a href="/one-pager-tools/csv-tool.html" id="nav-tools">Tools</a>
<div class="dropdown-content">
<a href="/one-pager-tools/csv-tool.html" id="nav-csv">CSV Tool</a>
<a href="https://md.colinknapp.com" id="nav-markdown" target="_blank" rel="noopener noreferrer">Markdown Tool</a>
<a href="https://qr.colinknapp.com" id="nav-qrcode" target="_blank" rel="noopener noreferrer">QR Code Tool</a>
</div>
</li>
</ul>
</nav>
<div class="container-fluid" role="main">

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Portfolio">
<title>Colin Knapp - Portfolio</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<!-- Main Content -->
<h1>Colin Knapp</h1>
<p><strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
<strong>Contact:</strong> <a href="mailto:recruitme2025@colinknapp.com">recruitme2025@colinknapp.com</a> | <a href="https://colinknapp.com">colinknapp.com</a><br>
<strong>Schedule a Meeting:</strong> <a href="https://cal.com/colink/30min" target="_blank">30 Minute Meeting</a></p>
<hr>
<h2>Highlights & Measurables</h2>
<ul>
<li><strong>Cybersecurity Leadership:</strong> Currently spearheading <em><a href="http://ViperWire.ca">ViperWire.ca</a></em>, the public-facing arm of my AI-powered cybersecurity and development consultancy, delivering cutting-edge protection for digital assets (2023-Present).</li>
<li><strong>Open-Source Impact:</strong> Co-created <em>FastAsyncWorldEdit</em> and <em>PlotSquared</em>, revolutionizing Minecraft development by enabling massive transformative edits—scaling from 50,000 server-crashing edits to billions without interruption—powering a $2 billion game brand with global contributor support (2014-Present).</li>
<!-- Additional list items would go here -->
</ul>
<!-- Additional content would go here -->
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

441
docker/resume/index.html Normal file
View File

@ -0,0 +1,441 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Portfolio">
<title>Colin Knapp - Portfolio</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<h1>Colin Knapp</h1>
<!-- Contact Information -->
<section class="contact-info">
<p>
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
<strong>Contact:</strong>
<a href="mailto:recruitme2025@colinknapp.com">recruitme2025@colinknapp.com</a> |
<a href="https://colinknapp.com">colinknapp.com</a><br>
<strong>Schedule a Meeting:</strong>
<a href="https://cal.com/colink/30min" target="_blank">30 Minute Meeting</a>
</p>
</section>
<hr>
<!-- Highlights Section -->
<section class="highlights">
<h2>Highlights & Measurables</h2>
<ul>
<li>
<strong>Cybersecurity Leadership:</strong>
Currently spearheading <em><a href="stories/viperwire.html">ViperWire.ca</a></em>,
the public-facing arm of my AI-powered cybersecurity and development consultancy,
delivering cutting-edge protection for digital assets (2023-Present).
</li>
<li>
<strong>Open-Source Impact:</strong>
Co-created <em><a href="stories/fawe-plotsquared.html">FastAsyncWorldEdit</a></em>
and <em><a href="stories/fawe-plotsquared.html">PlotSquared</a></em>,
revolutionizing Minecraft development by enabling massive transformative edits—scaling
from 50,000 server-crashing edits to billions without interruption—powering a $2 billion
game brand with global contributor support (2014-Present).
</li>
<li>
<strong>Team Leadership:</strong>
Managed a distributed team of 45 personnel at
<a href="stories/nitric-leadership.html">Nitric Concepts</a>,
fostering collaboration and deploying advanced DevSecOps practices (2018-2021).
</li>
<li>
<strong>On-Premises Innovation:</strong>
Architected self-managed, bare-metal infrastructure with orchestration for on-premises
deployments, delivering performant, scalable systems compliant with WCAG 2.0 AA.
Developed custom Docker solutions leveraging Docker Swarm and cluster management tools
like Salt Stack, Bash, and Python, working with web hosting tools and DNS infrastructure
to meet stringent government accessibility and compliance goals (2020-Present).
</li>
<li>
<strong>Government Projects:</strong>
Delivered scalable, secure learning management systems for the US government and
consulted on <a href="stories/airport-dns.html">Flint Bishop International Airport</a>'s
website and domain infrastructure via Addis Enterprises, building a geographically
redundant DNS cluster with an A+ standard resilient to extreme scenarios (2019-Present).
</li>
<li>
<strong>Healthcare Infrastructure:</strong>
Developed and deployed infrastructure for
<a href="stories/healthcare-platform.html">Improving MI Practices</a>,
a critical healthcare education platform, ensuring high availability and security
for sensitive medical training content (2023-Present).
</li>
<li>
<strong>Security Automation:</strong>
Created a Docker-based utility for automated WordPress malware removal and hardening,
successfully deployed to protect <a href="stories/wordpress-security.html">MLPP</a>
from persistent cyber attacks, reducing infection frequency from daily to zero (2023).
</li>
</ul>
</section>
<hr>
<!-- Employment Section -->
<section class="employment">
<h2>Employment</h2>
<!-- Addis Enterprises -->
<article class="position">
<h3>DevSecOps Consultant</h3>
<p class="company">Addis Enterprises</p>
<p class="timeframe">2019-Present</p>
<p class="overview">Leading infrastructure and security projects for government and healthcare clients,
specializing in scalable systems, compliance, and domain resilience.</p>
<ul>
<li>Delivered WCAG 2.0 AA compliant learning management systems for US government clients</li>
<li>Architected geographically redundant DNS infrastructure for Bishop Airport (bishopairport.org)</li>
<li>Developed automated malware eradication tools for healthcare platforms</li>
<li>Implemented CIS Level 1 and 2 security standards across multiple client environments</li>
</ul>
</article>
<!-- Nitric Concepts -->
<article class="position">
<h3>Chief of Operations / VP / Co-Founder</h3>
<p class="company">Nitric Concepts</p>
<p class="timeframe">2018-2021</p>
<p class="overview">Bootstrapped and led all operational aspects of a global gaming technology company,
managing software development, talent acquisition, and providing executive mentorship to drive significant business growth.</p>
<ul>
<li>Bootstrapped company from zero to an estimated $4M gross revenue with strong profit margins</li>
<li>Managed software development and talent acquisition for 45+ contractors across multiple timezones</li>
<li>Utilized revolutionary open source tooling developed with Athion to produce Nitric Concepts products</li>
<li>Co-developed FastAsyncWorldEdit and PlotSquared, birthing a sustainable 10-year open source brand
that continues today with paid maintainers</li>
<li>Established Jenkins CI/CD infrastructure in 2013 that continues running the open source ecosystem</li>
<li>Implemented DevSecOps practices using Docker, Fail2Ban, and Salt Stack for high-traffic gaming environments</li>
<li>Utilized Kanban/Trello to coordinate distributed teams and project workflows</li>
<li>Provided executive mentorship to CEO/Founder Andrew Karvelis, helping scale from side project earnings to
significant revenue growth</li>
<li>Established CI/CD pipelines and security practices for enterprise-scale gaming infrastructure</li>
</ul>
</article>
<!-- ViperWire -->
<article class="position">
<h3>Founder</h3>
<p class="company">ViperWire.ca</p>
<p class="timeframe">2023-Present</p>
<p class="overview">Building an AI-powered cybersecurity consultancy focused on making enterprise-grade
security accessible to small to medium businesses.</p>
<ul>
<li>Developing AI-augmented security analysis systems</li>
<li>Creating scalable security solutions for SMBs</li>
<li>Establishing cybersecurity consulting framework</li>
</ul>
</article>
<!-- MotherboardRepair.ca -->
<article class="position">
<h3>Co-Founder</h3>
<p class="company">MotherboardRepair.ca</p>
<p class="timeframe">2019-Present</p>
<p class="overview">Co-founded a company focused on reducing e-waste through circuit board repairs
and sustainable tech solutions.</p>
<ul>
<li>Leveraged industry expertise for sustainable electronics solutions</li>
<li>Established repair processes for high-value circuit board components</li>
<li>Promoted environmental responsibility in electronics industry</li>
</ul>
</article>
</section>
<hr>
<!-- Project Experience -->
<section class="project-experience">
<h2>Project Experience</h2>
<!-- DevSecOps Project -->
<article class="project">
<h3><a href="stories/airport-dns.html">Bishop Airport DNS Infrastructure</a></h3>
<p>
<strong>Timeframe:</strong> 2019-Present<br>
<strong>Overview:</strong> Collaborated on US government projects and Bishop Airport (bishopairport.org)
infrastructure via Addis Enterprises, focusing on scalable, secure systems and domain resilience.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Partnered with senior professionals to deliver learning management systems meeting WCAG 2.0 AA compliance for government clients.</li>
<li>Consulted for Bishop Airport, architecting a geographically redundant DNS cluster achieving an A+ standard, capable of withstanding extreme disruptions.</li>
<li>Provided exceptional client service through effective communication and tailored solutions.</li>
</ul>
<p><strong>Impact:</strong> Strengthened government digital infrastructure and ensured robust, resilient airport domain systems.</p>
<p><a href="stories/airport-dns.html" class="read-more">Read more about the Airport DNS project &rarr;</a></p>
</article>
<!-- Healthcare Platform Project -->
<article class="project">
<h3><a href="stories/healthcare-platform.html">Improving MI Practices Healthcare Platform</a></h3>
<p>
<strong>Timeframe:</strong> 2019-Present<br>
<strong>Overview:</strong> Led infrastructure design and operations for
<a href="stories/healthcare-platform.html">Improving MI Practices</a> through Addis Enterprises,
a critical healthcare education platform.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Designed and implemented secure, scalable infrastructure for healthcare education content delivery.</li>
<li>Administered CIS Level 1 and 2 security standards implementation for enhanced system hardening and security controls.</li>
<li>Implemented automated deployment pipelines and monitoring systems for high availability.</li>
</ul>
<p><strong>Impact:</strong> Enabled reliable delivery of critical healthcare training content to medical professionals while maintaining robust security standards.</p>
<p><a href="stories/healthcare-platform.html" class="read-more">Read more about the Healthcare Platform project &rarr;</a></p>
</article>
<!-- WordPress Security Project -->
<article class="project">
<h3><a href="stories/wordpress-security.html">MLPP WordPress Malware Eradication</a></h3>
<p>
<strong>Timeframe:</strong> 2023<br>
<strong>Overview:</strong> Tasked by Addis Enterprises to develop an automated solution for
WordPress malware removal and hardening to stop a malignant and ongoing infection.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Created a Docker-based utility for automated malware detection and removal.</li>
<li>Implemented hardening measures to prevent reinfection.</li>
<li>Successfully deployed to protect MLPP from persistent cyber attacks.</li>
</ul>
<p><strong>Impact:</strong> Reduced infection frequency from daily/weekly to zero, significantly improving site security and reliability.</p>
<p><a href="stories/wordpress-security.html" class="read-more">Read more about the WordPress Security project &rarr;</a></p>
</article>
<!-- YouTube Game Development Project -->
<article class="project">
<h3>YouTube Game Development & Cybersecurity</h3>
<p>
<strong>Timeframe:</strong> 2011-2022<br>
<strong>Overview:</strong> Developed custom Minecraft-based games for major YouTube celebrities and Multi-Creator Networks (MCNs),
including partnerships with Jordan Matthewson (Kootra) and TheCreatures MCN with 15 million subscriber reach.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Designed and developed custom Minecraft games to deliver engaging content for prominent YouTube creators and livestreams.</li>
<li>Partnered with Jordan Matthewson (Kootra) and TheCreatures MCN, providing gaming experiences for their 15 million subscriber network.</li>
<li>Collaborated with minor to major names in the YouTube community, including creators with massive followings during the platform's golden era.</li>
<li>Implemented DDoS defense, anti-phishing protocols, and data privacy measures for high-profile content and livestreams.</li>
<li>Managed hardware/software lifecycles and created comprehensive documentation for celebrity partnerships and MCN collaborations.</li>
</ul>
<p><strong>Impact:</strong> Delivered secure, seamless gaming experiences to millions of viewers through celebrity partnerships, MCN collaborations, and engaging content creation for livestreams. A critical incident where Jordan Matthewson was "swatted" during a live stream with 15,000 viewers on Twitch.tv became a pivotal moment that accelerated my focus on cybersecurity, ultimately leading to AddisEnterprises and partnerships with various US government organizations.</p>
<p><a href="stories/youtube-game-dev.html" class="read-more">Read more about YouTube Game Development &rarr;</a></p>
</article>
<!-- Web Design & Java Project -->
<article class="project">
<h3><a href="stories/web-design-java.html">Web Design & Java Plugin Development</a></h3>
<p>
<strong>Timeframe:</strong> 2011-2023<br>
<strong>Overview:</strong> Developed web solutions and Java plugins focusing on CI/CD efficiency
and client satisfaction, using strategic networking in IRC communities to bootstrap early career opportunities.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Lurked in IRC chat rooms of various plugin and software developers to assess reliable open source projects for early career dependencies.</li>
<li>Offered CI/CD services and Java Maven repository hosting to open source developers, building goodwill and reciprocal support networks.</li>
<li>Utilized Jenkins and GitLab CI/CD for streamlined workflows, leveraging a robust toolchain for rapid development.</li>
<li>Managed complex systems and ensured WCAG 2.0 AA accessibility standards.</li>
<li>Provided technical guidance and detailed client documentation, drawing on broad experience to resolve diverse issues.</li>
</ul>
<p><strong>Impact:</strong> Enhanced project delivery speed and quality for diverse computing environments through prolific and efficient development practices, while strategically building support networks in the open source community.</p>
<p><a href="stories/web-design-java.html" class="read-more">Read more about Web Design & Java Plugin Development &rarr;</a></p>
</article>
<!-- App Development Project -->
<article class="project">
<h3><a href="stories/app-development.html">Ad Marketing Link Tracking Tool</a></h3>
<p>
<strong>Timeframe:</strong> 2013-2018<br>
<strong>Overview:</strong> Developed an unbranded ad marketing link tracking tool for YouTubers
to manage ad read campaigns, enabling creators to release ad campaigns and receive payments directly.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Built a comprehensive ad campaign management system for content creators to track and monetize ad reads.</li>
<li>Designed user-friendly tools for real-time revenue monitoring and campaign performance tracking.</li>
<li>Implemented secure payment processing and data handling for creator monetization.</li>
<li>Created an unbranded solution that could be white-labeled for various influencer networks.</li>
</ul>
<p><strong>Impact:</strong> Developed a complete ad campaign management platform that would have empowered creators to maximize earnings through direct ad read monetization, though the project was shelved due to client changes.</p>
<p><a href="stories/app-development.html" class="read-more">Read more about the Ad Marketing Link Tracking Tool &rarr;</a></p>
</article>
<!-- NitricConcepts Project -->
<article class="project">
<h3><a href="stories/nitric-leadership.html">DevOps & Co-Founder at Nitric Concepts</a></h3>
<p>
<strong>Timeframe:</strong> 2018-2021<br>
<strong>Overview:</strong> Co-founded and mentored Andrew Karvelis in building Nitric Concepts from a side project
to a $4M gross revenue company, providing executive mentorship and operational leadership.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Provided executive mentorship to CEO/Founder Andrew Karvelis, helping scale from side project earnings to significant revenue growth.</li>
<li>Managed 45 contractors worldwide, implementing Docker, Fail2Ban, and Salt Stack as part of a comprehensive toolchain.</li>
<li>Co-developed <em><a href="stories/fawe-plotsquared.html">FastAsyncWorldEdit</a></em> and <em><a href="stories/fawe-plotsquared.html">PlotSquared</a></em>, enabling billions of seamless edits for Minecraft creators.</li>
<li>Bootstrapped the company from zero to an estimated $4M gross revenue with strong profit margins.</li>
<li>Utilized Kanban/Trello to coordinate distributed teams and project workflows across multiple timezones.</li>
</ul>
<p><strong>Impact:</strong> Transformed Nitric Concepts into a thriving multinational entity through prolific and efficient development, while mentoring Andrew Karvelis from side project earnings to being able to purchase a Corvette C7 in cash from one month's disposable income.</p>
<p><a href="stories/nitric-leadership.html" class="read-more">Read more about my leadership at Nitric Concepts &rarr;</a></p>
</article>
<!-- Entrepreneurial Ventures -->
<article class="project">
<h3>Entrepreneurial Ventures</h3>
<h4><a href="stories/athion-turnaround.html">Athion.net</a> Turnaround</h4>
<p>
<strong>Timeframe:</strong> 2013-2017<br>
<strong>Overview:</strong> Revitalized a struggling business into a self-sustaining operation in two weeks,
developing revolutionary open source tooling with Athion to produce Nitric Concepts products.<br>
<strong>Key Contributions:</strong> Optimized systems and streamlined operations with rapid, effective solutions,
creating innovative tooling that would later power major open source projects.<br>
<strong>Impact:</strong> Created a profitable, independent venture while developing foundational technology
that would enable future open source success.
</p>
<p><a href="stories/athion-turnaround.html" class="read-more">Read more about the Athion.net Turnaround &rarr;</a></p>
<h4><a href="stories/motherboard-repair.html">MotherboardRepair.ca</a></h4>
<p>
<strong>Timeframe:</strong> 2019-Present<br>
<strong>Overview:</strong> Co-founded a company reducing e-waste through circuit board repairs.<br>
<strong>Key Contributions:</strong> Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.<br>
<strong>Impact:</strong> Promoted environmental responsibility in electronics.
</p>
<p><a href="stories/motherboard-repair.html" class="read-more">Read more about MotherboardRepair.ca &rarr;</a></p>
</article>
</section>
<hr>
<!-- Additional Information -->
<section class="additional-info">
<h2>Additional Information</h2>
<h3>Personal Development</h3>
<p><strong>Timeframe:</strong> 2011-Present</p>
<ul>
<li>
<strong>Self-Taught Mastery:</strong> Continuously honed cybersecurity and systems management skills,
building a broad knowledge base to tackle unique challenges with a passion for innovation and problem-solving.
</li>
<li>
<strong>Open-Source Contributions:</strong> Actively maintain smaller self-run open-source projects;
previously led <em>OhMyForm</em> (sunset project now fully retired in favor of
<a href="https://formbricks.com/" target="_blank" rel="noopener noreferrer">Formbricks</a>),
an open source alternative to Google Forms and Office 365 that enabled data sovereignty
and privacy-focused form building, and contributed to <em><a href="stories/fawe-plotsquared.html">PlotSquared</a></em>,
<em><a href="stories/fawe-plotsquared.html">FastAsyncWorldEdit</a></em>, and <em>PlotHider</em>,
reflecting a prolific commitment to advancing technology.
</li>
<li>
<strong>Skill Maintenance:</strong> Regularly run Woodpecker CI and Gitea for on-premise source management,
testing, and deployment, employing security scanning and unit testing to ensure core functionality and
security baselines, alongside self-hosting exercises to sustain rapid, high-volume development capabilities
across a vast array of innovative projects.
</li>
</ul>
<p><a href="stories/open-source-success.html" class="read-more">Read more about my open source contributions &rarr;</a></p>
<h3>Relevant Links & Web Impact</h3>
<ul>
<li>
<strong>Repositories:</strong>
<a href="https://github.com/IntellectualSites/PlotSquared">PlotSquared</a>,
<a href="https://github.com/IntellectualSites/FastAsyncWorldEdit">FastAsyncWorldEdit</a>,
<a href="https://github.com/OhMyForm/OhMyForm">OhMyForm</a>,
<a href="https://github.com/IntellectualSites/plothider">PlotHider</a>
</li>
<li>
<strong>Projects:</strong>
<a href="stories/viperwire.html">ViperWire.ca</a>,
<a href="stories/nitric-leadership.html">Nitric Concepts</a>,
<a href="https://showerloop.cc">ShowerLoop</a>
</li>
</ul>
</section>
<hr>
<!-- Open Source Section -->
<section class="open-source" role="region" aria-labelledby="open-source-heading">
<h2 id="open-source-heading"><a href="stories/open-source-success.html">Open Source & Infrastructure</a></h2>
<div class="entry">
<h3><a href="stories/fawe-plotsquared.html">PlotSquared & FastAsyncWorldEdit</a></h3>
<p class="date">2013-Present</p>
<p class="overview">Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.</p>
<ul>
<li>Contributed to <a href="stories/fawe-plotsquared.html">PlotSquared</a>, a land management plugin with 572+ stars and 809+ forks</li>
<li>Enhanced <a href="stories/fawe-plotsquared.html">FastAsyncWorldEdit</a>, improving world manipulation performance with 664+ stars</li>
<li>Implemented security improvements and performance optimizations for large-scale server operations</li>
</ul>
<p><a href="stories/fawe-plotsquared.html" class="read-more">Read more about PlotSquared & FastAsyncWorldEdit &rarr;</a></p>
</div>
<div class="entry">
<h3>Athion.net Infrastructure</h3>
<p class="date">2013-Present</p>
<p class="overview">Established and maintained critical infrastructure supporting Nitric Concepts' official Minecraft Marketplace partnership, with PlotSquared and FastAsyncWorldEdit tooling used by content production companies in the $5 billion Minecraft gaming ecosystem.</p>
<ul>
<li>Set up and maintained <a href="https://ci.athion.net/" target="_blank">Jenkins CI/CD pipeline</a> since 2013, supporting Nitric Concepts' official marketplace partnership and the tooling used by content production companies</li>
<li>Hosted infrastructure enabling collaboration between developers and Microsoft for official Minecraft Marketplace content creation through Nitric Concepts</li>
<li>Implemented robust security measures and performance optimizations for high-traffic development environments supporting a $5 billion gaming brand</li>
</ul>
</div>
<div class="experience-item">
<h3><a href="stories/open-source-success.html">Software Engineer</a></h3>
<p class="company">Oh My Form (Sunset Project)</p>
<p class="date">2020 - 2024</p>
<p class="achievement">Led development of Oh My Form (now sunset and succeeded by <a href="https://formbricks.com/" target="_blank" rel="noopener noreferrer">Formbricks</a>), an open source alternative to Google Forms and Office 365 that enabled data sovereignty and privacy-focused form building, achieving over 1.5 million Docker pulls as verified by <a href="https://hub.docker.com/u/ohmyform" target="_blank" rel="noopener noreferrer">Docker Hub</a> and <a href="https://archive.is/lZHAT" target="_blank" rel="noopener noreferrer">archived</a>.</p>
<ul>
<li>Developed and maintained a secure, high-performance form builder application focused on data sovereignty</li>
<li>Created an open source alternative to Google Forms and Office 365 for privacy-conscious organizations</li>
<li>Implemented robust security measures and best practices for data protection</li>
<li>Optimized application performance and user experience while maintaining privacy standards</li>
</ul>
<p><a href="stories/open-source-success.html" class="read-more">Read more about my open source success &rarr;</a></p>
</div>
<div class="entry">
<h3>Home Infrastructure Cluster & WireGuard Mesh Networking</h3>
<p class="date">2020-Present</p>
<p class="overview">Built a comprehensive home infrastructure cluster using repurposed MacMini hardware for complete data sovereignty, including self-hosted email, DNS, and 100+ services, plus developed a WireGuard mesh networking tool for quantum-resistant networking deployment.</p>
<ul>
<li>Constructed a home cluster from repurposed MacMini hardware to maintain complete data sovereignty and avoid cloud dependencies</li>
<li>Self-hosted critical infrastructure including email, DNS, and over 100 additional services for complete data control</li>
<li>Developed a WireGuard mesh networking tool designed to simplify deployment of mesh networks as opposed to traditional hub-and-spoke architectures</li>
<li>Created infrastructure tooling that enables ease of deployment for quantum-resistant networking solutions</li>
<li>Implemented comprehensive self-hosting strategy to keep personal and business data out of cloud environments</li>
<li>Architected and deployed georedundant, nuclear war-resistant DNS cluster for clients</li>
</ul>
</div>
</section>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,35 @@
/* Additional inline styles to fix layout */
.container-fluid {
max-width: 100%;
padding: 0 15px;
}
.tool-container {
width: 100%;
max-width: 100%;
}
.form-group.full-width {
width: 100%;
max-width: 100%;
}
#csvInput {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* More aggressive fixes for textarea */
textarea#csvInput {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 12px !important;
font-family: 'Courier New', monospace !important;
}
/* Fix container width */
body {
max-width: 100% !important;
padding: 20px !important;

77
docker/resume/matomo.js Normal file
View File

@ -0,0 +1,77 @@
/*!!
* Matomo - free/libre analytics platform
*
* JavaScript tracking client
*
* @link https://piwik.org
* @source https://github.com/matomo-org/matomo/blob/master/js/piwik.js
* @license https://piwik.org/free-software/bsd/ BSD-3 Clause (also in js/LICENSE.txt)
* @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
*/
;if(typeof _paq!=="object"){_paq=[]}if(typeof window.Matomo!=="object"){window.Matomo=window.Piwik=(function(){var s,b={},A={},K=document,g=navigator,ac=screen,X=window,h=X.performance||X.mozPerformance||X.msPerformance||X.webkitPerformance,u=X.encodeURIComponent,W=X.decodeURIComponent,k=unescape,M=[],I,v,am=[],z=0,ag=0,Y=0,m=false,q="";function p(au){try{return W(au)}catch(av){return unescape(au)}}function N(av){var au=typeof av;return au!=="undefined"}function D(au){return typeof au==="function"}function aa(au){return typeof au==="object"}function y(au){return typeof au==="string"||au instanceof String}function al(au){return typeof au==="number"||au instanceof Number
}function ad(au){return N(au)&&(al(au)||(y(au)&&au.length))}function E(av){if(!av){return true}var au;for(au in av){if(Object.prototype.hasOwnProperty.call(av,au)){return false}}return true}function ap(au){var av=typeof console;if(av!=="undefined"&&console&&console.error){console.error(au)}}function ak(){var az,ay,aB,av,au;for(az=0;az<arguments.length;az+=1){au=null;if(arguments[az]&&arguments[az].slice){au=arguments[az].slice()}av=arguments[az];aB=av.shift();var aA,aw;var ax=y(aB)&&aB.indexOf("::")>0;if(ax){aA=aB.split("::");aw=aA[0];aB=aA[1];if("object"===typeof v[aw]&&"function"===typeof v[aw][aB]){v[aw][aB].apply(v[aw],av)}else{if(au){am.push(au)}}}else{for(ay=0;ay<M.length;ay++){if(y(aB)){aw=M[ay];var aC=aB.indexOf(".")>0;if(aC){aA=aB.split(".");if(aw&&"object"===typeof aw[aA[0]]){aw=aw[aA[0]];aB=aA[1]}else{if(au){am.push(au);break}}}if(aw[aB]){aw[aB].apply(aw,av)}else{var aD="The method '"+aB+'\' was not found in "_paq" variable. Please have a look at the Matomo tracker documentation: https://developer.matomo.org/api-reference/tracking-javascript';
ap(aD);if(!aC){throw new TypeError(aD)}}if(aB==="addTracker"){break}if(aB==="setTrackerUrl"||aB==="setSiteId"){break}}else{aB.apply(M[ay],av)}}}}}function at(ax,aw,av,au){if(ax.addEventListener){ax.addEventListener(aw,av,au);return true}if(ax.attachEvent){return ax.attachEvent("on"+aw,av)}ax["on"+aw]=av}function n(au){if(K.readyState==="complete"){au()}else{if(X.addEventListener){X.addEventListener("load",au,false)}else{if(X.attachEvent){X.attachEvent("onload",au)}}}}function r(ax){var au=false;if(K.attachEvent){au=K.readyState==="complete"}else{au=K.readyState!=="loading"}if(au){ax();return}var aw;if(K.addEventListener){at(K,"DOMContentLoaded",function av(){K.removeEventListener("DOMContentLoaded",av,false);if(!au){au=true;ax()}})}else{if(K.attachEvent){K.attachEvent("onreadystatechange",function av(){if(K.readyState==="complete"){K.detachEvent("onreadystatechange",av);if(!au){au=true;ax()}}});if(K.documentElement.doScroll&&X===X.top){(function av(){if(!au){try{K.documentElement.doScroll("left")
}catch(ay){setTimeout(av,0);return}au=true;ax()}}())}}}at(X,"load",function(){if(!au){au=true;ax()}},false)}function ah(av,aA,aB){if(!av){return""}var au="",ax,aw,ay,az;for(ax in b){if(Object.prototype.hasOwnProperty.call(b,ax)){az=b[ax]&&"function"===typeof b[ax][av];if(az){aw=b[ax][av];ay=aw(aA||{},aB);if(ay){au+=ay}}}}return au}function an(av){var au;m=true;ah("unload");au=new Date();var aw=au.getTimeAlias();if((s-aw)>3000){s=aw+3000}if(s){do{au=new Date()}while(au.getTimeAlias()<s)}}function o(aw,av){var au=K.createElement("script");au.type="text/javascript";au.src=aw;if(au.readyState){au.onreadystatechange=function(){var ax=this.readyState;if(ax==="loaded"||ax==="complete"){au.onreadystatechange=null;av()}}}else{au.onload=av}K.getElementsByTagName("head")[0].appendChild(au)}function O(){var au="";try{au=X.top.document.referrer}catch(aw){if(X.parent){try{au=X.parent.document.referrer}catch(av){au=""}}}if(au===""){au=K.referrer}return au}function t(au){var aw=new RegExp("^([a-z]+):"),av=aw.exec(au);
return av?av[1]:null}function d(au){var aw=new RegExp("^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)"),av=aw.exec(au);return av?av[1]:au}function H(au){return(/^[0-9][0-9]*(\.[0-9]+)?$/).test(au)}function R(aw,ax){var au={},av;for(av in aw){if(aw.hasOwnProperty(av)&&ax(aw[av])){au[av]=aw[av]}}return au}function C(aw){var au={},av;for(av in aw){if(aw.hasOwnProperty(av)){if(H(aw[av])){au[av]=Math.round(aw[av])}else{throw new Error('Parameter "'+av+'" provided value "'+aw[av]+'" is not valid. Please provide a numeric value.')}}}return au}function l(av){var aw="",au;for(au in av){if(av.hasOwnProperty(au)){aw+="&"+u(au)+"="+u(av[au])}}return aw}function ao(av,au){av=String(av);return av.lastIndexOf(au,0)===0}function V(av,au){av=String(av);return av.indexOf(au,av.length-au.length)!==-1}function B(av,au){av=String(av);return av.indexOf(au)!==-1}function f(av,au){av=String(av);return av.substr(0,av.length-au)}function J(ax,aw,az){ax=String(ax);if(!az){az=""}var au=ax.indexOf("#");var aA=ax.length;
if(au===-1){au=aA}var ay=ax.substr(0,au);var av=ax.substr(au,aA-au);if(ay.indexOf("?")===-1){ay+="?"}else{if(!V(ay,"?")){ay+="&"}}return ay+u(aw)+"="+u(az)+av}function j(av,aw){av=String(av);if(av.indexOf("?"+aw+"=")===-1&&av.indexOf("&"+aw+"=")===-1&&av.indexOf("#"+aw+"=")===-1){return av}var aB="";var aD=av.indexOf("#");if(aD!==-1){aB=av.substr(aD+1);av=av.substr(0,aD)}var ax=av.indexOf("?");var au="";var aA=av;if(ax>-1){au=av.substr(ax+1);aA=av.substr(0,ax)}var az=function(aF){var aH;var aG=aF.length-1;for(aG;aG>=0;aG--){aH=aF[aG].split("=")[0];if(aH===aw){aF.splice(aG,1)}}return aF};if(au){var aC=az(au.split("&")).join("&");if(aC){aA+="?"+aC}}if(aB&&aB.indexOf("=")>0){var ay=aB.charAt(0)==="?";if(ay){aB=aB.substr(1)}var aE=az(aB.split("&")).join("&");if(aE){aA+="#";if(ay){aA+="?"}aA+=aE}}else{if(aB){aA+="#"+aB}}return aA}function e(aw,av){var au="[\\?&#]"+av+"=([^&#]*)";var ay=new RegExp(au);var ax=ay.exec(aw);return ax?p(ax[1]):""}function a(au){if(au&&String(au)===au){return au.replace(/^\s+|\s+$/g,"")
}return au}function G(au){return unescape(u(au))}function ar(aJ){var aw=function(aP,aO){return(aP<<aO)|(aP>>>(32-aO))},aK=function(aR){var aP="",aQ,aO;for(aQ=7;aQ>=0;aQ--){aO=(aR>>>(aQ*4))&15;aP+=aO.toString(16)}return aP},az,aM,aL,av=[],aD=1732584193,aB=4023233417,aA=2562383102,ay=271733878,ax=3285377520,aI,aH,aG,aF,aE,aN,au,aC=[];aJ=G(aJ);au=aJ.length;for(aM=0;aM<au-3;aM+=4){aL=aJ.charCodeAt(aM)<<24|aJ.charCodeAt(aM+1)<<16|aJ.charCodeAt(aM+2)<<8|aJ.charCodeAt(aM+3);aC.push(aL)}switch(au&3){case 0:aM=2147483648;break;case 1:aM=aJ.charCodeAt(au-1)<<24|8388608;break;case 2:aM=aJ.charCodeAt(au-2)<<24|aJ.charCodeAt(au-1)<<16|32768;break;case 3:aM=aJ.charCodeAt(au-3)<<24|aJ.charCodeAt(au-2)<<16|aJ.charCodeAt(au-1)<<8|128;break}aC.push(aM);while((aC.length&15)!==14){aC.push(0)}aC.push(au>>>29);aC.push((au<<3)&4294967295);for(az=0;az<aC.length;az+=16){for(aM=0;aM<16;aM++){av[aM]=aC[az+aM]}for(aM=16;aM<=79;aM++){av[aM]=aw(av[aM-3]^av[aM-8]^av[aM-14]^av[aM-16],1)}aI=aD;aH=aB;aG=aA;aF=ay;aE=ax;for(aM=0;
aM<=19;aM++){aN=(aw(aI,5)+((aH&aG)|(~aH&aF))+aE+av[aM]+1518500249)&4294967295;aE=aF;aF=aG;aG=aw(aH,30);aH=aI;aI=aN}for(aM=20;aM<=39;aM++){aN=(aw(aI,5)+(aH^aG^aF)+aE+av[aM]+1859775393)&4294967295;aE=aF;aF=aG;aG=aw(aH,30);aH=aI;aI=aN}for(aM=40;aM<=59;aM++){aN=(aw(aI,5)+((aH&aG)|(aH&aF)|(aG&aF))+aE+av[aM]+2400959708)&4294967295;aE=aF;aF=aG;aG=aw(aH,30);aH=aI;aI=aN}for(aM=60;aM<=79;aM++){aN=(aw(aI,5)+(aH^aG^aF)+aE+av[aM]+3395469782)&4294967295;aE=aF;aF=aG;aG=aw(aH,30);aH=aI;aI=aN}aD=(aD+aI)&4294967295;aB=(aB+aH)&4294967295;aA=(aA+aG)&4294967295;ay=(ay+aF)&4294967295;ax=(ax+aE)&4294967295}aN=aK(aD)+aK(aB)+aK(aA)+aK(ay)+aK(ax);return aN.toLowerCase()}function af(aw,au,av){if(!aw){aw=""}if(!au){au=""}if(aw==="translate.googleusercontent.com"){if(av===""){av=au}au=e(au,"u");aw=d(au)}else{if(aw==="cc.bingj.com"||aw==="webcache.googleusercontent.com"||aw.slice(0,5)==="74.6."){au=K.links[0].href;aw=d(au)}}return[aw,au,av]}function P(av){var au=av.length;if(av.charAt(--au)==="."){av=av.slice(0,au)}if(av.slice(0,2)==="*."){av=av.slice(1)
}if(av.indexOf("/")!==-1){av=av.substr(0,av.indexOf("/"))}return av}function aq(av){av=av&&av.text?av.text:av;if(!y(av)){var au=K.getElementsByTagName("title");if(au&&N(au[0])){av=au[0].text}}return av}function T(au){if(!au){return[]}if(!N(au.children)&&N(au.childNodes)){return au.children}if(N(au.children)){return au.children}return[]}function Z(av,au){if(!av||!au){return false}if(av.contains){return av.contains(au)}if(av===au){return true}if(av.compareDocumentPosition){return !!(av.compareDocumentPosition(au)&16)}return false}function Q(aw,ax){if(aw&&aw.indexOf){return aw.indexOf(ax)}if(!N(aw)||aw===null){return -1}if(!aw.length){return -1}var au=aw.length;if(au===0){return -1}var av=0;while(av<au){if(aw[av]===ax){return av}av++}return -1}function i(aw){if(!aw){return false}function au(ay,az){if(X.getComputedStyle){return K.defaultView.getComputedStyle(ay,null)[az]}if(ay.currentStyle){return ay.currentStyle[az]}}function ax(ay){ay=ay.parentNode;while(ay){if(ay===K){return true}ay=ay.parentNode
}return false}function av(aA,aG,ay,aD,aB,aE,aC){var az=aA.parentNode,aF=1;if(!ax(aA)){return false}if(9===az.nodeType){return true}if("0"===au(aA,"opacity")||"none"===au(aA,"display")||"hidden"===au(aA,"visibility")){return false}if(!N(aG)||!N(ay)||!N(aD)||!N(aB)||!N(aE)||!N(aC)){aG=aA.offsetTop;aB=aA.offsetLeft;aD=aG+aA.offsetHeight;ay=aB+aA.offsetWidth;aE=aA.offsetWidth;aC=aA.offsetHeight}if(aw===aA&&(0===aC||0===aE)&&"hidden"===au(aA,"overflow")){return false}if(az){if(("hidden"===au(az,"overflow")||"scroll"===au(az,"overflow"))){if(aB+aF>az.offsetWidth+az.scrollLeft||aB+aE-aF<az.scrollLeft||aG+aF>az.offsetHeight+az.scrollTop||aG+aC-aF<az.scrollTop){return false}}if(aA.offsetParent===az){aB+=az.offsetLeft;aG+=az.offsetTop}return av(az,aG,ay,aD,aB,aE,aC)}return true}return av(aw)}var aj={htmlCollectionToArray:function(aw){var au=[],av;if(!aw||!aw.length){return au}for(av=0;av<aw.length;av++){au.push(aw[av])}return au},find:function(au){if(!document.querySelectorAll||!au){return[]}var av=document.querySelectorAll(au);
return this.htmlCollectionToArray(av)},findMultiple:function(aw){if(!aw||!aw.length){return[]}var av,ax;var au=[];for(av=0;av<aw.length;av++){ax=this.find(aw[av]);au=au.concat(ax)}au=this.makeNodesUnique(au);return au},findNodesByTagName:function(av,au){if(!av||!au||!av.getElementsByTagName){return[]}var aw=av.getElementsByTagName(au);return this.htmlCollectionToArray(aw)},makeNodesUnique:function(au){var az=[].concat(au);au.sort(function(aB,aA){if(aB===aA){return 0}var aD=Q(az,aB);var aC=Q(az,aA);if(aD===aC){return 0}return aD>aC?-1:1});if(au.length<=1){return au}var av=0;var ax=0;var ay=[];var aw;aw=au[av++];while(aw){if(aw===au[av]){ax=ay.push(av)}aw=au[av++]||null}while(ax--){au.splice(ay[ax],1)}return au},getAttributeValueFromNode:function(ay,aw){if(!this.hasNodeAttribute(ay,aw)){return}if(ay&&ay.getAttribute){return ay.getAttribute(aw)}if(!ay||!ay.attributes){return}var ax=(typeof ay.attributes[aw]);if("undefined"===ax){return}if(ay.attributes[aw].value){return ay.attributes[aw].value
}if(ay.attributes[aw].nodeValue){return ay.attributes[aw].nodeValue}var av;var au=ay.attributes;if(!au){return}for(av=0;av<au.length;av++){if(au[av].nodeName===aw){return au[av].nodeValue}}return null},hasNodeAttributeWithValue:function(av,au){var aw=this.getAttributeValueFromNode(av,au);return !!aw},hasNodeAttribute:function(aw,au){if(aw&&aw.hasAttribute){return aw.hasAttribute(au)}if(aw&&aw.attributes){var av=(typeof aw.attributes[au]);return"undefined"!==av}return false},hasNodeCssClass:function(aw,au){if(aw&&au&&aw.className){var av=typeof aw.className==="string"?aw.className.split(" "):[];if(-1!==Q(av,au)){return true}}return false},findNodesHavingAttribute:function(ay,aw,au){if(!au){au=[]}if(!ay||!aw){return au}var ax=T(ay);if(!ax||!ax.length){return au}var av,az;for(av=0;av<ax.length;av++){az=ax[av];if(this.hasNodeAttribute(az,aw)){au.push(az)}au=this.findNodesHavingAttribute(az,aw,au)}return au},findFirstNodeHavingAttribute:function(aw,av){if(!aw||!av){return}if(this.hasNodeAttribute(aw,av)){return aw
}var au=this.findNodesHavingAttribute(aw,av);if(au&&au.length){return au[0]}},findFirstNodeHavingAttributeWithValue:function(ax,aw){if(!ax||!aw){return}if(this.hasNodeAttributeWithValue(ax,aw)){return ax}var au=this.findNodesHavingAttribute(ax,aw);if(!au||!au.length){return}var av;for(av=0;av<au.length;av++){if(this.getAttributeValueFromNode(au[av],aw)){return au[av]}}},findNodesHavingCssClass:function(ay,ax,au){if(!au){au=[]}if(!ay||!ax){return au}if(ay.getElementsByClassName){var az=ay.getElementsByClassName(ax);return this.htmlCollectionToArray(az)}var aw=T(ay);if(!aw||!aw.length){return[]}var av,aA;for(av=0;av<aw.length;av++){aA=aw[av];if(this.hasNodeCssClass(aA,ax)){au.push(aA)}au=this.findNodesHavingCssClass(aA,ax,au)}return au},findFirstNodeHavingClass:function(aw,av){if(!aw||!av){return}if(this.hasNodeCssClass(aw,av)){return aw}var au=this.findNodesHavingCssClass(aw,av);if(au&&au.length){return au[0]}},isLinkElement:function(av){if(!av){return false}var au=String(av.nodeName).toLowerCase();
var ax=["a","area"];var aw=Q(ax,au);return aw!==-1},setAnyAttribute:function(av,au,aw){if(!av||!au){return}if(av.setAttribute){av.setAttribute(au,aw)}else{av[au]=aw}}};var x={CONTENT_ATTR:"data-track-content",CONTENT_CLASS:"matomoTrackContent",LEGACY_CONTENT_CLASS:"piwikTrackContent",CONTENT_NAME_ATTR:"data-content-name",CONTENT_PIECE_ATTR:"data-content-piece",CONTENT_PIECE_CLASS:"matomoContentPiece",LEGACY_CONTENT_PIECE_CLASS:"piwikContentPiece",CONTENT_TARGET_ATTR:"data-content-target",CONTENT_TARGET_CLASS:"matomoContentTarget",LEGACY_CONTENT_TARGET_CLASS:"piwikContentTarget",CONTENT_IGNOREINTERACTION_ATTR:"data-content-ignoreinteraction",CONTENT_IGNOREINTERACTION_CLASS:"matomoContentIgnoreInteraction",LEGACY_CONTENT_IGNOREINTERACTION_CLASS:"piwikContentIgnoreInteraction",location:undefined,findContentNodes:function(){var av="."+this.CONTENT_CLASS;var aw="."+this.LEGACY_CONTENT_CLASS;var au="["+this.CONTENT_ATTR+"]";var ax=aj.findMultiple([av,aw,au]);return ax},findContentNodesWithinNode:function(ax){if(!ax){return[]
}var av=aj.findNodesHavingCssClass(ax,this.CONTENT_CLASS);av=aj.findNodesHavingCssClass(ax,this.LEGACY_CONTENT_CLASS,av);var au=aj.findNodesHavingAttribute(ax,this.CONTENT_ATTR);if(au&&au.length){var aw;for(aw=0;aw<au.length;aw++){av.push(au[aw])}}if(aj.hasNodeAttribute(ax,this.CONTENT_ATTR)){av.push(ax)}else{if(aj.hasNodeCssClass(ax,this.CONTENT_CLASS)){av.push(ax)}else{if(aj.hasNodeCssClass(ax,this.LEGACY_CONTENT_CLASS)){av.push(ax)}}}av=aj.makeNodesUnique(av);return av},findParentContentNode:function(av){if(!av){return}var aw=av;var au=0;while(aw&&aw!==K&&aw.parentNode){if(aj.hasNodeAttribute(aw,this.CONTENT_ATTR)){return aw}if(aj.hasNodeCssClass(aw,this.CONTENT_CLASS)){return aw}if(aj.hasNodeCssClass(aw,this.LEGACY_CONTENT_CLASS)){return aw}aw=aw.parentNode;if(au>1000){break}au++}},findPieceNode:function(av){var au;au=aj.findFirstNodeHavingAttribute(av,this.CONTENT_PIECE_ATTR);if(!au){au=aj.findFirstNodeHavingClass(av,this.CONTENT_PIECE_CLASS)}if(!au){au=aj.findFirstNodeHavingClass(av,this.LEGACY_CONTENT_PIECE_CLASS)
}if(au){return au}return av},findTargetNodeNoDefault:function(au){if(!au){return}var av=aj.findFirstNodeHavingAttributeWithValue(au,this.CONTENT_TARGET_ATTR);if(av){return av}av=aj.findFirstNodeHavingAttribute(au,this.CONTENT_TARGET_ATTR);if(av){return av}av=aj.findFirstNodeHavingClass(au,this.CONTENT_TARGET_CLASS);if(av){return av}av=aj.findFirstNodeHavingClass(au,this.LEGACY_CONTENT_TARGET_CLASS);if(av){return av}},findTargetNode:function(au){var av=this.findTargetNodeNoDefault(au);if(av){return av}return au},findContentName:function(av){if(!av){return}var ay=aj.findFirstNodeHavingAttributeWithValue(av,this.CONTENT_NAME_ATTR);if(ay){return aj.getAttributeValueFromNode(ay,this.CONTENT_NAME_ATTR)}var au=this.findContentPiece(av);if(au){return this.removeDomainIfIsInLink(au)}if(aj.hasNodeAttributeWithValue(av,"title")){return aj.getAttributeValueFromNode(av,"title")}var aw=this.findPieceNode(av);if(aj.hasNodeAttributeWithValue(aw,"title")){return aj.getAttributeValueFromNode(aw,"title")}var ax=this.findTargetNode(av);
if(aj.hasNodeAttributeWithValue(ax,"title")){return aj.getAttributeValueFromNode(ax,"title")}},findContentPiece:function(av){if(!av){return}var ax=aj.findFirstNodeHavingAttributeWithValue(av,this.CONTENT_PIECE_ATTR);if(ax){return aj.getAttributeValueFromNode(ax,this.CONTENT_PIECE_ATTR)}var au=this.findPieceNode(av);var aw=this.findMediaUrlInNode(au);if(aw){return this.toAbsoluteUrl(aw)}},findContentTarget:function(aw){if(!aw){return}var ax=this.findTargetNode(aw);if(aj.hasNodeAttributeWithValue(ax,this.CONTENT_TARGET_ATTR)){return aj.getAttributeValueFromNode(ax,this.CONTENT_TARGET_ATTR)}var av;if(aj.hasNodeAttributeWithValue(ax,"href")){av=aj.getAttributeValueFromNode(ax,"href");return this.toAbsoluteUrl(av)}var au=this.findPieceNode(aw);if(aj.hasNodeAttributeWithValue(au,"href")){av=aj.getAttributeValueFromNode(au,"href");return this.toAbsoluteUrl(av)}},isSameDomain:function(au){if(!au||!au.indexOf){return false}if(0===au.indexOf(this.getLocation().origin)){return true}var av=au.indexOf(this.getLocation().host);
if(8>=av&&0<=av){return true}return false},removeDomainIfIsInLink:function(aw){var av="^https?://[^/]+";var au="^.*//[^/]+";if(aw&&aw.search&&-1!==aw.search(new RegExp(av))&&this.isSameDomain(aw)){aw=aw.replace(new RegExp(au),"");if(!aw){aw="/"}}return aw},findMediaUrlInNode:function(ay){if(!ay){return}var aw=["img","embed","video","audio"];var au=ay.nodeName.toLowerCase();if(-1!==Q(aw,au)&&aj.findFirstNodeHavingAttributeWithValue(ay,"src")){var ax=aj.findFirstNodeHavingAttributeWithValue(ay,"src");return aj.getAttributeValueFromNode(ax,"src")}if(au==="object"&&aj.hasNodeAttributeWithValue(ay,"data")){return aj.getAttributeValueFromNode(ay,"data")}if(au==="object"){var az=aj.findNodesByTagName(ay,"param");if(az&&az.length){var av;for(av=0;av<az.length;av++){if("movie"===aj.getAttributeValueFromNode(az[av],"name")&&aj.hasNodeAttributeWithValue(az[av],"value")){return aj.getAttributeValueFromNode(az[av],"value")}}}var aA=aj.findNodesByTagName(ay,"embed");if(aA&&aA.length){return this.findMediaUrlInNode(aA[0])
}}},trim:function(au){return a(au)},isOrWasNodeInViewport:function(az){if(!az||!az.getBoundingClientRect||az.nodeType!==1){return true}var ay=az.getBoundingClientRect();var ax=K.documentElement||{};var aw=ay.top<0;if(aw&&az.offsetTop){aw=(az.offsetTop+ay.height)>0}var av=ax.clientWidth;if(X.innerWidth&&av>X.innerWidth){av=X.innerWidth}var au=ax.clientHeight;if(X.innerHeight&&au>X.innerHeight){au=X.innerHeight}return((ay.bottom>0||aw)&&ay.right>0&&ay.left<av&&((ay.top<au)||aw))},isNodeVisible:function(av){var au=i(av);var aw=this.isOrWasNodeInViewport(av);return au&&aw},buildInteractionRequestParams:function(au,av,aw,ax){var ay="";if(au){ay+="c_i="+u(au)}if(av){if(ay){ay+="&"}ay+="c_n="+u(av)}if(aw){if(ay){ay+="&"}ay+="c_p="+u(aw)}if(ax){if(ay){ay+="&"}ay+="c_t="+u(ax)}if(ay){ay+="&ca=1"}return ay},buildImpressionRequestParams:function(au,av,aw){var ax="c_n="+u(au)+"&c_p="+u(av);if(aw){ax+="&c_t="+u(aw)}if(ax){ax+="&ca=1"}return ax},buildContentBlock:function(aw){if(!aw){return}var au=this.findContentName(aw);
var av=this.findContentPiece(aw);var ax=this.findContentTarget(aw);au=this.trim(au);av=this.trim(av);ax=this.trim(ax);return{name:au||"Unknown",piece:av||"Unknown",target:ax||""}},collectContent:function(ax){if(!ax||!ax.length){return[]}var aw=[];var au,av;for(au=0;au<ax.length;au++){av=this.buildContentBlock(ax[au]);if(N(av)){aw.push(av)}}return aw},setLocation:function(au){this.location=au},getLocation:function(){var au=this.location||X.location;if(!au.origin){au.origin=au.protocol+"//"+au.hostname+(au.port?":"+au.port:"")}return au},toAbsoluteUrl:function(av){if((!av||String(av)!==av)&&av!==""){return av}if(""===av){return this.getLocation().href}if(av.search(/^\/\//)!==-1){return this.getLocation().protocol+av}if(av.search(/:\/\//)!==-1){return av}if(0===av.indexOf("#")){return this.getLocation().origin+this.getLocation().pathname+av}if(0===av.indexOf("?")){return this.getLocation().origin+this.getLocation().pathname+av}if(0===av.search("^[a-zA-Z]{2,11}:")){return av}if(av.search(/^\//)!==-1){return this.getLocation().origin+av
}var au="(.*/)";var aw=this.getLocation().origin+this.getLocation().pathname.match(new RegExp(au))[0];return aw+av},isUrlToCurrentDomain:function(av){var aw=this.toAbsoluteUrl(av);if(!aw){return false}var au=this.getLocation().origin;if(au===aw){return true}if(0===String(aw).indexOf(au)){if(":"===String(aw).substr(au.length,1)){return false}return true}return false},setHrefAttribute:function(av,au){if(!av||!au){return}aj.setAnyAttribute(av,"href",au)},shouldIgnoreInteraction:function(au){if(aj.hasNodeAttribute(au,this.CONTENT_IGNOREINTERACTION_ATTR)){return true}if(aj.hasNodeCssClass(au,this.CONTENT_IGNOREINTERACTION_CLASS)){return true}if(aj.hasNodeCssClass(au,this.LEGACY_CONTENT_IGNOREINTERACTION_CLASS)){return true}return false}};function ab(av,ay){if(ay){return ay}av=x.toAbsoluteUrl(av);if(B(av,"?")){var ax=av.indexOf("?");av=av.slice(0,ax)}if(V(av,"matomo.php")){av=f(av,"matomo.php".length)}else{if(V(av,"piwik.php")){av=f(av,"piwik.php".length)}else{if(V(av,".php")){var au=av.lastIndexOf("/");
var aw=1;av=av.slice(0,au+aw)}}}if(V(av,"/js/")){av=f(av,"js/".length)}return av}function S(aA){var aC="Matomo_Overlay";var av=new RegExp("index\\.php\\?module=Overlay&action=startOverlaySession&idSite=([0-9]+)&period=([^&]+)&date=([^&]+)(&segment=[^&]*)?");var aw=av.exec(K.referrer);if(aw){var ay=aw[1];if(ay!==String(aA)){return false}var az=aw[2],au=aw[3],ax=aw[4];if(!ax){ax=""}else{if(ax.indexOf("&segment=")===0){ax=ax.substr("&segment=".length)}}X.name=aC+"###"+az+"###"+au+"###"+ax}var aB=X.name.split("###");return aB.length===4&&aB[0]===aC}function ae(av,aA,aw){var az=X.name.split("###"),ay=az[1],au=az[2],ax=az[3],aB=ab(av,aA);o(aB+"plugins/Overlay/client/client.js?v=1",function(){Matomo_Overlay_Client.initialize(aB,aw,ay,au,ax)})}function w(){var aw;try{aw=X.frameElement}catch(av){return true}if(N(aw)){return(aw&&String(aw.nodeName).toLowerCase()==="iframe")?true:false}try{return X.self!==X.top}catch(au){return true}}function U(ct,cn){var bV=this,bo="mtm_consent",c1="mtm_cookie_consent",da="mtm_consent_removed",ch=af(K.domain,X.location.href,O()),di=P(ch[0]),bZ=p(ch[1]),bA=p(ch[2]),dg=false,cx="GET",dC=cx,aQ="application/x-www-form-urlencoded; charset=UTF-8",cR=aQ,aM=ct||"",bU="",dr="",cD="",cj=cn||"",bL="",b0="",bf,bu="",dy=["3mf","7z","aac","apk","arc","arj","asc","asf","asx","avi","azw3","bin","bz","bz2","csv","deb","dmg","doc","docx","epub","exe","flv","gif","gz","gzip","hqx","ibooks","jar","jpeg","jpg","js","md5","mobi","mov","movie","mp2","mp3","mp4","mpg","mpeg","msi","msp","obj","odb","odf","odg","ods","odt","ogg","ogv","pdf","phps","png","ply","ppt","pptx","qt","qtm","ra","ram","rar","rpm","rtf","sea","sha","sha256","sha512","sig","sit","stl","tar","tbz","tbz2","tgz","torrent","txt","wav","wma","wmv","wpd","xls","xlsx","xml","xz","z","zip"],aG=[di],bM=[],cS=[".paypal.com"],cy=[],bY=[],bj=[],bW=500,dl=true,c7,bg,b4,b1,aw,cH=["pk_campaign","mtm_campaign","piwik_campaign","matomo_campaign","utm_campaign","utm_source","utm_medium"],bT=["pk_kwd","mtm_kwd","piwik_kwd","matomo_kwd","utm_term"],cV=["mtm_campaign","matomo_campaign","mtm_cpn","pk_campaign","piwik_campaign","pk_cpn","utm_campaign","mtm_keyword","matomo_kwd","mtm_kwd","pk_keyword","piwik_kwd","pk_kwd","utm_term","mtm_source","pk_source","utm_source","mtm_medium","pk_medium","utm_medium","mtm_content","pk_content","utm_content","mtm_cid","pk_cid","utm_id","mtm_clid","mtm_group","pk_group","mtm_placement","pk_placement"],bv="_pk_",aD="pk_vid",ba=180,dp,bC,b5=false,aR="Lax",bx=false,de,bp,dm=true,bI,c8=33955200000,cE=1800000,dx=15768000000,bd=true,bR=false,bs=false,b3=false,aZ=false,cq,b9={},cC={},bz={},bG=200,cN={},ds={},dz={},a3={},co=[],by=false,ck=false,cp=[],cu=false,cZ=false,ax=false,dA=false,db=false,aW=false,bn=w(),cT=null,dq=null,a0,bO,cl=ar,bB,aU,bN=false,cK=0,bH=["id","ses","cvar","ref"],cY=false,bP=null,c9=[],cM=[],aF=Y++,aE=false,dn=true,cW=false;
try{bu=K.title}catch(cU){bu=""}function aL(dN){if(bx&&dN!==da){return 0}var dL=new RegExp("(^|;)[ ]*"+dN+"=([^;]*)"),dM=dL.exec(K.cookie);return dM?W(dM[2]):0}bP=!aL(da);function dG(dP,dQ,dT,dS,dN,dO,dR){if(bx&&dP!==da){return}var dM;if(dT){dM=new Date();dM.setTime(dM.getTime()+dT)}if(!dR){dR="Lax"}K.cookie=dP+"="+u(dQ)+(dT?";expires="+dM.toGMTString():"")+";path="+(dS||"/")+(dN?";domain="+dN:"")+(dO?";secure":"")+";SameSite="+dR;if((!dT||dT>=0)&&aL(dP)!==String(dQ)){var dL="There was an error setting cookie `"+dP+"`. Please check domain and path.";ap(dL)}}function cf(dL){var dN,dM;if(dm!==true&&!cY){for(dM=0;dM<cH.length;dM++){dL=j(dL,cH[dM])}for(dM=0;dM<bT.length;dM++){dL=j(dL,bT[dM])}for(dM=0;dM<cV.length;dM++){dL=j(dL,cV[dM])}}dL=j(dL,aD);dL=j(dL,"ignore_referrer");dL=j(dL,"ignore_referer");for(dM=0;dM<cy.length;dM++){dL=j(dL,cy[dM])}if(b1){dN=new RegExp("#.*");return dL.replace(dN,"")}return dL}function b8(dN,dL){var dO=t(dL),dM;if(dO){return dL}if(dL.slice(0,1)==="/"){return t(dN)+"://"+d(dN)+dL
}dN=cf(dN);dM=dN.indexOf("?");if(dM>=0){dN=dN.slice(0,dM)}dM=dN.lastIndexOf("/");if(dM!==dN.length-1){dN=dN.slice(0,dM+1)}return dN+dL}function c5(dN,dL){var dM;dN=String(dN).toLowerCase();dL=String(dL).toLowerCase();if(dN===dL){return true}if(dL.slice(0,1)==="."){if(dN===dL.slice(1)){return true}dM=dN.length-dL.length;if((dM>0)&&(dN.slice(dM)===dL)){return true}}return false}function cB(dL){var dM=document.createElement("a");if(dL.indexOf("//")!==0&&dL.indexOf("http")!==0){if(dL.indexOf("*")===0){dL=dL.substr(1)}if(dL.indexOf(".")===0){dL=dL.substr(1)}dL="http://"+dL}dM.href=x.toAbsoluteUrl(dL);if(dM.pathname){return dM.pathname}return""}function be(dM,dL){if(!ao(dL,"/")){dL="/"+dL}if(!ao(dM,"/")){dM="/"+dM}var dN=(dL==="/"||dL==="/*");if(dN){return true}if(dM===dL){return true}dL=String(dL).toLowerCase();dM=String(dM).toLowerCase();if(V(dL,"*")){dL=dL.slice(0,-1);dN=(!dL||dL==="/");if(dN){return true}if(dM===dL){return true}return dM.indexOf(dL)===0}if(!V(dM,"/")){dM+="/"}if(!V(dL,"/")){dL+="/"
}return dM.indexOf(dL)===0}function aA(dP,dR){var dM,dL,dN,dO,dQ;for(dM=0;dM<aG.length;dM++){dO=P(aG[dM]);dQ=cB(aG[dM]);if(c5(dP,dO)&&be(dR,dQ)){return true}}return false}function a6(dO){var dM,dL,dN;for(dM=0;dM<aG.length;dM++){dL=P(aG[dM].toLowerCase());if(dO===dL){return true}if(dL.slice(0,1)==="."){if(dO===dL.slice(1)){return true}dN=dO.length-dL.length;if((dN>0)&&(dO.slice(dN)===dL)){return true}}}return false}function cJ(dL){var dM,dO,dQ,dN,dP;if(!dL.length||!cS.length){return false}dO=d(dL);dQ=cB(dL);if(dO.indexOf("www.")===0){dO=dO.substr(4)}for(dM=0;dM<cS.length;dM++){dN=P(cS[dM]);dP=cB(cS[dM]);if(dN.indexOf("www.")===0){dN=dN.substr(4)}if(c5(dO,dN)&&be(dQ,dP)){return true}}return false}function au(){if(q&&q.length>0){return true}q=e(X.location.href,"tracker_install_check");return q&&q.length>0}function cI(){if(au()&&aa(X)){X.close()}}function cF(dL,dN){dL=dL.replace("send_image=0","send_image=1");var dM=new Image(1,1);dM.onload=function(){I=0;if(typeof dN==="function"){dN({request:dL,trackerUrl:aM,success:true})
}};dM.onerror=function(){if(typeof dN==="function"){dN({request:dL,trackerUrl:aM,success:false})}};dM.src=aM+(aM.indexOf("?")<0?"?":"&")+dL;cI()}function c2(dL){if(dC==="POST"){return true}return dL&&(dL.length>2000||dL.indexOf('{"requests"')===0)}function aT(){return"object"===typeof g&&"function"===typeof g.sendBeacon&&"function"===typeof Blob}function bh(dP,dS,dR){var dN=aT();if(!dN){return false}var dO={type:"application/x-www-form-urlencoded; charset=UTF-8"};var dT=false;var dM=aM;try{var dL=new Blob([dP],dO);if(dR&&!c2(dP)){dL=new Blob([],dO);dM=dM+(dM.indexOf("?")<0?"?":"&")+dP}dT=g.sendBeacon(dM,dL)}catch(dQ){return false}if(dT&&typeof dS==="function"){dS({request:dP,trackerUrl:aM,success:true,isSendBeacon:true})}cI();return dT}function dw(dM,dN,dL){if(!N(dL)||null===dL){dL=true}if(m&&bh(dM,dN,dL)){return}setTimeout(function(){if(m&&bh(dM,dN,dL)){return}var dQ;try{var dP=X.XMLHttpRequest?new X.XMLHttpRequest():X.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):null;dP.open("POST",aM,true);
dP.onreadystatechange=function(){if(this.readyState===4&&!(this.status>=200&&this.status<300)){var dR=m&&bh(dM,dN,dL);if(!dR&&dL){cF(dM,dN)}else{if(typeof dN==="function"){dN({request:dM,trackerUrl:aM,success:false,xhr:this})}}}else{if(this.readyState===4&&(typeof dN==="function")){dN({request:dM,trackerUrl:aM,success:true,xhr:this})}}};dP.setRequestHeader("Content-Type",cR);dP.withCredentials=true;dP.send(dM)}catch(dO){dQ=m&&bh(dM,dN,dL);if(!dQ&&dL){cF(dM,dN)}else{if(typeof dN==="function"){dN({request:dM,trackerUrl:aM,success:false})}}}cI()},50)}function cv(dM){var dL=new Date();var dN=dL.getTime()+dM;if(!s||dN>s){s=dN}}function bl(){bn=true;cT=new Date().getTime()}function dF(){var dL=new Date().getTime();return !cT||(dL-cT)>bg}function aH(){if(dF()){b4()}}function a5(){if(K.visibilityState==="hidden"&&dF()){b4()}else{if(K.visibilityState==="visible"){cT=new Date().getTime()}}}function dJ(){if(aW||!bg){return}aW=true;at(X,"focus",bl);at(X,"blur",aH);at(X,"visibilitychange",a5);ag++;v.addPlugin("HeartBeat"+ag,{unload:function(){if(aW&&dF()){b4()
}}})}function c0(dP){var dM=new Date();var dL=dM.getTime();dq=dL;if(cZ&&dL<cZ){var dN=cZ-dL;setTimeout(dP,dN);cv(dN+50);cZ+=50;return}if(cZ===false){var dO=800;cZ=dL+dO}dP()}function aX(){if(aL(da)){bP=false}else{if(aL(bo)){bP=true}}}function b2(dO){var dN,dM="",dL="";for(dN in dz){if(Object.prototype.hasOwnProperty.call(dz,dN)){dL+="&"+dN+"="+dz[dN]}}if(a3){dM="&uadata="+u(X.JSON.stringify(a3))}if(dO instanceof Array){for(dN=0;dN<dO.length;dN++){dO[dN]+=dM+dL}}else{dO+=dM+dL}return dO}function av(){return N(g.userAgentData)&&D(g.userAgentData.getHighEntropyValues)}function cG(dL){if(by||ck){return}ck=true;a3={brands:g.userAgentData.brands,platform:g.userAgentData.platform};g.userAgentData.getHighEntropyValues(["brands","model","platform","platformVersion","uaFullVersion","fullVersionList","formFactors"]).then(function(dN){var dM;if(dN.fullVersionList){delete dN.brands;delete dN.uaFullVersion}a3=dN;by=true;ck=false;dL()},function(dM){by=true;ck=false;dL()})}function bS(dM,dL,dN){aX();if(!bP){c9.push([dM,dN]);
return}if(dn&&!by&&av()){co.push([dM,dN]);return}aE=true;if(!de&&dM){if(cY&&bP){dM+="&consent=1"}dM=b2(dM);c0(function(){if(dl&&bh(dM,dN,true)){cv(100);return}if(c2(dM)){dw(dM,dN)}else{cF(dM,dN)}cv(dL)})}if(!aW){dJ()}}function cA(dL){if(de){return false}return(dL&&dL.length)}function dv(dL,dP){if(!dP||dP>=dL.length){return[dL]}var dM=0;var dN=dL.length;var dO=[];for(dM;dM<dN;dM+=dP){dO.push(dL.slice(dM,dM+dP))}return dO}function dH(dM,dL){if(!cA(dM)){return}if(dn&&!by&&av()){co.push([dM,null]);return}if(!bP){c9.push([dM,null]);return}aE=true;c0(function(){var dP=dv(dM,50);var dN=0,dO;for(dN;dN<dP.length;dN++){dO='{"requests":["?'+b2(dP[dN]).join('","?')+'"],"send_image":0}';if(dl&&bh(dO,null,false)){cv(100)}else{dw(dO,null,false)}}cv(dL)})}function a2(dL){return bv+dL+"."+cj+"."+bB}function cc(dN,dM,dL){dG(dN,"",-129600000,dM,dL)}function ci(){if(bx){return"0"}if(!N(X.showModalDialog)&&N(g.cookieEnabled)){return g.cookieEnabled?"1":"0"}var dL=bv+"testcookie";dG(dL,"1",undefined,bC,dp,b5,aR);
var dM=aL(dL)==="1"?"1":"0";cc(dL);return dM}function bt(){bB=cl((dp||di)+(bC||"/")).slice(0,4)}function ay(){var dM,dL;for(dM=0;dM<co.length;dM++){dL=typeof co[dM][0];if(dL==="string"){bS(co[dM][0],bW,co[dM][1])}else{if(dL==="object"){dH(co[dM][0],bW)}}}co=[]}function c6(){if(!dn){return{}}if(av()){cG(ay)}if(N(dz.res)){return dz}var dM,dO,dQ={pdf:"application/pdf",qt:"video/quicktime",realp:"audio/x-pn-realaudio-plugin",wma:"application/x-mplayer2",fla:"application/x-shockwave-flash",java:"application/x-java-vm",ag:"application/x-silverlight"};if(!((new RegExp("MSIE")).test(g.userAgent))){if(g.mimeTypes&&g.mimeTypes.length){for(dM in dQ){if(Object.prototype.hasOwnProperty.call(dQ,dM)){dO=g.mimeTypes[dQ[dM]];dz[dM]=(dO&&dO.enabledPlugin)?"1":"0"}}}try{if(!((new RegExp("Edge[ /](\\d+[\\.\\d]+)")).test(g.userAgent))&&typeof navigator.javaEnabled!=="unknown"&&N(g.javaEnabled)&&g.javaEnabled()){dz.java="1"}}catch(dP){}if(!N(X.showModalDialog)&&N(g.cookieEnabled)){dz.cookie=g.cookieEnabled?"1":"0"
}else{dz.cookie=ci()}}var dN=parseInt(ac.width,10);var dL=parseInt(ac.height,10);dz.res=parseInt(dN,10)+"x"+parseInt(dL,10);return dz}function ca(){var dM=a2("cvar"),dL=aL(dM);if(dL&&dL.length){dL=X.JSON.parse(dL);if(aa(dL)){return dL}}return{}}function c3(){if(aZ===false){aZ=ca()}}function df(){var dL=c6();return cl((g.userAgent||"")+(g.platform||"")+X.JSON.stringify(dL)+(new Date()).getTime()+Math.random()).slice(0,16)}function aJ(){var dL=c6();return cl((g.userAgent||"")+(g.platform||"")+X.JSON.stringify(dL)).slice(0,6)}function bq(){return Math.floor((new Date()).getTime()/1000)}function aS(){var dM=bq();var dN=aJ();var dL=String(dM)+dN;return dL}function du(dN){dN=String(dN);var dQ=aJ();var dO=dQ.length;var dP=dN.substr(-1*dO,dO);var dM=parseInt(dN.substr(0,dN.length-dO),10);if(dM&&dP&&dP===dQ){var dL=bq();if(ba<=0){return true}if(dL>=dM&&dL<=(dM+ba)){return true}}return false}function dI(dL){if(!db){return""}var dP=e(dL,aD);if(!dP){return""}dP=String(dP);var dN=new RegExp("^[a-zA-Z0-9]+$");
if(dP.length===32&&dN.test(dP)){var dM=dP.substr(16,32);if(du(dM)){var dO=dP.substr(0,16);return dO}}return""}function dc(){if(!b0){b0=dI(bZ)}var dN=new Date(),dL=Math.round(dN.getTime()/1000),dM=a2("id"),dQ=aL(dM),dP,dO;if(dQ){dP=dQ.split(".");dP.unshift("0");if(b0.length){dP[1]=b0}return dP}if(b0.length){dO=b0}else{if("0"===ci()){dO=""}else{dO=df()}}dP=["1",dO,dL];return dP}function a9(){var dO=dc(),dM=dO[0],dN=dO[1],dL=dO[2];return{newVisitor:dM,uuid:dN,createTs:dL}}function aP(){var dO=new Date(),dM=dO.getTime(),dP=a9().createTs;var dL=parseInt(dP,10);var dN=(dL*1000)+c8-dM;return dN}function aV(dL){if(!cj){return}var dN=new Date(),dM=Math.round(dN.getTime()/1000);if(!N(dL)){dL=a9()}var dO=dL.uuid+"."+dL.createTs+".";dG(a2("id"),dO,aP(),bC,dp,b5,aR)}function bX(){var dL=aL(a2("ref"));if(dL.length){try{dL=X.JSON.parse(dL);if(aa(dL)){return dL}}catch(dM){}}return["","",0,""]}function bJ(dN){var dM=bv+"testcookie_domain";var dL="testvalue";dG(dM,dL,10000,null,dN,b5,aR);if(aL(dM)===dL){cc(dM,null,dN);
return true}return false}function aN(){var dM=bx;bx=false;var dL,dN;for(dL=0;dL<bH.length;dL++){dN=a2(bH[dL]);if(dN!==da&&dN!==bo&&0!==aL(dN)){cc(dN,bC,dp)}}bx=dM}function cg(dL){cj=dL}function dK(dP){if(!dP||!aa(dP)){return}var dO=[];var dN;for(dN in dP){if(Object.prototype.hasOwnProperty.call(dP,dN)){dO.push(dN)}}var dQ={};dO.sort();var dL=dO.length;var dM;for(dM=0;dM<dL;dM++){dQ[dO[dM]]=dP[dO[dM]]}return dQ}function cs(){dG(a2("ses"),"1",cE,bC,dp,b5,aR)}function br(){var dO="";var dM="abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";var dN=dM.length;var dL;for(dL=0;dL<6;dL++){dO+=dM.charAt(Math.floor(Math.random()*dN))}return dO}function aI(dM){if(cD!==""){dM+=cD;bs=true;return dM}if(!h){return dM}var dN=(typeof h.timing==="object")&&h.timing?h.timing:undefined;if(!dN){dN=(typeof h.getEntriesByType==="function")&&h.getEntriesByType("navigation")?h.getEntriesByType("navigation")[0]:undefined}if(!dN){return dM}var dL="";if(dN.connectEnd&&dN.fetchStart){if(dN.connectEnd<dN.fetchStart){return dM
}dL+="&pf_net="+Math.round(dN.connectEnd-dN.fetchStart)}if(dN.responseStart&&dN.requestStart){if(dN.responseStart<dN.requestStart){return dM}dL+="&pf_srv="+Math.round(dN.responseStart-dN.requestStart)}if(dN.responseStart&&dN.responseEnd){if(dN.responseEnd<dN.responseStart){return dM}dL+="&pf_tfr="+Math.round(dN.responseEnd-dN.responseStart)}if(N(dN.domLoading)){if(dN.domInteractive&&dN.domLoading){if(dN.domInteractive<dN.domLoading){return dM}dL+="&pf_dm1="+Math.round(dN.domInteractive-dN.domLoading)}}else{if(dN.domInteractive&&dN.responseEnd){if(dN.domInteractive<dN.responseEnd){return dM}dL+="&pf_dm1="+Math.round(dN.domInteractive-dN.responseEnd)}}if(dN.domComplete&&dN.domInteractive){if(dN.domComplete<dN.domInteractive){return dM}dL+="&pf_dm2="+Math.round(dN.domComplete-dN.domInteractive)}if(dN.loadEventEnd&&dN.loadEventStart){if(dN.loadEventEnd<dN.loadEventStart){return dM}dL+="&pf_onl="+Math.round(dN.loadEventEnd-dN.loadEventStart)}return dM+dL}function cr(dL){return e(dL,"ignore_referrer")==="1"||e(dL,"ignore_referer")==="1"
}function dB(){var dV,dO=new Date(),dP=Math.round(dO.getTime()/1000),d0,dN,dQ=1024,dX,dR,dM=a2("ses"),dU=a2("ref"),dT=aL(dM),dL=bX(),dZ=bf||bZ,dW,dS,dY={};dW=dL[0];dS=dL[1];d0=dL[2];dN=dL[3];if(!cr(dZ)&&!dT){if((!bI||!dW.length)&&(dm||cY)){for(dV in cH){if(Object.prototype.hasOwnProperty.call(cH,dV)){dW=e(dZ,cH[dV]);if(dW.length){break}}}for(dV in bT){if(Object.prototype.hasOwnProperty.call(bT,dV)){dS=e(dZ,bT[dV]);if(dS.length){break}}}}dX=d(bA);dR=dN.length?d(dN):"";if(dX.length&&!a6(dX)&&!cJ(bA)&&(!bI||!dR.length||a6(dR)||cJ(dN))){dN=bA}if(dN.length||dW.length){d0=dP;dL=[dW,dS,d0,cf(dN.slice(0,dQ))];dG(dU,X.JSON.stringify(dL),dx,bC,dp,b5,aR)}}if(dW.length){dY._rcn=u(dW)}if(dS.length){dY._rck=u(dS)}dY._refts=d0;if(String(dN).length){dY._ref=u(cf(dN.slice(0,dQ)))}return dY}function cL(dM,dY,dZ){var dX,dL=new Date(),dW=aZ,dS=a2("cvar"),d1=bf||bZ,dN=cr(d1);if(bx){aN()}if(de){return""}var d0=new RegExp("^file://","i");if(!cW&&(X.location.protocol==="file:"||d0.test(d1))){return""}c6();var dT=a9();
var dQ=K.characterSet||K.charset;if(!dQ||dQ.toLowerCase()==="utf-8"){dQ=null}dM+="&idsite="+cj+"&rec=1&r="+String(Math.random()).slice(2,8)+"&h="+dL.getHours()+"&m="+dL.getMinutes()+"&s="+dL.getSeconds()+"&url="+u(cf(d1))+(bA.length&&!cJ(bA)&&!dN?"&urlref="+u(cf(bA)):"")+(ad(bL)?"&uid="+u(bL):"")+"&_id="+dT.uuid+"&_idn="+dT.newVisitor+(dQ?"&cs="+u(dQ):"")+"&send_image=0";var dV=dB();for(dX in dV){if(Object.prototype.hasOwnProperty.call(dV,dX)){dM+="&"+dX+"="+dV[dX]}}var d3=[];if(dY){for(dX in dY){if(Object.prototype.hasOwnProperty.call(dY,dX)&&/^dimension\d+$/.test(dX)){var dO=dX.replace("dimension","");d3.push(parseInt(dO,10));d3.push(String(dO));dM+="&"+dX+"="+u(dY[dX]);delete dY[dX]}}}if(dY&&E(dY)){dY=null}for(dX in cN){if(Object.prototype.hasOwnProperty.call(cN,dX)){dM+="&"+dX+"="+u(cN[dX])}}for(dX in bz){if(Object.prototype.hasOwnProperty.call(bz,dX)){var dR=(-1===Q(d3,dX));if(dR){dM+="&dimension"+dX+"="+u(bz[dX])}}}if(dY){dM+="&data="+u(X.JSON.stringify(dY))}else{if(aw){dM+="&data="+u(X.JSON.stringify(aw))
}}function dP(d4,d5){var d6=X.JSON.stringify(d4);if(d6.length>2){return"&"+d5+"="+u(d6)}return""}var d2=dK(b9);var dU=dK(cC);dM+=dP(d2,"cvar");dM+=dP(dU,"e_cvar");if(aZ){dM+=dP(aZ,"_cvar");for(dX in dW){if(Object.prototype.hasOwnProperty.call(dW,dX)){if(aZ[dX][0]===""||aZ[dX][1]===""){delete aZ[dX]}}}if(b3){dG(dS,X.JSON.stringify(aZ),cE,bC,dp,b5,aR)}}if(bd&&bR&&!bs){dM=aI(dM);bs=true}if(aU){dM+="&pv_id="+aU}aV(dT);cs();dM+=ah(dZ,{tracker:bV,request:dM});if(dr.length){dM+="&"+dr}if(au()){dM+="&tracker_install_check="+q}if(D(cq)){dM=cq(dM)}return dM}b4=function bi(){var dL=new Date();dL=dL.getTime();if(!dq){return false}if(dq+bg<=dL){bV.ping();return true}return false};function bD(dO,dN,dS,dP,dL,dV){var dR="idgoal=0",dM=new Date(),dT=[],dU,dQ=String(dO).length;if(dQ){dR+="&ec_id="+u(dO)}dR+="&revenue="+dN;if(String(dS).length){dR+="&ec_st="+dS}if(String(dP).length){dR+="&ec_tx="+dP}if(String(dL).length){dR+="&ec_sh="+dL}if(String(dV).length){dR+="&ec_dt="+dV}if(ds){for(dU in ds){if(Object.prototype.hasOwnProperty.call(ds,dU)){if(!N(ds[dU][1])){ds[dU][1]=""
}if(!N(ds[dU][2])){ds[dU][2]=""}if(!N(ds[dU][3])||String(ds[dU][3]).length===0){ds[dU][3]=0}if(!N(ds[dU][4])||String(ds[dU][4]).length===0){ds[dU][4]=1}dT.push(ds[dU])}}dR+="&ec_items="+u(X.JSON.stringify(dT))}dR=cL(dR,aw,"ecommerce");bS(dR,bW);if(dQ){ds={}}}function cb(dL,dP,dO,dN,dM,dQ){if(String(dL).length&&N(dP)){bD(dL,dP,dO,dN,dM,dQ)}}function bF(dL){if(N(dL)){bD("",dL,"","","","")}}function cd(dM,dO,dN){if(!bN){aU=br()}var dL=cL("action_name="+u(aq(dM||bu)),dO,"log");if(bd&&!bs){dL=aI(dL)}bS(dL,bW,dN)}function bb(dN,dM){var dO,dL="(^| )(piwik[_-]"+dM+"|matomo[_-]"+dM;if(dN){for(dO=0;dO<dN.length;dO++){dL+="|"+dN[dO]}}dL+=")( |$)";return new RegExp(dL)}function a4(dL){return(aM&&dL&&0===String(dL).indexOf(aM))}function cP(dP,dL,dQ,dM){if(a4(dL)){return 0}var dO=bb(bY,"download"),dN=bb(bj,"link"),dR=new RegExp("\\.("+dy.join("|")+")([?&#]|$)","i");if(dN.test(dP)){return"link"}if(dM||dO.test(dP)||dR.test(dL)){return"download"}if(dQ){return 0}return"link"}function aC(dM){var dL;dL=dM.parentNode;
while(dL!==null&&N(dL)){if(aj.isLinkElement(dM)){break}dM=dL;dL=dM.parentNode}return dM}function dE(dQ){dQ=aC(dQ);if(!aj.hasNodeAttribute(dQ,"href")){return}if(!N(dQ.href)){return}var dP=aj.getAttributeValueFromNode(dQ,"href");var dM=dQ.pathname||cB(dQ.href);var dR=dQ.hostname||d(dQ.href);var dS=dR.toLowerCase();var dN=dQ.href.replace(dR,dS);var dO=new RegExp("^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto|tel):","i");if(!dO.test(dN)){var dL=cP(dQ.className,dN,aA(dS,dM),aj.hasNodeAttribute(dQ,"download"));if(dL){return{type:dL,href:dN}}}}function aY(dL,dM,dN,dO){var dP=x.buildInteractionRequestParams(dL,dM,dN,dO);if(!dP){return}return cL(dP,null,"contentInteraction")}function bm(dL,dM){if(!dL||!dM){return false}var dN=x.findTargetNode(dL);if(x.shouldIgnoreInteraction(dN)){return false}dN=x.findTargetNodeNoDefault(dL);if(dN&&!Z(dN,dM)){return false}return true}function cO(dN,dM,dP){if(!dN){return}var dL=x.findParentContentNode(dN);if(!dL){return}if(!bm(dL,dN)){return}var dO=x.buildContentBlock(dL);
if(!dO){return}if(!dO.target&&dP){dO.target=dP}return x.buildInteractionRequestParams(dM,dO.name,dO.piece,dO.target)}function a7(dM){if(!cp||!cp.length){return false}var dL,dN;for(dL=0;dL<cp.length;dL++){dN=cp[dL];if(dN&&dN.name===dM.name&&dN.piece===dM.piece&&dN.target===dM.target){return true}}return false}function a8(dL){return function(dP){if(!dL){return}var dN=x.findParentContentNode(dL);var dM;if(dP){dM=dP.target||dP.srcElement}if(!dM){dM=dL}if(!bm(dN,dM)){return}if(!dN){return false}var dQ=x.findTargetNode(dN);if(!dQ||x.shouldIgnoreInteraction(dQ)){return false}var dO=dE(dQ);if(dA&&dO&&dO.type){return dO.type}return bV.trackContentInteractionNode(dM,"click")}}function ce(dN){if(!dN||!dN.length){return}var dL,dM;for(dL=0;dL<dN.length;dL++){dM=x.findTargetNode(dN[dL]);if(dM&&!dM.contentInteractionTrackingSetupDone){dM.contentInteractionTrackingSetupDone=true;at(dM,"click",a8(dM))}}}function bK(dN,dO){if(!dN||!dN.length){return[]}var dL,dM;for(dL=0;dL<dN.length;dL++){if(a7(dN[dL])){dN.splice(dL,1);
dL--}else{cp.push(dN[dL])}}if(!dN||!dN.length){return[]}ce(dO);var dP=[];for(dL=0;dL<dN.length;dL++){dM=cL(x.buildImpressionRequestParams(dN[dL].name,dN[dL].piece,dN[dL].target),undefined,"contentImpressions");if(dM){dP.push(dM)}}return dP}function cX(dM){var dL=x.collectContent(dM);return bK(dL,dM)}function bk(dM){if(!dM||!dM.length){return[]}var dL;for(dL=0;dL<dM.length;dL++){if(!x.isNodeVisible(dM[dL])){dM.splice(dL,1);dL--}}if(!dM||!dM.length){return[]}return cX(dM)}function aO(dN,dL,dM){var dO=x.buildImpressionRequestParams(dN,dL,dM);return cL(dO,null,"contentImpression")}function dD(dO,dM){if(!dO){return}var dL=x.findParentContentNode(dO);var dN=x.buildContentBlock(dL);if(!dN){return}if(!dM){dM="Unknown"}return aY(dM,dN.name,dN.piece,dN.target)}function dd(dM,dO,dL,dN){return"e_c="+u(dM)+"&e_a="+u(dO)+(N(dL)?"&e_n="+u(dL):"")+(N(dN)?"&e_v="+u(dN):"")+"&ca=1"}function aB(dN,dP,dL,dO,dR,dQ){if(!ad(dN)||!ad(dP)){ap("Error while logging event: Parameters `category` and `action` must not be empty or filled with whitespaces");
return false}var dM=cL(dd(dN,dP,dL,dO),dR,"event");bS(dM,bW,dQ)}function cm(dL,dO,dM,dP){var dN=cL("search="+u(dL)+(dO?"&search_cat="+u(dO):"")+(N(dM)?"&search_count="+dM:""),dP,"sitesearch");bS(dN,bW)}function dh(dL,dP,dO,dN){var dM=cL("idgoal="+dL+(dP?"&revenue="+dP:""),dO,"goal");bS(dM,bW,dN)}function dt(dO,dL,dS,dR,dN){var dQ=dL+"="+u(cf(dO));var dM=cO(dN,"click",dO);if(dM){dQ+="&"+dM}var dP=cL(dQ,dS,"link");bS(dP,bW,dR)}function b7(dM,dL){if(dM!==""){return dM+dL.charAt(0).toUpperCase()+dL.slice(1)}return dL}function cw(dQ){var dP,dL,dO=["","webkit","ms","moz"],dN;if(!bp){for(dL=0;dL<dO.length;dL++){dN=dO[dL];if(Object.prototype.hasOwnProperty.call(K,b7(dN,"hidden"))){if(K[b7(dN,"visibilityState")]==="prerender"){dP=true}break}}}if(dP){at(K,dN+"visibilitychange",function dM(){K.removeEventListener(dN+"visibilitychange",dM,false);dQ()});return}dQ()}function bE(){var dM=bV.getVisitorId();var dL=aS();return dM+dL}function cz(dL){if(!dL){return}if(!aj.hasNodeAttribute(dL,"href")){return
}var dM=aj.getAttributeValueFromNode(dL,"href");if(!dM||a4(dM)){return}if(!bV.getVisitorId()){return}dM=j(dM,aD);var dN=bE();dM=J(dM,aD,dN);aj.setAnyAttribute(dL,"href",dM)}function bw(dO){var dP=aj.getAttributeValueFromNode(dO,"href");if(!dP){return false}dP=String(dP);var dM=dP.indexOf("//")===0||dP.indexOf("http://")===0||dP.indexOf("https://")===0;if(!dM){return false}var dL=dO.pathname||cB(dO.href);var dN=(dO.hostname||d(dO.href)).toLowerCase();if(aA(dN,dL)){if(!c5(di,P(dN))){return true}return false}return false}function c4(dL){var dM=dE(dL);if(dM&&dM.type){dM.href=p(dM.href);dt(dM.href,dM.type,undefined,null,dL);return}if(db){dL=aC(dL);if(bw(dL)){cz(dL)}}}function cQ(){return K.all&&!K.addEventListener}function dj(dL){var dN=dL.which;var dM=(typeof dL.button);if(!dN&&dM!=="undefined"){if(cQ()){if(dL.button&1){dN=1}else{if(dL.button&2){dN=3}else{if(dL.button&4){dN=2}}}}else{if(dL.button===0||dL.button==="0"){dN=1}else{if(dL.button&1){dN=2}else{if(dL.button&2){dN=3}}}}}return dN}function b6(dL){switch(dj(dL)){case 1:return"left";
case 2:return"middle";case 3:return"right"}}function bc(dL){return dL.target||dL.srcElement}function dk(dL){return dL==="A"||dL==="AREA"}function aK(dL){function dM(dO){var dP=bc(dO);var dQ=dP.nodeName;var dN=bb(bM,"ignore");while(!dk(dQ)&&dP&&dP.parentNode){dP=dP.parentNode;dQ=dP.nodeName}if(dP&&dk(dQ)&&!dN.test(dP.className)){return dP}}return function(dP){dP=dP||X.event;var dQ=dM(dP);if(!dQ){return}var dO=b6(dP);if(dP.type==="click"){var dN=false;if(dL&&dO==="middle"){dN=true}if(dQ&&!dN){c4(dQ)}}else{if(dP.type==="mousedown"){if(dO==="middle"&&dQ){a0=dO;bO=dQ}else{a0=bO=null}}else{if(dP.type==="mouseup"){if(dO===a0&&dQ===bO){c4(dQ)}a0=bO=null}else{if(dP.type==="contextmenu"){c4(dQ)}}}}}}function az(dO,dN,dL){var dM=typeof dN;if(dM==="undefined"){dN=true}at(dO,"click",aK(dN),dL);if(dN){at(dO,"mouseup",aK(dN),dL);at(dO,"mousedown",aK(dN),dL);at(dO,"contextmenu",aK(dN),dL)}}function a1(dM,dP,dQ){if(cu){return true}cu=true;var dR=false;var dO,dN;function dL(){dR=true}n(function(){function dS(dU){setTimeout(function(){if(!cu){return
}dR=false;dQ.trackVisibleContentImpressions();dS(dU)},dU)}function dT(dU){setTimeout(function(){if(!cu){return}if(dR){dR=false;dQ.trackVisibleContentImpressions()}dT(dU)},dU)}if(dM){dO=["scroll","resize"];for(dN=0;dN<dO.length;dN++){if(K.addEventListener){K.addEventListener(dO[dN],dL,false)}else{X.attachEvent("on"+dO[dN],dL)}}dT(100)}if(dP&&dP>0){dP=parseInt(dP,10);dS(dP)}})}var bQ={enabled:true,requests:[],timeout:null,interval:2500,sendRequests:function(){var dL=this.requests;this.requests=[];if(dL.length===1){bS(dL[0],bW)}else{dH(dL,bW)}},canQueue:function(){return !m&&this.enabled},pushMultiple:function(dM){if(!this.canQueue()){dH(dM,bW);return}var dL;for(dL=0;dL<dM.length;dL++){this.push(dM[dL])}},push:function(dL){if(!dL){return}if(!this.canQueue()){bS(dL,bW);return}bQ.requests.push(dL);if(this.timeout){clearTimeout(this.timeout);this.timeout=null}this.timeout=setTimeout(function(){bQ.timeout=null;bQ.sendRequests()},bQ.interval);var dM="RequestQueue"+aF;if(!Object.prototype.hasOwnProperty.call(b,dM)){b[dM]={unload:function(){if(bQ.timeout){clearTimeout(bQ.timeout)
}bQ.sendRequests()}}}}};bt();this.hasConsent=function(){return bP};this.getVisitorInfo=function(){if(!aL(a2("id"))){aV()}return dc()};this.getVisitorId=function(){return this.getVisitorInfo()[1]};this.getAttributionInfo=function(){return bX()};this.getAttributionCampaignName=function(){return bX()[0]};this.getAttributionCampaignKeyword=function(){return bX()[1]};this.getAttributionReferrerTimestamp=function(){return bX()[2]};this.getAttributionReferrerUrl=function(){return bX()[3]};this.setTrackerUrl=function(dL){aM=dL};this.getTrackerUrl=function(){return aM};this.getMatomoUrl=function(){return ab(this.getTrackerUrl(),bU)};this.getPiwikUrl=function(){return this.getMatomoUrl()};this.addTracker=function(dN,dM){if(!N(dN)||null===dN){dN=this.getTrackerUrl()}var dL=new U(dN,dM);M.push(dL);v.trigger("TrackerAdded",[this]);return dL};this.getSiteId=function(){return cj};this.setSiteId=function(dL){cg(dL)};this.resetUserId=function(){bL=""};this.setUserId=function(dL){if(ad(dL)){bL=dL}};this.setVisitorId=function(dM){var dL=/[0-9A-Fa-f]{16}/g;
if(y(dM)&&dL.test(dM)){b0=dM}else{ap("Invalid visitorId set"+dM)}};this.getUserId=function(){return bL};this.setCustomData=function(dL,dM){if(aa(dL)){aw=dL}else{if(!aw){aw={}}aw[dL]=dM}};this.getCustomData=function(){return aw};this.setCustomRequestProcessing=function(dL){cq=dL};this.appendToTrackingUrl=function(dL){dr=dL};this.getRequest=function(dL){return cL(dL)};this.addPlugin=function(dL,dM){b[dL]=dM};this.setCustomDimension=function(dL,dM){dL=parseInt(dL,10);if(dL>0){if(!N(dM)){dM=""}if(!y(dM)){dM=String(dM)}bz[dL]=dM}};this.getCustomDimension=function(dL){dL=parseInt(dL,10);if(dL>0&&Object.prototype.hasOwnProperty.call(bz,dL)){return bz[dL]}};this.deleteCustomDimension=function(dL){dL=parseInt(dL,10);if(dL>0){delete bz[dL]}};this.setCustomVariable=function(dM,dL,dP,dN){var dO;if(!N(dN)){dN="visit"}if(!N(dL)){return}if(!N(dP)){dP=""}if(dM>0){dL=!y(dL)?String(dL):dL;dP=!y(dP)?String(dP):dP;dO=[dL.slice(0,bG),dP.slice(0,bG)];if(dN==="visit"||dN===2){c3();aZ[dM]=dO}else{if(dN==="page"||dN===3){b9[dM]=dO
}else{if(dN==="event"){cC[dM]=dO}}}}};this.getCustomVariable=function(dM,dN){var dL;if(!N(dN)){dN="visit"}if(dN==="page"||dN===3){dL=b9[dM]}else{if(dN==="event"){dL=cC[dM]}else{if(dN==="visit"||dN===2){c3();dL=aZ[dM]}}}if(!N(dL)||(dL&&dL[0]==="")){return false}return dL};this.deleteCustomVariable=function(dL,dM){if(this.getCustomVariable(dL,dM)){this.setCustomVariable(dL,"","",dM)}};this.deleteCustomVariables=function(dL){if(dL==="page"||dL===3){b9={}}else{if(dL==="event"){cC={}}else{if(dL==="visit"||dL===2){aZ={}}}}};this.storeCustomVariablesInCookie=function(){b3=true};this.setLinkTrackingTimer=function(dL){bW=dL};this.getLinkTrackingTimer=function(){return bW};this.setDownloadExtensions=function(dL){if(y(dL)){dL=dL.split("|")}dy=dL};this.addDownloadExtensions=function(dM){var dL;if(y(dM)){dM=dM.split("|")}for(dL=0;dL<dM.length;dL++){dy.push(dM[dL])}};this.removeDownloadExtensions=function(dN){var dM,dL=[];if(y(dN)){dN=dN.split("|")}for(dM=0;dM<dy.length;dM++){if(Q(dN,dy[dM])===-1){dL.push(dy[dM])
}}dy=dL};this.setDomains=function(dL){aG=y(dL)?[dL]:dL;var dP=false,dN=0,dM;for(dN;dN<aG.length;dN++){dM=String(aG[dN]);if(c5(di,P(dM))){dP=true;break}var dO=cB(dM);if(dO&&dO!=="/"&&dO!=="/*"){dP=true;break}}if(!dP){aG.push(di)}};this.setExcludedReferrers=function(dL){cS=y(dL)?[dL]:dL};this.enableCrossDomainLinking=function(){db=true};this.disableCrossDomainLinking=function(){db=false};this.isCrossDomainLinkingEnabled=function(){return db};this.setCrossDomainLinkingTimeout=function(dL){ba=dL};this.getCrossDomainLinkingUrlParameter=function(){return u(aD)+"="+u(bE())};this.setIgnoreClasses=function(dL){bM=y(dL)?[dL]:dL};this.setRequestMethod=function(dL){if(dL){dC=String(dL).toUpperCase()}else{dC=cx}if(dC==="GET"){this.disableAlwaysUseSendBeacon()}};this.setRequestContentType=function(dL){cR=dL||aQ};this.setGenerationTimeMs=function(dL){ap("setGenerationTimeMs is no longer supported since Matomo 4. The call will be ignored. The replacement is setPagePerformanceTiming.")};this.setPagePerformanceTiming=function(dP,dR,dQ,dM,dS,dN){var dO={pf_net:dP,pf_srv:dR,pf_tfr:dQ,pf_dm1:dM,pf_dm2:dS,pf_onl:dN};
try{dO=R(dO,N);dO=C(dO);cD=l(dO);if(cD===""){ap("setPagePerformanceTiming() called without parameters. This function needs to be called with at least one performance parameter.");return}bs=false;bR=true}catch(dL){ap("setPagePerformanceTiming: "+dL.toString())}};this.setReferrerUrl=function(dL){bA=dL};this.setCustomUrl=function(dL){bf=b8(bZ,dL)};this.getCurrentUrl=function(){return bf||bZ};this.setDocumentTitle=function(dL){bu=dL};this.setPageViewId=function(dL){aU=dL;bN=true};this.getPageViewId=function(){return aU};this.setAPIUrl=function(dL){bU=dL};this.setDownloadClasses=function(dL){bY=y(dL)?[dL]:dL};this.setLinkClasses=function(dL){bj=y(dL)?[dL]:dL};this.setCampaignNameKey=function(dL){cH=y(dL)?[dL]:dL};this.setCampaignKeywordKey=function(dL){bT=y(dL)?[dL]:dL};this.discardHashTag=function(dL){b1=dL};this.setCookieNamePrefix=function(dL){bv=dL;if(aZ){aZ=ca()}};this.setCookieDomain=function(dL){var dM=P(dL);if(!bx&&!bJ(dM)){ap("Can't write cookie on domain "+dL)}else{dp=dM;bt()}};this.setExcludedQueryParams=function(dL){cy=y(dL)?[dL]:dL
};this.getCookieDomain=function(){return dp};this.hasCookies=function(){return"1"===ci()};this.setSessionCookie=function(dN,dM,dL){if(!dN){throw new Error("Missing cookie name")}if(!N(dL)){dL=cE}bH.push(dN);dG(a2(dN),dM,dL,bC,dp,b5,aR)};this.getCookie=function(dM){var dL=aL(a2(dM));if(dL===0){return null}return dL};this.setCookiePath=function(dL){bC=dL;bt()};this.getCookiePath=function(){return bC};this.setVisitorCookieTimeout=function(dL){c8=dL*1000};this.setSessionCookieTimeout=function(dL){cE=dL*1000};this.getSessionCookieTimeout=function(){return cE};this.setReferralCookieTimeout=function(dL){dx=dL*1000};this.setConversionAttributionFirstReferrer=function(dL){bI=dL};this.setSecureCookie=function(dL){if(dL&&location.protocol!=="https:"){ap("Error in setSecureCookie: You cannot use `Secure` on http.");return}b5=dL};this.setCookieSameSite=function(dL){dL=String(dL);dL=dL.charAt(0).toUpperCase()+dL.toLowerCase().slice(1);if(dL!=="None"&&dL!=="Lax"&&dL!=="Strict"){ap("Ignored value for sameSite. Please use either Lax, None, or Strict.");
return}if(dL==="None"){if(location.protocol==="https:"){this.setSecureCookie(true)}else{ap("sameSite=None cannot be used on http, reverted to sameSite=Lax.");dL="Lax"}}aR=dL};this.disableCookies=function(){bx=true;if(cj){aN()}};this.areCookiesEnabled=function(){return !bx};this.setCookieConsentGiven=function(){if(bx&&!de){bx=false;if(!dn){this.enableBrowserFeatureDetection()}if(cj&&aE){aV();var dL=cL("ping=1",null,"ping");bS(dL,bW)}}};this.requireCookieConsent=function(){if(this.getRememberedCookieConsent()){return false}this.disableCookies();return true};this.getRememberedCookieConsent=function(){return aL(c1)};this.forgetCookieConsentGiven=function(){cc(c1,bC,dp);this.disableCookies()};this.rememberCookieConsentGiven=function(dM){if(dM){dM=dM*60*60*1000}else{dM=30*365*24*60*60*1000}this.setCookieConsentGiven();var dL=new Date().getTime();dG(c1,dL,dM,bC,dp,b5,aR)};this.deleteCookies=function(){aN()};this.setDoNotTrack=function(dM){var dL=g.doNotTrack||g.msDoNotTrack;de=dM&&(dL==="yes"||dL==="1");
if(de){this.disableCookies()}};this.disableCampaignParameters=function(){dm=false};this.alwaysUseSendBeacon=function(){dl=true};this.disableAlwaysUseSendBeacon=function(){dl=false};this.addListener=function(dM,dL){az(dM,dL,false)};this.enableLinkTracking=function(dM){if(dA){return}dA=true;var dL=this;r(function(){ax=true;var dN=K.body;az(dN,dM,true)})};this.enableJSErrorTracking=function(){if(dg){return}dg=true;var dL=X.onerror;X.onerror=function(dQ,dO,dN,dP,dM){cw(function(){var dR="JavaScript Errors";var dS=dO+":"+dN;if(dP){dS+=":"+dP}if(Q(cM,dR+dS+dQ)===-1){cM.push(dR+dS+dQ);aB(dR,dS,dQ)}});if(dL){return dL(dQ,dO,dN,dP,dM)}return false}};this.disablePerformanceTracking=function(){bd=false};this.enableHeartBeatTimer=function(dL){dL=Math.max(dL||15,5);bg=dL*1000;if(dq!==null){dJ()}};this.disableHeartBeatTimer=function(){if(bg||aW){if(X.removeEventListener){X.removeEventListener("focus",bl);X.removeEventListener("blur",aH);X.removeEventListener("visibilitychange",a5)}else{if(X.detachEvent){X.detachEvent("onfocus",bl);
X.detachEvent("onblur",aH);X.detachEvent("visibilitychange",a5)}}}bg=null;aW=false};this.killFrame=function(){if(X.location!==X.top.location){X.top.location=X.location}};this.redirectFile=function(dL){if(X.location.protocol==="file:"){X.location=dL}};this.setCountPreRendered=function(dL){bp=dL};this.trackGoal=function(dL,dO,dN,dM){cw(function(){dh(dL,dO,dN,dM)})};this.trackLink=function(dM,dL,dO,dN){cw(function(){dt(dM,dL,dO,dN)})};this.getNumTrackedPageViews=function(){return cK};this.trackPageView=function(dL,dN,dM){cp=[];c9=[];cM=[];if(S(cj)){cw(function(){ae(aM,bU,cj)})}else{cw(function(){cK++;cd(dL,dN,dM)})}};this.disableBrowserFeatureDetection=function(){dn=false;dz={};if(av()){ay()}};this.enableBrowserFeatureDetection=function(){dn=true;c6()};this.trackAllContentImpressions=function(){if(S(cj)){return}cw(function(){r(function(){var dL=x.findContentNodes();var dM=cX(dL);bQ.pushMultiple(dM)})})};this.trackVisibleContentImpressions=function(dL,dM){if(S(cj)){return}if(!N(dL)){dL=true
}if(!N(dM)){dM=750}a1(dL,dM,this);cw(function(){n(function(){var dN=x.findContentNodes();var dO=bk(dN);bQ.pushMultiple(dO)})})};this.trackContentImpression=function(dN,dL,dM){if(S(cj)){return}dN=a(dN);dL=a(dL);dM=a(dM);if(!dN){return}dL=dL||"Unknown";cw(function(){var dO=aO(dN,dL,dM);bQ.push(dO)})};this.trackContentImpressionsWithinNode=function(dL){if(S(cj)||!dL){return}cw(function(){if(cu){n(function(){var dM=x.findContentNodesWithinNode(dL);var dN=bk(dM);bQ.pushMultiple(dN)})}else{r(function(){var dM=x.findContentNodesWithinNode(dL);var dN=cX(dM);bQ.pushMultiple(dN)})}})};this.trackContentInteraction=function(dN,dO,dL,dM){if(S(cj)){return}dN=a(dN);dO=a(dO);dL=a(dL);dM=a(dM);if(!dN||!dO){return}dL=dL||"Unknown";cw(function(){var dP=aY(dN,dO,dL,dM);if(dP){bQ.push(dP)}})};this.trackContentInteractionNode=function(dN,dM){if(S(cj)||!dN){return}var dL=null;cw(function(){dL=dD(dN,dM);if(dL){bQ.push(dL)}});return dL};this.logAllContentBlocksOnPage=function(){var dN=x.findContentNodes();var dL=x.collectContent(dN);
var dM=typeof console;if(dM!=="undefined"&&console&&console.log){console.log(dL)}};this.trackEvent=function(dM,dO,dL,dN,dQ,dP){cw(function(){aB(dM,dO,dL,dN,dQ,dP)})};this.trackSiteSearch=function(dL,dN,dM,dO){cp=[];cw(function(){cm(dL,dN,dM,dO)})};this.setEcommerceView=function(dP,dL,dN,dM){cN={};if(ad(dN)){dN=String(dN)}if(!N(dN)||dN===null||dN===false||!dN.length){dN=""}else{if(dN instanceof Array){dN=X.JSON.stringify(dN)}}var dO="_pkc";cN[dO]=dN;if(N(dM)&&dM!==null&&dM!==false&&String(dM).length){dO="_pkp";cN[dO]=dM}if(!ad(dP)&&!ad(dL)){return}if(ad(dP)){dO="_pks";cN[dO]=dP}if(!ad(dL)){dL=""}dO="_pkn";cN[dO]=dL};this.getEcommerceItems=function(){return JSON.parse(JSON.stringify(ds))};this.addEcommerceItem=function(dP,dL,dN,dM,dO){if(ad(dP)){ds[dP]=[String(dP),dL,dN,dM,dO]}};this.removeEcommerceItem=function(dL){if(ad(dL)){dL=String(dL);delete ds[dL]}};this.clearEcommerceCart=function(){ds={}};this.trackEcommerceOrder=function(dL,dP,dO,dN,dM,dQ){cb(dL,dP,dO,dN,dM,dQ)};this.trackEcommerceCartUpdate=function(dL){bF(dL)
};this.trackRequest=function(dM,dO,dN,dL){cw(function(){var dP=cL(dM,dO,dL);bS(dP,bW,dN)})};this.ping=function(){this.trackRequest("ping=1",null,null,"ping")};this.disableQueueRequest=function(){bQ.enabled=false};this.setRequestQueueInterval=function(dL){if(dL<1000){throw new Error("Request queue interval needs to be at least 1000ms")}bQ.interval=dL};this.queueRequest=function(dM,dL){cw(function(){var dN=dL?dM:cL(dM);bQ.push(dN)})};this.isConsentRequired=function(){return cY};this.getRememberedConsent=function(){var dL=aL(bo);if(aL(da)){if(dL){cc(bo,bC,dp)}return null}if(!dL||dL===0){return null}return dL};this.hasRememberedConsent=function(){return !!this.getRememberedConsent()};this.requireConsent=function(){cY=true;bP=this.hasRememberedConsent();if(!bP){bx=true}z++;b["CoreConsent"+z]={unload:function(){if(!bP){aN()}}}};this.setConsentGiven=function(dM){bP=true;if(!dn){this.enableBrowserFeatureDetection()}cc(da,bC,dp);var dN,dL;for(dN=0;dN<c9.length;dN++){dL=typeof c9[dN][0];if(dL==="string"){bS(c9[dN][0],bW,c9[dN][1])
}else{if(dL==="object"){dH(c9[dN][0],bW)}}}c9=[];if(!N(dM)||dM){this.setCookieConsentGiven()}};this.rememberConsentGiven=function(dN){if(dN){dN=dN*60*60*1000}else{dN=30*365*24*60*60*1000}var dL=true;this.setConsentGiven(dL);var dM=new Date().getTime();dG(bo,dM,dN,bC,dp,b5,aR)};this.forgetConsentGiven=function(dL){if(dL){dL=dL*60*60*1000}else{dL=30*365*24*60*60*1000}cc(bo,bC,dp);dG(da,new Date().getTime(),dL,bC,dp,b5,aR);this.forgetCookieConsentGiven();this.requireConsent()};this.isUserOptedOut=function(){return !bP};this.optUserOut=this.forgetConsentGiven;this.forgetUserOptOut=function(){this.setConsentGiven(false)};this.enableFileTracking=function(){cW=true};n(function(){setTimeout(function(){bR=true},0)});v.trigger("TrackerSetup",[this]);v.addPlugin("TrackerVisitorIdCookie"+aF,{unload:function(){if(av()&&!by){by=true;ay()}if(!aE){aV();dB()}}})}function L(){return{push:ak}}function c(az,ay){var aA={};var aw,ax;for(aw=0;aw<ay.length;aw++){var au=ay[aw];aA[au]=1;for(ax=0;ax<az.length;ax++){if(az[ax]&&az[ax][0]){var av=az[ax][0];
if(au===av){ak(az[ax]);delete az[ax];if(aA[av]>1&&av!=="addTracker"&&av!=="enableLinkTracking"){ap("The method "+av+' is registered more than once in "_paq" variable. Only the last call has an effect. Please have a look at the multiple Matomo trackers documentation: https://developer.matomo.org/guides/tracking-javascript-guide#multiple-piwik-trackers')}aA[av]++}}}}return az}var F=["addTracker","enableFileTracking","forgetCookieConsentGiven","requireCookieConsent","disableBrowserFeatureDetection","disableCampaignParameters","disableCookies","setTrackerUrl","setAPIUrl","enableCrossDomainLinking","setCrossDomainLinkingTimeout","setSessionCookieTimeout","setVisitorCookieTimeout","setCookieNamePrefix","setCookieSameSite","setSecureCookie","setCookiePath","setCookieDomain","setDomains","setUserId","setVisitorId","setSiteId","alwaysUseSendBeacon","disableAlwaysUseSendBeacon","enableLinkTracking","setCookieConsentGiven","requireConsent","setConsentGiven","disablePerformanceTracking","setPagePerformanceTiming","setExcludedQueryParams","setExcludedReferrers"];
function ai(aw,av){var au=new U(aw,av);M.push(au);_paq=c(_paq,F);for(I=0;I<_paq.length;I++){if(_paq[I]){ak(_paq[I])}}_paq=new L();v.trigger("TrackerAdded",[au]);return au}at(X,"beforeunload",an,false);at(X,"visibilitychange",function(){if(m){return}if(K.visibilityState==="hidden"){ah("unload")}},false);at(X,"online",function(){if(N(g.serviceWorker)){g.serviceWorker.ready.then(function(au){if(au&&au.sync){return au.sync.register("matomoSync")}},function(){})}},false);at(X,"message",function(az){if(!az||!az.origin){return}var aB,ax,av;var aC=d(az.origin);var ay=v.getAsyncTrackers();for(ax=0;ax<ay.length;ax++){av=d(ay[ax].getMatomoUrl());if(av===aC){aB=ay[ax];break}}if(!aB){return}var aw=null;try{aw=JSON.parse(az.data)}catch(aA){return}if(!aw){return}function au(aF){var aH=K.getElementsByTagName("iframe");for(ax=0;ax<aH.length;ax++){var aG=aH[ax];var aD=d(aG.src);if(aG.contentWindow&&N(aG.contentWindow.postMessage)&&aD===aC){var aE=JSON.stringify(aF);aG.contentWindow.postMessage(aE,az.origin)
}}}if(N(aw.maq_initial_value)){au({maq_opted_in:aw.maq_initial_value&&aB.hasConsent(),maq_url:aB.getMatomoUrl(),maq_optout_by_default:aB.isConsentRequired()})}else{if(N(aw.maq_opted_in)){ay=v.getAsyncTrackers();for(ax=0;ax<ay.length;ax++){aB=ay[ax];if(aw.maq_opted_in){aB.rememberConsentGiven()}else{aB.forgetConsentGiven()}}au({maq_confirm_opted_in:aB.hasConsent(),maq_url:aB.getMatomoUrl(),maq_optout_by_default:aB.isConsentRequired()})}}},false);Date.prototype.getTimeAlias=Date.prototype.getTime;v={initialized:false,JSON:X.JSON,DOM:{addEventListener:function(ax,aw,av,au){var ay=typeof au;if(ay==="undefined"){au=false}at(ax,aw,av,au)},onLoad:n,onReady:r,isNodeVisible:i,isOrWasNodeVisible:x.isNodeVisible},on:function(av,au){if(!A[av]){A[av]=[]}A[av].push(au)},off:function(aw,av){if(!A[aw]){return}var au=0;for(au;au<A[aw].length;au++){if(A[aw][au]===av){A[aw].splice(au,1)}}},trigger:function(aw,ax,av){if(!A[aw]){return}var au=0;for(au;au<A[aw].length;au++){A[aw][au].apply(av||X,ax)}},addPlugin:function(au,av){b[au]=av
},getTracker:function(av,au){if(!N(au)){au=this.getAsyncTracker().getSiteId()}if(!N(av)){av=this.getAsyncTracker().getTrackerUrl()}return new U(av,au)},getAsyncTrackers:function(){return M},addTracker:function(aw,av){var au;if(!M.length){au=ai(aw,av)}else{au=M[0].addTracker(aw,av)}return au},getAsyncTracker:function(ay,ax){var aw;if(M&&M.length&&M[0]){aw=M[0]}else{return ai(ay,ax)}if(!ax&&!ay){return aw}if((!N(ax)||null===ax)&&aw){ax=aw.getSiteId()}if((!N(ay)||null===ay)&&aw){ay=aw.getTrackerUrl()}var av,au=0;for(au;au<M.length;au++){av=M[au];if(av&&String(av.getSiteId())===String(ax)&&av.getTrackerUrl()===ay){return av}}},retryMissedPluginCalls:function(){var av=am;am=[];var au=0;for(au;au<av.length;au++){ak(av[au])}}};if(typeof define==="function"&&define.amd){define("piwik",[],function(){return v});define("matomo",[],function(){return v})}return v}())}
/*!!! pluginTrackerHook */
(function(){function b(){if("object"!==typeof _paq){return false}var c=typeof _paq.length;if("undefined"===c){return false
}return !!_paq.length}if(window&&"object"===typeof window.matomoPluginAsyncInit&&window.matomoPluginAsyncInit.length){var a=0;for(a;a<window.matomoPluginAsyncInit.length;a++){if(typeof window.matomoPluginAsyncInit[a]==="function"){window.matomoPluginAsyncInit[a]()}}}if(window&&window.piwikAsyncInit){window.piwikAsyncInit()}if(window&&window.matomoAsyncInit){window.matomoAsyncInit()}if(!window.Matomo.getAsyncTrackers().length){if(b()){window.Matomo.addTracker()}else{_paq={push:function(c){var d=typeof console;if(d!=="undefined"&&console&&console.error){console.error("_paq.push() was used but Matomo tracker was not initialized before the matomo.js file was loaded. Make sure to configure the tracker via _paq.push before loading matomo.js. Alternatively, you can create a tracker via Matomo.addTracker() manually and then use _paq.push but it may not fully work as tracker methods may not be executed in the correct order.",c)}}}}}window.Matomo.trigger("MatomoInitialized",[]);window.Matomo.initialized=true
}());(function(){var a=(typeof window.AnalyticsTracker);if(a==="undefined"){window.AnalyticsTracker=window.Matomo}}());if(typeof window.piwik_log!=="function"){window.piwik_log=function(c,e,g,f){function b(h){try{if(window["piwik_"+h]){return window["piwik_"+h]}}catch(i){}return}var d,a=window.Matomo.getTracker(g,e);a.setDocumentTitle(c);a.setCustomData(f);d=b("tracker_pause");if(d){a.setLinkTrackingTimer(d)}d=b("download_extensions");if(d){a.setDownloadExtensions(d)}d=b("hosts_alias");if(d){a.setDomains(d)}d=b("ignore_classes");if(d){a.setIgnoreClasses(d)}a.trackPageView();if(b("install_tracker")){piwik_track=function(i,j,k,h){a.setSiteId(j);a.setTrackerUrl(k);a.trackLink(i,h)};a.enableLinkTracking()}}}
/*!! @license-end */;

View File

@ -1,53 +0,0 @@
# Use $request_id as a pseudo-nonce for Content Security Policy (CSP)
map $request_id $nonce {
default "$request_id";
}
server {
listen 8080;
root /usr/share/nginx/html;
index resume.html;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), accelerometer=(), gyroscope=(), magnetometer=(), payment=(), usb=()" always;
# Updated Content Security Policy (CSP) with 'unsafe-inline' temporarily for style-src
add_header Content-Security-Policy "
default-src 'none';
script-src 'self' 'nonce-$nonce' https://matomo.nixc.us https://gist.github.com https://assets-cdn.github.com;
style-src 'self' 'nonce-$nonce' https://colinknapp.com https://getbootstrap.com https://fonts.googleapis.com 'unsafe-inline';
img-src 'self' https://matomo.nixc.us https://colinknapp.com https://hedgedoc.nixc.us https://assets-cdn.github.com https://github.com https://forkaweso.me https://ionicons.com https://twitter.com data:;
font-src 'self' https://fonts.gstatic.com https://github.com https://forkaweso.me data:;
connect-src 'self' https://matomo.nixc.us;
frame-ancestors 'self';
base-uri 'self';
form-action 'self';
" always;
# Cross-origin isolation headers
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
# Apply CORP header for the apple-touch-icon to allow cross-origin access
location /icons/apple-touch-icon.png {
add_header Cross-Origin-Resource-Policy "cross-origin";
}
# Use sub_filter to inject the nonce into inline <script> and <style> tags automatically
sub_filter '<script>' '<script nonce="$nonce">';
sub_filter '<style>' '<style nonce="$nonce">';
sub_filter_once off;
sub_filter_types text/html;
# Redirect demo.hedgedoc.org resources to hedgedoc.nixc.us
sub_filter "https://demo.hedgedoc.org" "https://hedgedoc.nixc.us";
location / {
try_files $uri $uri/ =404;
}
}

View File

@ -0,0 +1,132 @@
/* CSV Tool specific styles with aggressive fixes */
/* Fix container width */
body {
max-width: 100% !important;
padding: 20px !important;
box-sizing: border-box !important;
}
.container-fluid {
width: 100% !important;
max-width: 100% !important;
padding: 0 15px !important;
box-sizing: border-box !important;
margin: 0 auto !important;
}
.tool-container {
display: flex !important;
flex-direction: column !important;
gap: 1.5rem !important;
margin: 2rem 0 !important;
width: 100% !important;
box-sizing: border-box !important;
max-width: 100% !important;
}
.tool-controls {
background-color: var(--bg-secondary) !important;
padding: 1.5rem !important;
border-radius: 0.5rem !important;
display: flex !important;
flex-direction: column !important;
gap: 1rem !important;
margin-bottom: 1.5rem !important;
width: 100% !important;
box-sizing: border-box !important;
max-width: 100% !important;
}
.form-group {
margin-bottom: 1.5rem !important;
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
.full-width {
width: 100% !important;
max-width: 100% !important;
display: block !important;
box-sizing: border-box !important;
}
/* Aggressive fixes for textarea */
#csvInput,
textarea#csvInput {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 12px !important;
font-family: 'Courier New', monospace !important;
min-height: 250px !important;
white-space: pre !important;
tab-size: 4 !important;
-moz-tab-size: 4 !important;
resize: vertical !important;
overflow-x: auto !important;
line-height: 1.5 !important;
font-size: 14px !important;
letter-spacing: -0.2px !important;
border: 1px solid var(--border-color) !important;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
border-radius: 0.25rem !important;
}
/* Ensure the output area is also full width */
.tool-output {
background-color: var(--bg-secondary) !important;
padding: 1.5rem !important;
border-radius: 0.5rem !important;
overflow-x: auto !important;
margin-bottom: 1.5rem !important;
width: 100% !important;
border: 2px solid var(--accent-color) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
box-sizing: border-box !important;
max-width: 100% !important;
}
/* Fix table display */
.table-responsive {
overflow-x: auto !important;
max-width: 100% !important;
width: 100% !important;
margin-bottom: 2rem !important;
border-radius: 0.25rem !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05) !important;
}
.tool-table {
width: 100% !important;
border-collapse: separate !important;
border-spacing: 0 !important;
margin-bottom: 1.5rem !important;
text-align: left !important;
border: 1px solid var(--border-color) !important;
table-layout: auto !important;
}
/* Fix form controls */
.form-control {
width: 100% !important;
padding: 0.75rem !important;
border: 1px solid var(--border-color) !important;
background-color: var(--bg-primary) !important;
color: var(--text-primary) !important;
border-radius: 0.25rem !important;
font-family: inherit !important;
font-size: 1rem !important;
box-sizing: border-box !important;
}
/* Fix empty cell display */
.empty-cell {
color: #999 !important;
font-style: italic !important;
}

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - CSV Processing Tool">
<title>CSV Viewer - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css">
<link rel="stylesheet" href="tool-styles.css?v=2" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<link rel="stylesheet" href="csv-tool-fix.css?v=2" integrity="sha256-5oTxos9Qxwhor3qIwHSM12YyIZi5E+tHuFdYER0hXoI=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
<script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544="></script>
<style>
/* Additional inline styles to fix layout */
.container-fluid {
max-width: 100%;
padding: 0 15px;
}
.tool-container {
width: 100%;
max-width: 100%;
}
.form-group.full-width {
width: 100%;
max-width: 100%;
}
#csvInput {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
/* More aggressive fixes for textarea */
textarea#csvInput {
display: block !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
box-sizing: border-box !important;
margin: 0 !important;
padding: 12px !important;
font-family: 'Courier New', monospace !important;
}
/* Fix container width */
body {
max-width: 100% !important;
padding: 20px !important;
box-sizing: border-box !important;
}
</style>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<h1>CSV Viewer</h1>
<p>Simply paste CSV data below to view it as a formatted table.</p>
<div class="tool-container">
<div class="tool-controls">
<h2>Input Data</h2>
<div class="form-group full-width">
<label for="csvInput">Paste CSV Data:</label>
<textarea id="csvInput" class="form-control" rows="15" aria-label="CSV Input Area" placeholder="Paste your CSV data here to automatically view it as a table..."></textarea>
</div>
<div class="form-group">
<label for="delimiter">Delimiter:</label>
<select id="delimiter" class="form-control">
<option value="," selected>Comma (,)</option>
<option value=";">Semicolon (;)</option>
<option value="\t">Tab</option>
<option value="|">Pipe (|)</option>
</select>
</div>
<div class="form-group">
<label for="hasHeader">First row is header:</label>
<input type="checkbox" id="hasHeader" checked>
</div>
</div>
<div class="tool-output" id="output">
<h2>Output</h2>
<p class="alert alert-info">Paste CSV data above to view it as a table.</p>
</div>
</div>
<hr>
<h2>About This Tool</h2>
<p>This CSV Viewer allows you to:</p>
<ul>
<li>Paste and preview CSV data directly in your browser</li>
<li>Automatically view your data in a table format</li>
<li>Sort columns by clicking on column headers</li>
</ul>
<p>The tool processes everything in your browser - no data is sent to any server.</p>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
<!-- Load PapaParse first (local version) -->
<script src="../papaparse.min.js" integrity="sha256-Ue6wom48SQbpmwW9QIk7pyVDR5Bg36SetP67V2pDkxc="></script>
<!-- Then load our script -->
<script src="csv-tool.js?v=3" integrity="sha256-oRCvBUmDTuPb8XOF1vLYwhIrcj2kzMbEwX5QzUPAPQI="></script>
</body>
</html>

View File

@ -0,0 +1,329 @@
/**
* CSV Viewer functionality
* Automatically processes and displays CSV data when pasted
* Using Papa Parse for robust CSV handling
*/
document.addEventListener('DOMContentLoaded', function() {
// DOM Elements
const csvInput = document.getElementById('csvInput');
const delimiterSelect = document.getElementById('delimiter');
const hasHeaderCheckbox = document.getElementById('hasHeader');
const outputDiv = document.getElementById('output');
// Variables to store data
let csvData = [];
let headers = [];
let currentSortColumn = null;
let sortDirection = 1; // 1 for ascending, -1 for descending
// Add input event listener with debounce to process CSV when pasted
let debounceTimer;
csvInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
if (csvInput.value.trim() !== '') {
processCSV();
}
}, 300); // 300ms debounce delay
});
// Add paste event listener to format CSV data on paste
csvInput.addEventListener('paste', function(e) {
// Let the paste happen naturally, then process after a brief delay
setTimeout(function() {
const text = csvInput.value;
if (text && text.length > 0) {
// Auto-detect delimiter
autoDetectDelimiter(text);
// Process immediately after paste
processCSV();
}
}, 50); // Slightly longer delay to ensure paste completes
});
// Add change listeners to delimiter and header options to reprocess data
delimiterSelect.addEventListener('change', function() {
if (csvInput.value.trim() !== '') {
processCSV();
}
});
hasHeaderCheckbox.addEventListener('change', function() {
if (csvInput.value.trim() !== '') {
processCSV();
}
});
/**
* Auto-detect the delimiter in pasted CSV data
*/
function autoDetectDelimiter(text) {
// Count occurrences of common delimiters
const firstFewLines = text.split('\n').slice(0, 5).join('\n');
const counts = {
',': (firstFewLines.match(/,/g) || []).length,
';': (firstFewLines.match(/;/g) || []).length,
'\t': (firstFewLines.match(/\t/g) || []).length,
'|': (firstFewLines.match(/\|/g) || []).length
};
// Find the most common delimiter
let maxCount = 0;
let detectedDelimiter = ','; // default
for (const [delimiter, count] of Object.entries(counts)) {
if (count > maxCount) {
maxCount = count;
detectedDelimiter = delimiter;
}
}
// Set the delimiter dropdown
if (maxCount > 0) {
delimiterSelect.value = detectedDelimiter === '\t' ? '\\t' : detectedDelimiter;
}
}
/**
* Process the CSV data based on selected options
*/
function processCSV() {
const csvText = csvInput.value.trim();
if (!csvText) {
outputDiv.innerHTML = '<h3>Output</h3><p class="alert alert-info">Paste CSV data above to view it as a table.</p>';
return;
}
try {
// Show processing message
outputDiv.innerHTML = '<h3>Output</h3><p class="alert alert-info">Processing data...</p>';
// Parse CSV using Papa Parse
const delimiter = delimiterSelect.value;
const hasHeader = hasHeaderCheckbox.checked;
// Enhanced parsing options
Papa.parse(csvText, {
delimiter: delimiter,
header: hasHeader,
skipEmptyLines: 'greedy', // Skip truly empty lines
dynamicTyping: true, // Automatically convert numeric values
trimHeaders: true, // Trim whitespace from headers
complete: function(results) {
if (results.errors.length > 0) {
showError('Error parsing CSV: ' + results.errors[0].message);
return;
}
if (results.data.length === 0 || (results.data.length === 1 && Object.keys(results.data[0]).length === 0)) {
showError('No valid data found. Please check your CSV format and delimiter.');
return;
}
csvData = results.data;
// Handle headers
if (hasHeader) {
if (results.meta.fields && results.meta.fields.length > 0) {
headers = results.meta.fields.map(h => h.trim());
} else {
// Fallback if no headers detected
headers = Object.keys(results.data[0] || {}).map((_, i) => `Column${i + 1}`);
}
} else {
headers = Object.keys(results.data[0] || {}).map((_, i) => `Column${i + 1}`);
}
// Preview the data
previewData();
},
error: function(error) {
showError('Error processing CSV: ' + error.message);
}
});
} catch (error) {
showError('Error processing CSV: ' + error.message);
}
}
/**
* Preview the CSV data in a table with sortable columns
*/
function previewData() {
if (csvData.length === 0) {
showError('No data to preview.');
return;
}
// Limit preview to first 500 rows
const previewData = csvData.slice(0, 500);
// Generate table HTML
let tableHtml = '<h3>Data Preview</h3>';
tableHtml += `<p>Showing ${previewData.length} of ${csvData.length} rows</p>`;
tableHtml += '<div class="table-responsive"><table class="tool-table">';
// Table headers with sort functionality
tableHtml += '<thead><tr>';
headers.forEach(header => {
const isSorted = header === currentSortColumn;
const sortClass = isSorted ? (sortDirection > 0 ? 'sort-asc' : 'sort-desc') : '';
tableHtml += `<th class="${sortClass}" data-column="${header}">${header} ${isSorted ? (sortDirection > 0 ? '↑' : '↓') : ''}</th>`;
});
tableHtml += '</tr></thead>';
// Table body with improved cell formatting
tableHtml += '<tbody>';
previewData.forEach(row => {
tableHtml += '<tr>';
headers.forEach(header => {
const cellValue = row[header];
// Format cell value based on type
let formattedValue = '';
if (cellValue === null || cellValue === undefined) {
formattedValue = '<span class="empty-cell">(empty)</span>';
} else if (typeof cellValue === 'string') {
formattedValue = escapeHtml(cellValue);
} else {
formattedValue = String(cellValue);
}
tableHtml += `<td>${formattedValue}</td>`;
});
tableHtml += '</tr>';
});
tableHtml += '</tbody></table></div>';
// Add stats summary
const totalRows = csvData.length;
const totalColumns = headers.length;
tableHtml += `<div class="data-stats">
<div class="stat-item">
<span class="stat-label">Total Rows:</span>
<span class="stat-value">${totalRows}</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Columns:</span>
<span class="stat-value">${totalColumns}</span>
</div>
</div>`;
// Display in output div
outputDiv.innerHTML = tableHtml;
// Add click event listeners to table headers for sorting
const tableHeaders = outputDiv.querySelectorAll('th');
tableHeaders.forEach(th => {
th.addEventListener('click', () => {
const column = th.getAttribute('data-column');
sortData(column);
});
});
}
/**
* Escape HTML special characters to prevent XSS
*/
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Sort data by column
*/
function sortData(column) {
// Toggle sort direction if clicking the same column
if (column === currentSortColumn) {
sortDirection *= -1;
} else {
currentSortColumn = column;
sortDirection = 1;
}
// Sort the data
csvData.sort((a, b) => {
const valueA = a[column] !== undefined ? a[column] : '';
const valueB = b[column] !== undefined ? b[column] : '';
// Try to sort numerically if possible
if (typeof valueA === 'number' && typeof valueB === 'number') {
return (valueA - valueB) * sortDirection;
}
// Handle dates
const dateA = new Date(valueA);
const dateB = new Date(valueB);
if (!isNaN(dateA) && !isNaN(dateB)) {
return (dateA - dateB) * sortDirection;
}
// Otherwise sort alphabetically
return String(valueA).localeCompare(String(valueB)) * sortDirection;
});
// Update the preview
previewData();
}
/**
* Show error message
*/
function showError(message) {
// Clear any existing alerts
clearAlerts();
const alert = document.createElement('div');
alert.className = 'alert alert-error';
alert.textContent = message;
// Insert at the top of the output div
const firstChild = outputDiv.querySelector('h3') ?
outputDiv.querySelector('h3').nextSibling :
outputDiv.firstChild;
outputDiv.insertBefore(alert, firstChild);
}
/**
* Show success message
*/
function showSuccess(message) {
const alert = document.createElement('div');
alert.className = 'alert alert-success';
alert.textContent = message;
// Insert after the heading
const firstChild = outputDiv.querySelector('h3') ?
outputDiv.querySelector('h3').nextSibling :
outputDiv.firstChild;
outputDiv.insertBefore(alert, firstChild);
// Auto-hide success message after 3 seconds
setTimeout(() => {
if (alert.parentNode === outputDiv) {
alert.remove();
}
}, 3000);
}
/**
* Clear all alert messages
*/
function clearAlerts() {
const alerts = outputDiv.querySelectorAll('.alert');
alerts.forEach(alert => alert.remove());
}
// Check if there's already content in the textarea on page load
if (csvInput.value.trim() !== '') {
processCSV();
}
});

View File

@ -0,0 +1 @@
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - One Pager Tools">
<title>Colin Knapp Tools</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544=" crossorigin="anonymous"></script>
<!-- Add tool-specific scripts here -->
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<h1>Tool Name</h1>
<p>A brief description of what this tool does.</p>
<hr>
<!-- Tool-specific content goes here -->
<div class="tool-container">
<!-- This section would be customized for each tool -->
<div class="tool-controls">
<!-- Input controls -->
</div>
<div class="tool-output">
<!-- Results/output -->
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,423 @@
/* Additional styles for one-pager tools - UPDATED */
.tool-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin: 2rem 0;
width: 100%;
box-sizing: border-box;
max-width: 100%;
}
.tool-controls {
background-color: var(--bg-secondary);
padding: 1.5rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
width: 100%;
box-sizing: border-box;
max-width: 100%;
}
.tool-output {
background-color: var(--bg-secondary);
padding: 1.5rem;
border-radius: 0.5rem;
overflow-x: auto;
margin-bottom: 1.5rem;
width: 100%;
border: 2px solid var(--accent-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
box-sizing: border-box;
}
.tool-output h3 {
color: var(--accent-color);
margin-top: 0;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--accent-color);
}
#output {
position: relative;
}
#output::before {
content: "";
position: absolute;
top: -12px;
left: 20px;
}
/* Form controls styling to match the theme */
.form-group {
margin-bottom: 1.5rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.full-width {
width: 100%;
max-width: 100%;
display: block;
box-sizing: border-box;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
background-color: var(--bg-primary);
color: var(--text-primary);
border-radius: 0.25rem;
font-family: inherit;
font-size: 1rem;
box-sizing: border-box;
}
/* Make CSV input textarea wider and improve formatting */
#csvInput {
width: 100% !important;
min-height: 250px;
font-family: 'Courier New', monospace;
white-space: pre;
tab-size: 4;
-moz-tab-size: 4;
box-sizing: border-box;
max-width: 100%;
resize: vertical;
overflow-x: auto;
line-height: 1.5;
font-size: 14px;
letter-spacing: -0.2px;
padding: 12px;
margin: 0;
display: block;
flex: 1 1 auto;
}
/* Fix for textarea width issues */
textarea#csvInput {
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
box-sizing: border-box !important;
}
.btn {
padding: 0.75rem 1.5rem;
background-color: var(--accent-color);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s, opacity 0.2s;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.btn:hover {
background-color: var(--accent-hover);
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Accessibility features */
.btn:focus, .form-control:focus {
outline: 2px solid var(--focus-outline-color);
outline-offset: 2px;
}
/* Table styles for data display */
.table-responsive {
overflow-x: auto;
max-width: 100%;
margin-bottom: 2rem;
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.tool-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-bottom: 1.5rem;
text-align: left;
border: 1px solid var(--border-color);
table-layout: auto;
}
.tool-table th,
.tool-table td {
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
border-right: 1px solid var(--border-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.tool-table td {
white-space: pre-wrap;
word-break: break-word;
}
.tool-table th {
background-color: var(--accent-color);
color: white;
font-weight: 600;
position: sticky;
top: 0;
z-index: 10;
cursor: pointer;
user-select: none;
transition: background-color 0.2s;
}
.tool-table th:hover {
background-color: var(--accent-hover);
}
.tool-table th.sort-asc,
.tool-table th.sort-desc {
background-color: var(--accent-hover);
}
.tool-table tr:nth-child(even) {
background-color: var(--bg-tertiary);
}
.tool-table tr:hover {
background-color: var(--bg-hover);
}
/* File input styling */
.file-input-container {
position: relative;
margin-bottom: 1rem;
}
.file-input {
position: absolute;
left: -9999px;
}
.file-input-label {
display: inline-block;
padding: 0.5rem 1rem;
background-color: var(--button-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s, border-color 0.3s;
}
.file-input-label:hover {
background-color: var(--button-hover-bg);
border-color: var(--accent-color);
}
.file-name {
margin-left: 1rem;
font-style: italic;
}
/* Progress indicators */
.progress-container {
width: 100%;
background-color: var(--progress-bg);
border-radius: 4px;
margin: 1rem 0;
}
.progress-bar {
height: 10px;
background-color: var(--accent-color);
border-radius: 4px;
transition: width 0.3s ease;
}
/* Alert/notification styles */
.alert {
padding: 1rem 1rem 1rem 1.5rem;
margin-bottom: 1.5rem;
border-radius: 0.25rem;
font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.alert::before {
margin-right: 0.75rem;
font-size: 1.2rem;
}
.alert-info {
background-color: rgba(59, 130, 246, 0.1);
border-left: 4px solid rgb(59, 130, 246);
color: var(--text-primary);
}
.alert-info::before {
content: "";
}
.alert-success {
background-color: rgba(34, 197, 94, 0.15);
border-left: 4px solid rgb(34, 197, 94);
color: var(--text-primary);
}
.alert-success::before {
content: "✅";
}
.alert-error {
background-color: rgba(239, 68, 68, 0.15);
border-left: 4px solid rgb(239, 68, 68);
color: var(--text-primary);
}
.alert-error::before {
content: "⚠️";
}
/* CSV Example Section */
.csv-examples {
margin: 2rem 0;
padding: 1.5rem;
background-color: var(--bg-secondary);
border-radius: 0.5rem;
}
.csv-examples h3 {
margin-top: 0;
margin-bottom: 1rem;
}
.csv-examples p {
margin-bottom: 1rem;
}
.csv-examples button {
margin-right: 0.75rem;
margin-top: 0.5rem;
}
/* Checkbox styling */
input[type="checkbox"] {
width: 1.2rem;
height: 1.2rem;
vertical-align: middle;
margin-left: 0.5rem;
accent-color: var(--accent-color);
}
/* Section headers */
h2, h3 {
margin-top: 2rem;
margin-bottom: 1rem;
}
hr {
margin: 2.5rem 0;
border: 0;
height: 1px;
background-color: var(--border-color);
}
/* Accessibility styles */
.accessibility-notice {
font-size: 0.9rem;
margin-top: 30px;
padding: 15px;
background-color: var(--bg-secondary);
border-radius: 8px;
}
/* Improved responsive design */
@media (max-width: 768px) {
.tool-container {
padding: 0;
}
.tool-controls,
.tool-output {
padding: 1rem;
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.tool-table th,
.tool-table td {
padding: 0.5rem;
font-size: 0.9rem;
}
#csvInput {
min-height: 150px;
}
}
/* Data stats styling */
.data-stats {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1.5rem;
padding: 1rem;
background-color: var(--bg-tertiary);
border-radius: 0.25rem;
border-left: 4px solid var(--accent-color);
}
.stat-item {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
background-color: var(--bg-primary);
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.stat-label {
font-weight: 600;
margin-right: 0.5rem;
color: var(--accent-color);
}
.stat-value {
font-size: 1.1rem;
font-weight: bold;
}

View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Tool Example">
<title>Tool Example - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css">
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../utils.js" integrity="sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
<script src="tool-example.js" defer></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<!-- Main Content -->
<h1>Tool Example</h1>
<p>A simple example tool to demonstrate the includes system.</p>
<div class="tool-container">
<div class="tool-controls">
<h3>Tool Controls</h3>
<div class="form-group">
<label for="exampleInput">Example Input:</label>
<input type="text" id="exampleInput" class="form-control" placeholder="Enter some text...">
</div>
<div class="form-group">
<button id="exampleButton" class="btn">Process</button>
</div>
</div>
<div class="tool-output" id="output">
<p class="alert alert-info">Output will appear here.</p>
</div>
</div>
<hr>
<h2>About This Tool</h2>
<p>This is an example tool page that demonstrates how to use the includes system.</p>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

7
docker/resume/papaparse.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

99
docker/resume/sitemap.xml Normal file
View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://localhost:8080/index.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/one-pager-tools/csv-tool.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/airport-dns.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/app-development.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/athion-turnaround.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/fawe-plotsquared.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/healthcare-platform.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/index.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/motherboard-repair.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/nitric-leadership.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/open-source-success.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/showerloop.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/viperwire.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/web-design-java.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/wordpress-security.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>http://localhost:8080/stories/youtube-game-dev.html</loc>
<lastmod>2025-07-07T00:04:26+00:00</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
</urlset>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Airport DNS Infrastructure Case Study">
<title>Airport DNS Infrastructure - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Airport DNS Infrastructure</h1>
<p class="story-meta">Category: Infrastructure & Resilience | Date: 2019-Present</p>
<hr>
</div>
<div class="story-content">
<div class="placeholder-notice">
<h2>Coming Soon</h2>
<p>This case study is currently under development. Check back soon for the full story about my work consulting for Flint Bishop International Airport's website and domain infrastructure.</p>
<h3>What to Expect</h3>
<ul>
<li>The architecture of a geographically redundant DNS cluster achieving an A+ standard</li>
<li>Design considerations for infrastructure capable of withstanding extreme disruptions</li>
<li>Implementation details of high-availability systems for critical infrastructure</li>
<li>Security measures implemented to protect airport digital assets</li>
</ul>
<p>In the meantime, you can visit the airport's website at:</p>
<ul>
<li><a href="https://bishopairport.org" target="_blank">Flint Bishop International Airport</a></li>
</ul>
</div>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="/stories/wordpress-security.html" class="story-nav-link prev">Previous Story</a>
<a href="/stories/nitric-leadership.html" class="story-nav-link next">Next Story</a>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - App Development for Influencers Case Study">
<title>App Development for Influencers - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>App Development for Influencers</h1>
<p class="story-meta">Category: Mobile Development, Analytics | Date: 2013-2018</p>
<hr>
</div>
<div class="story-content">
<p>This case study explores the development of a specialized ad revenue tracking application designed to help content creators optimize their earnings and content strategies.</p>
<h2>The Challenge</h2>
<p>Content creators needed a reliable way to monitor their ad revenue in real-time and use that data to inform their content strategies, but existing solutions were either too complex or lacked the specific features needed by influencers.</p>
<blockquote>
"For content creators, understanding the relationship between content and revenue is crucial for sustainable growth. Our challenge was to make this data accessible and actionable."
</blockquote>
<h2>The Approach</h2>
<p>I developed a user-friendly application that focused on:</p>
<ul>
<li>Real-time revenue monitoring with intuitive visualizations</li>
<li>Correlation analysis between content types and revenue generation</li>
<li>Secure handling of sensitive financial data</li>
<li>Performance optimization for reliable operation</li>
<li>Cross-platform compatibility for diverse creator workflows</li>
</ul>
<h2>Technical Implementation</h2>
<p>The technical implementation involved several key components:</p>
<ul>
<li>Efficient data processing architecture for real-time analytics</li>
<li>Secure API integrations with multiple ad networks</li>
<li>Responsive user interface optimized for both mobile and desktop</li>
<li>Data encryption and privacy controls</li>
<li>Automated reporting and alert systems</li>
</ul>
<h2>Results & Impact</h2>
<p>The application empowered content creators to make data-driven decisions about their content strategies, resulting in measurable increases in ad revenue and audience engagement. Creators gained valuable insights into which content types performed best financially, allowing them to optimize their production efforts while maintaining authenticity.</p>
<h2>Lessons Learned</h2>
<p>This project highlighted the importance of balancing technical sophistication with user-friendly design, especially when creating tools for users who may not have technical backgrounds. It also demonstrated the value of close collaboration with end users throughout the development process to ensure the final product truly meets their needs.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="web-design-java.html" class="story-nav-link prev">Previous: Web Design & Java Plugin Development</a>
<a href="athion-turnaround.html" class="story-nav-link next">Next: Athion.net Turnaround</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>YouTube Game Development</h2>
<p class="story-excerpt">Custom game development for online creators.</p>
<a href="youtube-game-dev.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>Nitric Concepts Leadership</h2>
<p class="story-excerpt">Leading a global team in building secure, scalable gaming solutions.</p>
<a href="nitric-leadership.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Athion.net Turnaround Case Study">
<title>Athion.net Turnaround - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Athion.net Turnaround</h1>
<p class="story-meta">Category: Business Turnaround, System Optimization | Date: 2013-2017</p>
<hr>
</div>
<div class="story-content">
<p>This case study details the successful turnaround of Athion.net, transforming a struggling business into a self-sustaining operation in just two weeks through strategic system optimization and operational streamlining.</p>
<h2>The Challenge</h2>
<p>Athion.net was facing significant operational challenges that threatened its viability, requiring rapid intervention to optimize systems, reduce costs, and establish sustainable operations.</p>
<blockquote>
"The key to a successful business turnaround is identifying core inefficiencies and implementing targeted solutions that create immediate impact while building long-term sustainability."
</blockquote>
<h2>The Approach</h2>
<p>I implemented a comprehensive turnaround strategy that focused on:</p>
<ul>
<li>Rapid assessment of critical system inefficiencies</li>
<li>Implementation of targeted optimizations to reduce operational overhead</li>
<li>Streamlining of core business processes</li>
<li>Establishment of sustainable operational practices</li>
<li>Development of performance monitoring systems</li>
</ul>
<h2>Technical Implementation</h2>
<p>The technical implementation involved several key components:</p>
<ul>
<li>Server infrastructure optimization to reduce costs while improving performance</li>
<li>Implementation of automated monitoring and maintenance systems</li>
<li>Streamlining of deployment processes</li>
<li>Optimization of resource allocation</li>
<li>Implementation of efficient backup and recovery systems</li>
</ul>
<h2>Results & Impact</h2>
<p>Within just two weeks, Athion.net was transformed from a struggling operation into a self-sustaining business. The implemented optimizations significantly reduced operational costs while improving system performance and reliability. The business achieved financial stability and established a foundation for sustainable growth.</p>
<h2>Lessons Learned</h2>
<p>This turnaround demonstrated the importance of rapid, targeted interventions in business recovery situations. It highlighted how technical optimizations can directly impact business viability and the value of establishing sustainable operational practices. The experience reinforced the effectiveness of combining technical expertise with strategic business thinking to achieve meaningful results in challenging situations.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="app-development.html" class="story-nav-link prev">Previous: App Development for Influencers</a>
<a href="motherboard-repair.html" class="story-nav-link next">Next: MotherboardRepair.ca</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>Nitric Concepts Leadership</h2>
<p class="story-excerpt">Leading a global team in building secure, scalable gaming solutions.</p>
<a href="nitric-leadership.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>PlotSquared & FastAsyncWorldEdit</h2>
<p class="story-excerpt">Java plugin development for Minecraft server optimization.</p>
<a href="fawe-plotsquared.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - FastAsyncWorldEdit & PlotSquared Case Study">
<title>FastAsyncWorldEdit & PlotSquared - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>FastAsyncWorldEdit & PlotSquared</h1>
<p class="story-meta">Category: Open Source Development | Date: 2014-Present</p>
<hr>
</div>
<div class="story-content">
<div class="placeholder-notice">
<h2>Coming Soon</h2>
<p>This case study is currently under development. Check back soon for the full story about my contributions to FastAsyncWorldEdit and PlotSquared, two major Minecraft server plugins that revolutionized world editing capabilities.</p>
<h3>What to Expect</h3>
<ul>
<li>The technical challenges of scaling from 50,000 server-crashing edits to billions without interruption</li>
<li>How these tools power a $2 billion game brand</li>
<li>The collaborative development process with global contributors</li>
<li>Performance optimization techniques and architectural decisions</li>
</ul>
<p>In the meantime, you can explore these projects on GitHub:</p>
<ul>
<li><a href="https://github.com/IntellectualSites/FastAsyncWorldEdit" target="_blank">FastAsyncWorldEdit Repository</a></li>
<li><a href="https://github.com/IntellectualSites/PlotSquared" target="_blank">PlotSquared Repository</a></li>
</ul>
</div>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="/stories/" class="story-nav-link prev">Back to Stories</a>
<a href="/stories/healthcare-platform.html" class="story-nav-link next">Next Story</a>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Healthcare Platform Case Study">
<title>Healthcare Platform - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Healthcare Platform Infrastructure</h1>
<p class="story-meta">Category: Infrastructure & Security | Date: 2019-Present</p>
<hr>
</div>
<div class="story-content">
<div class="placeholder-notice">
<h2>Coming Soon</h2>
<p>This case study is currently under development. Check back soon for the full story about my work on the infrastructure for Improving MI Practices, a critical healthcare education platform.</p>
<h3>What to Expect</h3>
<ul>
<li>The design and implementation of secure, scalable infrastructure for healthcare education</li>
<li>Implementation of CIS Level 1 and 2 security standards</li>
<li>Automated deployment pipelines and monitoring systems for high availability</li>
<li>Technical challenges of handling sensitive healthcare training content</li>
</ul>
<p>In the meantime, you can visit the platform at:</p>
<ul>
<li><a href="https://www.improvingmipractices.org" target="_blank">Improving MI Practices</a></li>
<li><a href="https://archive.is/D5HIb" target="_blank">Archived Version</a></li>
</ul>
</div>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="/stories/fawe-plotsquared.html" class="story-nav-link prev">Previous Story</a>
<a href="/stories/wordpress-security.html" class="story-nav-link next">Next Story</a>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Home Infrastructure Cluster & WireGuard Mesh Networking - Colin Knapp">
<title>Home Infrastructure Cluster & WireGuard Mesh Networking - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Home Infrastructure Cluster & WireGuard Mesh Networking</h1>
<p class="story-meta">Category: Infrastructure, Networking, Data Sovereignty | Date: 2020-Present</p>
<hr>
</div>
<div class="story-content">
<h2>Data Sovereignty Through Self-Hosting</h2>
<p>Built a comprehensive home infrastructure cluster using repurposed MacMini hardware to maintain complete data sovereignty and avoid cloud dependencies. This project represents a complete shift away from cloud services, with self-hosted email, DNS, and over 100 additional services.</p>
<h3>Infrastructure Components</h3>
<ul>
<li><strong>Repurposed MacMini Cluster:</strong> Sustainable infrastructure using existing hardware</li>
<li><strong>Self-Hosted Email:</strong> Complete email sovereignty and control</li>
<li><strong>DNS Infrastructure:</strong> Custom DNS services for complete control</li>
<li><strong>100+ Services:</strong> Comprehensive self-hosting ecosystem</li>
<li><strong>Data Sovereignty:</strong> Complete control over personal and business data</li>
</ul>
<h3>WireGuard Mesh Networking Innovation</h3>
<p>Developed a custom WireGuard mesh networking tool designed to simplify deployment of mesh networks as opposed to traditional hub-and-spoke architectures. This tooling enables ease of deployment for quantum-resistant networking solutions.</p>
<h3>Technical Achievements</h3>
<ul>
<li><strong>Mesh vs Hub-Spoke:</strong> Alternative to traditional networking architectures</li>
<li><strong>Simplified Deployment:</strong> Tooling to make mesh networks easier to implement</li>
<li><strong>Quantum-Resistant:</strong> Future-proof networking solutions</li>
<li><strong>Infrastructure Innovation:</strong> Advancing networking technology</li>
</ul>
<h3>Impact</h3>
<p>This infrastructure represents a complete commitment to data sovereignty, infrastructure innovation, and building future-proof networking solutions. The home cluster provides complete control over personal and business data, while the WireGuard mesh networking tool advances the state of secure, distributed networking.</p>
</div>
<div class="story-footer">
<a href="../index.html" class="back-link">← Back to Portfolio</a>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Portfolio Stories and Case Studies">
<title>Colin Knapp - Stories & Case Studies</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<h1>Project Stories & Case Studies</h1>
<p>Detailed stories and elaborations of projects from my portfolio.</p>
<hr>
<div class="stories-grid">
<div class="story-card">
<h2>Airport DNS Infrastructure</h2>
<p class="story-excerpt">Category: Infrastructure & Resilience | Date: 2019-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="airport-dns.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>App Development for Influencers</h2>
<p class="story-excerpt">Category: Mobile Development, Analytics | Date: 2013-2018...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="app-development.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Athion.net Turnaround</h2>
<p class="story-excerpt">Category: Business Turnaround, System Optimization | Date: 2013-2017...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="athion-turnaround.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>FastAsyncWorldEdit & PlotSquared</h2>
<p class="story-excerpt">Category: Open Source Development | Date: 2014-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="fawe-plotsquared.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Healthcare Platform Infrastructure</h2>
<p class="story-excerpt">Category: Infrastructure & Security | Date: 2019-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="healthcare-platform.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Home Infrastructure Cluster & WireGuard Mesh Networking</h2>
<p class="story-excerpt">Category: Infrastructure, Networking, Data Sovereignty | Date: 2020-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="home-infrastructure.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>MotherboardRepair.ca</h2>
<p class="story-excerpt">Category: Entrepreneurship, Sustainable Technology | Date: 2019-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="motherboard-repair.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>DevOps & Leadership at Nitric Concepts</h2>
<p class="story-excerpt">Category: Team Leadership & DevOps | Date: 2018-2021...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="nitric-leadership.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Nuclear War-Resistant DNS Infrastructure</h2>
<p class="story-excerpt">Category: Infrastructure, Security, Government | Date: Confidential...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="nuclear-dns.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Building a Thriving Open Source Community</h2>
<p class="story-excerpt">Category: Open Source | Date: 2019-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="open-source-success.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>ShowerLoop Project</h2>
<p class="story-excerpt">Category: Web Development, Sustainability | Date: 2016...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="showerloop.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Building ViperWire: An AI-Powered Cybersecurity Consultancy</h2>
<p class="story-excerpt">Category: Cybersecurity | Date: 2023-Present...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="viperwire.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>Web Design & Java Plugin Development</h2>
<p class="story-excerpt">Category: Web Development, Java | Date: 2011-2023...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="web-design-java.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>WordPress Security Automation</h2>
<p class="story-excerpt">Category: Security & Automation | Date: 2023...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="wordpress-security.html" class="story-link">Read Full Story</a>
</div>
<div class="story-card">
<h2>YouTube Game Development & Cybersecurity</h2>
<p class="story-excerpt">Category: Game Development, Cybersecurity | Date: 2011-2022...</p>
<p class="story-meta">Category: Project | Date: Recent</p>
<a href="youtube-game-dev.html" class="story-link">Read Full Story</a>
</div>
</div>
<hr>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - MotherboardRepair.ca Case Study">
<title>MotherboardRepair.ca - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>MotherboardRepair.ca</h1>
<p class="story-meta">Category: Entrepreneurship, Sustainable Technology | Date: 2019-Present</p>
<hr>
</div>
<div class="story-content">
<p>This case study explores the founding and development of MotherboardRepair.ca, a company dedicated to reducing electronic waste through specialized circuit board repair services.</p>
<h2>The Challenge</h2>
<p>Electronic waste represents a growing environmental crisis, with millions of devices discarded annually due to minor, repairable circuit board issues. The challenge was to create a sustainable business model that addresses this problem while providing valuable technical services.</p>
<blockquote>
"In a world of planned obsolescence, repair represents both an environmental imperative and a technical challenge that requires specialized expertise."
</blockquote>
<h2>The Approach</h2>
<p>As co-founder, I helped establish a company focused on:</p>
<ul>
<li>Specialized circuit board repair services for various electronic devices</li>
<li>Development of sustainable repair methodologies</li>
<li>Implementation of a versatile toolchain for diagnosing and fixing complex electronic issues</li>
<li>Leveraging industry expertise to address challenging repair scenarios</li>
<li>Promoting environmental responsibility through electronics repair</li>
</ul>
<h2>Technical Implementation</h2>
<p>The technical implementation involved several key components:</p>
<ul>
<li>Establishment of a specialized repair laboratory with advanced diagnostic equipment</li>
<li>Development of repair protocols for various circuit board issues</li>
<li>Implementation of quality control processes</li>
<li>Creation of a customer-friendly service workflow</li>
<li>Documentation of repair methodologies for knowledge sharing</li>
</ul>
<h2>Results & Impact</h2>
<p>MotherboardRepair.ca has successfully established itself as a provider of specialized circuit board repair services, helping to extend the life of electronic devices that would otherwise be discarded. The company has made a measurable impact on electronic waste reduction while providing valuable technical services to clients.</p>
<h2>Lessons Learned</h2>
<p>This entrepreneurial venture has reinforced the importance of specialized technical knowledge in addressing environmental challenges. It has demonstrated that sustainable business models can be built around repair and reuse, challenging the prevailing culture of disposability in consumer electronics.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="athion-turnaround.html" class="story-nav-link prev">Previous: Athion.net Turnaround</a>
<a href="showerloop.html" class="story-nav-link next">Next: ShowerLoop Project</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>Athion.net Turnaround</h2>
<p class="story-excerpt">Transforming a struggling business into a self-sustaining operation.</p>
<a href="athion-turnaround.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>ShowerLoop Project</h2>
<p class="story-excerpt">Web development for eco-friendly recirculating shower system.</p>
<a href="showerloop.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Nitric Concepts Leadership Case Study">
<title>Nitric Concepts Leadership - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>DevOps & Leadership at Nitric Concepts</h1>
<p class="story-meta">Category: Team Leadership & DevOps | Date: 2018-2021</p>
<hr>
</div>
<div class="story-content">
<div class="placeholder-notice">
<h2>Coming Soon</h2>
<p>This case study is currently under development. Check back soon for the full story about my experience leading a global team at Nitric Concepts in building secure, scalable gaming solutions.</p>
<h3>What to Expect</h3>
<ul>
<li>The challenges and successes of managing 45 contractors worldwide</li>
<li>Implementation of Docker, Fail2Ban, and Salt Stack as part of a comprehensive toolchain</li>
<li>How we fostered a collaborative, innovative team culture across multiple time zones</li>
<li>The transformation of Nitric Concepts into a thriving multinational entity</li>
</ul>
<p>In the meantime, you can visit:</p>
<ul>
<li><a href="https://nitricconcepts.com" target="_blank">Nitric Concepts Website</a></li>
</ul>
</div>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="/stories/airport-dns.html" class="story-nav-link prev">Previous Story</a>
<a href="/stories/open-source-success.html" class="story-nav-link next">Next Story</a>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Nuclear War-Resistant DNS Infrastructure - Colin Knapp">
<title>Nuclear War-Resistant DNS Infrastructure - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Nuclear War-Resistant DNS Infrastructure</h1>
<p class="story-meta">Category: Infrastructure, Security, Government | Date: Confidential</p>
<hr>
</div>
<div class="story-content">
<h2>Mission-Critical Infrastructure Design</h2>
<p>Architected and deployed georedundant, nuclear war-resistant DNS clusters for confidential government and defense clients. This project represents the highest level of infrastructure security and resilience planning.</p>
<h3>Infrastructure Components</h3>
<ul>
<li><strong>Georedundant Architecture:</strong> Distributed across multiple geographic locations</li>
<li><strong>Nuclear War Resistance:</strong> Designed to survive extreme scenarios</li>
<li><strong>Confidential Government Clients:</strong> High-security clearance work</li>
<li><strong>Defense Infrastructure:</strong> Critical national security systems</li>
<li><strong>DNS Cluster Architecture:</strong> Sophisticated distributed DNS infrastructure</li>
</ul>
<h3>Technical Excellence</h3>
<ul>
<li><strong>Geographic Redundancy:</strong> Multiple locations for maximum resilience</li>
<li><strong>Extreme Scenario Planning:</strong> Nuclear war-resistant design</li>
<li><strong>Government Standards:</strong> Meeting highest security requirements</li>
<li><strong>National Security:</strong> Supporting critical government infrastructure</li>
</ul>
<h3>Impact</h3>
<p>This infrastructure showcases expertise in designing systems that can survive even the most extreme scenarios, supporting critical national security systems and confidential government operations. The work demonstrates the highest level of infrastructure security and resilience planning.</p>
<p><em>Note: Specific details of this project are confidential due to the nature of government and defense work.</em></p>
</div>
<div class="story-footer">
<a href="../index.html" class="back-link">← Back to Portfolio</a>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Open Source Community Success Case Study">
<title>Open Source Community Success - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Building a Thriving Open Source Community</h1>
<p class="story-meta">Category: Open Source | Date: 2019-Present</p>
<hr>
</div>
<div class="story-content">
<p>In 2019, I had the opportunity to take over a promising open source project called OhMyForm that was at risk of becoming abandoned. The original developer had created a solid foundation but was unable to continue maintaining it. Recognizing the project's potential and the community's need, I stepped in to not only maintain but significantly expand both the codebase and the community around it. After several years of successful development and growth, I made the decision to sunset OhMyForm in 2024 as the excellent <a href="https://formbricks.com/" target="_blank" rel="noopener noreferrer">Formbricks</a> project had vastly exceeded my own ambitions and provided a superior alternative for the community.</p>
<div class="story-image-container">
<img src="../images/docker-hub-stats.jpg" alt="Docker Hub statistics showing over 10 million pulls" class="story-image">
<p class="image-caption">The project's Docker Hub statistics showing over 10 million pulls</p>
</div>
<div class="story-image-container">
<img src="../images/discord-community.jpg" alt="Discord community statistics showing over 32,000 members with 4,297 online" class="story-image">
<p class="image-caption">Our thriving Discord community with over 32,000 members</p>
</div>
<h2>The Challenge</h2>
<p>When I took over the project, it faced several critical challenges:</p>
<ul>
<li>A codebase that was functional but needed significant modernization</li>
<li>Limited infrastructure for continuous integration, testing, and deployment</li>
<li>A small but passionate community with no structured way to communicate or collaborate</li>
<li>No sustainable funding model to support ongoing development and maintenance</li>
<li>Technical debt that was hindering new feature development and stability</li>
</ul>
<blockquote>
"The true measure of an open source project's success isn't just code quality or feature completeness—it's the health and engagement of the community that forms around it."
</blockquote>
<h2>The Approach</h2>
<p>I developed a comprehensive strategy that addressed both technical excellence and community building:</p>
<h3>1. Technical Infrastructure</h3>
<p>My first priority was establishing robust infrastructure to support sustainable development:</p>
<ul>
<li>Implemented a comprehensive CI/CD pipeline using GitHub Actions</li>
<li>Created Docker containers for easy deployment, which have now been pulled over 10 million times</li>
<li>Established automated testing with coverage requirements for all new code</li>
<li>Developed clear documentation for both users and contributors</li>
<li>Set up monitoring and observability tools to track usage and identify issues</li>
</ul>
<h3>2. Community Building</h3>
<p>In parallel with technical improvements, I focused on creating a welcoming, active community:</p>
<ul>
<li>Established a Discord server that has grown to over 32,000 members, with typically 4,000+ active at any time</li>
<li>Created structured channels for support, feature requests, showcase, and general discussion</li>
<li>Implemented community guidelines and moderation systems to maintain a positive environment</li>
<li>Organized regular community calls and update announcements</li>
<li>Recognized and celebrated community contributions</li>
</ul>
<h3>3. Sustainable Funding</h3>
<p>To ensure long-term viability, I established a sustainable funding model:</p>
<ul>
<li>Set up an OpenCollective account for transparent community funding</li>
<li>Developed clear funding goals tied to specific development milestones</li>
<li>Created a sponsorship program with appropriate recognition for corporate supporters</li>
<li>Established a governance model for fund allocation</li>
<li>Built several years of runway to ensure project stability</li>
</ul>
<h2>The Team</h2>
<p>As the project grew, I recruited and mentored a team of over 10 developers who now contribute regularly:</p>
<ul>
<li>Established clear contribution guidelines and review processes</li>
<li>Created an onboarding process for new contributors</li>
<li>Implemented a mentorship system pairing experienced developers with newcomers</li>
<li>Developed a governance structure that distributes decision-making authority</li>
<li>Set up regular team meetings and coordination channels</li>
</ul>
<h2>Results & Impact</h2>
<p>The project has achieved remarkable success under this new structure:</p>
<ul>
<li>Over 10 million Docker image pulls, demonstrating widespread adoption</li>
<li>A thriving Discord community with 32,000+ members and 4,000+ regularly active users</li>
<li>Sustainable funding through OpenCollective with several years of runway</li>
<li>A stable team of 10+ regular contributors</li>
<li>Significant expansion of features and capabilities</li>
<li>Improved code quality, test coverage, and documentation</li>
<li>Regular release cycles with clear roadmaps</li>
</ul>
<h2>Lessons Learned</h2>
<p>This experience provided valuable insights into open source project management:</p>
<h3>Technical Lessons</h3>
<p>Infrastructure investments pay dividends. The early focus on CI/CD, containerization, and automated testing dramatically improved both code quality and contributor experience. By making it easy to deploy, test, and contribute, we lowered barriers to entry and increased the pace of innovation.</p>
<p>Additionally, the decision to containerize the application early proved prescient, as it significantly simplified deployment across diverse environments and contributed to the project's widespread adoption.</p>
<h3>Community Lessons</h3>
<p>Community building requires intentional design. By creating structured spaces for different types of interactions (support, development, showcase, etc.), we enabled community members to engage in ways that matched their interests and expertise. The regular rhythm of updates and community calls helped maintain momentum and excitement.</p>
<p>Transparency in decision-making and fund management built trust with the community, which proved essential for sustainable growth and support.</p>
<h2>Future Directions</h2>
<p>In 2024, after careful consideration of the project's trajectory and the emergence of more comprehensive alternatives, I made the decision to sunset OhMyForm. The repository was officially archived on GitHub in October 2024. The excellent <a href="https://formbricks.com/" target="_blank" rel="noopener noreferrer">Formbricks</a> project had vastly exceeded my own ambitions for OhMyForm and offered the community a superior solution with more features, better support, and a stronger development team.</p>
<p>This decision reflects an important principle in open source stewardship: recognizing when another project better serves the community's needs and being willing to step aside rather than fragmenting efforts. I encouraged our users to migrate to Formbricks and worked to ensure a smooth transition for the community.</p>
<p>The lessons learned from building and eventually sunsetting OhMyForm continue to inform my approach to open source development and community management:</p>
<ul>
<li>The importance of clear communication during project transitions</li>
<li>How to responsibly wind down a project while supporting users</li>
<li>When to collaborate with or defer to other projects in the ecosystem</li>
<li>The value of focusing efforts where they can have the greatest impact</li>
<li>How to transfer community knowledge and relationships during transitions</li>
</ul>
<p>The success of this project demonstrates how technical excellence combined with intentional community building can transform an at-risk open source project into a thriving ecosystem that benefits thousands of users while providing sustainable opportunities for contributors. It also shows the importance of responsible stewardship throughout a project's entire lifecycle, including its conclusion.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="nitric-leadership.html" class="story-nav-link prev">Nitric Concepts Leadership</a>
<span class="story-nav-link next" style="visibility: hidden;">Next Story</span>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>FastAsyncWorldEdit & PlotSquared</h2>
<p class="story-excerpt">The technical challenges overcome in scaling Minecraft world editing from crashing at 50,000 edits to seamlessly handling billions.</p>
<a href="fawe-plotsquared.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>Nitric Concepts Leadership</h2>
<p class="story-excerpt">Managing a distributed team of 45 contractors and implementing DevSecOps practices across multiple timezones.</p>
<a href="nitric-leadership.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - ShowerLoop Project Case Study">
<title>ShowerLoop Project - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>ShowerLoop Project</h1>
<p class="story-meta">Category: Web Development, Sustainability | Date: 2016</p>
<hr>
</div>
<div class="story-content">
<p>This case study details the revitalization of the website for ShowerLoop, an innovative eco-friendly recirculating shower system project, with a focus on implementing WCAG 2.0 AA compliance and modern design principles.</p>
<h2>The Challenge</h2>
<p>The ShowerLoop project needed a website revamp that would effectively communicate its innovative water-saving technology while ensuring accessibility for all users and maintaining a modern, professional design that aligned with the project's environmental values.</p>
<blockquote>
"Creating an accessible, informative web presence for sustainability projects is essential for broadening their impact and reaching diverse audiences."
</blockquote>
<h2>The Approach</h2>
<p>I implemented a comprehensive website redesign that focused on:</p>
<ul>
<li>Implementation of WCAG 2.0 AA compliance standards</li>
<li>Responsive design for optimal viewing across devices</li>
<li>Improved user experience and information architecture</li>
<li>Enhanced technical documentation for the project</li>
<li>Modern design principles that reflected the project's innovative nature</li>
</ul>
<h2>Technical Implementation</h2>
<p>The technical implementation involved several key components:</p>
<ul>
<li>Semantic HTML structure for improved accessibility</li>
<li>Responsive CSS framework implementation</li>
<li>Accessibility testing and remediation</li>
<li>Performance optimization for faster loading</li>
<li>Documentation of maintenance procedures</li>
</ul>
<h2>Results & Impact</h2>
<p>The revamped website significantly enhanced the ShowerLoop project's online presence, making information about this innovative water conservation technology more accessible to a wider audience. The implementation of accessibility standards ensured that the project's message could reach people of all abilities, while the improved design and user experience helped to effectively communicate the project's environmental benefits.</p>
<h2>Lessons Learned</h2>
<p>This project reinforced the importance of accessibility in web design, particularly for projects focused on sustainability and environmental impact. It demonstrated that technical excellence in web development can directly contribute to the success of innovative environmental initiatives by improving their reach and impact.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="motherboard-repair.html" class="story-nav-link prev">Previous: MotherboardRepair.ca</a>
<a href="index.html" class="story-nav-link next">Back to Stories</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>Web Design & Java Plugin Development</h2>
<p class="story-excerpt">Web development with a focus on accessibility and efficiency.</p>
<a href="web-design-java.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>MotherboardRepair.ca</h2>
<p class="story-excerpt">Reducing e-waste through circuit board repairs.</p>
<a href="motherboard-repair.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,272 @@
/* Additional styles for stories and case studies */
.stories-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem;
margin: 2rem 0;
}
.story-card {
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 1.5rem;
background-color: var(--card-bg);
transition: transform 0.3s, box-shadow 0.3s;
height: auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
margin: 0;
}
.story-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
}
.story-card h2 {
margin-top: 0;
font-size: 1.5rem;
color: var(--heading-color);
}
.story-excerpt {
flex-grow: 1;
margin-bottom: 1rem;
}
.story-meta {
font-size: 0.85rem;
color: var(--meta-text-color);
margin-bottom: 1rem;
font-style: italic;
}
.story-link {
display: inline-block;
padding: 0.5rem 1rem;
background-color: #004494; /* Darker blue for better contrast */
color: #ffffff; /* Bright white for better contrast */
text-decoration: none;
border-radius: 4px;
font-weight: 700; /* Increased from 500 to 700 for better visibility */
transition: background-color 0.3s;
text-align: center;
align-self: flex-start;
}
/* Dark mode specific styles for story links */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .story-link {
background-color: #0056b3; /* Darker blue for dark mode */
color: #ffffff; /* Bright white */
}
:root:not([data-theme='light']) .story-link:hover {
background-color: #003d82; /* Even darker blue for hover in dark mode */
}
}
html[data-theme='dark'] .story-link {
background-color: #0056b3; /* Darker blue for dark mode */
color: #ffffff; /* Bright white */
}
html[data-theme='dark'] .story-link:hover {
background-color: #003d82; /* Even darker blue for hover in dark mode */
}
.story-link:hover {
background-color: #003366; /* Darker hover color for better contrast */
}
/* Individual story page styles */
.story-header {
margin-bottom: 2em;
}
.story-header h1 {
margin-bottom: 0.5rem;
}
.story-header .story-meta {
margin-top: 0;
margin-bottom: 1rem;
}
/* Story image container and caption styles */
.story-image-container {
margin: 2rem 0;
text-align: center;
}
.story-image {
width: 100%;
max-width: 800px;
height: auto;
border-radius: 4px;
margin: 0 0 0.5rem;
border: 1px solid var(--border-color);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.image-caption {
font-size: 0.9rem;
color: var(--meta-text-color);
margin: 0.5rem 0 1.5rem;
font-style: italic;
text-align: center;
}
.story-content {
line-height: 1.7;
max-width: 800px;
margin: 0 auto;
margin-bottom: 2em;
}
.story-content h2 {
margin-top: 2rem;
margin-bottom: 1rem;
}
.story-content p {
margin-bottom: 1.5rem;
}
.story-content blockquote {
border-left: 4px solid var(--accent-color);
padding-left: 1rem;
margin-left: 0;
margin-right: 0;
font-style: italic;
color: var(--blockquote-color);
}
.story-footer {
margin-top: 3em;
padding-top: 2em;
border-top: 1px solid var(--border-color);
}
.related-stories {
margin-top: 2rem;
}
.related-stories h3 {
margin-bottom: 1em;
}
.related-stories-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5em;
}
.story-nav {
display: flex;
justify-content: space-between;
margin-bottom: 2em;
}
.story-nav-link {
padding: 0.5em 1em;
background-color: var(--theme-bg);
border: 1px solid var(--border-color);
border-radius: 4px;
text-decoration: none;
transition: all 0.3s ease;
color: #004494;
}
.story-nav-link:hover {
background-color: var(--theme-hover);
text-decoration: none;
color: #003366;
}
.story-nav-link.prev::before {
content: "← ";
}
.story-nav-link.next::after {
content: " →";
}
/* Responsive adjustments */
@media (max-width: 768px) {
.stories-grid {
grid-template-columns: 1fr;
}
.related-stories-list {
grid-template-columns: 1fr;
}
.story-nav {
flex-direction: column;
gap: 1rem;
}
.story-nav-link {
text-align: center;
}
}
@media (max-width: 600px) {
.related-stories-list {
grid-template-columns: 1fr;
}
}
/* Accessibility enhancements */
.story-link:focus {
outline: 2px solid var(--focus-outline-color);
outline-offset: 2px;
}
/* Utility classes */
.hidden {
visibility: hidden;
}
/* Placeholder notice styling */
.placeholder-notice {
background-color: var(--theme-bg);
border: 2px dashed var(--accent-color);
border-radius: 8px;
padding: 2em;
margin: 2em 0;
text-align: center;
}
.placeholder-notice h2 {
color: var(--accent-color);
margin-bottom: 1em;
}
.placeholder-notice h3 {
margin-top: 1.5em;
margin-bottom: 0.5em;
}
.placeholder-notice ul {
display: inline-block;
text-align: left;
margin: 1em auto;
}
.placeholder-notice li {
margin: 0.5em 0;
}
.placeholder-notice a {
font-weight: bold;
color: #004494;
text-decoration: underline;
}
.placeholder-notice a:hover {
color: #003366;
}

View File

@ -0,0 +1,216 @@
/* Story-specific styles */
.story-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.story-header {
margin-bottom: 20px;
text-align: center;
}
.story-header h1 {
font-size: 2em;
margin-bottom: 10px;
}
.story-meta {
font-style: italic;
color: #555;
margin-bottom: 10px;
}
.story-content {
line-height: 1.6;
}
.story-content p {
margin-bottom: 15px;
}
.story-content h2 {
margin-top: 30px;
margin-bottom: 15px;
font-size: 1.5em;
}
.story-content h3 {
margin-top: 25px;
margin-bottom: 10px;
font-size: 1.3em;
}
.story-content ul, .story-content ol {
margin-bottom: 15px;
margin-left: 20px;
}
.story-content li {
margin-bottom: 5px;
}
.story-content blockquote {
margin: 20px 0;
padding: 10px 20px;
border-left: 4px solid #ccc;
background-color: #f9f9f9;
font-style: italic;
}
.story-content img {
max-width: 100%;
height: auto;
margin: 20px 0;
display: block;
}
.story-content .image-caption {
text-align: center;
font-style: italic;
margin-top: -15px;
margin-bottom: 20px;
color: #666;
}
.story-content code {
font-family: monospace;
background-color: #f0f0f0;
padding: 2px 4px;
border-radius: 3px;
}
.story-content pre {
background-color: #f0f0f0;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin-bottom: 20px;
}
.story-content pre code {
background-color: transparent;
padding: 0;
}
.story-navigation {
display: flex;
justify-content: space-between;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #ddd;
}
.story-nav-link {
padding: 8px 15px;
background-color: #f0f0f0;
color: #004494; /* Darker blue for 7:1+ contrast ratio */
text-decoration: none;
border-radius: 4px;
transition: background-color 0.2s;
}
.story-nav-link:hover {
background-color: #e0e0e0;
}
.story-nav-link.prev::before {
content: "← ";
}
.story-nav-link.next::after {
content: " →";
}
.story-tags {
margin-top: 20px;
font-size: 0.9em;
}
.story-tag {
display: inline-block;
background-color: #e0e0e0;
color: #333;
padding: 3px 8px;
margin-right: 5px;
margin-bottom: 5px;
border-radius: 3px;
}
/* Story index page styles */
.stories-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.story-card {
background-color: #f5f5f5;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.story-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.story-card-content {
padding: 15px;
}
.story-card h2 {
margin-top: 0;
margin-bottom: 10px;
font-size: 1.3em;
}
.story-card p {
margin-bottom: 15px;
color: #555;
font-size: 0.9em;
}
.story-card-link {
display: inline-block;
padding: 5px 10px;
background-color: #f0f0f0;
color: #004494; /* Darker blue for 7:1+ contrast ratio */
text-decoration: none;
border-radius: 3px;
font-size: 0.9em;
transition: background-color 0.2s;
}
.story-card-link:hover {
background-color: #e0e0e0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.stories-grid {
grid-template-columns: 1fr;
}
.story-container {
padding: 15px;
}
.story-header h1 {
font-size: 1.8em;
}
.story-content h2 {
font-size: 1.4em;
}
.story-content h3 {
font-size: 1.2em;
}
}

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Story Example">
<title>Story Example - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<!-- Main Content -->
<div class="story-header">
<h1>Story Title Example</h1>
<p class="story-meta">Category: Example | Date: 2023-Present</p>
<hr>
</div>
<div class="story-content">
<p>This is an example story content paragraph.</p>
<h2>Story Section</h2>
<p>This is a section of the story.</p>
<!-- Additional content would go here -->
</div>
<div class="story-footer">
<div class="story-nav">
<a href="#" class="story-nav-link prev">Previous Story</a>
<a href="#" class="story-nav-link next">Next Story</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>Related Story Title</h2>
<p class="story-excerpt">Brief description of the related story content.</p>
<a href="#" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - [Story Title] Case Study">
<title>[Story Title] - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>[Story Title]</h1>
<p class="story-meta">Category: [Category] | Date: [Date Range]</p>
<hr>
</div>
<div class="story-content">
<!-- Replace with actual content -->
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vehicula, nisl vel ultricies aliquet, nisi urna posuere nibh, vel dapibus sapien libero ac nisi. Phasellus non volutpat orci, eu eleifend diam. Integer scelerisque euismod sem, vel euismod orci viverra a.</p>
<h2>The Challenge</h2>
<p>Cras consectetur dolor vel arcu luctus, a hendrerit tortor ultricies. Fusce eu eros vel mauris fermentum volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer rhoncus tortor vitae turpis tincidunt, sed congue lacus tempor.</p>
<blockquote>
"This project represented a significant challenge that required innovative thinking and cutting-edge technology solutions."
</blockquote>
<h2>The Approach</h2>
<p>Praesent feugiat tortor non tortor maximus, at congue nibh tincidunt. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi vulputate velit non enim euismod, sit amet maximus mauris consectetur. Donec placerat arcu non faucibus condimentum.</p>
<!-- Sample image placement -->
<!-- <img src="images/story-image.jpg" alt="Description of the image" class="story-image"> -->
<h2>Technical Implementation</h2>
<p>Aenean non auctor mauris, id dapibus turpis. Fusce vestibulum mi sit amet sem commodo, id aliquam magna sollicitudin. Donec vel libero cursus, venenatis justo sit amet, faucibus nisi. Vestibulum nec suscipit mi. Ut fringilla scelerisque eros, id vestibulum lectus vehicula id.</p>
<h2>Results & Impact</h2>
<p>Suspendisse potenti. Curabitur pharetra neque quis dolor pretium, nec feugiat metus varius. Nulla facilisi. Nam ut justo sed leo viverra iaculis. Aliquam eget risus vitae quam dapibus dignissim at vel lectus. Proin convallis velit non dictum faucibus.</p>
<h2>Lessons Learned</h2>
<p>Proin malesuada facilisis felis, quis placerat erat dignissim nec. Morbi hendrerit elit vitae orci interdum mattis. Curabitur imperdiet velit ut libero egestas, at accumsan arcu vehicula. Nullam ac orci et velit efficitur condimentum.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="#" class="story-nav-link prev">Previous Story</a>
<a href="#" class="story-nav-link next">Next Story</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>Related Story Title</h2>
<p class="story-excerpt">Brief description of the related story content.</p>
<a href="#" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>Another Related Story</h2>
<p class="story-excerpt">Brief description of another related story.</p>
<a href="#" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - ViperWire Cybersecurity Case Study">
<title>ViperWire Cybersecurity - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Building ViperWire: An AI-Powered Cybersecurity Consultancy</h1>
<p class="story-meta">Category: Cybersecurity | Date: 2023-Present</p>
<hr>
</div>
<div class="story-content">
<p>In early 2023, I identified a critical gap in the cybersecurity market: small to medium-sized businesses were increasingly becoming targets for sophisticated cyber attacks, but lacked access to enterprise-grade security solutions that could adapt to rapidly evolving threats. This observation led to the creation of ViperWire, an AI-powered cybersecurity consultancy designed to democratize access to advanced security measures.</p>
<h2>The Challenge</h2>
<p>The cybersecurity landscape in 2023 presented several unique challenges:</p>
<ul>
<li>The increasing sophistication of attacks targeting SMBs with limited security budgets</li>
<li>A shortage of cybersecurity professionals capable of addressing modern threats</li>
<li>The rapid evolution of attack vectors requiring constant vigilance and adaptation</li>
<li>The need for solutions that could scale from small businesses to larger enterprises</li>
</ul>
<blockquote>
"The typical SMB faces the same threat actors as Fortune 500 companies, but with a fraction of the resources to defend themselves. This asymmetry creates a perfect storm where businesses are increasingly vulnerable while security solutions remain inaccessible."
</blockquote>
<h2>The Approach</h2>
<p>I built ViperWire around three core principles that would differentiate it in the market:</p>
<h3>1. AI-Augmented Security Analysis</h3>
<p>Rather than attempting to replace human expertise with AI, I designed systems where AI tools augment human analysts, dramatically increasing their efficiency and effectiveness. This approach began with custom-built monitoring tools that use machine learning to identify behavioral anomalies and prioritize potential threats, allowing human experts to focus on the most critical issues.</p>
<h3>2. Accessible Enterprise-Grade Protection</h3>
<p>By leveraging containerization, infrastructure-as-code, and modular security components, I created scalable security systems that could be rapidly deployed across organizations of varying sizes. This technical architecture allowed ViperWire to deliver enterprise-caliber protection at price points accessible to smaller organizations.</p>
<h3>3. Continuous Adaptation</h3>
<p>I implemented a continuous security improvement cycle that incorporated threat intelligence feeds, regular penetration testing, and automated vulnerability scanning. This approach ensured that security postures evolved in tandem with emerging threats rather than reacting after incidents occurred.</p>
<h2>Technical Implementation</h2>
<p>The technical architecture of ViperWire comprises several innovative components:</p>
<h3>Threat Detection Infrastructure</h3>
<p>I built a distributed monitoring system using a combination of open-source tools (Wazuh, Suricata, OSSEC) enhanced with custom machine learning models to detect anomalous network and system behaviors. The architecture utilizes Kubernetes for orchestration and Prometheus/Grafana for metrics visualization, with custom alerting thresholds tuned to each client's environment.</p>
<h3>Response Automation</h3>
<p>To counter the speed of modern attacks, I developed an automated response framework using Python and Ansible that could isolate compromised systems, revoke credentials, and implement temporary access controls within seconds of a confirmed threat detection. This system reduced the mean time to respond from hours to minutes, significantly limiting potential damage.</p>
<h3>Security Assessment Pipeline</h3>
<p>For proactive security, I created an assessment pipeline incorporating static analysis, dynamic testing, and configuration auditing. This suite leverages Docker containers for consistent, reproducible security tests across different environments and includes custom scanners for emerging vulnerabilities not yet covered by commercial tools.</p>
<h2>Results & Impact</h2>
<p>In its first year, ViperWire has achieved several notable successes:</p>
<ul>
<li>Successfully prevented ransomware attacks at two clients who had been targeted, saving an estimated $500,000 in potential losses</li>
<li>Reduced security alert noise by 87% through improved detection algorithms, allowing for more focused attention on genuine threats</li>
<li>Decreased mean time to detection of security incidents from 24+ hours to under 15 minutes</li>
<li>Enabled five small businesses to achieve compliance with industry security standards that were previously beyond their reach</li>
</ul>
<h2>Lessons Learned</h2>
<p>Building ViperWire has provided valuable insights into both technical and business aspects of cybersecurity:</p>
<h3>Technical Lessons</h3>
<p>The most effective security solutions combine multiple detection methodologies rather than relying on any single approach. Our hybrid model of behavioral analysis, signature detection, and anomaly identification proved far more effective than any individual method alone.</p>
<p>Additionally, I discovered that properly tuned automation dramatically reduces false positives—the bane of many security operations—while still capturing genuine threats. The key was implementing progressive verification steps that validate alerts before triggering high-impact responses.</p>
<h3>Business Lessons</h3>
<p>Perhaps most importantly, I learned that transparency builds trust in security services. By providing clients with clear visibility into threat detection processes and plainly explaining technical concepts, ViperWire was able to build stronger relationships and encourage better security practices within client organizations.</p>
<h2>Future Directions</h2>
<p>Looking ahead, ViperWire is expanding into several promising areas:</p>
<ul>
<li>Developing specialized security solutions for IoT environments in manufacturing settings</li>
<li>Creating educational resources to help clients build internal security capabilities</li>
<li>Expanding AI capabilities to provide predictive threat intelligence specific to each client's industry</li>
</ul>
<p>The founding principle of ViperWire—that sophisticated security should be accessible to organizations of all sizes—continues to guide its evolution and growth.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="/stories/" class="story-nav-link prev">Back to Stories</a>
<a href="fawe-plotsquared.html" class="story-nav-link next">FastAsyncWorldEdit & PlotSquared</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>WordPress Security Automation</h2>
<p class="story-excerpt">How I developed a Docker-based solution that eliminated persistent malware attacks on a high-profile website.</p>
<a href="wordpress-security.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>Healthcare Platform Infrastructure</h2>
<p class="story-excerpt">An in-depth look at the infrastructure design and security implementation for the Improving MI Practices healthcare platform.</p>
<a href="healthcare-platform.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Web Design & Java Plugin Development Case Study">
<title>Web Design & Java Plugin Development - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>Web Design & Java Plugin Development</h1>
<p class="story-meta">Category: Web Development, Java | Date: 2011-2023</p>
<hr>
</div>
<div class="story-content">
<p>This case study details my extensive experience in web solution development and Java plugin creation, with a focus on implementing efficient CI/CD workflows and ensuring client satisfaction across diverse projects.</p>
<h2>The Challenge</h2>
<p>Creating high-quality web solutions and Java plugins that meet diverse client needs while maintaining efficient development workflows, accessibility standards, and comprehensive documentation.</p>
<blockquote>
"Effective web and plugin development requires not just technical expertise, but also a commitment to accessibility, documentation, and streamlined workflows."
</blockquote>
<h2>The Approach</h2>
<p>I implemented a comprehensive development methodology that emphasized:</p>
<ul>
<li>Utilization of Jenkins and GitLab CI/CD for streamlined workflows</li>
<li>Implementation of robust toolchains for rapid development</li>
<li>Adherence to WCAG 2.0 AA accessibility standards</li>
<li>Creation of detailed client documentation</li>
<li>Efficient management of complex systems</li>
</ul>
<h2>Technical Implementation</h2>
<p>The technical implementation involved several key components:</p>
<ul>
<li>Modern web development frameworks and libraries</li>
<li>Java plugin architecture optimized for performance</li>
<li>Automated testing and deployment pipelines</li>
<li>Accessibility testing and remediation</li>
<li>Cross-platform compatibility testing</li>
</ul>
<h2>Results & Impact</h2>
<p>The implementation of these practices significantly enhanced project delivery speed and quality across diverse computing environments. Clients benefited from accessible, well-documented solutions that met their specific needs while maintaining high standards of quality and performance.</p>
<h2>Lessons Learned</h2>
<p>This work reinforced the importance of combining technical expertise with strong documentation practices and accessibility considerations. It also highlighted the value of efficient CI/CD pipelines in maintaining consistent quality across diverse projects and environments.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="youtube-game-dev.html" class="story-nav-link prev">Previous: YouTube Game Development</a>
<a href="app-development.html" class="story-nav-link next">Next: App Development for Influencers</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>PlotSquared & FastAsyncWorldEdit</h2>
<p class="story-excerpt">Java plugin development for Minecraft server optimization.</p>
<a href="fawe-plotsquared.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>ShowerLoop Project</h2>
<p class="story-excerpt">Web development for eco-friendly recirculating shower system.</p>
<a href="showerloop.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - WordPress Security Automation Case Study">
<title>WordPress Security Automation - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>WordPress Security Automation</h1>
<p class="story-meta">Category: Security & Automation | Date: 2023</p>
<hr>
</div>
<div class="story-content">
<div class="placeholder-notice">
<h2>Coming Soon</h2>
<p>This case study is currently under development. Check back soon for the full story about my work developing an automated solution for WordPress malware removal and hardening.</p>
<h3>What to Expect</h3>
<ul>
<li>The creation of a Docker-based utility for automated malware detection and removal</li>
<li>Implementation of hardening measures to prevent reinfection</li>
<li>Successful deployment to protect MLPP from persistent cyber attacks</li>
<li>How infection frequency was reduced from daily/weekly to zero</li>
</ul>
<p>In the meantime, you can visit the protected site at:</p>
<ul>
<li><a href="https://mlpp.org" target="_blank">MLPP</a></li>
</ul>
</div>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="/stories/healthcare-platform.html" class="story-nav-link prev">Previous Story</a>
<a href="/stories/airport-dns.html" class="story-nav-link next">Next Story</a>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - YouTube Game Development & Cybersecurity Case Study">
<title>YouTube Game Development & Cybersecurity - Colin Knapp Case Study</title>
<link rel="icon" type="image/x-icon" href="../favicon.ico">
<link rel="stylesheet" href="../styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=" crossorigin="anonymous">
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
<script src="../includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="stories.css" integrity="sha256-O+OMb48leSKvekhMTDUK1y6+WG9x33kA0eDw00wUwkY=">
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<div class="container-fluid" role="main">
<div class="story-header">
<h1>YouTube Game Development & Cybersecurity</h1>
<p class="story-meta">Category: Game Development, Cybersecurity | Date: 2011-2022</p>
<hr>
</div>
<div class="story-content">
<p>This case study explores my experience designing custom video games for prominent online creators, with a focus on integrating advanced cybersecurity measures to protect both creators and their audiences.</p>
<h2>The Challenge</h2>
<p>Creating immersive gaming experiences for large YouTube audiences while simultaneously protecting against DDoS attacks, phishing attempts, and ensuring data privacy for millions of users.</p>
<blockquote>
"Building games for online creators requires a delicate balance between engaging gameplay and robust security measures to protect both the creators and their communities."
</blockquote>
<h2>The Approach</h2>
<p>I developed a comprehensive approach that combined innovative game design with multi-layered security protocols:</p>
<ul>
<li>Custom game development tailored to each creator's brand and audience</li>
<li>Implementation of DDoS defense systems to ensure continuous availability</li>
<li>Anti-phishing protocols to protect creator and user accounts</li>
<li>Data privacy measures compliant with global regulations</li>
<li>Comprehensive documentation and training for creators</li>
</ul>
<h2>Technical Implementation</h2>
<p>The technical implementation involved several key components:</p>
<ul>
<li>Game server architecture designed for scalability and security</li>
<li>Real-time monitoring and threat detection systems</li>
<li>User authentication and authorization frameworks</li>
<li>Data encryption for sensitive information</li>
<li>Regular security audits and penetration testing</li>
</ul>
<h2>Results & Impact</h2>
<p>The custom gaming experiences reached millions of users worldwide, providing seamless, secure gameplay while protecting creators' digital assets and user data. The implementation of robust security measures prevented numerous potential breaches and ensured continuous availability even during high-traffic events.</p>
<h2>Lessons Learned</h2>
<p>This work highlighted the importance of integrating security considerations from the earliest stages of game development, rather than treating it as an afterthought. It also demonstrated the value of creating comprehensive documentation and training materials to empower creators to maintain security best practices long-term.</p>
</div>
<div class="story-footer">
<div class="story-nav">
<a href="index.html" class="story-nav-link prev">Back to Stories</a>
<a href="web-design-java.html" class="story-nav-link next">Next: Web Design & Java Plugin Development</a>
</div>
<div class="related-stories">
<h3>Related Stories</h3>
<div class="related-stories-list">
<div class="story-card">
<h2>WordPress Security Automation</h2>
<p class="story-excerpt">Automated solution for WordPress malware removal and hardening.</p>
<a href="wordpress-security.html" class="story-link">Read Story</a>
</div>
<div class="story-card">
<h2>ViperWire</h2>
<p class="story-excerpt">AI-powered cybersecurity and development consultancy.</p>
<a href="viperwire.html" class="story-link">Read Story</a>
</div>
</div>
</div>
</div>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

386
docker/resume/styles.css Normal file
View File

@ -0,0 +1,386 @@
:root {
--bg-color: #ffffff;
--text-color: #333333;
--accent-color: #0056b3;
--border-color: #e0e0e0;
--hover-color: #003d82;
--theme-bg: #f5f5f5;
--theme-border: #ddd;
--theme-hover: #e0e0e0;
--date-color: #555555;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #eaeaea;
--bg-hover: #f0f0f0;
--text-primary: #333333;
--button-bg: #f5f5f5;
--button-hover-bg: #e0e0e0;
--focus-outline-color: #0056b3;
--progress-bg: #e0e0e0;
--accent-hover: #003d82;
}
/* Dark theme variables when system prefers dark mode (auto setting) */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--accent-color: #5fa9ff;
--border-color: #404040;
--hover-color: #8ac2ff;
--theme-bg: #2d2d2d;
--theme-border: #404040;
--theme-hover: #3d3d3d;
--date-color: #a0a0a0;
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--bg-hover: #333333;
--text-primary: #e0e0e0;
--button-bg: #2d2d2d;
--button-hover-bg: #3d3d3d;
--focus-outline-color: #5fa9ff;
--progress-bg: #404040;
--accent-hover: #8ac2ff;
}
}
/* Light theme variables when manually selected */
html[data-theme='light'] {
--bg-color: #ffffff;
--text-color: #333333;
--accent-color: #0056b3;
--border-color: #e0e0e0;
--hover-color: #003d82;
--theme-bg: #f5f5f5;
--theme-border: #ddd;
--theme-hover: #e0e0e0;
--date-color: #555555;
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #eaeaea;
--bg-hover: #f0f0f0;
--text-primary: #333333;
--button-bg: #f5f5f5;
--button-hover-bg: #e0e0e0;
--focus-outline-color: #0056b3;
--progress-bg: #e0e0e0;
--accent-hover: #003d82;
}
/* Dark theme variables when manually selected */
html[data-theme='dark'] {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--accent-color: #5fa9ff;
--border-color: #404040;
--hover-color: #8ac2ff;
--theme-bg: #2d2d2d;
--theme-border: #404040;
--theme-hover: #3d3d3d;
--date-color: #a0a0a0;
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #3d3d3d;
--bg-hover: #333333;
--text-primary: #e0e0e0;
--button-bg: #2d2d2d;
--button-hover-bg: #3d3d3d;
--focus-outline-color: #5fa9ff;
--progress-bg: #404040;
--accent-hover: #8ac2ff;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--bg-color);
margin: 0;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
/* Container that can expand to full width */
.container-fluid {
width: 100%;
max-width: 100%;
padding-right: 15px;
padding-left: 15px;
margin-right: auto;
margin-left: auto;
box-sizing: border-box;
}
h1, h2, h3 {
color: var(--text-color);
margin-top: 1.5em;
margin-bottom: 0.5em;
}
h1 {
font-size: 2.5em;
border-bottom: 2px solid var(--accent-color);
padding-bottom: 0.3em;
}
h2 {
font-size: 2em;
color: var(--accent-color);
}
h3 {
font-size: 1.5em;
}
a {
color: var(--accent-color);
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-thickness: 1px;
font-weight: 500;
transition: all 0.3s ease;
}
a:hover, a:focus {
color: var(--hover-color);
text-decoration-thickness: 2px;
outline: none;
}
a:focus {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
.section {
margin-bottom: 2em;
padding: 1em;
border: 1px solid var(--border-color);
border-radius: 5px;
}
.entry {
margin-bottom: 1.5em;
}
.date {
color: var(--date-color);
font-style: italic;
margin: 0.5em 0;
}
.overview {
font-weight: 500;
margin: 0.5em 0;
}
ul {
margin: 0.5em 0;
padding-left: 1.5em;
}
li {
margin: 0.3em 0;
}
hr {
border: none;
border-top: 1px solid var(--border-color);
margin: 2em 0;
}
.theme-switch {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
gap: 10px;
}
.theme-switch button {
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: background-color 0.3s;
}
.theme-switch button:hover {
background-color: rgba(128, 128, 128, 0.2);
}
@media (max-width: 600px) {
body {
padding: 10px;
}
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
h3 {
font-size: 1.2em;
}
}
/* Navigation styles */
.main-nav {
display: flex;
justify-content: center;
margin: 1rem 0;
}
.main-nav ul {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 1rem;
border-radius: 4px;
background-color: var(--theme-bg);
padding: 0.5rem 1rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.main-nav li {
margin: 0;
padding: 0;
position: relative;
}
.main-nav a {
display: block;
padding: 0.5rem 1rem;
text-decoration: none;
color: var(--text-color);
font-weight: 500;
border-radius: 4px;
transition: background-color 0.3s, color 0.3s;
}
.main-nav a:hover {
background-color: var(--theme-hover);
color: var(--accent-color);
}
.main-nav a.active {
background-color: var(--accent-color);
color: white;
font-weight: 700;
}
/* Add specific styles for dark mode active navigation */
html[data-theme='dark'] .main-nav a.active {
background-color: #0056b3;
color: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) .main-nav a.active {
background-color: #0056b3;
color: #ffffff;
}
}
/* Dropdown styles */
.main-nav .dropdown {
position: relative;
cursor: pointer;
}
.main-nav .dropdown > a::after {
content: "▼";
font-size: 0.7em;
margin-left: 0.5em;
vertical-align: middle;
}
.main-nav .dropdown-content {
display: none;
position: absolute;
top: 100%;
left: 0;
background-color: var(--theme-bg);
min-width: 160px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
z-index: 1000;
border-radius: 4px;
padding: 0.5rem 0;
margin-top: 0.5rem;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0s linear 0.2s;
}
.main-nav .dropdown:hover .dropdown-content,
.main-nav .dropdown:focus-within .dropdown-content {
display: block;
opacity: 1;
visibility: visible;
transition: opacity 0.2s ease, visibility 0s linear 0s;
}
.main-nav .dropdown-content a {
padding: 0.5rem 1rem;
display: block;
text-align: left;
white-space: nowrap;
}
.main-nav .dropdown-content a.active {
background-color: var(--accent-color);
color: white;
}
/* Responsive navigation */
@media (max-width: 600px) {
.main-nav ul {
flex-direction: column;
gap: 0.5rem;
}
.main-nav a {
text-align: center;
}
.main-nav .dropdown-content {
position: static;
box-shadow: none;
margin-top: 0;
padding-left: 1rem;
}
}
.read-more {
display: inline-block;
margin-top: 0.5em;
font-weight: 500;
color: var(--accent-color);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s ease;
}
.read-more:hover, .read-more:focus {
border-bottom-color: var(--accent-color);
text-decoration: none;
}
.read-more::after {
content: "→";
margin-left: 0.3em;
transition: transform 0.2s ease;
}
.read-more:hover::after, .read-more:focus::after {
transform: translateX(3px);
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - Template with Includes">
<title>Template with Includes - Colin Knapp</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css" integrity="sha256-Y+6RTuKMnPfNa1TjCQCcFhxwo0G+xNy7t1MaAvn5SuU=">
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
<script src="includes.js" integrity="sha256-q9ac7XWqnIASoBRfs4I4hpSMlnxGARofcEw0cSFfn/E="></script>
</head>
<body>
<!-- Header Include -->
<div id="header-include"></div>
<!-- Main Content -->
<h1>Page Title</h1>
<p>This is the main content of the page.</p>
<hr>
<h2>Section Title</h2>
<p>This is a section of content.</p>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

44
docker/resume/test-csv-tool.sh Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
# =====================================================================
# test-csv-tool.sh - Test the CSV tool functionality
# =====================================================================
# This script checks if the CSV tool page loads without CSP errors
# =====================================================================
echo "=== Testing CSV Tool ==="
# Create a test CSV file
echo "Name,Age,City
John,30,New York
Jane,25,San Francisco
Bob,40,Chicago" > test.csv
# Check if the page loads properly
echo "Checking if the CSV tool page loads properly..."
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/one-pager-tools/csv-tool.html)
if [ "$RESPONSE" -eq 200 ]; then
echo "✅ CSV tool page loads successfully (HTTP $RESPONSE)"
else
echo "❌ CSV tool page failed to load (HTTP $RESPONSE)"
exit 1
fi
# Check for CSP errors in the response headers
echo "Checking for CSP errors in response headers..."
CSP_HEADER=$(curl -s -I http://localhost:8080/one-pager-tools/csv-tool.html | grep -i "Content-Security-Policy")
if [ -n "$CSP_HEADER" ]; then
echo "✅ CSP header found in response"
else
echo "❌ CSP header not found in response"
exit 1
fi
# Clean up
rm -f test.csv
echo "=== CSV Tool Test Completed Successfully ==="
echo "The CSV tool appears to be working correctly."
echo "You can manually test it by visiting: http://localhost:8080/one-pager-tools/csv-tool.html"
echo "and pasting CSV data into the textarea."

55
docker/resume/test-matomo.sh Executable file
View File

@ -0,0 +1,55 @@
#!/bin/bash
# Test Matomo Analytics Integration
echo "🔍 Testing Matomo Analytics Integration..."
echo "================================================"
# Test 1: Check if CSP includes Matomo hash
echo "1. Checking Content Security Policy..."
CSP_RESPONSE=$(curl -s -I https://colinknapp.com | grep -i "content-security-policy")
if echo "$CSP_RESPONSE" | grep -q "sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM="; then
echo "✅ CSP includes Matomo script hash"
else
echo "❌ CSP missing Matomo script hash"
fi
if echo "$CSP_RESPONSE" | grep -q "https://metrics.nixc.us"; then
echo "✅ CSP allows metrics.nixc.us domain"
else
echo "❌ CSP missing metrics.nixc.us domain"
fi
# Test 2: Check if Matomo script is in header include
echo ""
echo "2. Checking Matomo script in header include..."
HEADER_RESPONSE=$(curl -s https://colinknapp.com/includes/header.html)
if echo "$HEADER_RESPONSE" | grep -q "_paq"; then
echo "✅ Matomo script found in header include"
else
echo "❌ Matomo script missing from header include"
fi
# Test 3: Test Matomo tracking pixel
echo ""
echo "3. Testing Matomo tracking pixel..."
PIXEL_RESPONSE=$(curl -s -w "%{http_code}" "https://metrics.nixc.us/matomo.php?idsite=3&rec=1&action_name=test&url=https://colinknapp.com" -o /dev/null)
if [ "$PIXEL_RESPONSE" = "200" ]; then
echo "✅ Matomo tracking pixel responds (HTTP 200)"
else
echo "❌ Matomo tracking pixel failed (HTTP $PIXEL_RESPONSE)"
fi
# Test 4: Check if matomo.js is accessible
echo ""
echo "4. Testing Matomo JavaScript file..."
JS_RESPONSE=$(curl -s -w "%{http_code}" "https://metrics.nixc.us/matomo.js" -o /dev/null)
if [ "$JS_RESPONSE" = "200" ]; then
echo "✅ Matomo JavaScript file accessible (HTTP 200)"
else
echo "❌ Matomo JavaScript file failed (HTTP $JS_RESPONSE)"
fi
echo ""
echo "================================================"
echo "🎯 Test complete! If all tests pass, Matomo should be working."
echo "💡 Visit https://colinknapp.com and check browser console for any CSP errors."
echo "📊 Check your Matomo dashboard at https://metrics.nixc.us for real-time data."

View File

@ -0,0 +1,19 @@
{"level":"info","ts":1751831831.5810199,"msg":"maxprocs: Leaving GOMAXPROCS=8: CPU quota undefined"}
{"level":"info","ts":1751831831.5824819,"msg":"GOMEMLIMIT is updated","package":"github.com/KimMachineGun/automemlimit/memlimit","GOMEMLIMIT":7730941132,"previous":9223372036854775807}
{"level":"info","ts":1751831831.582569,"msg":"using config from file","file":"Caddyfile.local"}
{"level":"info","ts":1751831831.585237,"msg":"adapted config to JSON","adapter":"caddyfile"}
{"level":"warn","ts":1751831831.585273,"msg":"Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies","adapter":"caddyfile","file":"Caddyfile.local","line":15}
{"level":"info","ts":1751831831.597684,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
{"level":"info","ts":1751831831.5983381,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0x140004cb480"}
{"level":"warn","ts":1751831831.600636,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":8080"}
{"level":"warn","ts":1751831831.600668,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":8080"}
{"level":"info","ts":1751831831.600673,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
{"level":"info","ts":1751831831.6013281,"msg":"autosaved config (load with --resume flag)","file":"/Users/computerpro/Library/Application Support/Caddy/autosave.json"}
{"level":"info","ts":1751831831.601336,"msg":"serving initial configuration"}
{"level":"info","ts":1751831831.607745,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/Users/computerpro/Library/Application Support/Caddy","instance":"bb1987a6-f2f6-4230-a2aa-e3b16b9f988e","try_again":1751918231.607744,"try_again_in":86399.99999975}
{"level":"info","ts":1751831831.6082098,"logger":"tls","msg":"finished cleaning storage units"}
{"level":"info","ts":1751833051.9393191,"msg":"shutting down apps, then terminating","signal":"SIGTERM"}
{"level":"warn","ts":1751833051.944179,"msg":"exiting; byeee!! 👋","signal":"SIGTERM"}
{"level":"info","ts":1751833051.9507608,"logger":"http","msg":"servers shutting down with eternal grace period"}
{"level":"info","ts":1751833051.9561532,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"}
{"level":"info","ts":1751833051.956237,"msg":"shutdown complete","signal":"SIGTERM","exit_code":0}

72
docker/resume/theme.js Normal file
View File

@ -0,0 +1,72 @@
document.addEventListener('DOMContentLoaded', function() {
const themeToggle = document.getElementById('themeToggle');
const html = document.documentElement;
// Check if themeToggle exists before proceeding
if (!themeToggle) {
console.log('Theme toggle button not found on this page');
return;
}
// Check for saved theme preference, default to auto
const savedTheme = localStorage.getItem('theme') || 'auto';
// Set initial value for aria-checked attribute
themeToggle.setAttribute('aria-checked', savedTheme !== 'auto');
updateTheme(savedTheme);
function updateTheme(theme) {
// Update button state and labels
const themeLabels = {
light: 'Theme mode: Light',
dark: 'Theme mode: Dark',
auto: 'Theme mode: Auto'
};
themeToggle.setAttribute('aria-label', themeLabels[theme]);
themeToggle.setAttribute('aria-checked', theme !== 'auto');
// Update button icon
const themeIcons = {
light: '🌞',
dark: '🌙',
auto: '🌓'
};
themeToggle.textContent = themeIcons[theme];
if (theme === 'auto') {
html.removeAttribute('data-theme');
} else {
html.setAttribute('data-theme', theme);
}
}
themeToggle.addEventListener('click', () => {
const currentTheme = html.getAttribute('data-theme') || 'auto';
let newTheme;
switch(currentTheme) {
case 'light':
newTheme = 'dark';
break;
case 'dark':
newTheme = 'auto';
break;
default:
newTheme = 'light';
}
updateTheme(newTheme);
localStorage.setItem('theme', newTheme);
});
// Handle keyboard navigation
themeToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
themeToggle.click();
}
});
});

80
docker/resume/update-all.sh Executable file
View File

@ -0,0 +1,80 @@
#!/bin/bash
# =====================================================================
# update-all.sh - Run all update scripts
# =====================================================================
# This script runs all update scripts in the correct order:
# 1. Generate sitemap.xml
# 2. Update navigation menu from sitemap
# 3. Update stories page from sitemap
# 4. Update CSP hashes
# 5. Apply accessibility fixes
# =====================================================================
set -e
echo "=== Running all update scripts ==="
# Get the directory of this script
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
# Generate sitemap.xml
echo "=== Generating sitemap.xml ==="
if [ -f "$SCRIPT_DIR/generate-sitemap.sh" ]; then
"$SCRIPT_DIR/generate-sitemap.sh"
else
echo "⚠️ generate-sitemap.sh not found, skipping"
fi
# Update navigation from sitemap
echo "=== Updating navigation from sitemap ==="
if [ -f "$SCRIPT_DIR/update-nav-from-sitemap.js" ]; then
node "$SCRIPT_DIR/update-nav-from-sitemap.js"
else
echo "⚠️ update-nav-from-sitemap.js not found, skipping"
fi
# Update stories page from sitemap
echo "=== Updating stories page from sitemap ==="
if [ -f "$SCRIPT_DIR/update-stories-page.js" ]; then
node "$SCRIPT_DIR/update-stories-page.js"
else
echo "⚠️ update-stories-page.js not found, skipping"
fi
# Update CSP hashes
echo "=== Updating CSP hashes ==="
if [ -f "$SCRIPT_DIR/update-csp-hashes.sh" ]; then
"$SCRIPT_DIR/update-csp-hashes.sh"
else
echo "⚠️ update-csp-hashes.sh not found, skipping"
fi
# Ensure accessibility fixes are applied
echo "=== Ensuring accessibility fixes are applied ==="
# Check if stories.css exists
if [ -f "$SCRIPT_DIR/stories/stories.css" ]; then
# Check if the file already has the accessibility fixes
if ! grep -q "color: #004494;" "$SCRIPT_DIR/stories/stories.css"; then
echo "Applying accessibility fixes to stories.css..."
# Use sed to add the color property to story-nav-link class
sed -i '' 's/\.story-nav-link {/\.story-nav-link {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css"
sed -i '' 's/\.story-nav-link:hover {/\.story-nav-link:hover {\n color: #003366; \/* Even darker on hover for better visibility *\//g' "$SCRIPT_DIR/stories/stories.css"
# Update placeholder-notice links
sed -i '' 's/\.placeholder-notice a {/\.placeholder-notice a {\n color: #004494; \/* Darker blue for 7:1+ contrast ratio *\//g' "$SCRIPT_DIR/stories/stories.css"
# Add hover state for placeholder-notice links if it doesn't exist
if ! grep -q "\.placeholder-notice a:hover" "$SCRIPT_DIR/stories/stories.css"; then
echo -e "\n.placeholder-notice a:hover {\n color: #003366; /* Even darker on hover */\n}" >> "$SCRIPT_DIR/stories/stories.css"
fi
echo "Accessibility fixes applied to stories.css"
else
echo "Accessibility fixes already present in stories.css"
fi
else
echo "⚠️ stories/stories.css not found, skipping accessibility fixes"
fi
echo "=== All updates completed successfully ==="
echo "To apply changes, restart the server using: ./caddy.sh"

View File

@ -0,0 +1,164 @@
#!/bin/bash
# =====================================================================
# update-csp-hashes.sh - Update Content Security Policy hashes
# =====================================================================
# This script updates the CSP hashes for:
# 1. All JavaScript and CSS files
# 2. All inline style attributes in HTML files
# 3. Adds CSP meta tags to HTML files
# After running this script, restart the server using:
# ./caddy.sh
# =====================================================================
set -e
echo "Updating CSP hashes for all JavaScript, CSS files, and inline styles..."
# Directory containing the files
BASE_DIR="$(pwd)"
# Check if we're in a Docker environment
if [ -f "/etc/caddy/Caddyfile" ]; then
CADDYFILE="/etc/caddy/Caddyfile"
else
CADDYFILE="$BASE_DIR/Caddyfile"
fi
TEMP_INLINE_HASHES_FILE=$(mktemp)
# Arrays to store hashes
SCRIPT_HASHES=()
STYLE_HASHES=()
# Calculate hash for a file
calculate_hash() {
local file=$1
sha256sum "$file" | awk '{print $1}' | xxd -r -p | base64
}
# Calculate hash for inline style
calculate_inline_hash() {
local style_content=$1
echo -n "$style_content" | sha256sum | awk '{print $1}' | xxd -r -p | base64
}
# Process JavaScript files
echo "Processing JavaScript files..."
for js_file in $(find "$BASE_DIR" -name "*.js" -type f); do
echo "Processing $js_file"
file_name=$(basename "$js_file")
hash=$(calculate_hash "$js_file")
SCRIPT_HASHES+=("'sha256-$hash'")
# Update HTML files that reference this JS file
for html_file in $(find "$BASE_DIR" -name "*.html" -type f); do
if grep -q "$file_name" "$html_file"; then
echo "Updating $file_name in $html_file"
# Create a temporary file for the replacement
tmp_file=$(mktemp)
# For files with existing integrity attribute
if grep -q "$file_name.*integrity" "$html_file"; then
# Use awk for safer text processing
awk -v fname="$file_name" -v newhash="$hash" '
{
if ($0 ~ fname && $0 ~ /integrity/) {
gsub(/integrity="sha256-[^"]*"/, "integrity=\"sha256-" newhash "\"");
}
print;
}' "$html_file" > "$tmp_file"
else
# Add integrity attribute if it doesn't exist
awk -v fname="$file_name" -v newhash="$hash" '
{
if ($0 ~ fname && $0 ~ /src/ && !($0 ~ /integrity/)) {
gsub(/src="[^"]*"/, "&" " integrity=\"sha256-" newhash "\"");
}
print;
}' "$html_file" > "$tmp_file"
fi
# Replace original file with modified content
mv "$tmp_file" "$html_file"
fi
done
done
# Process CSS files
echo "Processing CSS files..."
for css_file in $(find "$BASE_DIR" -name "*.css" -type f); do
echo "Processing $css_file"
file_name=$(basename "$css_file")
hash=$(calculate_hash "$css_file")
STYLE_HASHES+=("'sha256-$hash'")
# Update HTML files that reference this CSS file
for html_file in $(find "$BASE_DIR" -name "*.html" -type f); do
if grep -q "$file_name" "$html_file"; then
echo "Updating $file_name in $html_file"
# Create a temporary file for the replacement
tmp_file=$(mktemp)
# For files with existing integrity attribute
if grep -q "$file_name.*integrity" "$html_file"; then
# Use awk for safer text processing
awk -v fname="$file_name" -v newhash="$hash" '
{
if ($0 ~ fname && $0 ~ /integrity/) {
gsub(/integrity="sha256-[^"]*"/, "integrity=\"sha256-" newhash "\"");
}
print;
}' "$html_file" > "$tmp_file"
else
# Add integrity attribute if it doesn't exist
awk -v fname="$file_name" -v newhash="$hash" '
{
if ($0 ~ fname && $0 ~ /href/ && !($0 ~ /integrity/)) {
gsub(/href="[^"]*"/, "&" " integrity=\"sha256-" newhash "\"");
}
print;
}' "$html_file" > "$tmp_file"
fi
# Replace original file with modified content
mv "$tmp_file" "$html_file"
fi
done
done
# Find and process inline styles - using a more thorough approach
echo "Processing HTML files for inline styles..."
find "$BASE_DIR" -name "*.html" -type f | while read -r html_file; do
echo "Processing $html_file for inline styles..."
# Use a more comprehensive grep pattern to catch all inline styles
# This includes both style="..." and style = "..." patterns
grep -o 'style\s*=\s*"[^"]*"' "$html_file" | sed 's/style\s*=\s*"\(.*\)"/\1/' | while read -r style_content; do
if [ -n "$style_content" ]; then
hash=$(calculate_inline_hash "$style_content")
echo "Found inline style: '$style_content'"
echo "Calculated hash: sha256-$hash"
echo "'sha256-$hash'" >> "$TEMP_INLINE_HASHES_FILE"
fi
done
done
# Sort and remove duplicates from inline style hashes
if [ -f "$TEMP_INLINE_HASHES_FILE" ]; then
sort -u "$TEMP_INLINE_HASHES_FILE" > "${TEMP_INLINE_HASHES_FILE}.sorted"
mv "${TEMP_INLINE_HASHES_FILE}.sorted" "$TEMP_INLINE_HASHES_FILE"
# Add inline style hashes to the STYLE_HASHES array
while read -r hash; do
STYLE_HASHES+=("$hash")
done < "$TEMP_INLINE_HASHES_FILE"
# Clean up
rm -f "$TEMP_INLINE_HASHES_FILE"
fi
# Combine all hashes for CSP
echo "Skipping CSP header updates (disabled)"
echo "CSP hashes updated successfully!"
echo "To apply changes, restart the server using: ./caddy.sh"

View File

@ -0,0 +1,38 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Create a simple ICO file directly (base64 encoded)
function generateFaviconICO() {
// This is a minimal 16x16 ICO file with a blue square and white "CK" text
// The data is base64 encoded
const icoBase64 = `
AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAVlZWAFZWVgBWVlYAVlZWAFZWVgBWVlYAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZWVgBWVlYCVlZWQFZWVoBWVlaAVlZWQFZWVgJWVlYAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZWVgBWVlYGVlZWaFZWVuJWVlb/VlZW/1ZWVuJWVlZoVlZW
BlZWVgAAAAAAAAAAAAAAAAAAAAAAVlZWAFZWVgBWVlYmVlZWtVZWVv9WVlb/VlZW/1ZWVv9WVlb/
VlZWtVZWViZWVlYAVlZWAAAAAAAAAAAAAAAAAFZWVgBWVlY0VlZW1VZWVv9WVlb/VlZW/1ZWVv9W
Vlb/VlZW/1ZWVtVWVlY0VlZWAAAAAAAAAAAAAAAAAAAAAABWVlYSVlZWrVZWVv9WVlb/VlZW/1ZW
Vv9WVlb/VlZW/1ZWVv9WVlatVlZWElZWVgAAAAAAAAAAAAAAAAAAAAAAVlZWRlZWVvFWVlb/VlZW
/1ZWVv9WVlb/VlZW/1ZWVv9WVlbxVlZWRlZWVgAAAAAAAAAAAAAAAAAAAAAAVlZWRlZWVvFWVlb/
VlZW/1ZWVv9WVlb/VlZW/1ZWVv9WVlbxVlZWRlZWVgAAAAAAAAAAAAAAAAAAAAAAVlZWElZWVq1W
Vlb/VlZW/1ZWVv9WVlb/VlZW/1ZWVv9WVlb/VlZWrVZWVhJWVlYAAAAAAAAAAAAAAAAAAAAAAFZW
VjRWVlbVVlZW/1ZWVv9WVlb/VlZW/1ZWVv9WVlb/VlZW1VZWVjRWVlYAAAAAAAAAAAAAAAAAVlZW
AFZWVgBWVlYmVlZWtVZWVv9WVlb/VlZW/1ZWVv9WVlb/VlZWtVZWViZWVlYAVlZWAAAAAAAAAAAA
AAAAAAAAAAAAAFZWVgZWVlZoVlZW4lZWVv9WVlb/VlZW4lZWVmhWVlYGVlZWAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAFZWVgJWVlZAVlZWgFZWVoBWVlZAVlZWAlZWVgAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVlZWAFZWVgBWVlYAVlZWAFZWVgBWVlYAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
`.trim();
// Write the base64 data to the ICO file
const icoBuffer = Buffer.from(icoBase64, 'base64');
fs.writeFileSync(path.join(__dirname, 'favicon.ico'), icoBuffer);
console.log('Generated favicon.ico');
}
// Run the generator
generateFaviconICO();

View File

@ -0,0 +1,133 @@
#!/usr/bin/env node
/**
* update-nav-from-sitemap.js
*
* This script reads the sitemap.xml file and updates the navigation menu in header.html
* to include all story pages found in the sitemap.
*
* No external dependencies required.
*/
const fs = require('fs');
const path = require('path');
// Configuration
const SITEMAP_PATH = path.join(__dirname, 'sitemap.xml');
const HEADER_PATH = path.join(__dirname, 'includes', 'header.html');
const STORIES_PATH_PREFIX = '/stories/';
const STORIES_PATH_EXCLUDE = ['index.html', 'story-with-includes.html', 'template-story.html'];
// Helper function to get a friendly name from a filename
function getFriendlyName(filename) {
// Remove .html extension
let name = filename.replace('.html', '');
// Replace hyphens with spaces
name = name.replace(/-/g, ' ');
// Capitalize first letter of each word
return name.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// Simple XML parser for sitemap (no external dependencies)
function extractUrlsFromSitemap(sitemapContent) {
const urls = [];
const matches = sitemapContent.match(/<loc>([^<]+)<\/loc>/g) || [];
for (const match of matches) {
const url = match.replace('<loc>', '').replace('</loc>', '');
urls.push(url);
}
return urls;
}
// Main function
async function updateNavFromSitemap() {
console.log('Updating navigation menu from sitemap.xml...');
try {
// Read sitemap.xml
const sitemapContent = fs.readFileSync(SITEMAP_PATH, 'utf8');
const urls = extractUrlsFromSitemap(sitemapContent);
// Extract story URLs from sitemap
const storyPages = [];
for (const url of urls) {
// Check if this is a story page
if (url.includes(STORIES_PATH_PREFIX)) {
const filename = url.split('/').pop();
// Skip excluded files
if (!STORIES_PATH_EXCLUDE.includes(filename)) {
storyPages.push({
filename,
url: STORIES_PATH_PREFIX + filename,
name: getFriendlyName(filename)
});
}
}
}
// Sort story pages alphabetically by name
storyPages.sort((a, b) => a.name.localeCompare(b.name));
// Read header.html
let headerContent = fs.readFileSync(HEADER_PATH, 'utf8');
// Find the stories dropdown content
const dropdownStartMarker = '<a href="/stories/" id="nav-stories">Stories</a>';
const dropdownEndMarker = '</div>';
const dropdownStartIndex = headerContent.indexOf(dropdownStartMarker);
if (dropdownStartIndex === -1) {
throw new Error('Could not find stories dropdown in header.html');
}
// Find the start of the dropdown content
const contentStartIndex = headerContent.indexOf('<div class="dropdown-content">', dropdownStartIndex);
if (contentStartIndex === -1) {
throw new Error('Could not find dropdown content in header.html');
}
// Find the end of the dropdown content
const contentEndIndex = headerContent.indexOf(dropdownEndMarker, contentStartIndex);
if (contentEndIndex === -1) {
throw new Error('Could not find end of dropdown content in header.html');
}
// Generate new dropdown content
let newDropdownContent = '<div class="dropdown-content">\n';
storyPages.forEach(page => {
const navId = 'nav-' + page.filename.replace('.html', '').replace(/-/g, '');
newDropdownContent += ` <a href="${page.url}" id="${navId}">${page.name}</a>\n`;
});
// Replace dropdown content in header.html
const newHeaderContent =
headerContent.substring(0, contentStartIndex) +
newDropdownContent +
' ' + dropdownEndMarker +
headerContent.substring(contentEndIndex + dropdownEndMarker.length);
// Write updated header.html
fs.writeFileSync(HEADER_PATH, newHeaderContent, 'utf8');
console.log(`Updated navigation menu with ${storyPages.length} story pages`);
return true;
} catch (error) {
console.error('Error updating navigation menu:', error);
return false;
}
}
// Execute if run directly
if (require.main === module) {
updateNavFromSitemap();
}
module.exports = { updateNavFromSitemap };

View File

@ -0,0 +1,185 @@
#!/usr/bin/env node
/**
* update-stories-page.js
*
* This script reads the sitemap.xml file and updates the stories/index.html page
* to include all story pages found in the sitemap.
*
* No external dependencies required.
*/
const fs = require('fs');
const path = require('path');
// Configuration
const SITEMAP_PATH = path.join(__dirname, 'sitemap.xml');
const STORIES_INDEX_PATH = path.join(__dirname, 'stories', 'index.html');
const STORIES_PATH_PREFIX = '/stories/';
const STORIES_PATH_EXCLUDE = ['index.html', 'story-with-includes.html', 'template-story.html'];
// Helper function to get a friendly name from a filename
function getFriendlyName(filename) {
// Remove .html extension
let name = filename.replace('.html', '');
// Split by hyphens and capitalize each word
name = name.split('-').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
return name;
}
// Simple XML parser for sitemap (no external dependencies)
function extractUrlsFromSitemap(sitemapContent) {
const urls = [];
const matches = sitemapContent.match(/<loc>([^<]+)<\/loc>/g) || [];
for (const match of matches) {
const url = match.replace('<loc>', '').replace('</loc>', '');
urls.push(url);
}
return urls;
}
// Helper function to extract metadata from a story file
async function extractStoryMetadata(storyPath) {
try {
const content = fs.readFileSync(storyPath, 'utf8');
// Default metadata
let metadata = {
title: getFriendlyName(path.basename(storyPath)),
excerpt: 'Detailed case study and project information.',
category: 'Project',
date: 'Recent'
};
// Try to extract title from h1
const h1Match = content.match(/<h1[^>]*>(.*?)<\/h1>/i);
if (h1Match && h1Match[1]) {
metadata.title = h1Match[1].trim();
}
// Try to extract excerpt from first paragraph after h1
const excerptMatch = content.match(/<h1[^>]*>.*?<\/h1>\s*<p[^>]*>(.*?)<\/p>/is);
if (excerptMatch && excerptMatch[1]) {
metadata.excerpt = excerptMatch[1].trim().substring(0, 150) + '...';
}
// Try to extract category and date
const categoryMatch = content.match(/<strong>Category:<\/strong>\s*(.*?)(?:<|$)/i);
if (categoryMatch && categoryMatch[1]) {
metadata.category = categoryMatch[1].trim();
}
const dateMatch = content.match(/<strong>(?:Date|Timeframe):<\/strong>\s*(.*?)(?:<|$)/i);
if (dateMatch && dateMatch[1]) {
metadata.date = dateMatch[1].trim();
}
return metadata;
} catch (error) {
console.error(`Error extracting metadata from ${storyPath}:`, error);
return {
title: getFriendlyName(path.basename(storyPath)),
excerpt: 'Detailed case study and project information.',
category: 'Project',
date: 'Recent'
};
}
}
// Main function
async function updateStoriesPage() {
try {
console.log('Updating stories page from sitemap.xml...');
// Check if sitemap.xml exists
if (!fs.existsSync(SITEMAP_PATH)) {
console.error('Sitemap.xml not found at:', SITEMAP_PATH);
return;
}
// Check if stories/index.html exists
if (!fs.existsSync(STORIES_INDEX_PATH)) {
console.error('Stories index page not found at:', STORIES_INDEX_PATH);
return;
}
// Read sitemap.xml
const sitemapContent = fs.readFileSync(SITEMAP_PATH, 'utf8');
const allUrls = extractUrlsFromSitemap(sitemapContent);
// Find all story URLs in sitemap
const storyUrls = [];
for (const url of allUrls) {
if (url.includes(STORIES_PATH_PREFIX)) {
const filename = url.split('/').pop();
if (!STORIES_PATH_EXCLUDE.includes(filename)) {
storyUrls.push({
url: url,
filename: filename
});
}
}
}
console.log(`Found ${storyUrls.length} story pages in sitemap.`);
// Read stories/index.html
const storiesIndexContent = fs.readFileSync(STORIES_INDEX_PATH, 'utf8');
// Extract metadata for each story
const storyCards = [];
for (const storyUrl of storyUrls) {
const storyPath = path.join(__dirname, 'stories', storyUrl.filename);
if (fs.existsSync(storyPath)) {
const metadata = await extractStoryMetadata(storyPath);
storyCards.push({
filename: storyUrl.filename,
title: metadata.title,
excerpt: metadata.excerpt,
category: metadata.category,
date: metadata.date
});
}
}
// Generate HTML for story cards
let storiesGridHtml = '<div class="stories-grid">\n';
for (const card of storyCards) {
storiesGridHtml += `
<div class="story-card">
<h2>${card.title}</h2>
<p class="story-excerpt">${card.excerpt}</p>
<p class="story-meta">Category: ${card.category} | Date: ${card.date}</p>
<a href="${card.filename}" class="story-link">Read Full Story</a>
</div>
`;
}
storiesGridHtml += ' </div>';
// Replace the stories grid in the index.html
const updatedContent = storiesIndexContent.replace(
/<div class="stories-grid">[\s\S]*?<\/div>\s*<hr>/,
`${storiesGridHtml}\n\n <hr>`
);
// Write the updated content back to the file
fs.writeFileSync(STORIES_INDEX_PATH, updatedContent, 'utf8');
console.log(`Updated stories page with ${storyCards.length} story cards.`);
} catch (error) {
console.error('Error updating stories page:', error);
}
}
// Run the main function
updateStoriesPage();

60
docker/resume/utils.js Normal file
View File

@ -0,0 +1,60 @@
// Utility functions for the resume website
/**
* Debounce a function to limit its execution rate
* @param {Function} func - The function to debounce
* @param {number} wait - The number of milliseconds to delay
* @returns {Function} - The debounced function
*/
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Check if an element is in the viewport
* @param {Element} element - The element to check
* @returns {boolean} - Whether the element is in the viewport
*/
export function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}
/**
* Format a date in a consistent way
* @param {Date|string} date - The date to format
* @returns {string} - The formatted date string
*/
export function formatDate(date) {
if (!date) return '';
const d = new Date(date);
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long'
});
}
/**
* Safely get a value from an object using a path
* @param {Object} obj - The object to traverse
* @param {string} path - The path to the value
* @param {*} defaultValue - The default value if not found
* @returns {*} - The value at the path or the default value
*/
export function get(obj, path, defaultValue = undefined) {
return path.split('.')
.reduce((acc, part) => acc && acc[part], obj) ?? defaultValue;
}

2955
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

23
package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "resume-site",
"version": "1.0.0",
"description": "Resume website with accessibility testing",
"main": "index.js",
"scripts": {
"test": "tests/run-all-tests.sh",
"test:accessibility": "tests/accessibility/run-accessibility-tests.sh",
"test:a11y:axe": "node tests/accessibility/axe-test.js",
"test:a11y:pa11y": "tests/accessibility/pa11y-test.sh",
"test:a11y:manual": "echo 'Please complete the manual checklist at tests/accessibility/manual-checklist.md'",
"start": "cd docker/resume && ./caddy.sh"
},
"devDependencies": {
"axe-core": "^4.10.3",
"lighthouse": "^10.0.0",
"pa11y": "^9.0.0",
"playwright": "^1.53.2"
},
"dependencies": {
"marked": "^16.1.1"
}
}

68
playwright.config.js Normal file
View File

@ -0,0 +1,68 @@
// @ts-check
const { defineConfig, devices } = require('@playwright/test');
/**
* @see https://playwright.dev/docs/test-configuration
*/
module.exports = defineConfig({
testDir: './tests',
/* Maximum time one test can run for. */
timeout: 60 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
*/
timeout: 5000
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://localhost:8080',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
/* Run local dev server before starting the tests */
webServer: {
command: 'node tests/serve.js',
url: 'http://localhost:8080',
reuseExistingServer: !process.env.CI,
},
});

View File

@ -1,3 +1,4 @@
# build 0
networks:
traefik:
external: true
@ -21,7 +22,7 @@ services:
traefik.enable: "true"
traefik.http.routers.production_resume.tls: "true"
traefik.http.services.production_resume.loadbalancer.server.port: "8080"
traefik.http.routers.production_resume.rule: "Host(`resume.colinknapp.com`, `colinknapp.com`)"
traefik.http.routers.production_resume.rule: "Host(`resume.colinknapp.com`) || Host(`colinknapp.com`)"
traefik.http.routers.production_resume.entrypoints: "websecure"
traefik.http.routers.production_resume.tls.certresolver: "letsencryptresolver"
traefik.http.routers.production_resume.service: "production_resume"

View File

@ -1,4 +1,3 @@
version: "3.7"
networks:
traefik:
external: true
@ -12,8 +11,7 @@ services:
update_config:
order: start-first
failure_action: rollback
delay: 5s
delay: 10s
delay: 0s
parallelism: 1
restart_policy:
condition: on-failure
@ -22,7 +20,7 @@ services:
traefik.enable: "true"
traefik.http.routers.staging_resume.tls: "true"
traefik.http.services.staging_resume.loadbalancer.server.port: "8080"
traefik.http.routers.staging_resume.rule: "Host(`staging.resume.colinknapp.com`, `staging.colinknapp.com`)"
traefik.http.routers.staging_resume.rule: "Host(`staging.resume.colinknapp.com`) || Host(`staging.colinknapp.com`)"
traefik.http.routers.staging_resume.entrypoints: "websecure"
traefik.http.routers.staging_resume.tls.certresolver: "letsencryptresolver"
traefik.http.routers.staging_resume.service: "staging_resume"

97
tests/README.md Normal file
View File

@ -0,0 +1,97 @@
# Resume Site Tests
This directory contains tests for the resume site.
## Directory Structure
- `unit/`: Unit tests for JavaScript files and other components
- `integration/`: Integration tests that test components working together
- `e2e/`: End-to-end tests that simulate user interactions
- `*.test.js`, `*.spec.js`: Playwright test files
- `lighthouse.js`: Lighthouse performance and accessibility tests
- `serve.js`: Simple Node.js server for testing
- `server.js`: Alternative server implementation
- `pre-test-setup.sh`: Script to set up the test environment, including updating CSP hashes
## Running Tests
To run all tests:
```bash
./run-all-tests.sh
```
Or using npm:
```bash
npm test
```
This will:
1. Run the pre-test setup script to update CSP hashes
2. Check if the server is running
3. Run all shell script tests
4. Run Playwright tests if available
5. Run Lighthouse tests
## Content Security Policy (CSP) Testing
The CSP hash update process is an important part of the testing framework. It ensures that:
1. All JavaScript and CSS files have integrity hashes
2. All inline styles have proper CSP hashes
3. The Caddyfile and HTML files have the correct CSP headers/meta tags
The `pre-test-setup.sh` script runs the `update-csp-hashes.sh` script to update all CSP hashes before running the tests. This ensures that any changes to the website are properly reflected in the CSP hashes.
The `csp-hash-test.sh` integration test checks if the CSP hash update process is working properly by verifying that:
- CSP headers are present in the response
- CSP headers contain the required directives
- JavaScript and CSS files have integrity attributes
- HTML files have CSP meta tags
## Running Specific Tests
### JavaScript Tests
```bash
npm run test:js
```
### Lighthouse Tests
```bash
npm run test:lighthouse
```
### Starting the Test Server
```bash
npm run serve
```
## Adding New Tests
### Shell Script Tests
Add new test scripts to the appropriate directory:
- `unit/`: For unit tests
- `integration/`: For integration tests
- `e2e/`: For end-to-end tests
Shell script tests should:
- Be executable bash scripts
- Return exit code 0 for success, non-zero for failure
- For integration and e2e tests, accept a base URL as the first argument
### Playwright Tests
Add new Playwright tests with the `.test.js` or `.spec.js` extension in the tests directory.
## Test Requirements
As per the project guidelines, all tests must:
- Pass for both mobile and desktop viewports
- Maintain Lighthouse scores: 100/100 for accessibility and SEO
- Include meaningful assertions, not placeholders

View File

@ -0,0 +1,22 @@
/**
* Main accessibility test runner
*
* This file is used by Jest to run the accessibility tests
*/
const { execSync } = require('child_process');
const path = require('path');
describe('Accessibility Tests', () => {
test('Run accessibility tests', () => {
try {
// Run the accessibility tests
const scriptPath = path.join(__dirname, 'accessibility', 'run-accessibility-tests.sh');
execSync(`bash ${scriptPath}`, { stdio: 'inherit' });
} catch (error) {
// If the tests fail, the script will exit with a non-zero code
// Jest will catch this and mark the test as failed
throw new Error('Accessibility tests failed');
}
});
});

View File

@ -0,0 +1,64 @@
# Accessibility Testing Framework
This directory contains tools and scripts for testing the website's accessibility compliance with WCAG 2.1 AAA standards.
## Overview
The accessibility testing framework uses multiple approaches:
1. **Automated Testing with Pa11y**: Command-line accessibility testing using Pa11y
2. **Automated Testing with axe-core**: JavaScript-based accessibility testing using axe-core
3. **Manual Testing Checklist**: A comprehensive checklist for aspects that can't be automated
## Running Tests
To run all accessibility tests:
```bash
npm run test:accessibility
```
Or run directly:
```bash
./tests/accessibility/run-accessibility-tests.sh [BASE_URL]
```
The default BASE_URL is `http://localhost:8080` if not specified.
## Test Components
### Pa11y Tests
Pa11y tests are run using the `pa11y-test.sh` script, which:
- Tests multiple pages against WCAG 2.1 AAA standards
- Generates JSON reports for each page
- Creates a summary report
### axe-core Tests
axe-core tests are run using the `playwright-axe.js` script, which:
- Uses Playwright to load pages
- Injects axe-core into each page
- Runs accessibility tests against WCAG 2.1 AAA standards
- Generates detailed reports
### Manual Testing
Some aspects of accessibility can't be automatically tested. The `manual-checklist.md` file provides a comprehensive checklist for manual testing, focusing on AAA-level criteria.
## Reports
Test reports are saved in the `tests/reports/` directory:
- Individual page reports (e.g., `pa11y-index.json`, `axe-index.json`)
- Summary reports (`pa11y-summary.json`, `axe-summary.json`)
- Combined accessibility report (`accessibility-summary.json`)
## Requirements
- Node.js
- NPM packages: pa11y, axe-core, playwright
- For Pa11y: `npm install -g pa11y` or use npx

View File

@ -0,0 +1,167 @@
/**
* WCAG 2.1 AAA compliance test using axe-core
*
* This test runs axe-core against all pages of the website to check for WCAG 2.1 AAA compliance.
* It tests for issues related to:
* - Color contrast (7:1 ratio for AAA)
* - Text spacing
* - Heading structure
* - ARIA attributes
* - And many other WCAG 2.1 AAA criteria
*/
const { chromium } = require('playwright');
const axeCore = require('axe-core');
const fs = require('fs');
const path = require('path');
// URLs to test
const BASE_URL = process.argv[2] || 'http://localhost:8080'\;
const PAGES = [
'/',
'/stories/',
'/stories/open-source-success.html',
'/stories/viperwire.html',
'/one-pager-tools/csv-tool.html'
];
// Create reports directory if it doesn't exist
const reportsDir = path.join(__dirname, '../reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
async function runAxe(page, pagePath) {
// Inject axe-core into the page
await page.evaluate(() => {
if (!window.axe) {
// This would normally be done by injecting the axe-core script
// but for this example, we'll assume axe-core is already available
console.log('Warning: axe-core not available in the page');
}
});
// Run axe with WCAG 2.1 AAA rules
const results = await page.evaluate(() => {
return new Promise(resolve => {
if (!window.axe) {
resolve({ error: 'axe-core not available' });
return;
}
axe.run(document, {
runOnly: {
type: 'tag',
values: ['wcag2aaa']
},
resultTypes: ['violations', 'incomplete', 'inapplicable'],
rules: {
'color-contrast': { enabled: true, options: { noScroll: true } }
}
})
.then(results => resolve(results))
.catch(err => resolve({ error: err.toString() }));
});
});
return results;
}
async function testPage(browser, pageUrl) {
const page = await browser.newPage();
console.log(`Testing ${pageUrl}...`);
try {
// Navigate to the page
await page.goto(pageUrl, { waitUntil: 'networkidle' });
// Run axe-core tests
const results = await runAxe(page, pageUrl);
// Save results to file
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
// Log results summary
if (results.error) {
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
return { url: pageUrl, success: false, error: results.error };
}
const { violations, incomplete, passes, inapplicable } = results;
console.log(`Results for ${pageUrl}:`);
console.log(`- Violations: ${violations.length}`);
console.log(`- Incomplete: ${incomplete.length}`);
console.log(`- Passes: ${passes.length}`);
console.log(`- Inapplicable: ${inapplicable.length}`);
// Print violations
if (violations.length > 0) {
console.log('\nViolations:');
violations.forEach((violation, i) => {
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
console.log(` ${violation.description}`);
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
console.log(` Elements: ${violation.nodes.length}`);
});
}
return {
url: pageUrl,
success: violations.length === 0,
violations: violations.length,
incomplete: incomplete.length,
passes: passes.length
};
} catch (error) {
console.error(`Error testing ${pageUrl}:`, error);
return { url: pageUrl, success: false, error: error.toString() };
} finally {
await page.close();
}
}
async function runTests() {
const browser = await chromium.launch();
const results = [];
try {
// Test each page
for (const pagePath of PAGES) {
const pageUrl = `${BASE_URL}${pagePath}`;
const result = await testPage(browser, pageUrl);
results.push(result);
}
// Save overall results
const overallReport = {
timestamp: new Date().toISOString(),
baseUrl: BASE_URL,
pages: results,
summary: {
total: results.length,
passed: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
}
};
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
// Print overall summary
console.log('\n=== Overall Summary ===');
console.log(`Total pages tested: ${overallReport.summary.total}`);
console.log(`Pages passed: ${overallReport.summary.passed}`);
console.log(`Pages failed: ${overallReport.summary.failed}`);
// Exit with appropriate code
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
} catch (error) {
console.error('Error running tests:', error);
process.exit(1);
} finally {
await browser.close();
}
}
runTests();

View File

@ -0,0 +1,537 @@
/**
* Generate a detailed accessibility report for WCAG 2.1 AAA compliance
*
* This script analyzes all pages and generates a comprehensive report
* of accessibility issues that need to be fixed for AAA compliance
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const axeCore = require('axe-core');
// URLs to test - expand this list to cover all pages
const BASE_URL = process.argv[2] || 'http://localhost:8080'\;
const PAGES = [
'/',
'/stories/',
'/stories/open-source-success.html',
'/stories/viperwire.html',
'/stories/airport-dns.html',
'/stories/wordpress-security.html',
'/stories/nitric-leadership.html',
'/stories/healthcare-platform.html',
'/stories/web-design-java.html',
'/stories/motherboard-repair.html',
'/stories/fawe-plotsquared.html',
'/stories/showerloop.html',
'/stories/athion-turnaround.html',
'/stories/youtube-game-dev.html',
'/stories/app-development.html',
'/one-pager-tools/csv-tool.html'
];
// Create reports directory if it doesn't exist
const reportsDir = path.join(__dirname, '../reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
// Issues storage
const allIssues = {
byPage: {},
byRule: {},
summary: {
totalPages: 0,
pagesWithIssues: 0,
totalIssues: 0,
issuesByImpact: {
critical: 0,
serious: 0,
moderate: 0,
minor: 0
}
}
};
async function runAxe(page, pageUrl) {
try {
// Use a file URL to load axe-core from a local file to avoid CSP issues
const axeScriptPath = path.join(__dirname, 'axe-core.js');
fs.writeFileSync(axeScriptPath, axeCore.source);
// Add the script as a file
await page.addScriptTag({ path: axeScriptPath });
// Run axe with WCAG 2.1 AAA rules
const results = await page.evaluate(() => {
return new Promise(resolve => {
axe.run(document, {
runOnly: {
type: 'tag',
values: ['wcag2aaa']
},
resultTypes: ['violations', 'incomplete'],
rules: {
'color-contrast': { enabled: true, options: { noScroll: true } }
}
})
.then(results => resolve(results))
.catch(err => resolve({ error: err.toString() }));
});
});
// Clean up temporary file
fs.unlinkSync(axeScriptPath);
return results;
} catch (error) {
console.error('Error running axe:', error);
return { error: error.toString() };
}
}
async function testPage(browser, pageUrl) {
const context = await browser.newContext({
bypassCSP: true
});
const page = await context.newPage();
console.log(`Testing ${pageUrl}...`);
try {
// Navigate to the page
await page.goto(pageUrl, { waitUntil: 'networkidle' });
// Run axe-core tests
const results = await runAxe(page, pageUrl);
// Save results to file
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
// Log results summary
if (results.error) {
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
return { url: pageUrl, success: false, error: results.error };
}
const { violations, incomplete } = results;
console.log(`Results for ${pageUrl}:`);
console.log(`- Violations: ${violations.length}`);
console.log(`- Incomplete: ${incomplete.length}`);
// Track issues for detailed report
const pageIssues = [];
// Process violations
if (violations.length > 0) {
console.log('\nViolations:');
violations.forEach((violation, i) => {
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
console.log(` ${violation.description}`);
console.log(` WCAG: ${violation.tags.filter(t => t.startsWith('wcag')).join(', ')}`);
console.log(` Elements: ${violation.nodes.length}`);
// Add to issues tracking
violation.nodes.forEach(node => {
const issue = {
rule: violation.id,
impact: violation.impact,
description: violation.description,
help: violation.help,
helpUrl: violation.helpUrl,
wcagTags: violation.tags.filter(t => t.startsWith('wcag')),
element: node.html,
target: node.target,
failureSummary: node.failureSummary,
elementData: node.any.map(d => d.data || {})
};
pageIssues.push(issue);
// Update rule-based tracking
if (!allIssues.byRule[violation.id]) {
allIssues.byRule[violation.id] = {
id: violation.id,
description: violation.description,
help: violation.help,
helpUrl: violation.helpUrl,
impact: violation.impact,
wcagTags: violation.tags.filter(t => t.startsWith('wcag')),
occurrences: []
};
}
allIssues.byRule[violation.id].occurrences.push({
page: pageUrl,
element: node.html,
target: node.target,
failureSummary: node.failureSummary,
elementData: node.any.map(d => d.data || {})
});
// Update summary stats
allIssues.summary.totalIssues++;
if (violation.impact) {
allIssues.summary.issuesByImpact[violation.impact]++;
}
});
});
}
// Store page issues
allIssues.byPage[pageUrl] = {
url: pageUrl,
issueCount: pageIssues.length,
issues: pageIssues
};
// Update summary
allIssues.summary.totalPages++;
if (pageIssues.length > 0) {
allIssues.summary.pagesWithIssues++;
}
return {
url: pageUrl,
success: violations.length === 0,
violations: violations.length,
incomplete: incomplete.length
};
} catch (error) {
console.error(`Error testing ${pageUrl}:`, error);
return { url: pageUrl, success: false, error: error.toString() };
} finally {
await page.close();
await context.close();
}
}
function generateFixRecommendations() {
const recommendations = {};
// Process color contrast issues
if (allIssues.byRule['color-contrast-enhanced']) {
const colorIssues = allIssues.byRule['color-contrast-enhanced'];
recommendations.colorContrast = {
title: 'Color Contrast Issues',
description: 'The following color combinations need to be updated to meet WCAG 2.1 AAA requirements (7:1 contrast ratio):',
colorPairs: []
};
// Group by color pairs
const colorPairMap = {};
colorIssues.occurrences.forEach(occurrence => {
occurrence.elementData.forEach(data => {
if (data.fgColor && data.bgColor) {
const key = `${data.fgColor}/${data.bgColor}`;
if (!colorPairMap[key]) {
colorPairMap[key] = {
fgColor: data.fgColor,
bgColor: data.bgColor,
contrastRatio: data.contrastRatio,
requiredRatio: data.expectedContrastRatio,
pages: new Set(),
elements: []
};
}
colorPairMap[key].pages.add(occurrence.page);
colorPairMap[key].elements.push({
html: occurrence.element,
page: occurrence.page
});
}
});
});
// Convert to array and add suggestions
Object.values(colorPairMap).forEach(pair => {
const suggestion = {
foreground: pair.fgColor,
background: pair.bgColor,
currentRatio: pair.contrastRatio,
requiredRatio: pair.requiredRatio,
pagesAffected: Array.from(pair.pages),
elementCount: pair.elements.length,
suggestedFixes: []
};
// Generate suggested fixes
// Make foreground darker
const darkerFg = darkenColor(pair.fgColor, 0.2);
suggestion.suggestedFixes.push({
type: 'Darken foreground',
color: darkerFg,
description: `Change foreground color from ${pair.fgColor} to ${darkerFg}`
});
// Make background lighter
const lighterBg = lightenColor(pair.bgColor, 0.2);
suggestion.suggestedFixes.push({
type: 'Lighten background',
color: lighterBg,
description: `Change background color from ${pair.bgColor} to ${lighterBg}`
});
recommendations.colorContrast.colorPairs.push(suggestion);
});
}
return recommendations;
}
// Helper function to darken a hex color
function darkenColor(hex, amount) {
return adjustColor(hex, -amount);
}
// Helper function to lighten a hex color
function lightenColor(hex, amount) {
return adjustColor(hex, amount);
}
// Helper function to adjust a hex color
function adjustColor(hex, amount) {
let r = parseInt(hex.substring(1, 3), 16);
let g = parseInt(hex.substring(3, 5), 16);
let b = parseInt(hex.substring(5, 7), 16);
if (amount > 0) {
// Lighten
r = Math.min(255, Math.round(r + (255 - r) * amount));
g = Math.min(255, Math.round(g + (255 - g) * amount));
b = Math.min(255, Math.round(b + (255 - b) * amount));
} else {
// Darken
amount = -amount;
r = Math.max(0, Math.round(r * (1 - amount)));
g = Math.max(0, Math.round(g * (1 - amount)));
b = Math.max(0, Math.round(b * (1 - amount)));
}
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
async function runTests() {
const browser = await chromium.launch();
const results = [];
try {
// Test each page
for (const pagePath of PAGES) {
const pageUrl = `${BASE_URL}${pagePath}`;
const result = await testPage(browser, pageUrl);
results.push(result);
}
// Generate detailed report
const recommendations = generateFixRecommendations();
const detailedReport = {
timestamp: new Date().toISOString(),
baseUrl: BASE_URL,
summary: allIssues.summary,
recommendations: recommendations,
issuesByRule: allIssues.byRule
};
// Save detailed report
fs.writeFileSync(
path.join(reportsDir, 'accessibility-detailed-report.json'),
JSON.stringify(detailedReport, null, 2)
);
// Generate HTML report
const htmlReport = generateHtmlReport(detailedReport);
fs.writeFileSync(
path.join(reportsDir, 'accessibility-report.html'),
htmlReport
);
console.log('\n=== Detailed Report Generated ===');
console.log(`Total pages tested: ${detailedReport.summary.totalPages}`);
console.log(`Pages with issues: ${detailedReport.summary.pagesWithIssues}`);
console.log(`Total issues found: ${detailedReport.summary.totalIssues}`);
console.log(`Issues by impact:`);
Object.entries(detailedReport.summary.issuesByImpact).forEach(([impact, count]) => {
if (count > 0) {
console.log(` - ${impact}: ${count}`);
}
});
console.log(`\nDetailed report saved to: ${path.join(reportsDir, 'accessibility-detailed-report.json')}`);
console.log(`HTML report saved to: ${path.join(reportsDir, 'accessibility-report.html')}`);
// Exit with appropriate code
process.exit(detailedReport.summary.totalIssues > 0 ? 1 : 0);
} catch (error) {
console.error('Error running tests:', error);
process.exit(1);
} finally {
await browser.close();
}
}
function generateHtmlReport(report) {
const colorPairs = report.recommendations.colorContrast ? report.recommendations.colorContrast.colorPairs : [];
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessibility Report - WCAG 2.1 AAA Compliance</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1, h2, h3, h4 {
color: #2c3e50;
}
.summary {
background-color: #f8f9fa;
border-radius: 5px;
padding: 20px;
margin-bottom: 30px;
}
.issue-count {
font-size: 24px;
font-weight: bold;
}
.color-pair {
margin-bottom: 30px;
border: 1px solid #ddd;
padding: 15px;
border-radius: 5px;
}
.color-sample {
display: inline-block;
width: 100px;
height: 50px;
margin-right: 10px;
border: 1px solid #ddd;
}
.color-info {
display: inline-block;
vertical-align: top;
}
.suggestion {
margin-top: 10px;
padding: 10px;
background-color: #f0f7ff;
border-radius: 5px;
}
.pages-list {
margin-top: 10px;
font-size: 14px;
}
.impact-critical { color: #d9534f; }
.impact-serious { color: #f0ad4e; }
.impact-moderate { color: #5bc0de; }
.impact-minor { color: #5cb85c; }
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h1>Accessibility Report - WCAG 2.1 AAA Compliance</h1>
<p>Generated on: ${new Date(report.timestamp).toLocaleString()}</p>
<div class="summary">
<h2>Summary</h2>
<p><span class="issue-count">${report.summary.totalIssues}</span> issues found across ${report.summary.pagesWithIssues} pages (${report.summary.totalPages} pages tested)</p>
<p>Issues by impact:</p>
<ul>
${report.summary.issuesByImpact.critical > 0 ? `<li><span class="impact-critical">Critical: ${report.summary.issuesByImpact.critical}</span></li>` : ''}
${report.summary.issuesByImpact.serious > 0 ? `<li><span class="impact-serious">Serious: ${report.summary.issuesByImpact.serious}</span></li>` : ''}
${report.summary.issuesByImpact.moderate > 0 ? `<li><span class="impact-moderate">Moderate: ${report.summary.issuesByImpact.moderate}</span></li>` : ''}
${report.summary.issuesByImpact.minor > 0 ? `<li><span class="impact-minor">Minor: ${report.summary.issuesByImpact.minor}</span></li>` : ''}
</ul>
</div>
<h2>Color Contrast Issues</h2>
${colorPairs.length === 0 ? '<p>No color contrast issues found.</p>' : ''}
${colorPairs.map(pair => `
<div class="color-pair">
<h3>Color Combination</h3>
<div>
<div class="color-sample" style="background-color: ${pair.foreground}"></div>
<div class="color-info">
<p>Foreground: ${pair.foreground}</p>
</div>
</div>
<div>
<div class="color-sample" style="background-color: ${pair.background}"></div>
<div class="color-info">
<p>Background: ${pair.background}</p>
</div>
</div>
<p>Current contrast ratio: ${pair.currentRatio} (Required: ${pair.requiredRatio})</p>
<p>Found on ${pair.elementCount} element${pair.elementCount !== 1 ? 's' : ''} across ${pair.pagesAffected.length} page${pair.pagesAffected.length !== 1 ? 's' : ''}</p>
<div class="pages-list">
<p>Pages affected:</p>
<ul>
${pair.pagesAffected.map(page => `<li>${page}</li>`).join('')}
</ul>
</div>
<h4>Suggested Fixes</h4>
${pair.suggestedFixes.map(fix => `
<div class="suggestion">
<h5>${fix.type}</h5>
<div class="color-sample" style="background-color: ${fix.color}"></div>
<div class="color-info">
<p>${fix.description}</p>
</div>
</div>
`).join('')}
</div>
`).join('')}
<h2>Issues by Rule</h2>
<table>
<thead>
<tr>
<th>Rule</th>
<th>Impact</th>
<th>Description</th>
<th>Occurrences</th>
<th>WCAG Criteria</th>
</tr>
</thead>
<tbody>
${Object.values(report.issuesByRule).map(rule => `
<tr>
<td><a href="${rule.helpUrl}" target="_blank">${rule.id}</a></td>
<td class="impact-${rule.impact}">${rule.impact}</td>
<td>${rule.description}</td>
<td>${rule.occurrences.length}</td>
<td>${rule.wcagTags.join(', ')}</td>
</tr>
`).join('')}
</tbody>
</table>
</body>
</html>`;
}
runTests();

View File

@ -0,0 +1,212 @@
/**
* Generate a comprehensive accessibility report
* This script analyzes the axe-core and Pa11y test results and generates a report
* of all accessibility issues that need to be fixed
*/
const fs = require('fs');
const path = require('path');
// Directory containing test reports
const reportsDir = path.join(__dirname, '../reports');
// Output report file
const outputFile = path.join(reportsDir, 'accessibility-issues.md');
// Get all axe-core report files
const axeReports = fs.readdirSync(reportsDir)
.filter(file => file.startsWith('axe-') && file.endsWith('.json'))
.map(file => {
try {
const content = fs.readFileSync(path.join(reportsDir, file), 'utf8');
return { file, content: JSON.parse(content) };
} catch (error) {
console.error(`Error reading ${file}:`, error.message);
return { file, content: null };
}
})
.filter(report => report.content !== null);
// Get all Pa11y report files
const pa11yReports = fs.readdirSync(reportsDir)
.filter(file => file.startsWith('pa11y') && file.endsWith('.json') && !file.includes('summary'))
.map(file => {
try {
const content = fs.readFileSync(path.join(reportsDir, file), 'utf8');
return { file, content: JSON.parse(content) };
} catch (error) {
console.error(`Error reading ${file}:`, error.message);
return { file, content: null };
}
})
.filter(report => report.content !== null);
// Generate report
let report = `# Accessibility Issues Report
Generated on: ${new Date().toISOString()}
This report lists all accessibility issues found by automated testing tools.
Fixing these issues will help achieve WCAG 2.1 AAA compliance.
## Summary
`;
// Process axe-core reports
const axeIssues = [];
axeReports.forEach(report => {
if (report.content && report.content.violations) {
report.content.violations.forEach(violation => {
violation.nodes.forEach(node => {
axeIssues.push({
page: report.file.replace('axe-', '').replace('.json', ''),
id: violation.id,
impact: violation.impact,
description: violation.description,
help: violation.help,
helpUrl: violation.helpUrl,
element: node.html || node.target?.join(', ') || 'Unknown element',
wcag: violation.tags.filter(t => t.startsWith('wcag')).join(', ')
});
});
});
}
});
// Process Pa11y reports
const pa11yIssues = [];
pa11yReports.forEach(report => {
if (report.content && report.content.issues) {
report.content.issues.forEach(issue => {
pa11yIssues.push({
page: report.file.replace('pa11y', '').replace('.json', ''),
code: issue.code,
type: issue.type,
message: issue.message,
context: issue.context,
selector: issue.selector
});
});
}
});
// Add summary counts to report
report += `- Total pages tested: ${axeReports.length}
- Pages with axe-core issues: ${axeReports.filter(r => r.content?.violations?.length > 0).length}
- Total axe-core issues: ${axeIssues.length}
- Pages with Pa11y issues: ${pa11yReports.filter(r => r.content?.issues?.length > 0).length}
- Total Pa11y issues: ${pa11yIssues.length}
## Common Issues
`;
// Group issues by type
const issuesByType = {};
axeIssues.forEach(issue => {
if (!issuesByType[issue.id]) {
issuesByType[issue.id] = {
count: 0,
description: issue.description,
help: issue.help,
helpUrl: issue.helpUrl,
wcag: issue.wcag,
pages: new Set()
};
}
issuesByType[issue.id].count++;
issuesByType[issue.id].pages.add(issue.page);
});
// Sort issues by count
const sortedIssues = Object.entries(issuesByType)
.sort((a, b) => b[1].count - a[1].count);
// Add common issues to report
sortedIssues.forEach(([id, info]) => {
report += `### ${id} (${info.count} occurrences)
- **Description**: ${info.description}
- **Help**: ${info.help}
- **WCAG**: ${info.wcag}
- **Pages affected**: ${Array.from(info.pages).join(', ')}
- **More info**: ${info.helpUrl}
`;
});
// Add detailed issues by page
report += `## Issues by Page
`;
// Group issues by page
const issuesByPage = {};
axeIssues.forEach(issue => {
if (!issuesByPage[issue.page]) {
issuesByPage[issue.page] = [];
}
issuesByPage[issue.page].push(issue);
});
// Add page-specific issues to report
Object.entries(issuesByPage).forEach(([page, issues]) => {
report += `### ${page}
`;
issues.forEach(issue => {
report += `- **${issue.id}** (${issue.impact}): ${issue.help}
- Element: \`${issue.element.substring(0, 100)}${issue.element.length > 100 ? '...' : ''}\`
- WCAG: ${issue.wcag}
`;
});
});
// Add recommended fixes
report += `## Recommended Fixes
Based on the issues found, here are the recommended fixes:
`;
// Add specific recommendations for common issues
if (issuesByType['color-contrast-enhanced']) {
report += `### Color Contrast Issues
1. Update link colors to meet 7:1 contrast ratio:
- Current blue link color (#0056b3) on light background (#f5f5f5) has 6.45:1 ratio
- Recommended: Change to darker blue (#004494) for 7:1+ ratio
2. Update CSS in the following files:
- \`docker/resume/styles.css\`: Update link colors
- \`docker/resume/stories/stories.css\`: Update story-specific link colors
`;
}
// Add implementation plan
report += `## Implementation Plan
1. Fix color contrast issues first (highest impact)
2. Address any document structure issues
3. Fix remaining issues by priority (serious > moderate > minor)
4. Re-run tests to verify fixes
## Testing Instructions
To test accessibility compliance:
\`\`\`bash
# Run all accessibility tests
npm run test:accessibility
# Generate updated report
node tests/accessibility/generate-report.js
\`\`\`
`;
// Write report to file
fs.writeFileSync(outputFile, report);
console.log(`Accessibility issues report generated at: ${outputFile}`);

View File

@ -0,0 +1,119 @@
# Manual Accessibility Testing Checklist
This checklist covers aspects of WCAG 2.1 AAA compliance that cannot be fully automated and require manual verification.
## Instructions
1. For each page in the site, go through this checklist
2. Mark each item as:
- ✅ Pass
- ❌ Fail
- N/A Not applicable
3. For any failures, note the specific issue and location
## Pages to Test
- Home page (/)
- Stories index (/stories/)
- Individual story pages (e.g., /stories/open-source-success.html)
- Tools pages (e.g., /one-pager-tools/csv-tool.html)
## Checklist
### Perceivable
#### 1.2.6 Sign Language (AAA)
- [ ] Sign language interpretation is provided for all prerecorded audio content in synchronized media
#### 1.2.7 Extended Audio Description (AAA)
- [ ] Extended audio description is provided for all prerecorded video content
#### 1.2.8 Media Alternative (AAA)
- [ ] Alternative for time-based media is provided for all prerecorded synchronized media and for all prerecorded video-only media
#### 1.2.9 Audio-only (Live) (AAA)
- [ ] Alternative for time-based media that presents equivalent information for live audio-only content is provided
#### 1.3.6 Identify Purpose (AAA)
- [ ] The purpose of UI components, icons, and regions can be programmatically determined
#### 1.4.6 Contrast (Enhanced) (AAA)
- [ ] Visual presentation of text and images of text has a contrast ratio of at least 7:1
- [ ] Large text (over 18pt or 14pt bold) has a contrast ratio of at least 4.5:1
#### 1.4.9 Images of Text (No Exception) (AAA)
- [ ] Images of text are only used for pure decoration or where a particular presentation of text is essential to the information being conveyed
### Operable
#### 2.1.3 Keyboard (No Exception) (AAA)
- [ ] All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes
#### 2.2.3 No Timing (AAA)
- [ ] Timing is not an essential part of the event or activity presented by the content
#### 2.2.4 Interruptions (AAA)
- [ ] Interruptions can be postponed or suppressed by the user
#### 2.2.5 Re-authenticating (AAA)
- [ ] When an authenticated session expires, the user can continue the activity without loss of data after re-authenticating
#### 2.2.6 Timeouts (AAA)
- [ ] Users are warned of the duration of any user inactivity that could cause data loss
#### 2.3.2 Three Flashes (AAA)
- [ ] Web pages do not contain anything that flashes more than three times in any one second period
#### 2.3.3 Animation from Interactions (AAA)
- [ ] Motion animation triggered by interaction can be disabled
#### 2.4.8 Location (AAA)
- [ ] Information about the user's location within a set of web pages is available
#### 2.4.9 Link Purpose (Link Only) (AAA)
- [ ] A mechanism is available to allow the purpose of each link to be identified from link text alone
#### 2.4.10 Section Headings (AAA)
- [ ] Section headings are used to organize the content
#### 2.5.5 Target Size (AAA)
- [ ] The size of the target for pointer inputs is at least 44 by 44 CSS pixels
#### 2.5.6 Concurrent Input Mechanisms (AAA)
- [ ] Web content does not restrict use of input modalities available on a platform
### Understandable
#### 3.1.3 Unusual Words (AAA)
- [ ] A mechanism is available for identifying specific definitions of words or phrases used in an unusual or restricted way
#### 3.1.4 Abbreviations (AAA)
- [ ] A mechanism for identifying the expanded form or meaning of abbreviations is available
#### 3.1.5 Reading Level (AAA)
- [ ] When text requires reading ability more advanced than the lower secondary education level, supplemental content is available
#### 3.1.6 Pronunciation (AAA)
- [ ] A mechanism is available for identifying specific pronunciation of words where meaning is ambiguous without knowing the pronunciation
#### 3.2.5 Change on Request (AAA)
- [ ] Changes of context are initiated only by user request or a mechanism is available to turn off such changes
#### 3.3.5 Help (AAA)
- [ ] Context-sensitive help is available
#### 3.3.6 Error Prevention (All) (AAA)
- [ ] For web pages that require the user to submit information, at least one of the following is true:
- Submissions are reversible
- Data entered by the user is checked for input errors and the user is provided an opportunity to correct them
- A mechanism is available for reviewing, confirming, and correcting information before finalizing the submission
## Notes and Issues
<!-- Document any issues found during manual testing here -->
## Tester Information
- Name:
- Date:
- Browser/assistive technology used:

100
tests/accessibility/pa11y-test.sh Executable file
View File

@ -0,0 +1,100 @@
#!/bin/bash
# =====================================================================
# pa11y-test.sh - Run Pa11y accessibility tests
# =====================================================================
# This script runs Pa11y accessibility tests for WCAG 2.1 AAA compliance
# =====================================================================
set -e
# Check if base URL is provided
if [ -z "$1" ]; then
BASE_URL="http://localhost:8080"
else
BASE_URL="$1"
fi
TESTS_DIR="$(dirname "$0")"
REPORTS_DIR="$TESTS_DIR/../reports"
mkdir -p "$REPORTS_DIR"
echo "=== Testing WCAG 2.1 AAA Compliance with Pa11y ==="
echo "Using base URL: $BASE_URL"
# Get URLs from sitemap
SITEMAP_URL="$BASE_URL/sitemap.xml"
echo "Fetching pages from sitemap: $SITEMAP_URL"
URLS=($(curl -s "$SITEMAP_URL" | grep -o '<loc>.*</loc>' | sed 's/<loc>//;s/<\/loc>//'))
if [ ${#URLS[@]} -eq 0 ]; then
echo "No URLs found in sitemap, or sitemap could not be fetched."
exit 1
fi
echo "Found ${#URLS[@]} pages to test."
# Track failures
FAILURES=0
# Run tests for each URL
for url in "${URLS[@]}"; do
echo "Testing ${url}..."
# Create a report file name from the URL
report_file_name="pa11y-$(echo "$url" | sed -e 's|https://.*\.com/||' -e 's|/|-|g' -e 's/\.html//').json"
report_file="$REPORTS_DIR/$report_file_name"
# Run Pa11y and save results
if npx pa11y --standard WCAG2AAA --reporter json --ignore "content-security-policy" "${url}" > "${report_file}" 2>/dev/null; then
issues=$(jq '.issues | length' "${report_file}" 2>/dev/null || echo 0)
echo "Found ${issues} issues on ${url}"
# Check if there are issues
if [ "${issues}" -gt 0 ]; then
FAILURES=$((FAILURES + 1))
# Print summary of issues
echo "Issues summary:"
jq -r '.issues[] | "- \(.type): \(.message)\n Selector: \(.selector)"' "${report_file}" 2>/dev/null | head -n 10
# If there are more than 5 issues, indicate that
if [ "${issues}" -gt 5 ]; then
echo "... and $((issues - 5)) more issues."
fi
fi
else
echo "Error running Pa11y on ${url}"
# Check if a report file was created with error info
if [ -f "${report_file}" ]; then
error_message=$(jq -r '.error' "${report_file}" 2>/dev/null)
if [ -n "$error_message" ]; then
echo " Pa11y error: $error_message"
fi
fi
FAILURES=$((FAILURES + 1))
fi
echo "---"
done
# Create summary report
jq -n --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg baseUrl "${BASE_URL}" \
--arg failures "${FAILURES}" \
--arg total "${#URLS[@]}" \
'{
"timestamp": $timestamp,
"baseUrl": $baseUrl,
"summary": {
"total": ($total | tonumber),
"failures": ($failures | tonumber),
"passed": (($total | tonumber) - ($failures | tonumber))
}
}' > "${REPORTS_DIR}/pa11y-summary.json"
# Exit with appropriate status
if [ "${FAILURES}" -gt 0 ]; then
exit 1
else
exit 0
fi

View File

@ -0,0 +1,199 @@
/**
* WCAG 2.1 AAA compliance test using Playwright and axe-core
*
* This script uses Playwright to load pages and axe-core to test them for accessibility issues.
* It's an alternative to the axe-test.js script that properly injects axe-core into the page.
*/
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
const axeCore = require('axe-core');
// Base URL to test
const BASE_URL = process.argv[2] || 'http://localhost:8080';
// Create reports directory if it doesn't exist
const reportsDir = path.join(__dirname, '../reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
async function getPagesFromSitemap(sitemapUrl) {
try {
const response = await fetch(sitemapUrl);
if (!response.ok) {
throw new Error(`Failed to fetch sitemap: ${response.statusText}`);
}
const sitemapText = await response.text();
const urls = sitemapText.match(/<loc>(.*?)<\/loc>/g) || [];
return urls.map(url => {
const urlContent = url.replace(/<\/?loc>/g, '');
const urlObject = new URL(urlContent);
return urlObject.pathname;
});
} catch (error) {
console.error(`Error reading sitemap: ${error}`);
// Fallback to a default list if sitemap fails
return [
'/',
'/stories/',
'/stories/open-source-success.html',
'/stories/viperwire.html',
'/one-pager-tools/csv-tool.html'
];
}
}
async function runAxe(page, pageUrl) {
try {
// Use a file URL to load axe-core from a local file to avoid CSP issues
const axeScriptPath = path.join(__dirname, 'axe-core.js');
fs.writeFileSync(axeScriptPath, axeCore.source);
// Add the script as a file
await page.addScriptTag({ path: axeScriptPath, type: 'text/javascript' });
// Run axe with WCAG 2.1 AAA rules
const results = await page.evaluate(() => {
if (typeof axe === 'undefined') {
return { error: 'axe-core not loaded' };
}
return new Promise(resolve => {
axe.run(document, {
runOnly: {
type: 'tag',
values: ['wcag2aaa']
},
resultTypes: ['violations', 'incomplete', 'inapplicable'],
rules: {
'color-contrast': { enabled: true, options: { noScroll: true } }
}
})
.then(results => resolve(results))
.catch(err => resolve({ error: err.toString() }));
});
});
// Clean up temporary file
fs.unlinkSync(axeScriptPath);
return results;
} catch (error) {
console.error('Error running axe:', error);
return { error: error.toString() };
}
}
async function testPage(browser, pageUrl) {
const context = await browser.newContext({
bypassCSP: true
});
const page = await context.newPage();
console.log(`Testing ${pageUrl}...`);
try {
// Navigate to the page
await page.goto(pageUrl, { waitUntil: 'networkidle' });
// Run axe-core tests
const results = await runAxe(page, pageUrl);
// Save results to file
const fileName = pageUrl === BASE_URL ? 'index' : pageUrl.replace(BASE_URL, '').replace(/\//g, '-').replace(/^-/, '');
const reportPath = path.join(reportsDir, `axe-${fileName || 'index'}.json`);
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
// Log results summary
if (results.error) {
console.error(`Error running axe-core on ${pageUrl}:`, results.error);
return { url: pageUrl, success: false, error: results.error };
}
const { violations, incomplete, passes, inapplicable } = results;
console.log(`Results for ${pageUrl}:`);
console.log(`- Violations: ${violations.length}`);
console.log(`- Incomplete: ${incomplete.length}`);
console.log(`- Passes: ${passes.length}`);
console.log(`- Inapplicable: ${inapplicable.length}`);
// Print violations
if (violations.length > 0) {
console.log('\nViolations:');
violations.forEach((violation, i) => {
console.log(`${i + 1}. ${violation.id} - ${violation.help} (Impact: ${violation.impact})`);
console.log(` Description: ${violation.description}`);
console.log(` Help: ${violation.helpUrl}`);
violation.nodes.forEach(node => {
console.log(` - Element: ${node.html}`);
console.log(` Selector: ${node.target.join(', ')}`);
});
});
}
return {
url: pageUrl,
success: violations.length === 0,
violations: violations.length,
incomplete: incomplete.length,
passes: passes.length
};
} catch (error) {
console.error(`Error testing ${pageUrl}:`, error);
return { url: pageUrl, success: false, error: error.toString() };
} finally {
await page.close();
await context.close();
}
}
async function runTests() {
const browser = await chromium.launch();
const results = [];
const sitemapUrl = `${BASE_URL}/sitemap.xml`;
console.log(`Fetching pages from sitemap: ${sitemapUrl}`);
const pagesToTest = await getPagesFromSitemap(sitemapUrl);
console.log(`Found ${pagesToTest.length} pages to test.`);
try {
// Test each page
for (const pagePath of pagesToTest) {
const pageUrl = `${BASE_URL}${pagePath.startsWith('/') ? '' : '/'}${pagePath}`;
const result = await testPage(browser, pageUrl);
results.push(result);
}
// Save overall results
const overallReport = {
timestamp: new Date().toISOString(),
baseUrl: BASE_URL,
pages: results,
summary: {
total: results.length,
passed: results.filter(r => r.success).length,
failed: results.filter(r => !r.success).length
}
};
fs.writeFileSync(path.join(reportsDir, 'axe-summary.json'), JSON.stringify(overallReport, null, 2));
// Print overall summary
console.log('\n=== Overall Summary ===');
console.log(`Total pages tested: ${overallReport.summary.total}`);
console.log(`Pages passed: ${overallReport.summary.passed}`);
console.log(`Pages failed: ${overallReport.summary.failed}`);
// Exit with appropriate code
process.exit(overallReport.summary.failed > 0 ? 1 : 0);
} catch (error) {
console.error('Error running tests:', error);
process.exit(1);
} finally {
await browser.close();
}
}
runTests();

View File

@ -0,0 +1,103 @@
#!/bin/bash
# =====================================================================
# run-accessibility-tests.sh - Run all accessibility tests
# =====================================================================
# This script runs all accessibility tests for WCAG 2.1 AAA compliance
# =====================================================================
set -e
# Check if base URL is provided
if [ -z "$1" ]; then
BASE_URL="http://localhost:8080"
else
BASE_URL="$1"
fi
TESTS_DIR="$(dirname "$0")"
REPORTS_DIR="$TESTS_DIR/../reports"
mkdir -p "$REPORTS_DIR"
echo "=== Running Accessibility Tests ==="
echo "Using base URL: $BASE_URL"
# Track test results
FAILED_TESTS=0
# Run Pa11y tests
echo "Running Pa11y tests..."
if command -v npx &> /dev/null && npx pa11y --version &> /dev/null; then
if "$TESTS_DIR/pa11y-test.sh" "$BASE_URL"; then
echo "✅ Pa11y tests passed"
else
echo "❌ Pa11y tests failed"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
else
echo "❌ Pa11y not installed, skipping Pa11y tests"
echo " Install with: npm install -g pa11y"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
# Run axe-core tests with Playwright
echo "Running axe-core tests with Playwright..."
if command -v node &> /dev/null; then
if [ -f "$TESTS_DIR/playwright-axe.js" ]; then
if node "$TESTS_DIR/playwright-axe.js" "$BASE_URL"; then
echo "✅ axe-core tests passed"
else
echo "❌ axe-core tests failed"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
else
echo "❌ playwright-axe.js not found"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
else
echo "❌ Node.js not installed, skipping axe-core tests"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
# Remind about manual testing
echo "Don't forget to complete the manual testing checklist:"
echo "$TESTS_DIR/manual-checklist.md"
# Generate combined report
echo "Generating combined accessibility report..."
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
cat > "$REPORTS_DIR/accessibility-summary.json" << EOJSON
{
"timestamp": "$TIMESTAMP",
"baseUrl": "$BASE_URL",
"summary": {
"automated": {
"pa11y": {
"status": "$([ -f "$REPORTS_DIR/pa11y-summary.json" ] && echo "completed" || echo "failed")",
"report": "pa11y-summary.json"
},
"axe": {
"status": "$([ -f "$REPORTS_DIR/axe-summary.json" ] && echo "completed" || echo "failed")",
"report": "axe-summary.json"
}
},
"manual": {
"status": "pending",
"checklist": "../accessibility/manual-checklist.md"
}
}
}
EOJSON
# Print summary
echo "=== Accessibility Test Summary ==="
echo "Tests completed at: $TIMESTAMP"
echo "Reports saved to: $REPORTS_DIR"
if [ "$FAILED_TESTS" -gt 0 ]; then
echo "$FAILED_TESTS test suites failed"
exit 1
else
echo "✅ All automated test suites passed"
echo "⚠️ Manual testing still required - see checklist"
exit 0
fi

Some files were not shown because too many files have changed in this diff Show More