forked from colin/resume
Compare commits
148 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
87328de610 | |
|
|
32636fa0cb | |
|
|
cfaa727b88 | |
|
|
6d2c7dc5f1 | |
|
|
b60d3a89ca | |
|
|
51ac30cbc4 | |
|
|
f27f505281 | |
|
|
b862b5f234 | |
|
|
9d4e2a0d5f | |
|
|
e858606a0f | |
|
|
92d5ed0801 | |
|
|
fcd62e071a | |
|
|
b4b7dcb079 | |
|
|
374d29018a | |
|
|
cb409efb34 | |
|
|
70085f5258 | |
|
|
c092d07783 | |
|
|
3c03708951 | |
|
|
9165562b2a | |
|
|
5b422a67b2 | |
|
|
7a3665abae | |
|
|
73c4533926 | |
|
|
1ba37bbd75 | |
|
|
457ca26983 | |
|
|
049db7424f | |
|
|
85ddbf561d | |
|
|
8b1a1e23bf | |
|
|
19182d6d21 | |
|
|
83c0ae74f7 | |
|
|
73b83fc3fd | |
|
|
1c1bb4ebb9 | |
|
|
f29321803e | |
|
|
fda73c6638 | |
|
|
db17d83418 | |
|
|
14b03869af | |
|
|
f7f7bcf7dc | |
|
|
f4e36517fb | |
|
|
3c91e05ca3 | |
|
|
c5d81426ec | |
|
|
7b036a1a58 | |
|
|
384fb993f5 | |
|
|
fa41ef711f | |
|
|
bb48db67e7 | |
|
|
7968da9b60 | |
|
|
467b7dcd1a | |
|
|
f2d7ba2f5d | |
|
|
dd60d798b4 | |
|
|
20c813c7ea | |
|
|
baf66debf3 | |
|
|
526da797c5 | |
|
|
3c35c5eba0 | |
|
|
cbb5a04563 | |
|
|
af1a5efb60 | |
|
|
8f35aa626c | |
|
|
82bc0eb349 | |
|
|
7e06e335a9 | |
|
|
bd7aca54ee | |
|
|
8909ece16c | |
|
|
d785776966 | |
|
|
1f88b2c1b3 | |
|
|
f882ab7d3b | |
|
|
494e4a568e | |
|
|
d7eea4853f | |
|
|
67a51b14e4 | |
|
|
60d1208d9d | |
|
|
39d5d2c6aa | |
|
|
008af14d12 | |
|
|
8757dd1c37 | |
|
|
35b7a11faf | |
|
|
681b195b24 | |
|
|
6d974577ad | |
|
|
066703bd42 | |
|
|
9ff754b262 | |
|
|
2c43fe7784 | |
|
|
f0d296f108 | |
|
|
869b08ec0e | |
|
|
2b37907c27 | |
|
|
6c1e85c0e5 | |
|
|
ea3a2b95e4 | |
|
|
7071c08a30 | |
|
|
83e02bab67 | |
|
|
f1da69a7f9 | |
|
|
26136fef61 | |
|
|
b1e4cbaa4e | |
|
|
633302d1e6 | |
|
|
f62ca2219e | |
|
|
69bd8f2bb4 | |
|
|
f6f9aeda99 | |
|
|
2d50f99b65 | |
|
|
c4a45ef8fd | |
|
|
a1e2afabb5 | |
|
|
be50c5de9c | |
|
|
34f659be3f | |
|
|
7aa1337538 | |
|
|
aa3afc9b4c | |
|
|
77517079a7 | |
|
|
5ac1c24481 | |
|
|
911842dc06 | |
|
|
04e5a9fa34 | |
|
|
4f9596bbee | |
|
|
ab493a89f8 | |
|
|
ff0e765b31 | |
|
|
10e340c341 | |
|
|
cd94db9c03 | |
|
|
0b693d7d2b | |
|
|
7a5666ffda | |
|
|
d2e6cc7db8 | |
|
|
3853b6ba6f | |
|
|
b93b8564de | |
|
|
e97ef265ff | |
|
|
ad26460c92 | |
|
|
2cdd7341c0 | |
|
|
ab5f8e774e | |
|
|
a5583c3afe | |
|
|
3a9068b883 | |
|
|
3d0dd2c361 | |
|
|
3598c99b9f | |
|
|
4434650aac | |
|
|
0f81e0318e | |
|
|
630ef90df1 | |
|
|
cc0142f000 | |
|
|
0c3c133431 | |
|
|
8c35ab5296 | |
|
|
a10eea979f | |
|
|
39caf88782 | |
|
|
015f8ce76f | |
|
|
3e2a32c1cf | |
|
|
90b9d2dd1b | |
|
|
7b80d9dfa0 | |
|
|
71e142b82e | |
|
|
1c328df0c7 | |
|
|
885914812d | |
|
|
905b480a2e | |
|
|
f739edc7eb | |
|
|
0b46750148 | |
|
|
ac3d30d597 | |
|
|
0fbd77f073 | |
|
|
a62baf40e1 | |
|
|
ece9887a5b | |
|
|
bfae2029b8 | |
|
|
2e9c196d8a | |
|
|
12f3ca9a3b | |
|
|
18ece0205f | |
|
|
759dfa290e | |
|
|
1e46b4cc50 | |
|
|
79c5a6d935 | |
|
|
de1f9d3364 | |
|
|
f963abe2ed |
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Test results and reports
|
||||||
|
tests/reports/
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
|
||||||
|
# Node modules
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
# build 0
|
|
||||||
labels:
|
labels:
|
||||||
location: manager
|
location: manager
|
||||||
|
|
||||||
|
|
@ -122,22 +121,22 @@ steps:
|
||||||
event: [push, cron]
|
event: [push, cron]
|
||||||
|
|
||||||
# Wait for Deploy Completion
|
# Wait for Deploy Completion
|
||||||
wait-for-deploy-production:
|
# wait-for-deploy-production:
|
||||||
name: wait-for-deploy-production
|
# name: wait-for-deploy-production
|
||||||
image: woodpeckerci/plugin-git
|
# image: woodpeckerci/plugin-git
|
||||||
commands:
|
# commands:
|
||||||
- echo "Waiting for deploy step to complete rollout."
|
# - echo "Waiting for deploy step to complete rollout."
|
||||||
- sleep 60
|
# - sleep 60
|
||||||
when:
|
# when:
|
||||||
branch: main
|
# branch: main
|
||||||
event: push
|
# event: push
|
||||||
|
|
||||||
# Post-Deployment Smoke Tests
|
# Post-Deployment Smoke Tests
|
||||||
post-deploy-smoke-tests-git-nixc-us:
|
# post-deploy-smoke-tests-git-nixc-us:
|
||||||
name: run-post-deploy-smoke-tests-git-nixc-us
|
# name: run-post-deploy-smoke-tests-git-nixc-us
|
||||||
image: codeberg.org/nixius/playwright:latest
|
# image: codeberg.org/nixius/playwright:latest
|
||||||
environment:
|
# environment:
|
||||||
BASE_URL: "https://git.nixc.us"
|
# BASE_URL: "https://git.nixc.us"
|
||||||
when:
|
# when:
|
||||||
branch: main
|
# branch: main
|
||||||
event: push
|
# event: push
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
# 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 # Build trigger Wed Oct 15 20:36:46 EDT 2025
|
||||||
|
# Build trigger Wed Oct 15 20:36:46 EDT 2025
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
lucky-ddg:
|
resume:
|
||||||
build:
|
build:
|
||||||
context: ./docker/lucky-ddg/
|
context: ./docker/resume/
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile.production
|
||||||
image: git.nixc.us/nixius/lucky-ddg:production
|
image: git.nixc.us/colin/resume:production
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
services:
|
services:
|
||||||
lucky-ddg:
|
resume:
|
||||||
build:
|
build:
|
||||||
context: ./docker/lucky-ddg/
|
context: ./docker/resume/
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
image: git.nixc.us/nixius/lucky-ddg:staging
|
image: git.nixc.us/colin/resume:staging
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
resume:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: resume-caddy
|
||||||
|
working_dir: /srv
|
||||||
|
volumes:
|
||||||
|
- ./docker/resume:/srv:ro
|
||||||
|
ports:
|
||||||
|
- "8081:8080"
|
||||||
|
command: ["caddy", "run", "--config", "/srv/Caddyfile.local"]
|
||||||
|
restart: unless-stopped
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* PDF Generation Script
|
||||||
|
*
|
||||||
|
* Uses Puppeteer to render each HTML page to PDF.
|
||||||
|
* Run with: node generate-pdfs.js
|
||||||
|
*
|
||||||
|
* Prerequisites: npm install puppeteer
|
||||||
|
*/
|
||||||
|
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const SITE_DIR = path.join(__dirname, 'resume');
|
||||||
|
const PDF_DIR = path.join(SITE_DIR, 'pdfs');
|
||||||
|
const PORT = 8765;
|
||||||
|
|
||||||
|
// MIME types for static file serving
|
||||||
|
const MIME_TYPES = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.eot': 'application/vnd.ms-fontobject',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all HTML files in a directory recursively
|
||||||
|
*/
|
||||||
|
function findHtmlFiles(dir, baseDir = dir) {
|
||||||
|
const files = [];
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Skip pdfs directory, node_modules, includes, one-pager-tools, and hidden directories
|
||||||
|
if (entry.name === 'pdfs' || entry.name === 'node_modules' || entry.name === 'includes' || entry.name === 'one-pager-tools' || entry.name.startsWith('.')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
files.push(...findHtmlFiles(fullPath, baseDir));
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
||||||
|
// Skip template files and utility files
|
||||||
|
if (entry.name.includes('template') || entry.name.includes('with-includes') || entry.name === 'csv-tool-output.html') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
|
files.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a simple static file server
|
||||||
|
*/
|
||||||
|
function createServer() {
|
||||||
|
return http.createServer((req, res) => {
|
||||||
|
let urlPath = req.url.split('?')[0];
|
||||||
|
if (urlPath === '/') urlPath = '/index.html';
|
||||||
|
|
||||||
|
const filePath = path.join(SITE_DIR, urlPath);
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('Not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure directory exists
|
||||||
|
*/
|
||||||
|
function ensureDir(dirPath) {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate PDF for a single HTML file
|
||||||
|
*/
|
||||||
|
async function generatePdf(browser, htmlFile) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Convert file path to URL path
|
||||||
|
const urlPath = '/' + htmlFile.replace(/\\/g, '/');
|
||||||
|
const url = `http://localhost:${PORT}${urlPath}`;
|
||||||
|
|
||||||
|
// Determine output PDF path
|
||||||
|
const pdfRelativePath = htmlFile.replace(/\.html$/, '.pdf');
|
||||||
|
const pdfPath = path.join(PDF_DIR, pdfRelativePath);
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
ensureDir(path.dirname(pdfPath));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Navigate to the page and wait for content to load
|
||||||
|
await page.goto(url, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: 60000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for includes.js to finish loading header and footer
|
||||||
|
// Check if header-include element exists and has content
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const headerInclude = document.getElementById('header-include');
|
||||||
|
const footerInclude = document.getElementById('footer-include');
|
||||||
|
|
||||||
|
// If neither element exists, page doesn't use includes - that's fine
|
||||||
|
if (!headerInclude && !footerInclude) return true;
|
||||||
|
|
||||||
|
// If header exists but is empty, wait
|
||||||
|
if (headerInclude && headerInclude.innerHTML.trim() === '') return false;
|
||||||
|
|
||||||
|
// If footer exists but is empty, wait
|
||||||
|
if (footerInclude && footerInclude.innerHTML.trim() === '') return false;
|
||||||
|
|
||||||
|
// If header exists, check if nav is loaded (indicates includes.js finished)
|
||||||
|
if (headerInclude) {
|
||||||
|
const nav = headerInclude.querySelector('nav');
|
||||||
|
if (!nav) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
timeout: 30000,
|
||||||
|
polling: 100 // Check every 100ms
|
||||||
|
});
|
||||||
|
} catch (waitError) {
|
||||||
|
// If waiting for includes times out, continue anyway
|
||||||
|
// Some pages might not use includes or might load differently
|
||||||
|
console.warn(`Warning: Includes may not have fully loaded for ${htmlFile}, continuing anyway...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional wait for any remaining JavaScript to finish
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
// Generate PDF with retry logic for transient errors
|
||||||
|
let retries = 2;
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
while (retries > 0) {
|
||||||
|
try {
|
||||||
|
await page.pdf({
|
||||||
|
path: pdfPath,
|
||||||
|
format: 'A4',
|
||||||
|
printBackground: true,
|
||||||
|
margin: {
|
||||||
|
top: '20mm',
|
||||||
|
right: '15mm',
|
||||||
|
bottom: '20mm',
|
||||||
|
left: '15mm'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✓ Generated: ${pdfRelativePath}`);
|
||||||
|
return; // Success, exit retry loop
|
||||||
|
} catch (pdfError) {
|
||||||
|
lastError = pdfError;
|
||||||
|
// Check if it's a recoverable error
|
||||||
|
if (pdfError.message.includes('detached') ||
|
||||||
|
pdfError.message.includes('closed') ||
|
||||||
|
pdfError.message.includes('Target closed')) {
|
||||||
|
retries--;
|
||||||
|
if (retries > 0) {
|
||||||
|
console.warn(`Retrying PDF generation for ${htmlFile} (${retries} attempts remaining)...`);
|
||||||
|
// Wait a bit before retrying
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
// Re-navigate to the page if it was closed
|
||||||
|
try {
|
||||||
|
await page.goto(url, {
|
||||||
|
waitUntil: 'networkidle2',
|
||||||
|
timeout: 60000
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
} catch (navError) {
|
||||||
|
// If navigation fails, break out of retry loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-recoverable error, don't retry
|
||||||
|
throw pdfError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, all retries failed
|
||||||
|
throw lastError || new Error('PDF generation failed after retries');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`✗ Failed: ${htmlFile} - ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await page.close();
|
||||||
|
} catch (closeError) {
|
||||||
|
// Ignore close errors - page may have been closed already
|
||||||
|
// This can happen if the page navigated or was closed during processing
|
||||||
|
// Common error: "Protocol error: Connection closed" or "Target closed"
|
||||||
|
if (!closeError.message.includes('closed') && !closeError.message.includes('Target closed')) {
|
||||||
|
// Only log if it's not a "page already closed" error
|
||||||
|
console.warn(`Warning closing page for ${htmlFile}: ${closeError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log('PDF Generation Script');
|
||||||
|
console.log('=====================\n');
|
||||||
|
|
||||||
|
// Find all HTML files
|
||||||
|
const htmlFiles = findHtmlFiles(SITE_DIR);
|
||||||
|
console.log(`Found ${htmlFiles.length} HTML files to process\n`);
|
||||||
|
|
||||||
|
if (htmlFiles.length === 0) {
|
||||||
|
console.log('No HTML files found. Exiting.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean and create PDF directory
|
||||||
|
if (fs.existsSync(PDF_DIR)) {
|
||||||
|
fs.rmSync(PDF_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
ensureDir(PDF_DIR);
|
||||||
|
|
||||||
|
// Start local server
|
||||||
|
const server = createServer();
|
||||||
|
await new Promise(resolve => server.listen(PORT, resolve));
|
||||||
|
console.log(`Local server started on port ${PORT}\n`);
|
||||||
|
|
||||||
|
// Launch browser
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: 'new',
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate PDFs for each HTML file
|
||||||
|
for (const htmlFile of htmlFiles) {
|
||||||
|
await generatePdf(browser, htmlFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✓ PDF generation complete! Files saved to: ${PDF_DIR}`);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# Use the official Python image from Docker Hub
|
|
||||||
FROM python:3.9-slim
|
|
||||||
|
|
||||||
# Set environment variables
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
# Install duckduckgo_search globally
|
|
||||||
RUN pip install --no-cache-dir duckduckgo_search
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the application code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Expose the Flask port
|
|
||||||
EXPOSE 5000
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
CMD ["python", "app.py"]
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import subprocess
|
|
||||||
from flask import Flask, request, redirect
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
@app.route('/search')
|
|
||||||
def search():
|
|
||||||
query = request.args.get('q')
|
|
||||||
if not query:
|
|
||||||
return "Query parameter 'q' is missing.", 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Execute the ddgs CLI command to perform the search
|
|
||||||
result = subprocess.run(
|
|
||||||
['ddgs', 'text', '-k', query, '-m', '1'],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
# Parse the output to extract the first result URL
|
|
||||||
output_lines = result.stdout.splitlines()
|
|
||||||
for line in output_lines:
|
|
||||||
if line.startswith('http'):
|
|
||||||
return redirect(line)
|
|
||||||
return "No results found.", 404
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return f"An error occurred: {e}", 500
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run(host='0.0.0.0', port=5000)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
flask==2.2.2
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "docker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"generate-pdfs": "node generate-pdfs.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"puppeteer": "^21.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
FROM caddy:2.7-alpine
|
||||||
|
|
||||||
|
# Install dependencies including Chromium and required libraries for Puppeteer
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
bash \
|
||||||
|
chromium \
|
||||||
|
nss \
|
||||||
|
freetype \
|
||||||
|
freetype-dev \
|
||||||
|
harfbuzz \
|
||||||
|
ca-certificates \
|
||||||
|
ttf-freefont
|
||||||
|
|
||||||
|
# Set Puppeteer to use system Chromium
|
||||||
|
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
|
||||||
|
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /srv
|
||||||
|
|
||||||
|
# Copy website files
|
||||||
|
COPY . /srv
|
||||||
|
|
||||||
|
# Install npm dependencies for PDF generation
|
||||||
|
RUN cd /srv && npm install --production
|
||||||
|
|
||||||
|
# Run all update scripts (sitemap, navigation, stories, CSP hashes, accessibility fixes)
|
||||||
|
RUN cd /srv && \
|
||||||
|
chmod +x update-all.sh && \
|
||||||
|
./update-all.sh
|
||||||
|
|
||||||
|
# Generate PDFs for all pages (only if they don't already exist)
|
||||||
|
RUN if [ ! -d "/srv/pdfs" ] || [ -z "$(ls -A /srv/pdfs 2>/dev/null)" ]; then \
|
||||||
|
cd /srv && npm run generate-pdfs; \
|
||||||
|
else \
|
||||||
|
echo "PDFs already exist, skipping generation"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Start Caddy with the local Caddyfile
|
||||||
|
CMD ["caddy", "run", "--config", "/srv/Caddyfile.local"]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
FROM git.nixc.us/colin/resume:staging
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# Resume Website
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
|
After cloning the repository, run the setup script from the repository root to install git hooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./setup-git-hooks.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs a pre-push hook that automatically generates PDFs before pushing.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<meta name="description" content="Colin Knapp - Private Consulting Packs">
|
||||||
|
<title>Consulting Packs (Private) - Colin Knapp</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
<style>
|
||||||
|
.pricing-section {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
.pricing-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.pricing-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pricing-card {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: var(--card-bg);
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||||
|
}
|
||||||
|
.pricing-card h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.pricing-card p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.pricing-features {
|
||||||
|
margin: 0.75rem 0 1rem 1.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.pricing-features li {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
.cta-row {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.btn-consulting {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.btn-consulting:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
.intro-section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<div class="intro-section">
|
||||||
|
<h1>Consulting Packs</h1>
|
||||||
|
<p>This page is unlisted. For specialized work only.</p>
|
||||||
|
<p>Architecture and implementation guidance. Async research between sessions as needed. Weekly summary with clear next steps.</p>
|
||||||
|
<p>We offer AI risk assessments and help with bespoke AI toolbuilding. Building AI tooling that ensures you have xray vision into the things you need done in your segment of the world—we want to help you realise your <span id="current-year"></span> cyborg potential.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section class="pricing-section">
|
||||||
|
<div class="pricing-grid">
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h3>20-Hour Pack (Prepaid Discount)</h3>
|
||||||
|
<p>Buy 20 hours at a time to save on per-hour rates. Pricing and taxes shown at checkout.</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<a class="btn-consulting" href="https://buy.stripe.com/3cI4gybxh3WI8H27ox4ko0J" target="_blank" rel="nofollow noopener">Buy 20-Hour Pack</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pricing-card">
|
||||||
|
<h3>AI Consultation (1 Hour)</h3>
|
||||||
|
<p>AI risk assessments, bespoke toolbuilding, feasibility studies, and architecture guidance.</p>
|
||||||
|
<div class="cta-row">
|
||||||
|
<a class="btn-consulting" href="https://buy.stripe.com/8x29ASgRB3WI8H26kt4ko0N" target="_blank" rel="nofollow noopener">Book AI Consultation</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Set current year dynamically
|
||||||
|
document.getElementById('current-year').textContent = new Date().getFullYear();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<!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="stylesheet" href="../styles.css">
|
||||||
|
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<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-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 218.21 218.21"><defs><style>.cls-1{fill:#1d3557;}.cls-2{fill:#457b9d;}.cls-3{fill:#e63946;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M137.19,18.06A3.74,3.74,0,0,0,140.86,15l-3.47-3.46c-.3-.3-.61-.58-.91-.87a3.73,3.73,0,0,0,.71,7.4Z"/><path class="cls-1" d="M140.48,53.17h7.24a17.34,17.34,0,0,1,17.33,17.32v7.25H175a7.24,7.24,0,0,0,5.12-2.12l10.7-10.7-45-45a10.37,10.37,0,0,1-5.33,4.14Z"/><path class="cls-1" d="M175,84.31h-9.91v7.47h14.27a3.29,3.29,0,0,1,0,6.57H165.05v7.47h52.88a2.4,2.4,0,0,1,.28,0,39.79,39.79,0,0,0-11.52-25L195.43,69.56l-10.7,10.7A13.73,13.73,0,0,1,175,84.31Z"/><path class="cls-1" d="M112.39,53.16h7.48V38.9a3.28,3.28,0,1,1,6.56,0V53.16h7.48V24.09a10.3,10.3,0,0,1-3.08-17.86A40,40,0,0,0,112.36,0a2.4,2.4,0,0,1,0,.28Z"/><path class="cls-2" d="M14.32,84.76A3.74,3.74,0,0,0,15,77.35l-3.47,3.47c-.3.3-.58.61-.87.92A3.73,3.73,0,0,0,14.32,84.76Z"/><path class="cls-2" d="M53.16,105.82V98.35H38.9a3.29,3.29,0,1,1,0-6.57H53.16V84.31H24.09A10.3,10.3,0,0,1,6.23,87.39,40,40,0,0,0,0,105.85a2.4,2.4,0,0,1,.28,0Z"/><path class="cls-1" d="M84.31,43.26v9.9h7.47V38.9a3.29,3.29,0,1,1,6.57,0V53.16h7.47V.28a2.4,2.4,0,0,1,0-.28,39.8,39.8,0,0,0-25,11.53L69.56,22.79l10.7,10.7A13.73,13.73,0,0,1,84.31,43.26Z"/><path class="cls-2" d="M81,200.16a3.74,3.74,0,0,0-3.67,3.06l3.47,3.47c.3.3.61.58.92.87a3.73,3.73,0,0,0-.72-7.4Z"/><path class="cls-2" d="M105.82,165.05H98.35v14.27a3.29,3.29,0,0,1-6.57,0V165.05H84.31v29.08A10.29,10.29,0,0,1,87.39,212a39.84,39.84,0,0,0,18.46,6.23,2.4,2.4,0,0,1,0-.28Z"/><path class="cls-1" d="M203.89,133.46a3.73,3.73,0,0,0-.67,7.4l3.47-3.47c.3-.3.58-.61.87-.91A3.74,3.74,0,0,0,203.89,133.46Z"/><path class="cls-3" d="M194.13,140.48H165.05v7.24a17,17,0,0,1-.55,4.33,3.28,3.28,0,0,1-3.17,2.47,3.24,3.24,0,0,1-.82-.11,3.29,3.29,0,0,1-2.37-4,10.5,10.5,0,0,0,.34-2.69V70.49a10.78,10.78,0,0,0-10.76-10.76H70.49a11,11,0,0,0-2.69.34,3.28,3.28,0,0,1-1.64-6.36,17.54,17.54,0,0,1,4.33-.55h7.25v-9.9a7.24,7.24,0,0,0-2.12-5.13l-10.7-10.7-45,45a10.43,10.43,0,0,1,4.14,5.34H53.17V70.49a17.53,17.53,0,0,1,.54-4.33,3.28,3.28,0,1,1,6.36,1.64,11,11,0,0,0-.34,2.69v77.23a10.78,10.78,0,0,0,10.76,10.76h77.23a10.63,10.63,0,0,0,2.7-.34,3.28,3.28,0,0,1,1.63,6.36,17,17,0,0,1-4.33.55h-7.24V175a7.19,7.19,0,0,0,2.12,5.12l10.7,10.7,45-45A10.27,10.27,0,0,1,194.13,140.48Zm-46.46-30.77L140.21,117l-4.5-1.94,1.83-4.75Zm-14.46,4.84-10.43-3.84,2.34-2.67-3.84-10.13L147,85.49Zm-.8,1.54-2.58,5.71H112l9.64-9.63Zm-4.08-38.27-.46,14.71-6.34,2.71-3.25-6.58ZM119,96.5l-9.59,4.41-9.5-4.41,9.5-26.94Zm-10.26,5.75v18.84l-13-13.42,3.41-9.76Zm-8.08-13.43-2.92,6.42L91.26,92l-.41-14Zm-3.34,8.43-4.17,10.92,2.59,2.42L85.59,114,71.92,85.24ZM81.67,110.09l1.67,4.54-4.71,2-7.46-7.13Zm15.68,23.3-30-10.67,17.3-6.09,3.73,7,18.58.07Zm-8-11.59-3-6,10.68-3.84,10,9.88Zm21,26.85h-1.59V123.3h1.59Zm0-46.48,9.42-3.84,3.5,9.34-12.92,13.42Zm11.42,31.22-10.09-10,19.29.17,3.16-6.74,16.74,5.9Z"/><path class="cls-2" d="M133.91,175v-9.91h-7.48v14.27a3.29,3.29,0,0,1-6.57,0V165.05h-7.47v52.88a2.4,2.4,0,0,1,0,.28,39.76,39.76,0,0,0,25-11.52l11.26-11.26L138,184.73A13.72,13.72,0,0,1,133.91,175Z"/><path class="cls-2" d="M43.26,133.91h9.9v-7.48H38.9a3.29,3.29,0,1,1,0-6.57H53.16v-7.47H.28a2.4,2.4,0,0,1-.28,0,39.75,39.75,0,0,0,11.53,25l11.26,11.26L33.49,138A13.72,13.72,0,0,1,43.26,133.91Z"/><path class="cls-1" d="M165.05,112.39v7.47h14.27a3.29,3.29,0,0,1,0,6.57H165.05v7.48h29.08A10.29,10.29,0,0,1,212,130.83a39.88,39.88,0,0,0,6.23-18.47,2.4,2.4,0,0,1-.28,0Z"/><path class="cls-2" d="M77.74,165.05H70.49a17.34,17.34,0,0,1-17.32-17.33v-7.24H43.26a7.2,7.2,0,0,0-5.13,2.12l-10.7,10.7,45,45a10.33,10.33,0,0,1,5.34-4.14Z"/></g></g></svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1,245 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* PDF Generation Script
|
||||||
|
*
|
||||||
|
* Uses Puppeteer to render each HTML page to PDF.
|
||||||
|
* Run with: node generate-pdfs.js
|
||||||
|
*
|
||||||
|
* Prerequisites: npm install puppeteer
|
||||||
|
*/
|
||||||
|
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const SITE_DIR = __dirname; // Running from /srv in Docker, which contains all site files
|
||||||
|
const PDF_DIR = path.join(SITE_DIR, 'pdfs');
|
||||||
|
const PORT = 8765;
|
||||||
|
|
||||||
|
// MIME types for static file serving
|
||||||
|
const MIME_TYPES = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.ttf': 'font/ttf',
|
||||||
|
'.eot': 'application/vnd.ms-fontobject',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all HTML files in a directory recursively
|
||||||
|
*/
|
||||||
|
function findHtmlFiles(dir, baseDir = dir) {
|
||||||
|
const files = [];
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// Skip pdfs directory, node_modules, and hidden directories
|
||||||
|
if (entry.name === 'pdfs' || entry.name === 'node_modules' || entry.name.startsWith('.')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
files.push(...findHtmlFiles(fullPath, baseDir));
|
||||||
|
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
||||||
|
// Skip template files
|
||||||
|
if (entry.name.includes('template') || entry.name.includes('with-includes')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const relativePath = path.relative(baseDir, fullPath);
|
||||||
|
files.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a simple static file server
|
||||||
|
*/
|
||||||
|
function createServer() {
|
||||||
|
return http.createServer((req, res) => {
|
||||||
|
let urlPath = req.url.split('?')[0];
|
||||||
|
if (urlPath === '/') urlPath = '/index.html';
|
||||||
|
|
||||||
|
const filePath = path.join(SITE_DIR, urlPath);
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end('Not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure directory exists
|
||||||
|
*/
|
||||||
|
function ensureDir(dirPath) {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate PDF for a single HTML file
|
||||||
|
*/
|
||||||
|
async function generatePdf(browser, htmlFile) {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Convert file path to URL path
|
||||||
|
const urlPath = '/' + htmlFile.replace(/\\/g, '/');
|
||||||
|
const url = `http://localhost:${PORT}${urlPath}`;
|
||||||
|
|
||||||
|
// Determine output PDF path
|
||||||
|
const pdfRelativePath = htmlFile.replace(/\.html$/, '.pdf');
|
||||||
|
const pdfPath = path.join(PDF_DIR, pdfRelativePath);
|
||||||
|
|
||||||
|
// Ensure output directory exists
|
||||||
|
ensureDir(path.dirname(pdfPath));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Navigate to the page and wait for content to load
|
||||||
|
await page.goto(url, {
|
||||||
|
waitUntil: 'networkidle0',
|
||||||
|
timeout: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for includes to load - check if header-include exists and has content
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const headerInclude = document.getElementById('header-include');
|
||||||
|
// If header-include exists, wait for it to have content (includes loaded)
|
||||||
|
// If it doesn't exist, that's fine too (page doesn't use includes)
|
||||||
|
if (headerInclude) {
|
||||||
|
return headerInclude.innerHTML.trim().length > 0 ||
|
||||||
|
document.querySelector('.main-nav') !== null;
|
||||||
|
}
|
||||||
|
return true; // No header-include, page is ready
|
||||||
|
}, { timeout: 10000 });
|
||||||
|
} catch (waitError) {
|
||||||
|
// If waiting for includes times out, continue anyway
|
||||||
|
console.warn(`Warning: Includes may not have loaded for ${htmlFile}, continuing...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit more for any remaining JavaScript to finish
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// Generate PDF
|
||||||
|
await page.pdf({
|
||||||
|
path: pdfPath,
|
||||||
|
format: 'A4',
|
||||||
|
printBackground: true,
|
||||||
|
margin: {
|
||||||
|
top: '20mm',
|
||||||
|
right: '15mm',
|
||||||
|
bottom: '20mm',
|
||||||
|
left: '15mm'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✓ Generated: ${pdfRelativePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`✗ Failed: ${htmlFile} - ${error.message}`);
|
||||||
|
// Don't re-throw - let the caller handle it
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await page.close();
|
||||||
|
} catch (closeError) {
|
||||||
|
// Page might already be closed, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
console.log('PDF Generation Script');
|
||||||
|
console.log('=====================\n');
|
||||||
|
|
||||||
|
// Find all HTML files
|
||||||
|
const htmlFiles = findHtmlFiles(SITE_DIR);
|
||||||
|
console.log(`Found ${htmlFiles.length} HTML files to process\n`);
|
||||||
|
|
||||||
|
if (htmlFiles.length === 0) {
|
||||||
|
console.log('No HTML files found. Exiting.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean and create PDF directory
|
||||||
|
if (fs.existsSync(PDF_DIR)) {
|
||||||
|
fs.rmSync(PDF_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
ensureDir(PDF_DIR);
|
||||||
|
|
||||||
|
// Start local server
|
||||||
|
const server = createServer();
|
||||||
|
await new Promise(resolve => server.listen(PORT, resolve));
|
||||||
|
console.log(`Local server started on port ${PORT}\n`);
|
||||||
|
|
||||||
|
// Launch browser
|
||||||
|
// Use system Chromium if available (in Docker), otherwise use Puppeteer's bundled Chrome
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: 'new',
|
||||||
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
'--disable-dev-shm-usage',
|
||||||
|
'--disable-accelerated-2d-canvas',
|
||||||
|
'--no-first-run',
|
||||||
|
'--no-zygote',
|
||||||
|
'--single-process',
|
||||||
|
'--disable-gpu'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let failCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Generate PDFs for each HTML file
|
||||||
|
for (const htmlFile of htmlFiles) {
|
||||||
|
try {
|
||||||
|
await generatePdf(browser, htmlFile);
|
||||||
|
successCount++;
|
||||||
|
} catch (error) {
|
||||||
|
failCount++;
|
||||||
|
console.error(`Failed to generate PDF for ${htmlFile}:`, error.message);
|
||||||
|
// Continue with next file instead of stopping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✓ PDF generation complete!`);
|
||||||
|
console.log(` Success: ${successCount}, Failed: ${failCount}`);
|
||||||
|
console.log(` Files saved to: ${PDF_DIR}`);
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/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)"
|
||||||
|
# Use production domain by default, can be overridden with SITEMAP_DOMAIN env var
|
||||||
|
DOMAIN="${SITEMAP_DOMAIN:-https://colinknapp.com}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Skip consulting-packs.html as it's a private unlisted page
|
||||||
|
if [[ "$rel_path" == "consulting-packs.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")"
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<!-- This is a placeholder for the Discord community screenshot. Replace with the actual image. -->
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<!-- This is a placeholder for the Docker Hub stats screenshot. Replace with the actual image. -->
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// Check if we are in a section that corresponds to a dropdown
|
||||||
|
if (currentPath.includes('/stories/')) {
|
||||||
|
const storiesLink = document.getElementById('nav-stories');
|
||||||
|
if (storiesLink) storiesLink.classList.add('active');
|
||||||
|
} else if (currentPath.includes('/one-pager-tools/')) {
|
||||||
|
const toolsLink = document.getElementById('nav-tools');
|
||||||
|
if (toolsLink) toolsLink.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic check for all dropdown links
|
||||||
|
// This iterates through all links in dropdown contents and checks if the current path matches
|
||||||
|
const dropdownLinks = document.querySelectorAll('.dropdown-content a');
|
||||||
|
dropdownLinks.forEach(link => {
|
||||||
|
const href = link.getAttribute('href');
|
||||||
|
// Check if current path ends with the href (handling relative paths)
|
||||||
|
// or if the href is contained in the current path
|
||||||
|
if (href && (currentPath.endsWith(href) || currentPath === href)) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process header and footer placeholders
|
||||||
|
const headerElement = document.getElementById('header-include');
|
||||||
|
const footerElement = document.getElementById('footer-include');
|
||||||
|
|
||||||
|
if (headerElement) {
|
||||||
|
// Add favicon link when header is included
|
||||||
|
// First remove any existing favicon
|
||||||
|
const existingFavicon = document.querySelector('link[rel="icon"]');
|
||||||
|
if (existingFavicon) {
|
||||||
|
existingFavicon.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
const faviconLink = document.createElement('link');
|
||||||
|
faviconLink.rel = 'icon';
|
||||||
|
faviconLink.type = 'image/svg+xml';
|
||||||
|
faviconLink.href = '/favicon.svg';
|
||||||
|
document.head.appendChild(faviconLink);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# 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
|
||||||
|
- `stories/story-with-includes.html`: Example of a story page
|
||||||
|
- `one-pager-tools/tool-with-includes.html`: Example of a tool page
|
||||||
|
|
||||||
|
|
||||||
|
## 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>
|
||||||
|
```
|
||||||
|
|
||||||
|
Once you've created your includes, you can add them to your HTML files as shown above.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
# 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="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
|
||||||
|
- `/stories/story-with-includes.html`: Example of a story page
|
||||||
|
- `/one-pager-tools/tool-with-includes.html`: Example of a tool page
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<p id="pdf-download-link" class="pdf-download" style="display: none;">
|
||||||
|
<a href="#" id="pdf-link" download>Download this page as PDF</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
// Determine PDF path based on current page URL
|
||||||
|
var path = window.location.pathname;
|
||||||
|
if (path === '/' || path === '') path = '/index.html';
|
||||||
|
|
||||||
|
// Convert .html to .pdf and prepend /pdfs/
|
||||||
|
var pdfPath = '/pdfs' + path.replace(/\.html$/, '.pdf');
|
||||||
|
|
||||||
|
// Check if PDF exists, then show link
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('HEAD', pdfPath, true);
|
||||||
|
xhr.onreadystatechange = function() {
|
||||||
|
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||||
|
var container = document.getElementById('pdf-download-link');
|
||||||
|
var link = document.getElementById('pdf-link');
|
||||||
|
if (container && link) {
|
||||||
|
link.href = pdfPath;
|
||||||
|
container.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<a href="#main-content" class="skip-to-content">Skip to content</a>
|
||||||
|
|
||||||
|
<!-- Matomo -->
|
||||||
|
<script>
|
||||||
|
var _paq = window._paq = window._paq || [];
|
||||||
|
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||||
|
_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=u+'matomo.js'; s.parentNode.insertBefore(g,s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- End Matomo Code -->
|
||||||
|
|
||||||
|
<!-- PostHog -->
|
||||||
|
<script>
|
||||||
|
!function(t,e){var o,n,p,r;e.__SV||(window.posthog && window.posthog.__loaded)||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init zr Wr fi Br Gr ci Nr Hr capture Ui calculateEventProperties Kr register register_once register_for_session unregister unregister_for_session Zr getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey displaySurvey cancelPendingSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Xr Jr createPersonProfile Qr jr ts opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing get_explicit_consent_status is_capturing clear_opt_in_out_capturing Vr debug O Yr getPageViewId captureTraceFeedback captureTraceMetric Or".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
||||||
|
|
||||||
|
posthog.init('phc_3WDvcJlYYXlBVYL8vC1raT0gMfjkMuCyOpXdmgjK0CK', {
|
||||||
|
api_host: 'https://eu.i.posthog.com',
|
||||||
|
defaults: '2025-11-30',
|
||||||
|
person_profiles: 'identified_only', // or 'always' to create profiles for anonymous users as well
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<!-- End PostHog 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="/resumes/business-development.html" id="nav-resumes">Resumes</a>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a href="/resumes/business-development.html" id="nav-businessdev">Business Development</a>
|
||||||
|
<a href="/resumes/devsecops.html" id="nav-devsecops">DevSecOps</a>
|
||||||
|
<a href="/resumes/team-leadership.html" id="nav-teamleadership">Team Leadership</a>
|
||||||
|
<a href="/resumes/tool-building.html" id="nav-toolbuilding">Tool Building</a>
|
||||||
|
</div>
|
||||||
|
</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" class="nav-story-tbd">Airport Dns</a>
|
||||||
|
<a href="/stories/app-development.html" id="nav-appdevelopment" class="nav-story-written">App Development</a>
|
||||||
|
<a href="/stories/athion-turnaround.html" id="nav-athionturnaround" class="nav-story-written">Athion Turnaround</a>
|
||||||
|
<a href="/stories/fawe-plotsquared.html" id="nav-faweplotsquared" class="nav-story-tbd">Fawe Plotsquared</a>
|
||||||
|
<a href="/stories/healthcare-platform.html" id="nav-healthcareplatform" class="nav-story-tbd">Healthcare Platform</a>
|
||||||
|
<a href="/stories/home-infrastructure.html" id="nav-homeinfrastructure" class="nav-story-written">Home Infrastructure</a>
|
||||||
|
<a href="/stories/motherboard-repair.html" id="nav-motherboardrepair" class="nav-story-written">Motherboard Repair</a>
|
||||||
|
<a href="/stories/nitric-leadership.html" id="nav-nitricleadership" class="nav-story-tbd">Nitric Leadership</a>
|
||||||
|
<a href="/stories/nuclear-dns.html" id="nav-nucleardns" class="nav-story-written">Nuclear Dns</a>
|
||||||
|
<a href="/stories/open-source-success.html" id="nav-opensourcesuccess" class="nav-story-written">Open Source Success</a>
|
||||||
|
<a href="/stories/scansnap-webdav.html" id="nav-scansnapwebdav" class="nav-story-written">Scansnap Webdav</a>
|
||||||
|
<a href="/stories/showerloop.html" id="nav-showerloop" class="nav-story-written">Showerloop</a>
|
||||||
|
<a href="/stories/viperwire.html" id="nav-viperwire" class="nav-story-written">Viperwire</a>
|
||||||
|
<a href="/stories/web-design-java.html" id="nav-webdesignjava" class="nav-story-written">Web Design Java</a>
|
||||||
|
<a href="/stories/wordpress-security.html" id="nav-wordpresssecurity" class="nav-story-tbd">Wordpress Security</a>
|
||||||
|
<a href="/stories/youtube-game-dev.html" id="nav-youtubegamedev" class="nav-story-written">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="/one-pager-tools/utm-tool.html" id="nav-utm">UTM Builder</a>
|
||||||
|
<a href="https://md.colinknapp.com" id="nav-markdown" target="_blank" rel="noopener noreferrer">Markdown Tool</a>
|
||||||
|
<a href="https://nix.colinknapp.com" id="nav-nix" target="_blank" rel="noopener noreferrer">NixOS Validator</a>
|
||||||
|
<a href="https://qr.colinknapp.com" id="nav-qrcode" target="_blank" rel="noopener noreferrer">QR Code Tool</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li><a href="https://meet.colinknapp.com" id="nav-meet" target="_blank" rel="noopener noreferrer" title="No-account web meetings without software">Meet</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,321 @@
|
||||||
|
<!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 - DevSecOps consultant, cybersecurity expert, and open-source advocate. Explore resumes, project stories, and free tools.">
|
||||||
|
<title>Colin Knapp - DevSecOps & Cybersecurity Expert</title>
|
||||||
|
<link rel="stylesheet" href="styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
<style>
|
||||||
|
.hero {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .tagline {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
color: var(--date-color);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-card p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-button:hover {
|
||||||
|
background: var(--hover-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-section {
|
||||||
|
margin: 3rem 0;
|
||||||
|
padding: 2rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link {
|
||||||
|
display: block;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-link strong {
|
||||||
|
display: block;
|
||||||
|
color: var(--accent-color);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent-color);
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link:hover {
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-box {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-left: 4px solid var(--accent-color);
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero .tagline {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<section class="hero">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p class="tagline">DevSecOps Consultant | Cybersecurity Expert | Open-Source Advocate</p>
|
||||||
|
<p>Architecting secure infrastructure, building powerful tools, and leading teams to success.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Call to Action Cards -->
|
||||||
|
<section class="cta-grid">
|
||||||
|
<div class="cta-card">
|
||||||
|
<h3>View Resumes</h3>
|
||||||
|
<p>Explore specialized resumes tailored for different roles and industries, from DevSecOps to business development.</p>
|
||||||
|
<a href="/resumes/portfolio.html" class="cta-button">Browse Resumes</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cta-card">
|
||||||
|
<h3>Read Project Stories</h3>
|
||||||
|
<p>Dive into detailed case studies and stories from real-world projects spanning cybersecurity, infrastructure, and open-source development.</p>
|
||||||
|
<a href="/stories/" class="cta-button">Explore Stories</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cta-card">
|
||||||
|
<h3>Get In Touch</h3>
|
||||||
|
<p>Interested in consulting services, collaboration opportunities, or have questions? Let's connect.</p>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer" class="cta-button">Contact Me</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Highlights -->
|
||||||
|
<section class="highlights">
|
||||||
|
<h2>What I Do</h2>
|
||||||
|
<div class="highlight-box">
|
||||||
|
<h3>DevSecOps & Infrastructure</h3>
|
||||||
|
<p>Leading infrastructure and security projects for government and healthcare clients. Expert in building scalable, compliant systems with WCAG 2.0 AA standards, CIS security benchmarks, and geographically redundant architectures.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="highlight-box">
|
||||||
|
<h3>Open-Source Leadership</h3>
|
||||||
|
<p>Co-creator of FastAsyncWorldEdit and PlotSquared, tools that power millions of Minecraft servers worldwide with 10M+ downloads. Former maintainer of OhMyForm with 1.5M+ Docker pulls.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="highlight-box">
|
||||||
|
<h3>Team Leadership & Mentorship</h3>
|
||||||
|
<p>Managed distributed teams of 45+ professionals across multiple time zones. Co-founded and scaled Nitric Concepts from zero to $4M+ in revenue through strategic leadership and operational excellence.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Resume Options -->
|
||||||
|
<section>
|
||||||
|
<h2>Specialized Resumes</h2>
|
||||||
|
<p>Choose the resume that best fits your needs:</p>
|
||||||
|
<div class="quick-links">
|
||||||
|
<a href="/resumes/portfolio.html" class="quick-link">Full Portfolio</a>
|
||||||
|
<a href="/resumes/devsecops.html" class="quick-link">DevSecOps</a>
|
||||||
|
<a href="/resumes/team-leadership.html" class="quick-link">Team Leadership</a>
|
||||||
|
<a href="/resumes/business-development.html" class="quick-link">Business Development</a>
|
||||||
|
<a href="/resumes/tool-building.html" class="quick-link">Tool Building</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Free Tools Section -->
|
||||||
|
<section class="tools-section">
|
||||||
|
<h2>Free Tools & Services</h2>
|
||||||
|
<p>Check out these free, privacy-focused tools I've built and maintained:</p>
|
||||||
|
|
||||||
|
<div class="tools-grid">
|
||||||
|
<a href="/one-pager-tools/csv-tool.html" class="tool-link">
|
||||||
|
<strong>CSV Tool</strong>
|
||||||
|
<span>Process and analyze CSV files directly in your browser</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/one-pager-tools/utm-tool.html" class="tool-link">
|
||||||
|
<strong>UTM Builder</strong>
|
||||||
|
<span>Generate campaign tracking URLs with UTM parameters</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://md.colinknapp.com" target="_blank" rel="noopener noreferrer" class="tool-link">
|
||||||
|
<strong>Markdown Tool</strong>
|
||||||
|
<span>Live markdown editor and previewer</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://nix.colinknapp.com" target="_blank" rel="noopener noreferrer" class="tool-link">
|
||||||
|
<strong>NixOS Validator</strong>
|
||||||
|
<span>Validate and test NixOS configurations</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://qr.colinknapp.com" target="_blank" rel="noopener noreferrer" class="tool-link">
|
||||||
|
<strong>QR Code Tool</strong>
|
||||||
|
<span>Generate QR codes instantly</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="https://meet.colinknapp.com" target="_blank" rel="noopener noreferrer" class="tool-link">
|
||||||
|
<strong>Meet</strong>
|
||||||
|
<span>No-account video meetings without software</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="margin-top: 1.5rem; text-align: center; color: var(--date-color);">
|
||||||
|
All tools are free, respect your privacy, and require no account creation.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Featured Projects -->
|
||||||
|
<section>
|
||||||
|
<h2>Featured Projects</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong><a href="stories/viperwire.html">ViperWire.ca</a></strong> - AI-powered cybersecurity consultancy focused on enterprise-grade security for SMBs
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong><a href="stories/airport-dns.html">Bishop Airport DNS Infrastructure</a></strong> - Geographically redundant, A+ rated DNS cluster for critical government infrastructure
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong><a href="stories/healthcare-platform.html">Healthcare Platform</a></strong> - Secure infrastructure for Improving MI Practices with CIS Level 1 & 2 compliance
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong><a href="stories/fawe-plotsquared.html">FastAsyncWorldEdit & PlotSquared</a></strong> - Revolutionary open-source tools powering the $5B Minecraft ecosystem
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong><a href="stories/nitric-leadership.html">Nitric Concepts</a></strong> - Co-founded and scaled to $4M+ revenue managing 45+ global contractors
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="text-align: center; margin-top: 1.5rem;">
|
||||||
|
<a href="/stories/" class="cta-button">View All Project Stories</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Contact Section -->
|
||||||
|
<section class="contact-info">
|
||||||
|
<h2>Get In Touch</h2>
|
||||||
|
<p>
|
||||||
|
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer">Contact via MotherboardRepair.ca</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em>Note: MotherboardRepair.ca offers a wide range of services and is currently my main focus. Please use the contact form there for all inquiries.</em>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Load same-named .md and render into .story-content using Marked. No fallbacks.
|
||||||
|
(function () {
|
||||||
|
function getMarkdownPath() {
|
||||||
|
var parts = (window.location.pathname || '').split('/');
|
||||||
|
var last = parts[parts.length - 1] || '';
|
||||||
|
if (!last) return '';
|
||||||
|
return last.replace(/\.html?$/i, '.md');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMarked(callback) {
|
||||||
|
if (window.marked && typeof window.marked.parse === 'function') {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js';
|
||||||
|
script.async = true;
|
||||||
|
script.onload = callback;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMarkdown(container, text) {
|
||||||
|
if (!window.marked || typeof window.marked.parse !== 'function') {
|
||||||
|
container.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (typeof window.marked.setOptions === 'function') {
|
||||||
|
window.marked.setOptions({ gfm: true, breaks: true });
|
||||||
|
}
|
||||||
|
container.innerHTML = window.marked.parse(text);
|
||||||
|
} catch (_) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
var container = document.querySelector('.story-content');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// No fallback: clear immediately
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
var mdPath = getMarkdownPath();
|
||||||
|
if (!mdPath) return;
|
||||||
|
|
||||||
|
loadMarked(function () {
|
||||||
|
fetch(mdPath, { cache: 'no-cache' })
|
||||||
|
.then(function (res) { if (!res.ok) throw new Error('md'); return res.text(); })
|
||||||
|
.then(function (text) { renderMarkdown(container, text); })
|
||||||
|
.catch(function () { container.innerHTML = ''; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 */;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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="Free browser-based CSV viewer tool. Paste CSV data to instantly view as formatted table with sorting, customizable delimiters, and client-side processing for privacy.">
|
||||||
|
<title>CSV Viewer Tool - Free Online CSV Parser</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css">
|
||||||
|
<link rel="stylesheet" href="tool-styles.css?v=2" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<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-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -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, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
});
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,41 @@
|
||||||
|
<!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="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
<!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="stylesheet" href="../styles.css">
|
||||||
|
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<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-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></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>
|
||||||
|
|
@ -0,0 +1,353 @@
|
||||||
|
<!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="Free UTM URL builder tool. Generate tracking URLs with UTM parameters for marketing campaigns. Client-side processing for privacy.">
|
||||||
|
<title>UTM URL Generator - Free Campaign URL Builder</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css">
|
||||||
|
<link rel="stylesheet" href="tool-styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
<style>
|
||||||
|
.utm-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label .required {
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label .optional {
|
||||||
|
color: var(--date-color);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(69, 123, 157, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input.error {
|
||||||
|
border-color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .hint {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--date-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-params-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-params-section h3 {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-param-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-param-row input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-param-row input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-param {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #e63946;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-param:hover {
|
||||||
|
background: #c1121f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-param {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-param:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url {
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url.empty {
|
||||||
|
color: var(--date-color);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url.valid {
|
||||||
|
border-color: #2a9d8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generated-url.invalid {
|
||||||
|
border-color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-section {
|
||||||
|
margin-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: var(--accent-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-copy:disabled {
|
||||||
|
background: var(--border-color);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback {
|
||||||
|
color: #2a9d8f;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-feedback.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown th,
|
||||||
|
.url-breakdown td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown th {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-breakdown td {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message.error {
|
||||||
|
background: rgba(230, 57, 70, 0.1);
|
||||||
|
color: #e63946;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message.success {
|
||||||
|
background: rgba(42, 157, 143, 0.1);
|
||||||
|
color: #2a9d8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.validation-message.info {
|
||||||
|
background: rgba(69, 123, 157, 0.1);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>UTM URL Generator</h1>
|
||||||
|
<p>Build tracking URLs with UTM parameters for your marketing campaigns.</p>
|
||||||
|
|
||||||
|
<div class="tool-container">
|
||||||
|
<div class="utm-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="baseUrl">Website URL <span class="required">*</span></label>
|
||||||
|
<input type="url" id="baseUrl" placeholder="https://example.com/page" aria-required="true">
|
||||||
|
<span class="hint">The full URL of the page you want to track</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmSource">Campaign Source <span class="required">*</span></label>
|
||||||
|
<input type="text" id="utmSource" placeholder="google, newsletter, facebook" aria-required="true">
|
||||||
|
<span class="hint">utm_source: Identifies which site sent the traffic</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmMedium">Campaign Medium <span class="required">*</span></label>
|
||||||
|
<input type="text" id="utmMedium" placeholder="cpc, email, social" aria-required="true">
|
||||||
|
<span class="hint">utm_medium: The marketing medium (e.g., cpc, email, social)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmCampaign">Campaign Name <span class="required">*</span></label>
|
||||||
|
<input type="text" id="utmCampaign" placeholder="spring_sale, product_launch" aria-required="true">
|
||||||
|
<span class="hint">utm_campaign: The specific campaign name</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmTerm">Campaign Term <span class="optional">(optional)</span></label>
|
||||||
|
<input type="text" id="utmTerm" placeholder="running+shoes, marketing+software">
|
||||||
|
<span class="hint">utm_term: Paid search keywords</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="utmContent">Campaign Content <span class="optional">(optional)</span></label>
|
||||||
|
<input type="text" id="utmContent" placeholder="logolink, textlink, banner_v1">
|
||||||
|
<span class="hint">utm_content: Differentiate ads or links pointing to the same URL</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="custom-params-section">
|
||||||
|
<h3>Custom Parameters <span class="optional">(optional)</span></h3>
|
||||||
|
<div id="customParams"></div>
|
||||||
|
<button type="button" class="btn-add-param" id="addParamBtn" aria-label="Add custom parameter">+ Add Parameter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="output-section">
|
||||||
|
<h2>Generated URL</h2>
|
||||||
|
<div id="validationMessage" class="validation-message" aria-live="polite"></div>
|
||||||
|
<div id="generatedUrl" class="generated-url empty" aria-live="polite">
|
||||||
|
Enter a URL and required parameters to generate your tracking URL
|
||||||
|
</div>
|
||||||
|
<div class="copy-section">
|
||||||
|
<button type="button" class="btn-copy" id="copyBtn" disabled aria-label="Copy URL to clipboard">Copy URL</button>
|
||||||
|
<span class="copy-feedback" id="copyFeedback">Copied!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="url-breakdown" id="urlBreakdown" style="display: none;">
|
||||||
|
<h3>URL Parameters</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Parameter</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="breakdownBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>About This Tool</h2>
|
||||||
|
<p>This UTM URL Generator helps you create campaign tracking URLs with:</p>
|
||||||
|
<ul>
|
||||||
|
<li>All standard UTM parameters (source, medium, campaign, term, content)</li>
|
||||||
|
<li>Custom parameters for additional tracking needs</li>
|
||||||
|
<li>Live URL preview as you type</li>
|
||||||
|
<li>Proper URL encoding for special characters</li>
|
||||||
|
<li>One-click copy to clipboard</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>
|
||||||
|
|
||||||
|
<script src="utm-tool.js" integrity="sha256-OCaMaX1b760MdMS5SEHYi8sKrGLV71QhLzQ+7cFuC3I="></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
/**
|
||||||
|
* UTM URL Generator functionality
|
||||||
|
* Builds tracking URLs with UTM parameters
|
||||||
|
* Client-side only - no data sent to server
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// DOM Elements
|
||||||
|
const baseUrlInput = document.getElementById('baseUrl');
|
||||||
|
const utmSourceInput = document.getElementById('utmSource');
|
||||||
|
const utmMediumInput = document.getElementById('utmMedium');
|
||||||
|
const utmCampaignInput = document.getElementById('utmCampaign');
|
||||||
|
const utmTermInput = document.getElementById('utmTerm');
|
||||||
|
const utmContentInput = document.getElementById('utmContent');
|
||||||
|
const customParamsContainer = document.getElementById('customParams');
|
||||||
|
const addParamBtn = document.getElementById('addParamBtn');
|
||||||
|
const generatedUrlDiv = document.getElementById('generatedUrl');
|
||||||
|
const validationMessage = document.getElementById('validationMessage');
|
||||||
|
const copyBtn = document.getElementById('copyBtn');
|
||||||
|
const copyFeedback = document.getElementById('copyFeedback');
|
||||||
|
const urlBreakdown = document.getElementById('urlBreakdown');
|
||||||
|
const breakdownBody = document.getElementById('breakdownBody');
|
||||||
|
|
||||||
|
// Current generated URL
|
||||||
|
let currentUrl = '';
|
||||||
|
let customParamCount = 0;
|
||||||
|
|
||||||
|
// Debounce timer
|
||||||
|
let debounceTimer;
|
||||||
|
|
||||||
|
// Add input listeners to all UTM fields
|
||||||
|
const utmInputs = [
|
||||||
|
baseUrlInput,
|
||||||
|
utmSourceInput,
|
||||||
|
utmMediumInput,
|
||||||
|
utmCampaignInput,
|
||||||
|
utmTermInput,
|
||||||
|
utmContentInput
|
||||||
|
];
|
||||||
|
|
||||||
|
utmInputs.forEach(input => {
|
||||||
|
input.addEventListener('input', debounceGenerate);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add custom parameter button
|
||||||
|
addParamBtn.addEventListener('click', addCustomParam);
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
copyBtn.addEventListener('click', copyToClipboard);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounced URL generation
|
||||||
|
*/
|
||||||
|
function debounceGenerate() {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(generateUrl, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom parameter row
|
||||||
|
*/
|
||||||
|
function addCustomParam() {
|
||||||
|
customParamCount++;
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'custom-param-row';
|
||||||
|
row.id = `customParam${customParamCount}`;
|
||||||
|
row.innerHTML = `
|
||||||
|
<input type="text" placeholder="Parameter name" class="custom-key" aria-label="Custom parameter name">
|
||||||
|
<input type="text" placeholder="Value" class="custom-value" aria-label="Custom parameter value">
|
||||||
|
<button type="button" class="btn-remove-param" aria-label="Remove parameter">×</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add input listeners
|
||||||
|
const keyInput = row.querySelector('.custom-key');
|
||||||
|
const valueInput = row.querySelector('.custom-value');
|
||||||
|
keyInput.addEventListener('input', debounceGenerate);
|
||||||
|
valueInput.addEventListener('input', debounceGenerate);
|
||||||
|
|
||||||
|
// Add remove listener
|
||||||
|
const removeBtn = row.querySelector('.btn-remove-param');
|
||||||
|
removeBtn.addEventListener('click', function() {
|
||||||
|
row.remove();
|
||||||
|
debounceGenerate();
|
||||||
|
});
|
||||||
|
|
||||||
|
customParamsContainer.appendChild(row);
|
||||||
|
keyInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate URL format
|
||||||
|
*/
|
||||||
|
function isValidUrl(string) {
|
||||||
|
try {
|
||||||
|
const url = new URL(string);
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all custom parameters
|
||||||
|
*/
|
||||||
|
function getCustomParams() {
|
||||||
|
const params = [];
|
||||||
|
const rows = customParamsContainer.querySelectorAll('.custom-param-row');
|
||||||
|
rows.forEach(row => {
|
||||||
|
const key = row.querySelector('.custom-key').value.trim();
|
||||||
|
const value = row.querySelector('.custom-value').value.trim();
|
||||||
|
if (key && value) {
|
||||||
|
params.push({ key, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the UTM URL
|
||||||
|
*/
|
||||||
|
function generateUrl() {
|
||||||
|
const baseUrl = baseUrlInput.value.trim();
|
||||||
|
const source = utmSourceInput.value.trim();
|
||||||
|
const medium = utmMediumInput.value.trim();
|
||||||
|
const campaign = utmCampaignInput.value.trim();
|
||||||
|
const term = utmTermInput.value.trim();
|
||||||
|
const content = utmContentInput.value.trim();
|
||||||
|
const customParams = getCustomParams();
|
||||||
|
|
||||||
|
// Clear previous state
|
||||||
|
validationMessage.textContent = '';
|
||||||
|
validationMessage.className = 'validation-message';
|
||||||
|
generatedUrlDiv.className = 'generated-url';
|
||||||
|
|
||||||
|
// Check if we have any input at all
|
||||||
|
if (!baseUrl) {
|
||||||
|
generatedUrlDiv.textContent = 'Enter a URL to start generating your tracking URL';
|
||||||
|
generatedUrlDiv.classList.add('empty');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
urlBreakdown.style.display = 'none';
|
||||||
|
currentUrl = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate base URL format
|
||||||
|
if (!isValidUrl(baseUrl)) {
|
||||||
|
generatedUrlDiv.textContent = baseUrl;
|
||||||
|
generatedUrlDiv.classList.add('invalid');
|
||||||
|
showValidation('Enter a valid URL (starting with http:// or https://)', 'error');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
urlBreakdown.style.display = 'none';
|
||||||
|
currentUrl = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the URL with whatever parameters are filled in
|
||||||
|
try {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
|
||||||
|
// Add UTM parameters only if they have values
|
||||||
|
if (source) {
|
||||||
|
url.searchParams.set('utm_source', source);
|
||||||
|
}
|
||||||
|
if (medium) {
|
||||||
|
url.searchParams.set('utm_medium', medium);
|
||||||
|
}
|
||||||
|
if (campaign) {
|
||||||
|
url.searchParams.set('utm_campaign', campaign);
|
||||||
|
}
|
||||||
|
if (term) {
|
||||||
|
url.searchParams.set('utm_term', term);
|
||||||
|
}
|
||||||
|
if (content) {
|
||||||
|
url.searchParams.set('utm_content', content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom parameters
|
||||||
|
customParams.forEach(param => {
|
||||||
|
url.searchParams.set(param.key, param.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
currentUrl = url.toString();
|
||||||
|
generatedUrlDiv.textContent = currentUrl;
|
||||||
|
generatedUrlDiv.classList.add('valid');
|
||||||
|
copyBtn.disabled = false;
|
||||||
|
|
||||||
|
// Show hint about missing recommended fields
|
||||||
|
const missing = [];
|
||||||
|
if (!source) missing.push('source');
|
||||||
|
if (!medium) missing.push('medium');
|
||||||
|
if (!campaign) missing.push('campaign');
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
showValidation(`Tip: Add ${missing.join(', ')} for complete tracking`, 'info');
|
||||||
|
} else {
|
||||||
|
showValidation('URL ready to copy', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update breakdown
|
||||||
|
updateBreakdown(url, customParams);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showValidation('Error generating URL: ' + error.message, 'error');
|
||||||
|
generatedUrlDiv.classList.add('invalid');
|
||||||
|
copyBtn.disabled = true;
|
||||||
|
urlBreakdown.style.display = 'none';
|
||||||
|
currentUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show validation message
|
||||||
|
*/
|
||||||
|
function showValidation(message, type) {
|
||||||
|
validationMessage.textContent = message;
|
||||||
|
validationMessage.className = `validation-message ${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the URL breakdown table
|
||||||
|
*/
|
||||||
|
function updateBreakdown(url, customParams) {
|
||||||
|
urlBreakdown.style.display = 'block';
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
// Base URL
|
||||||
|
rows.push({ param: 'Base URL', value: url.origin + url.pathname });
|
||||||
|
|
||||||
|
// UTM parameters
|
||||||
|
const utmParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
|
||||||
|
utmParams.forEach(param => {
|
||||||
|
const value = url.searchParams.get(param);
|
||||||
|
if (value) {
|
||||||
|
rows.push({ param, value });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Custom parameters
|
||||||
|
customParams.forEach(param => {
|
||||||
|
rows.push({ param: param.key, value: param.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
breakdownBody.innerHTML = rows.map(row => `
|
||||||
|
<tr>
|
||||||
|
<th>${escapeHtml(row.param)}</th>
|
||||||
|
<td>${escapeHtml(row.value)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape HTML to prevent XSS
|
||||||
|
*/
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = text;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy URL to clipboard
|
||||||
|
*/
|
||||||
|
async function copyToClipboard() {
|
||||||
|
if (!currentUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(currentUrl);
|
||||||
|
copyFeedback.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyFeedback.classList.remove('show');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = currentUrl;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.left = '-9999px';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
copyFeedback.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
copyFeedback.classList.remove('show');
|
||||||
|
}, 2000);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to copy:', e);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "docker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"generate-pdfs": "node generate-pdfs.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"puppeteer": "^21.0.0"
|
||||||
|
},
|
||||||
|
"puppeteer": {
|
||||||
|
"skipChromiumDownload": true
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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 - Business Development Resume: Revenue growth, client relations, and entrepreneurship experience.">
|
||||||
|
<title>Business Development - Colin Knapp Resume</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p class="resume-focus"><strong>Focus:</strong> Business Development & Revenue Growth</p>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<section class="contact-info">
|
||||||
|
<p>
|
||||||
|
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer">Contact via MotherboardRepair.ca</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Professional Summary -->
|
||||||
|
<section class="summary">
|
||||||
|
<h2>Professional Summary</h2>
|
||||||
|
<p>
|
||||||
|
Results-driven entrepreneur and business development professional with a proven track record of
|
||||||
|
bootstrapping ventures from zero to significant revenue. Experienced in client relations,
|
||||||
|
strategic partnerships, and transforming struggling businesses into profitable operations.
|
||||||
|
Strong background in mentoring executives and scaling operations across multiple industries.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Key Achievements -->
|
||||||
|
<section class="highlights">
|
||||||
|
<h2>Key Achievements</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Revenue Growth:</strong>
|
||||||
|
Bootstrapped <a href="../stories/nitric-leadership.html">Nitric Concepts</a> from zero to
|
||||||
|
an estimated $4M gross revenue with strong profit margins (2018-2021).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Executive Mentorship:</strong>
|
||||||
|
Provided strategic guidance to CEO/Founder Andrew Karvelis, helping scale from side project
|
||||||
|
earnings to significant revenue growth and financial success.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Business Turnaround:</strong>
|
||||||
|
Revitalized <a href="../stories/athion-turnaround.html">Athion.net</a>, transforming a
|
||||||
|
struggling business into a self-sustaining, profitable operation within two weeks (2013-2017).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Client Portfolio:</strong>
|
||||||
|
Built and maintained relationships with government clients, healthcare organizations,
|
||||||
|
and YouTube celebrities including partnerships with 15+ million subscriber networks.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Sustainable Venture:</strong>
|
||||||
|
Co-founded <a href="../stories/motherboard-repair.html">MotherboardRepair.ca</a>,
|
||||||
|
establishing a company focused on sustainable electronics solutions (2019-Present).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Employment History -->
|
||||||
|
<section class="employment">
|
||||||
|
<h2>Relevant Experience</h2>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Chief of Operations / VP / Co-Founder</h3>
|
||||||
|
<p class="company">Nitric Concepts</p>
|
||||||
|
<p class="timeframe">2018-2021</p>
|
||||||
|
<ul>
|
||||||
|
<li>Bootstrapped company from zero to an estimated $4M gross revenue with strong profit margins</li>
|
||||||
|
<li>Managed talent acquisition and contractor relationships for 45+ personnel across multiple timezones</li>
|
||||||
|
<li>Provided executive mentorship to CEO/Founder, guiding strategic business decisions</li>
|
||||||
|
<li>Established partnerships enabling collaboration with Microsoft for official Minecraft Marketplace content</li>
|
||||||
|
<li>Utilized Kanban/Trello to coordinate distributed teams and optimize project workflows</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Founder</h3>
|
||||||
|
<p class="company">ViperWire.ca</p>
|
||||||
|
<p class="timeframe">2023-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Building an AI-powered cybersecurity consultancy targeting SMB market</li>
|
||||||
|
<li>Developing scalable security solutions accessible to small and medium businesses</li>
|
||||||
|
<li>Establishing consulting framework for enterprise-grade security delivery</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Co-Founder</h3>
|
||||||
|
<p class="company">MotherboardRepair.ca</p>
|
||||||
|
<p class="timeframe">2019-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Co-founded company focused on reducing e-waste through circuit board repairs</li>
|
||||||
|
<li>Leveraged industry expertise to establish sustainable tech solutions business</li>
|
||||||
|
<li>Developed repair processes for high-value circuit board components</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Business Operations</h3>
|
||||||
|
<p class="company">Athion.net</p>
|
||||||
|
<p class="timeframe">2013-2017</p>
|
||||||
|
<ul>
|
||||||
|
<li>Revitalized struggling business into self-sustaining operation within two weeks</li>
|
||||||
|
<li>Optimized systems and streamlined operations with rapid, effective solutions</li>
|
||||||
|
<li>Created foundational technology that enabled future open source success</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Client Development</h3>
|
||||||
|
<p class="company">YouTube & MCN Partnerships</p>
|
||||||
|
<p class="timeframe">2011-2022</p>
|
||||||
|
<ul>
|
||||||
|
<li>Partnered with Jordan Matthewson (Kootra) and TheCreatures MCN with 15 million subscriber reach</li>
|
||||||
|
<li>Collaborated with prominent YouTube creators during the platform's golden era</li>
|
||||||
|
<li>Delivered custom gaming experiences for celebrity partnerships and livestreams</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Skills -->
|
||||||
|
<section class="skills">
|
||||||
|
<h2>Core Competencies</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Business Development:</strong> Revenue growth, client acquisition, partnership development</li>
|
||||||
|
<li><strong>Leadership:</strong> Executive mentorship, team management, strategic planning</li>
|
||||||
|
<li><strong>Operations:</strong> Process optimization, workflow management, Kanban/Agile methodologies</li>
|
||||||
|
<li><strong>Entrepreneurship:</strong> Venture bootstrapping, business turnarounds, sustainable growth</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section class="portfolio-link">
|
||||||
|
<p><a href="/">View Full Portfolio →</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<!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 - DevSecOps Resume: Infrastructure, security automation, and compliance expertise.">
|
||||||
|
<title>DevSecOps - Colin Knapp Resume</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p class="resume-focus"><strong>Focus:</strong> DevSecOps & Security Infrastructure</p>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<section class="contact-info">
|
||||||
|
<p>
|
||||||
|
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer">Contact via MotherboardRepair.ca</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Professional Summary -->
|
||||||
|
<section class="summary">
|
||||||
|
<h2>Professional Summary</h2>
|
||||||
|
<p>
|
||||||
|
DevSecOps consultant with extensive experience architecting secure, resilient infrastructure
|
||||||
|
for government and healthcare clients. Expertise in compliance standards (WCAG 2.0 AA, CIS Level 1/2),
|
||||||
|
security automation, and building geographically redundant systems. Proven track record of
|
||||||
|
implementing Docker-based solutions and automated security tooling for enterprise environments.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Key Achievements -->
|
||||||
|
<section class="highlights">
|
||||||
|
<h2>Key Achievements</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Government Infrastructure:</strong>
|
||||||
|
Delivered WCAG 2.0 AA compliant learning management systems for US government clients
|
||||||
|
through <a href="../stories/airport-dns.html">Addis Enterprises</a> (2019-Present).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>DNS Resilience:</strong>
|
||||||
|
Architected geographically redundant DNS cluster for
|
||||||
|
<a href="../stories/airport-dns.html">Bishop Airport</a> achieving A+ standard,
|
||||||
|
capable of withstanding extreme disruptions including nuclear scenarios.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Security Automation:</strong>
|
||||||
|
Created Docker-based utility for automated
|
||||||
|
<a href="../stories/wordpress-security.html">WordPress malware removal</a>,
|
||||||
|
reducing infection frequency from daily to zero (2023).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Healthcare Compliance:</strong>
|
||||||
|
Implemented CIS Level 1 and 2 security standards for
|
||||||
|
<a href="../stories/healthcare-platform.html">Improving MI Practices</a>
|
||||||
|
healthcare education platform.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>CI/CD Infrastructure:</strong>
|
||||||
|
Established Jenkins CI/CD pipeline in 2013 that continues running the IntellectualSites
|
||||||
|
open source ecosystem supporting a $5 billion gaming brand.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Employment History -->
|
||||||
|
<section class="employment">
|
||||||
|
<h2>Relevant Experience</h2>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>DevSecOps Consultant</h3>
|
||||||
|
<p class="company">Addis Enterprises</p>
|
||||||
|
<p class="timeframe">2019-Present</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>
|
||||||
|
<li>Designed secure, scalable infrastructure for healthcare education content delivery</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Founder</h3>
|
||||||
|
<p class="company">ViperWire.ca</p>
|
||||||
|
<p class="timeframe">2023-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Building AI-powered cybersecurity consultancy for enterprise-grade security</li>
|
||||||
|
<li>Developing AI-augmented security analysis systems</li>
|
||||||
|
<li>Creating scalable security solutions for SMBs</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Chief of Operations / VP / Co-Founder</h3>
|
||||||
|
<p class="company">Nitric Concepts</p>
|
||||||
|
<p class="timeframe">2018-2021</p>
|
||||||
|
<ul>
|
||||||
|
<li>Implemented DevSecOps practices using Docker, Fail2Ban, and Salt Stack for high-traffic gaming environments</li>
|
||||||
|
<li>Established CI/CD pipelines and security practices for enterprise-scale gaming infrastructure</li>
|
||||||
|
<li>Managed security for distributed team operations across multiple timezones</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Infrastructure Engineer</h3>
|
||||||
|
<p class="company">Self-Hosted Infrastructure</p>
|
||||||
|
<p class="timeframe">2020-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Built comprehensive home infrastructure cluster using repurposed MacMini hardware for complete data sovereignty</li>
|
||||||
|
<li>Self-hosted critical infrastructure including email, DNS, and over 100 additional services</li>
|
||||||
|
<li>Developed WireGuard mesh networking tool for quantum-resistant networking deployment</li>
|
||||||
|
<li>Regularly run Woodpecker CI and Gitea for on-premise source management, testing, and deployment</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Technical Skills -->
|
||||||
|
<section class="skills">
|
||||||
|
<h2>Technical Skills</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Containerization:</strong> Docker, Docker Swarm, container orchestration</li>
|
||||||
|
<li><strong>CI/CD:</strong> Jenkins, Woodpecker CI, GitLab CI/CD, automated deployment pipelines</li>
|
||||||
|
<li><strong>Security:</strong> Fail2Ban, malware detection/removal, security hardening, penetration testing</li>
|
||||||
|
<li><strong>Infrastructure:</strong> Salt Stack, DNS (BIND, PowerDNS), WireGuard, bare-metal servers</li>
|
||||||
|
<li><strong>Compliance:</strong> WCAG 2.0 AA, CIS Level 1/2, government security standards</li>
|
||||||
|
<li><strong>Monitoring:</strong> High availability systems, automated monitoring, incident response</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Projects -->
|
||||||
|
<section class="projects">
|
||||||
|
<h2>Notable Projects</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="../stories/airport-dns.html">Bishop Airport DNS Infrastructure</a> - Geographically redundant A+ rated DNS cluster</li>
|
||||||
|
<li><a href="../stories/healthcare-platform.html">Improving MI Practices</a> - HIPAA-aware healthcare education platform</li>
|
||||||
|
<li><a href="../stories/wordpress-security.html">WordPress Security Automation</a> - Docker-based malware eradication tool</li>
|
||||||
|
<li><a href="../stories/nuclear-dns.html">Nuclear-Resistant DNS</a> - Extreme resilience infrastructure design</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section class="portfolio-link">
|
||||||
|
<p><a href="/">View Full Portfolio →</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,443 @@
|
||||||
|
<!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="DevSecOps consultant and cybersecurity expert. Led teams of 45+, built open-source tools with 10M+ downloads, and architected resilient infrastructure for government and healthcare clients.">
|
||||||
|
<title>Full Portfolio - Colin Knapp Resume</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p class="resume-focus"><strong>Full Portfolio</strong></p>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<section class="contact-info">
|
||||||
|
<p>
|
||||||
|
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer">Contact via MotherboardRepair.ca</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<em>Note: MotherboardRepair.ca offers a wide range of services and is currently my main focus. Please use the contact form there for all inquiries.</em>
|
||||||
|
</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 →</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 →</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 →</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 →</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>Idled 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 →</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 →</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 significant revenue growth and financial success.</p>
|
||||||
|
<p><a href="../stories/nitric-leadership.html" class="read-more">Read more about my leadership at Nitric Concepts →</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 →</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 →</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 →</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 →</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 →</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>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
<!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 - Team Leadership Resume: Management, mentorship, and distributed team coordination experience.">
|
||||||
|
<title>Team Leadership - Colin Knapp Resume</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p class="resume-focus"><strong>Focus:</strong> Team Leadership & Management</p>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<section class="contact-info">
|
||||||
|
<p>
|
||||||
|
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer">Contact via MotherboardRepair.ca</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Professional Summary -->
|
||||||
|
<section class="summary">
|
||||||
|
<h2>Professional Summary</h2>
|
||||||
|
<p>
|
||||||
|
Experienced leader with a proven track record of managing distributed teams of 45+ personnel
|
||||||
|
across multiple timezones. Strong background in executive mentorship, fostering collaboration,
|
||||||
|
and implementing efficient workflows using Kanban/Agile methodologies. Skilled at building
|
||||||
|
high-performing teams and guiding organizations from startup to significant revenue growth.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Key Achievements -->
|
||||||
|
<section class="highlights">
|
||||||
|
<h2>Key Achievements</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Large Team Management:</strong>
|
||||||
|
Managed a distributed team of 45 personnel at
|
||||||
|
<a href="../stories/nitric-leadership.html">Nitric Concepts</a>,
|
||||||
|
fostering collaboration across multiple timezones (2018-2021).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Executive Mentorship:</strong>
|
||||||
|
Provided strategic guidance to CEO/Founder Andrew Karvelis, helping scale from side project
|
||||||
|
earnings to $4M gross revenue with strong profit margins.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Open Source Leadership:</strong>
|
||||||
|
Led and coordinated global contributor community for
|
||||||
|
<a href="../stories/fawe-plotsquared.html">PlotSquared and FastAsyncWorldEdit</a>,
|
||||||
|
birthing a sustainable 10-year open source brand that continues today with paid maintainers.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Project Coordination:</strong>
|
||||||
|
Utilized Kanban/Trello to coordinate distributed teams and project workflows,
|
||||||
|
enabling efficient collaboration across multiple projects and timezones.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Talent Development:</strong>
|
||||||
|
Managed software development and talent acquisition for contractors worldwide,
|
||||||
|
building effective remote-first team culture.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Employment History -->
|
||||||
|
<section class="employment">
|
||||||
|
<h2>Leadership Experience</h2>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Chief of Operations / VP / Co-Founder</h3>
|
||||||
|
<p class="company">Nitric Concepts</p>
|
||||||
|
<p class="timeframe">2018-2021</p>
|
||||||
|
<ul>
|
||||||
|
<li>Managed 45+ contractors worldwide across multiple timezones</li>
|
||||||
|
<li>Provided executive mentorship to CEO/Founder Andrew Karvelis</li>
|
||||||
|
<li>Bootstrapped company operations from zero to $4M gross revenue</li>
|
||||||
|
<li>Utilized Kanban/Trello to coordinate distributed teams and project workflows</li>
|
||||||
|
<li>Established collaborative relationships with Microsoft for official Minecraft Marketplace partnership</li>
|
||||||
|
<li>Built and maintained remote-first team culture for high-performance delivery</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Open Source Project Lead</h3>
|
||||||
|
<p class="company">IntellectualSites (PlotSquared & FastAsyncWorldEdit)</p>
|
||||||
|
<p class="timeframe">2013-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Co-created and led development of major open source projects with 10M+ downloads</li>
|
||||||
|
<li>Coordinated global contributor community spanning multiple countries</li>
|
||||||
|
<li>Established Jenkins CI/CD infrastructure in 2013 that continues running today</li>
|
||||||
|
<li>Built sustainable open source brand with paid maintainers continuing the legacy</li>
|
||||||
|
<li>Created tooling used by content production companies in the $5 billion Minecraft ecosystem</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Founder</h3>
|
||||||
|
<p class="company">ViperWire.ca</p>
|
||||||
|
<p class="timeframe">2023-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Founded AI-powered cybersecurity consultancy</li>
|
||||||
|
<li>Establishing consulting framework and team structure for scaling</li>
|
||||||
|
<li>Building client relationships and service delivery processes</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Co-Founder</h3>
|
||||||
|
<p class="company">MotherboardRepair.ca</p>
|
||||||
|
<p class="timeframe">2019-Present</p>
|
||||||
|
<ul>
|
||||||
|
<li>Co-founded sustainable electronics repair company</li>
|
||||||
|
<li>Established operational processes and team workflows</li>
|
||||||
|
<li>Built partnerships for component sourcing and service delivery</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="position">
|
||||||
|
<h3>Project Lead</h3>
|
||||||
|
<p class="company">Oh My Form (Open Source)</p>
|
||||||
|
<p class="timeframe">2020-2024</p>
|
||||||
|
<ul>
|
||||||
|
<li>Led development of privacy-focused form builder with 1.5M+ Docker pulls</li>
|
||||||
|
<li>Coordinated contributor community and managed project roadmap</li>
|
||||||
|
<li>Successfully sunset project and transitioned users to Formbricks</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Leadership Skills -->
|
||||||
|
<section class="skills">
|
||||||
|
<h2>Leadership Competencies</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Team Management:</strong> Distributed team leadership, remote-first culture, cross-timezone coordination</li>
|
||||||
|
<li><strong>Mentorship:</strong> Executive coaching, talent development, career guidance</li>
|
||||||
|
<li><strong>Project Management:</strong> Kanban, Trello, Agile methodologies, workflow optimization</li>
|
||||||
|
<li><strong>Communication:</strong> Stakeholder management, client relations, technical documentation</li>
|
||||||
|
<li><strong>Strategic Planning:</strong> Business development, operational scaling, partnership building</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Team Metrics -->
|
||||||
|
<section class="metrics">
|
||||||
|
<h2>Leadership Metrics</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Team Size:</strong> Managed up to 45 contractors simultaneously</li>
|
||||||
|
<li><strong>Revenue Impact:</strong> Guided growth from $0 to $4M gross revenue</li>
|
||||||
|
<li><strong>Project Scale:</strong> Open source tools with 10M+ downloads</li>
|
||||||
|
<li><strong>Community:</strong> Global contributor network spanning 10+ years</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section class="portfolio-link">
|
||||||
|
<p><a href="/">View Full Portfolio →</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
<!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 Building Resume: Open source development, CI/CD, and custom software solutions.">
|
||||||
|
<title>Tool Building - Colin Knapp Resume</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4="></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<h1>Colin Knapp</h1>
|
||||||
|
<p class="resume-focus"><strong>Focus:</strong> Tool Building & Software Development</p>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<section class="contact-info">
|
||||||
|
<p>
|
||||||
|
<strong>Location:</strong> Kitchener-Waterloo, Ontario, Canada<br>
|
||||||
|
<strong>Contact:</strong>
|
||||||
|
<a href="https://motherboardrepair.ca/contact.html" target="_blank" rel="noopener noreferrer">Contact via MotherboardRepair.ca</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Professional Summary -->
|
||||||
|
<section class="summary">
|
||||||
|
<h2>Professional Summary</h2>
|
||||||
|
<p>
|
||||||
|
Prolific software developer and tool builder with a track record of creating impactful open source
|
||||||
|
projects achieving 10M+ downloads. Expertise in building performance-critical applications,
|
||||||
|
CI/CD infrastructure, and custom automation tools. Strong background in Java, Python, Docker,
|
||||||
|
and modern web technologies with a passion for solving complex technical challenges.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Key Achievements -->
|
||||||
|
<section class="highlights">
|
||||||
|
<h2>Key Achievements</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Open Source Impact:</strong>
|
||||||
|
Co-created <a href="../stories/fawe-plotsquared.html">FastAsyncWorldEdit</a> and
|
||||||
|
<a href="../stories/fawe-plotsquared.html">PlotSquared</a>, enabling massive transformative
|
||||||
|
edits—scaling from 50,000 server-crashing edits to billions without interruption.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Download Scale:</strong>
|
||||||
|
Built tools powering a $2 billion game brand with 10M+ downloads and global contributor support.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>CI/CD Pioneer:</strong>
|
||||||
|
Established Jenkins CI/CD infrastructure in 2013 that continues running the IntellectualSites
|
||||||
|
open source ecosystem today.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Security Tooling:</strong>
|
||||||
|
Created Docker-based utility for automated
|
||||||
|
<a href="../stories/wordpress-security.html">WordPress malware removal</a> and hardening,
|
||||||
|
reducing infection frequency from daily to zero.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Privacy-Focused Development:</strong>
|
||||||
|
Led development of <a href="../stories/open-source-success.html">OhMyForm</a>,
|
||||||
|
an open source Google Forms alternative achieving 1.5M+ Docker pulls.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Major Projects -->
|
||||||
|
<section class="projects">
|
||||||
|
<h2>Notable Tools & Projects</h2>
|
||||||
|
|
||||||
|
<article class="project">
|
||||||
|
<h3><a href="../stories/fawe-plotsquared.html">FastAsyncWorldEdit</a></h3>
|
||||||
|
<p class="stats">664+ GitHub stars | Performance-critical Minecraft world manipulation</p>
|
||||||
|
<ul>
|
||||||
|
<li>Improved world manipulation performance enabling billions of seamless edits</li>
|
||||||
|
<li>Implemented security improvements and performance optimizations for large-scale operations</li>
|
||||||
|
<li>Created async architecture to prevent server crashes during massive operations</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="project">
|
||||||
|
<h3><a href="../stories/fawe-plotsquared.html">PlotSquared</a></h3>
|
||||||
|
<p class="stats">572+ GitHub stars | 809+ forks | Land management system</p>
|
||||||
|
<ul>
|
||||||
|
<li>Co-developed comprehensive land management plugin for Minecraft servers</li>
|
||||||
|
<li>Built permission system and plot claiming mechanics for millions of users</li>
|
||||||
|
<li>Established sustainable open source project with paid maintainers</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="project">
|
||||||
|
<h3><a href="../stories/open-source-success.html">OhMyForm</a></h3>
|
||||||
|
<p class="stats">1.5M+ Docker pulls | Privacy-focused form builder</p>
|
||||||
|
<ul>
|
||||||
|
<li>Led development of open source alternative to Google Forms and Office 365</li>
|
||||||
|
<li>Enabled data sovereignty and privacy-focused form building for organizations</li>
|
||||||
|
<li>Successfully sunset project and transitioned users to Formbricks</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="project">
|
||||||
|
<h3>WireGuard Mesh Networking Tool</h3>
|
||||||
|
<p class="stats">Infrastructure automation | Quantum-resistant networking</p>
|
||||||
|
<ul>
|
||||||
|
<li>Developed tool to simplify deployment of mesh networks vs traditional hub-and-spoke</li>
|
||||||
|
<li>Created infrastructure tooling for quantum-resistant networking solutions</li>
|
||||||
|
<li>Built automation for complex multi-node VPN configurations</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="project">
|
||||||
|
<h3><a href="../stories/wordpress-security.html">WordPress Malware Eradication Tool</a></h3>
|
||||||
|
<p class="stats">Docker-based | Security automation</p>
|
||||||
|
<ul>
|
||||||
|
<li>Created Docker-based utility for automated malware detection and removal</li>
|
||||||
|
<li>Implemented hardening measures to prevent reinfection</li>
|
||||||
|
<li>Deployed to protect client sites from persistent cyber attacks</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="project">
|
||||||
|
<h3><a href="../stories/app-development.html">Ad Marketing Link Tracking Tool</a></h3>
|
||||||
|
<p class="stats">Full-stack development | Creator monetization</p>
|
||||||
|
<ul>
|
||||||
|
<li>Built comprehensive ad campaign management system for content creators</li>
|
||||||
|
<li>Designed real-time revenue monitoring and campaign performance tracking</li>
|
||||||
|
<li>Implemented secure payment processing and data handling</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Technical Skills -->
|
||||||
|
<section class="skills">
|
||||||
|
<h2>Technical Skills</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Languages:</strong> Java, Python, JavaScript/TypeScript, Bash, SQL</li>
|
||||||
|
<li><strong>CI/CD:</strong> Jenkins, Woodpecker CI, GitLab CI/CD, GitHub Actions</li>
|
||||||
|
<li><strong>Containerization:</strong> Docker, Docker Swarm, container orchestration</li>
|
||||||
|
<li><strong>Infrastructure:</strong> Salt Stack, WireGuard, DNS, bare-metal servers</li>
|
||||||
|
<li><strong>Web Technologies:</strong> HTML5, CSS3, Node.js, REST APIs</li>
|
||||||
|
<li><strong>Version Control:</strong> Git, Gitea, GitHub, GitLab</li>
|
||||||
|
<li><strong>Testing:</strong> Unit testing, security scanning, automated QA</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<!-- Open Source Contributions -->
|
||||||
|
<section class="contributions">
|
||||||
|
<h2>Open Source Contributions</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/IntellectualSites/PlotSquared" target="_blank" rel="noopener noreferrer">PlotSquared</a> - Land management system</li>
|
||||||
|
<li><a href="https://github.com/IntellectualSites/FastAsyncWorldEdit" target="_blank" rel="noopener noreferrer">FastAsyncWorldEdit</a> - High-performance world editing</li>
|
||||||
|
<li><a href="https://github.com/OhMyForm/OhMyForm" target="_blank" rel="noopener noreferrer">OhMyForm</a> - Privacy-focused form builder (sunset)</li>
|
||||||
|
<li><a href="https://github.com/IntellectualSites/plothider" target="_blank" rel="noopener noreferrer">PlotHider</a> - Plot visibility management</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<section class="portfolio-link">
|
||||||
|
<p><a href="/">View Full Portfolio →</a></p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Include -->
|
||||||
|
<div id="footer-include"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/index.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/one-pager-tools/csv-tool.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/one-pager-tools/utm-tool.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/resumes/business-development.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/resumes/devsecops.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/resumes/portfolio.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/resumes/team-leadership.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/resumes/tool-building.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/airport-dns.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/app-development.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/athion-turnaround.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/fawe-plotsquared.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/healthcare-platform.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/home-infrastructure.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/index.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/motherboard-repair.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/nitric-leadership.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/nuclear-dns.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/open-source-success.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/scansnap-webdav.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/showerloop.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/viperwire.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/web-design-java.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/wordpress-security.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://colinknapp.com/stories/youtube-game-dev.html</loc>
|
||||||
|
<lastmod>2025-12-02T18:18:20+00:00</lastmod>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
|
|
@ -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="Architected geographically redundant DNS infrastructure for Flint Bishop International Airport, achieving A+ resilience standards for critical aviation infrastructure.">
|
||||||
|
<title>Airport DNS Infrastructure - Resilient DNS Design</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# airport dns
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<!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="Developed ad revenue tracking application for content creators with real-time analytics, secure API integrations, and data-driven content optimization tools.">
|
||||||
|
<title>Ad Revenue Tracking App for Influencers</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# app development
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<!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="Revitalized struggling business into profitable operation in two weeks, developing revolutionary open-source tooling that powered major Minecraft projects.">
|
||||||
|
<title>Athion.net Business Turnaround - Colin Knapp</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# athion turnaround
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -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="Co-created FastAsyncWorldEdit and PlotSquared, scaling Minecraft world editing from 50K server-crashing edits to billions seamlessly, powering a $2B game brand.">
|
||||||
|
<title>FastAsyncWorldEdit & PlotSquared - Minecraft Tools</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# fawe plotsquared
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -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="Designed secure, scalable infrastructure for Improving MI Practices healthcare education platform with CIS Level 1-2 security standards and high availability.">
|
||||||
|
<title>Healthcare Platform Infrastructure - Secure Design</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# healthcare platform
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -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="Built self-hosted infrastructure cluster with 100+ services for data sovereignty and developed WireGuard mesh networking tool for quantum-resistant networking.">
|
||||||
|
<title>Home Infrastructure & WireGuard Mesh Networking</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<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-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# home infrastructure
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
<!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="Explore detailed case studies and project stories from Colin Knapp's portfolio: cybersecurity, infrastructure, open source development, and team leadership.">
|
||||||
|
<title>Project Stories & Case Studies - Colin Knapp</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>ScanSnap Scanner Service for buildersclub.ca</h2>
|
||||||
|
<p class="story-excerpt">Detailed case study and project information.</p>
|
||||||
|
<p class="story-meta">Category: Project | Date: 2025-Present</p>
|
||||||
|
<a href="scansnap-webdav.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>
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<!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="Co-founded sustainable electronics repair company reducing e-waste through circuit board repairs and promoting environmental responsibility in tech industry.">
|
||||||
|
<title>MotherboardRepair.ca - Sustainable Tech Solutions</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# motherUoard repair
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -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="Led distributed team of 45 contractors at Nitric Concepts, bootstrapping company to $4M revenue while implementing DevSecOps practices across timezones.">
|
||||||
|
<title>Nitric Concepts Leadership - Team Management</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=" crossorigin="anonymous">
|
||||||
|
<script src="../theme.js" integrity="sha256-+dDNTo7WAOmn2YC875+vn9oH4UkMwlVOGlARp2uq3A4=" crossorigin="anonymous"></script>
|
||||||
|
<script src="../includes.js" integrity="sha256-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE=" 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" id="main-content">
|
||||||
|
<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>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# nitric leadership
|
||||||
|
|
||||||
|
> Draft placeholder. Content to be written.
|
||||||
|
|
||||||
|
- Summary: TBD
|
||||||
|
- Key outcomes: TBD
|
||||||
|
- Tech stack: TBD
|
||||||
|
- Challenges: TBD
|
||||||
|
- Results: TBD
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
<!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="Architected georedundant, nuclear war-resistant DNS cluster for government clients, ensuring extreme resilience and A+ security standards for critical infrastructure.">
|
||||||
|
<title>Nuclear War-Resistant DNS Infrastructure</title>
|
||||||
|
<link rel="stylesheet" href="../styles.css" integrity="sha256-tLnsiikQm2NSRAs2kbQC0RXVKxDn7vYaJnjcn1K9cFY=">
|
||||||
|
<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-9xjDTj+I8+VR4H5siP5/kVeCSZ/FLBk/sPCClevm4PE="></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header Include -->
|
||||||
|
<div id="header-include"></div>
|
||||||
|
|
||||||
|
<div class="container-fluid" role="main" id="main-content">
|
||||||
|
<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>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue