forked from colin/resume
2
0
Fork 0

Compare commits

..

148 Commits
main ... main

Author SHA1 Message Date
Colin 87328de610
Force CI update
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-12-11 14:16:00 -05:00
Colin 32636fa0cb
Auto-update generated PDFs
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-11 14:14:02 -05:00
Colin cfaa727b88
Trigger CI pipeline 2025-12-11 14:11:56 -05:00
Colin 6d2c7dc5f1
Auto-update generated PDFs
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-12-11 13:19:49 -05:00
Colin b60d3a89ca
Increase PDF generation timeout and use networkidle2 2025-12-11 13:18:16 -05:00
Colin 51ac30cbc4
Auto-update generated PDFs 2025-12-11 13:18:16 -05:00
Colin f27f505281
Add PostHog analytics script to header 2025-12-11 13:18:16 -05:00
Leopere b862b5f234
Auto-update navigation menu from sitemap using script
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/tag/woodpecker Pipeline was successful Details
2025-12-06 18:25:49 -05:00
Leopere 9d4e2a0d5f
Correctly mark all written stories as bold in navigation
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-06 18:22:23 -05:00
Leopere e858606a0f
Update navigation bolding to only highlight specific, meaningful stories
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-06 18:11:41 -05:00
Leopere 92d5ed0801
Fix favicon, resolve merge conflict, and remove markdown-loader from completed stories 2025-12-06 17:51:41 -05:00
Colin fcd62e071a
Auto-update generated PDFs
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-12-02 13:19:40 -05:00
Colin b4b7dcb079
Update navigation to bold written stories and use normal font for TBD 2025-12-02 13:18:29 -05:00
Colin 374d29018a
Auto-update generated PDFs
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-02 13:16:33 -05:00
Colin cb409efb34
Update pre-push hook to auto-commit and push generated PDFs 2025-12-02 13:15:15 -05:00
Colin 70085f5258
Exclude tools and utility files from PDF generation
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-02 13:13:43 -05:00
Colin c092d07783
Exclude includes directory from PDF generation
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-12-02 12:31:34 -05:00
Colin 3c03708951
Regenerate PDFs and update pre-push hook to always generate
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-02 12:28:57 -05:00
Colin 9165562b2a
bump
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-12-02 11:29:42 -05:00
Colin 5b422a67b2
Add UTM URL Generator tool with live preview and pre-generated PDFs
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Add utm-tool.html and utm-tool.js for UTM URL generation
- Live URL generation as user types (no button required)
- Support for all standard UTM parameters plus custom parameters
- Add UTM Builder to nav dropdown and index.html tools section
- Pre-generate all PDFs for faster Docker builds
- Update git hooks for PDF generation
2025-12-02 11:18:49 -05:00
Colin 7a3665abae
bump 2025-12-02 09:20:41 -05:00
Colin 73c4533926
Fix PDF generation: wait for includes to load and improve error handling
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-12-01 14:49:58 -05:00
Colin 1ba37bbd75
Simplify favicon: use only SVG, remove ICO/PNG files and hardcoded links
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-12-01 13:42:10 -05:00
Colin 457ca26983
Add colored asterisks to navigation for story completion status
ci/woodpecker/push/woodpecker Pipeline failed Details
- Add red asterisks (*) for unwritten stories in navigation
- Add green asterisks (*) for written stories in navigation
- Update CSS with specific selectors and !important flags for visibility
- Update integrity hashes for styles.css across all HTML files
2025-11-30 23:09:37 -05:00
Colin 049db7424f
bump
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-11-30 21:30:49 -05:00
Colin 85ddbf561d
Add targeted resume extracts for business development, devsecops, team leadership, and tool building
ci/woodpecker/push/woodpecker Pipeline failed Details
- Created 4 focused resume pages in /resumes/ directory
- Added Resumes dropdown to navigation menu
- Updated sitemap.xml to include new resume pages
2025-11-30 21:02:37 -05:00
Colin 8b1a1e23bf
Fix Puppeteer Chrome launch in Alpine Linux Docker
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Install Chromium and required system libraries (nss, freetype, harfbuzz, etc.)
- Set PUPPETEER_SKIP_CHROMIUM_DOWNLOAD to use system Chromium
- Add additional Chrome args for better compatibility in container
- Configure package.json to skip Chromium download
- Fixes 'Failed to launch the browser process' error
2025-11-30 16:40:29 -05:00
Colin 19182d6d21
Add PDF generation to Docker build process
ci/woodpecker/push/woodpecker Pipeline failed Details
- Update Dockerfile to install npm and run PDF generation during build
- Move generate-pdfs.js and package.json to docker/resume/ for Docker context
- Update generate-pdfs.js to work from /srv directory in container
- PDFs are now generated automatically during Docker build before deployment
- PDFs will be available at /pdfs/ path matching HTML file structure
2025-11-30 16:11:30 -05:00
Colin 83c0ae74f7
Add PDF generation for static site pages
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Add generate-pdfs.js script using Puppeteer to render HTML pages to PDF
- Update package.json with puppeteer dependency and npm script
- Add dynamic PDF download link to footer (checks if PDF exists, shows link)
- Add docker/resume/pdfs/ to .gitignore (generated at deploy time)

Run 'npm install && npm run generate-pdfs' during deployment
2025-11-30 15:46:21 -05:00
Colin 73b83fc3fd
Add Meet navigation button for web meetings tool
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added Meet link to main navigation pointing to meet.colinknapp.com
- Includes title attribute describing it as a no-account web meetings tool
- Opens in new tab with proper security attributes
2025-11-28 16:09:49 -05:00
Colin 1c1bb4ebb9
Update Matomo tracking code to enable full tracking features
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Simplified Matomo code in header.html to use standard tracking configuration
- Removed tracking-limiting settings (setDocumentTitle, setCookieDomain, setDomains, disableCookies)
- Fixed matomo.js script source to load from metrics.nixc.us domain
- All pages using includes system now have full Matomo tracking enabled
2025-11-18 14:49:49 -05:00
Colin f29321803e
Fix contact section: remove awkward colinknapp.com link
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-11-18 14:35:05 -05:00
Colin fda73c6638
Update contact section: remove email/Calendly, direct to MotherboardRepair.ca contact form
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-11-18 14:28:47 -05:00
Colin db17d83418
Update favicon to boot-logo.svg and prioritize SVG in favicon links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-11-18 14:23:14 -05:00
Colin 14b03869af
Optimize SEO meta tags and update sitemap for production
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Updated meta descriptions and titles for all HTML pages with concise, keyword-rich content
- Optimized main pages (index.html, stories/index.html) with compelling descriptions
- Enhanced all 15 story pages with unique, SEO-optimized meta tags
- Improved CSV tool page SEO description
- Updated generate-sitemap.sh to use production domain (https://colinknapp.com)
- Regenerated sitemap.xml with production URLs and current timestamps
- Verified and updated all integrity hashes using update-csp-hashes.sh
2025-11-18 14:11:22 -05:00
Colin f7f7bcf7dc
Add NixOS Validator link to header navigation
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-11-18 11:17:09 -05:00
Leopere f4e36517fb
Update CSP integrity hashes for styles.css
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-27 14:34:01 -04:00
Leopere 3c91e05ca3
Simplify consulting packs page: move common info to top, simplify pack descriptions
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-27 14:12:02 -04:00
Leopere c5d81426ec
Update consulting packs with prepaid discount info and enhanced AI services
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-27 14:03:25 -04:00
Leopere 7b036a1a58
Add unlisted consulting packs page with Stripe URLs
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-27 13:51:51 -04:00
Leopere 384fb993f5
Add skip-to-content accessibility link to header nav
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added skip-to-content link at top of header partial
- Added CSS styling for hidden-by-default, visible-on-focus behavior
- Added id='main-content' to all HTML pages for skip link target
- Improves keyboard navigation and screen reader accessibility
2025-10-21 10:46:00 -04:00
colin fa41ef711f Update docker/resume/stories/scansnap-webdav.html
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-19 17:03:49 -04:00
Leopere bb48db67e7
Add Windows instructions to ScanSnap page and simplify language
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added Windows File Explorer connection instructions
- Changed technical terms to simpler language (e.g., 'web file server' instead of 'network endpoint')
- Added Windows compatibility to the benefits list
- Updated technology list to include Windows integration
- Kept the clear URL references and simple instructions
2025-10-19 16:47:05 -04:00
Leopere 7968da9b60
Merge branch 'main' of git.nixc.us:colin/resume
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-19 16:34:06 -04:00
Leopere 467b7dcd1a
Completely simplify ScanSnap story page
- Removed all technical WebDAV details and jargon
- Made the URL http://192.168.0.119:9876 more prominent
- Simplified the page to focus on how to use the scanner service
- Added clear step-by-step instructions for connecting and scanning
- Removed unnecessary code examples and technical implementation details
2025-10-19 16:33:55 -04:00
colin f2d7ba2f5d Delete docker/resume/index.html.backup
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-19 16:31:50 -04:00
Leopere dd60d798b4
Remove ScanSnap WebDAV section from main page
ci/woodpecker/push/woodpecker Pipeline failed Details
- Completely removed the ScanSnap WebDAV Service project section from the main index.html page
- The detailed project page in stories/scansnap-webdav.html still exists for reference if needed
2025-10-19 16:31:39 -04:00
Leopere 20c813c7ea
Remove unused development utility scripts
ci/woodpecker/push/woodpecker Pipeline failed Details
- Deleted convert-favicon.sh, generate-favicon.js, update-favicon.js - unused favicon generation scripts
- Deleted convert-to-includes.js - utility for converting HTML to use includes system
- Updated documentation to remove references to deleted scripts
- These were development utilities that are no longer needed for the site
2025-10-19 16:27:05 -04:00
Leopere baf66debf3
Remove unnecessary index-with-includes.html template file
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Deleted index-with-includes.html as it was just an incomplete template
- Updated documentation to remove references to the deleted file
- The main index.html file is the complete, production resume
2025-10-19 16:24:46 -04:00
Leopere 526da797c5
Fix ScanSnap WebDAV service URL and simplify explanations
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Corrected URL from 192.168.1.119 to 192.168.0.119:9876
- Simplified technical explanations to focus on user experience
- Added direct URL prominently in project descriptions
- Removed complex WebDAV implementation details
- Maintained key functionality information (fast scanning, auto-cleanup at 3am)
2025-10-19 16:17:40 -04:00
Leopere 3c35c5eba0
Rewrite ScanSnap story with natural, engaging narrative
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Transformed from technical documentation to compelling project story
- Added buildersclub.ca member access link (http://192.168.1.119:9876)
- Included real-world problem solving and decision-making narrative
- Emphasized practical impact and time savings
- Maintained technical depth while improving readability
- Added 'Lessons Learned' section for authenticity
2025-10-19 15:58:20 -04:00
Leopere cbb5a04563
Merge branch 'main' of git.nixc.us:colin/resume
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-19 15:35:12 -04:00
Leopere af1a5efb60
Add ScanSnap WebDAV Service project for buildersclub.ca
ci/woodpecker/push/woodpecker Pipeline failed Details
- Added comprehensive project entry to portfolio
- Created detailed story page with technical implementation details
- Highlights high-performance receipt digitization capabilities
- Documents macOS Finder WebDAV compatibility solutions
- Includes performance metrics and business impact analysis
2025-10-19 15:35:07 -04:00
Colin 8f35aa626c
Update homepage: change 'lurked' to 'idled' and remove car purchase reference
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-15 21:24:53 -04:00
Colin 82bc0eb349
build: trigger CI build 20251015-203646
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-10-15 20:36:46 -04:00
Colin 7e06e335a9
dev: add docker compose for caddy, map 8081; add story placeholders (.md); update SRI via script; local compose run instructions 2025-10-15 16:58:31 -04:00
Leopere bd7aca54ee Avoid CORP block by serving matomo.js same-origin
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Fetch matomo.js to repo and serve as /matomo.js
- Update header include to load /matomo.js while keeping trackerUrl to metrics.nixc.us
2025-08-07 17:36:56 -04:00
Leopere 8909ece16c Disable CSP injection logic in update-csp-hashes.sh (no-op)
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-08-07 17:28:21 -04:00
Leopere d785776966 Remove CSP entirely for Matomo compatibility
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Remove CSP header from Caddyfile.local
- Strip CSP meta tags from all HTML
- Stop update-csp-hashes.sh from injecting CSP meta tags
2025-08-07 17:17:00 -04:00
Leopere 1f88b2c1b3 fixup
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-08-07 17:07:56 -04:00
Leopere f882ab7d3b Execute script tags in included HTML so Matomo runs
ci/woodpecker/push/woodpecker Pipeline failed Details
- Recreate and execute <script> tags from header/footer includes and head includes
- Ensures Matomo inline snippet runs when header is injected via includes.js
2025-08-07 17:04:31 -04:00
Leopere 494e4a568e Fix Matomo CSP by adding inline script hash
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added SHA256 hash for Matomo inline script to CSP
- Hash: sha256-aSi4/F2xxTg7cs3QbVq7ncUMa1ivQeVC8umnPRDtFyM=
- This should allow the Matomo tracking script to execute properly
- Updated all HTML files with new CSP headers
2025-07-30 14:51:34 -04:00
Leopere d7eea4853f Add Matomo analytics tracking code to all pages
ci/woodpecker/push/woodpecker Pipeline was successful Details
- Added Matomo tracking script to header.html include
- Updated CSP to allow metrics.nixc.us domain for script-src, img-src, and connect-src
- Modified update-csp-hashes.sh to include metrics.nixc.us in CSP directives
- Updated all HTML files with new CSP headers
- Tracking code will now work across all portfolio pages
2025-07-30 14:14:24 -04:00
Leopere 67a51b14e4 Add story placeholders and QR tool navigation - Add home infrastructure and nuclear DNS story placeholders - Add QR code tool to navigation dropdown - Update stories index with new entries - Rebuild and test local environment
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-25 10:48:45 -04:00
Leopere 60d1208d9d Update latest changes
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-23 10:56:48 -04:00
colin 39d5d2c6aa Add markdown tool link to navigation dropdown
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-07 11:25:16 -04:00
colin 008af14d12 Update OhMyForm timeline to reflect official archival in October 2024
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 20:05:00 -04:00
colin 8757dd1c37 Update OhMyForm references to indicate it's a sunset project succeeded by Formbricks
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 19:51:37 -04:00
colin 35b7a11faf Update sitemap timestamps
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 19:37:52 -04:00
colin 681b195b24 Fix story link contrast for better accessibility 2025-07-06 19:33:07 -04:00
colin 6d974577ad Fix story link contrast for better accessibility
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 19:12:28 -04:00
colin 066703bd42 Fix accessibility issues and update sitemap domain for local testing
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 18:58:20 -04:00
colin 9ff754b262 Fix navigation contrast in dark mode for better accessibility
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 18:39:26 -04:00
colin 2c43fe7784 Fix duplicate footer issues and remove npm dependency
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 18:36:59 -04:00
colin f0d296f108 Add WCAG 2.1 AAA accessibility testing framework 2025-07-06 16:14:32 -04:00
colin 869b08ec0e Integrate CSP hash update process into test framework 2025-07-06 16:05:03 -04:00
colin 2b37907c27 Fix URL for Improving MI Practices website
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 15:25:28 -04:00
colin 6c1e85c0e5 Update favicon with professional resume design
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 15:08:42 -04:00
colin ea3a2b95e4 Add placeholder story pages for all sections on the main page 2025-07-06 14:46:41 -04:00
colin 7071c08a30 Add procedurally generated favicon with multiple formats and update includes.js to load favicon links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 14:16:30 -04:00
colin 83e02bab67 Add cross-linking from main page to story pages with styled read-more links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 14:09:09 -04:00
colin f1da69a7f9 Fix theme toggle button by moving initialization to includes.js
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 12:20:39 -04:00
colin 26136fef61 Fix CSV tool by adding papaparse.min.js to Dockerfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:52:50 -04:00
colin b1e4cbaa4e Fix viperwire.html to use standard header and footer structure
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:49:56 -04:00
colin 633302d1e6 Add placeholder story pages with 'Coming Soon' notices
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:46:56 -04:00
colin f62ca2219e Fix production site: Copy includes and stories directories in Dockerfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:40:58 -04:00
colin 69bd8f2bb4 Fix production issue: Add missing includes.js file to Dockerfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:34:08 -04:00
colin f6f9aeda99 Fix Docker build: Remove invalid COPY syntax and use volume mounts for optional directories
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-07-06 11:27:37 -04:00
colin 2d50f99b65 Fix Docker build issues: Replace shasum with sha256sum and handle Caddyfile path correctly
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 11:24:00 -04:00
colin c4a45ef8fd Fix navigation menu closing too quickly with transition delay and keyboard support
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 11:20:32 -04:00
colin a1e2afabb5 Save all current changes to resume project
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-07-06 11:16:43 -04:00
Your Name be50c5de9c fixing deploy step
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-06-05 13:11:42 -04:00
colin 34f659be3f Update stack.staging.yml
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-06-05 13:04:40 -04:00
colin 7aa1337538 Update stack.staging.yml
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-06-05 13:04:10 -04:00
colin aa3afc9b4c Update stack.production.yml
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-06-05 13:03:46 -04:00
Leopere 77517079a7 update to rules and tests
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-05-10 17:14:38 -04:00
Leopere 5ac1c24481 Fix CSP for PDF download button by moving to external script
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-05-10 17:04:31 -04:00
Leopere 911842dc06 Fix CSP for PDF download button by moving to external script
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-05-10 16:27:15 -04:00
Leopere 04e5a9fa34 Add download as PDF button and update test configurations
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-05-10 16:12:46 -04:00
Leopere 4f9596bbee fix: update Cal.com meeting URL to correct username
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-22 21:39:33 -04:00
Leopere ab493a89f8 fix: remove 60-minute meeting option due to URL issues
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-22 21:06:25 -04:00
Leopere ff0e765b31 fix: update 60-minute meeting URL to correct format
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-22 20:53:41 -04:00
Your Name 10e340c341 Add Cal.com calendar meeting links
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-21 22:14:57 -04:00
Leopere cd94db9c03 temporarily disable HSTS to resolve certificate provisioning issues
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-15 16:33:47 -04:00
colin 0b693d7d2b Update docker/resume/index.html
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-04-03 11:37:33 -04:00
Your Name 7a5666ffda Add Subresource Integrity (SRI) hashes to script and style tags
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:55:44 -04:00
Your Name d2e6cc7db8 Update CSP to use hash for styles.css and remove unsafe-inline
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:54:33 -04:00
Your Name 3853b6ba6f Remove unsupported protocols directive (HTTP/2 enabled by default in Caddy 2)
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:49:32 -04:00
Your Name b93b8564de Update Dockerfile.production with multi-stage build and security hardening
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:41:26 -04:00
Your Name e97ef265ff Update Docker configuration with production build and security hardening
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:40:52 -04:00
Your Name ad26460c92 Add production Dockerfile with multi-stage build and security hardening
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:36:21 -04:00
Your Name 2cdd7341c0 Remove Brotli compression (not available in default Caddy image)
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:35:43 -04:00
Your Name ab5f8e774e Update CSP to use script hashes instead of unsafe-inline
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:31:57 -04:00
Your Name a5583c3afe Remove test results from git tracking and update .gitignore
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:30:32 -04:00
Your Name 3a9068b883 Remove TLS directives (handled by reverse proxy)
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:24:43 -04:00
Your Name 3d0dd2c361 Optimize Caddyfile for better performance
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 09:24:24 -04:00
Your Name 3598c99b9f Update resume with Oh My Form Docker pulls and comprehensive experience
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:18:30 -04:00
Your Name 4434650aac Add Oh My Form Docker pulls achievement
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 09:00:48 -04:00
Your Name 0f81e0318e Add utils.js to Docker build and update CSP with hash
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 05:05:59 -04:00
Your Name 630ef90df1 Add utils.js with SHA-256 hash in CSP
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 05:01:34 -04:00
Your Name cc0142f000 Add local header testing infrastructure
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:58:51 -04:00
Your Name 0c3c133431 Add comprehensive README with testing instructions
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:43:41 -04:00
Your Name 8c35ab5296 Add Playwright configuration file
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:43:14 -04:00
Your Name a10eea979f Add Playwright and Lighthouse testing infrastructure
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:42:57 -04:00
Your Name 39caf88782 Enhance theme toggle accessibility and confirm auto mode default
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:39:44 -04:00
Your Name 015f8ce76f Improve link accessibility with underlines and better contrast
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:36:03 -04:00
Your Name 3e2a32c1cf Revert CSP configuration to stable version without unsafe-inline
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:32:25 -04:00
Your Name 90b9d2dd1b Fix theme toggle CSS and add data-theme attribute handling
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:29:22 -04:00
Your Name 7b80d9dfa0 Comment out wait-for-deploy-production step in Woodpecker CI config
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:27:35 -04:00
Your Name 71e142b82e Fix Docker configuration to include theme.js and styles.css files
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:23:32 -04:00
Your Name 1c328df0c7 Switch from SRI to nonce-based CSP approach for better script handling
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:20:58 -04:00
Your Name 885914812d Debug CSP: temporarily allow inline scripts and remove SRI requirement
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:16:50 -04:00
Your Name 905b480a2e Fix SRI hash for theme.js to match deployed content
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 04:14:39 -04:00
Your Name f739edc7eb Add form-action 'none' to CSP for additional security
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:11:35 -04:00
Your Name 0b46750148 Enhance CSP with default-src 'none' for maximum security
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:08:37 -04:00
Your Name ac3d30d597 Fix CSP issues by moving inline script to external file and adding SRI
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 04:04:47 -04:00
colin 0fbd77f073 Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 00:23:16 -04:00
colin a62baf40e1 Update docker/resume/index.html
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 00:22:58 -04:00
colin ece9887a5b Add docker/resume/styles.css
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 00:22:44 -04:00
colin bfae2029b8 Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-31 00:16:14 -04:00
colin 2e9c196d8a Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-31 00:14:49 -04:00
colin 12f3ca9a3b Update docker/resume/index.html
ci/woodpecker/push/woodpecker Pipeline was successful Details
2025-03-30 23:41:58 -04:00
colin 18ece0205f Update docker/resume/Caddyfile
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 22:46:05 -04:00
colin 759dfa290e Update docker/resume/resume.html
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 22:45:41 -04:00
colin 1e46b4cc50 Update .woodpecker.yml
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:30:20 -04:00
colin 79c5a6d935 Update docker/resume/nginx.conf
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:28:01 -04:00
colin de1f9d3364 Update docker/resume/Dockerfile
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:27:41 -04:00
colin f963abe2ed Update docker/resume/resume.html
ci/woodpecker/push/woodpecker Pipeline failed Details
2025-03-30 21:22:47 -04:00
166 changed files with 16855 additions and 100 deletions

View File

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

View File

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

View File

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

49
.cursorignore Normal file
View File

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

16
.gitignore vendored Normal file
View File

@ -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

View File

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

97
README.md Normal file
View File

@ -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

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

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

View File

@ -1,6 +1,6 @@
services:
lucky-ddg:
resume:
build:
context: ./docker/lucky-ddg/
dockerfile: Dockerfile
image: git.nixc.us/nixius/lucky-ddg:production
context: ./docker/resume/
dockerfile: Dockerfile.production
image: git.nixc.us/colin/resume:production

View File

@ -1,6 +1,8 @@
services:
lucky-ddg:
resume:
build:
context: ./docker/lucky-ddg/
context: ./docker/resume/
dockerfile: Dockerfile
image: git.nixc.us/nixius/lucky-ddg:staging
image: git.nixc.us/colin/resume:staging
ports:
- "8080:8080"

13
docker-compose.yml Normal file
View File

@ -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

281
docker/generate-pdfs.js Normal file
View File

@ -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);
});

View File

@ -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"]

View File

@ -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)

View File

@ -1 +0,0 @@
flask==2.2.2

1154
docker/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
docker/package.json Normal file
View File

@ -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"
}
}

101
docker/resume/Caddyfile Normal file
View File

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

View File

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

45
docker/resume/Dockerfile Normal file
View File

@ -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"]

View File

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

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

@ -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

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

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

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

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

View File

@ -0,0 +1,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>

View File

@ -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>

View File

@ -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

View File

@ -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);
});

View File

@ -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")"

View File

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

View File

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

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

@ -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);
}
});
});
});
}
});

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

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

@ -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>

View File

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

View File

@ -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();
}
})();

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

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

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,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>

View File

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

View File

@ -0,0 +1,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>

View File

@ -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>

View File

@ -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">&times;</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);
}
}
});

View File

@ -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
}
}

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

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.

View File

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Colin Knapp - 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 &rarr;</a></p>
</section>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -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 &rarr;</a></p>
</section>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -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 &rarr;</a></p>
</article>
<!-- Healthcare Platform Project -->
<article class="project">
<h3><a href="../stories/healthcare-platform.html">Improving MI Practices Healthcare Platform</a></h3>
<p>
<strong>Timeframe:</strong> 2019-Present<br>
<strong>Overview:</strong> Led infrastructure design and operations for
<a href="../stories/healthcare-platform.html">Improving MI Practices</a> through Addis Enterprises,
a critical healthcare education platform.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Designed and implemented secure, scalable infrastructure for healthcare education content delivery.</li>
<li>Administered CIS Level 1 and 2 security standards implementation for enhanced system hardening and security controls.</li>
<li>Implemented automated deployment pipelines and monitoring systems for high availability.</li>
</ul>
<p><strong>Impact:</strong> Enabled reliable delivery of critical healthcare training content to medical professionals while maintaining robust security standards.</p>
<p><a href="../stories/healthcare-platform.html" class="read-more">Read more about the Healthcare Platform project &rarr;</a></p>
</article>
<!-- WordPress Security Project -->
<article class="project">
<h3><a href="../stories/wordpress-security.html">MLPP WordPress Malware Eradication</a></h3>
<p>
<strong>Timeframe:</strong> 2023<br>
<strong>Overview:</strong> Tasked by Addis Enterprises to develop an automated solution for
WordPress malware removal and hardening to stop a malignant and ongoing infection.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Created a Docker-based utility for automated malware detection and removal.</li>
<li>Implemented hardening measures to prevent reinfection.</li>
<li>Successfully deployed to protect MLPP from persistent cyber attacks.</li>
</ul>
<p><strong>Impact:</strong> Reduced infection frequency from daily/weekly to zero, significantly improving site security and reliability.</p>
<p><a href="../stories/wordpress-security.html" class="read-more">Read more about the WordPress Security project &rarr;</a></p>
</article>
<!-- YouTube Game Development Project -->
<article class="project">
<h3>YouTube Game Development & Cybersecurity</h3>
<p>
<strong>Timeframe:</strong> 2011-2022<br>
<strong>Overview:</strong> Developed custom Minecraft-based games for major YouTube celebrities and Multi-Creator Networks (MCNs),
including partnerships with Jordan Matthewson (Kootra) and TheCreatures MCN with 15 million subscriber reach.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Designed and developed custom Minecraft games to deliver engaging content for prominent YouTube creators and livestreams.</li>
<li>Partnered with Jordan Matthewson (Kootra) and TheCreatures MCN, providing gaming experiences for their 15 million subscriber network.</li>
<li>Collaborated with minor to major names in the YouTube community, including creators with massive followings during the platform's golden era.</li>
<li>Implemented DDoS defense, anti-phishing protocols, and data privacy measures for high-profile content and livestreams.</li>
<li>Managed hardware/software lifecycles and created comprehensive documentation for celebrity partnerships and MCN collaborations.</li>
</ul>
<p><strong>Impact:</strong> Delivered secure, seamless gaming experiences to millions of viewers through celebrity partnerships, MCN collaborations, and engaging content creation for livestreams. A critical incident where Jordan Matthewson was "swatted" during a live stream with 15,000 viewers on Twitch.tv became a pivotal moment that accelerated my focus on cybersecurity, ultimately leading to AddisEnterprises and partnerships with various US government organizations.</p>
<p><a href="../stories/youtube-game-dev.html" class="read-more">Read more about YouTube Game Development &rarr;</a></p>
</article>
<!-- Web Design & Java Project -->
<article class="project">
<h3><a href="../stories/web-design-java.html">Web Design & Java Plugin Development</a></h3>
<p>
<strong>Timeframe:</strong> 2011-2023<br>
<strong>Overview:</strong> Developed web solutions and Java plugins focusing on CI/CD efficiency
and client satisfaction, using strategic networking in IRC communities to bootstrap early career opportunities.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>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 &rarr;</a></p>
</article>
<!-- App Development Project -->
<article class="project">
<h3><a href="../stories/app-development.html">Ad Marketing Link Tracking Tool</a></h3>
<p>
<strong>Timeframe:</strong> 2013-2018<br>
<strong>Overview:</strong> Developed an unbranded ad marketing link tracking tool for YouTubers
to manage ad read campaigns, enabling creators to release ad campaigns and receive payments directly.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Built a comprehensive ad campaign management system for content creators to track and monetize ad reads.</li>
<li>Designed user-friendly tools for real-time revenue monitoring and campaign performance tracking.</li>
<li>Implemented secure payment processing and data handling for creator monetization.</li>
<li>Created an unbranded solution that could be white-labeled for various influencer networks.</li>
</ul>
<p><strong>Impact:</strong> Developed a complete ad campaign management platform that would have empowered creators to maximize earnings through direct ad read monetization, though the project was shelved due to client changes.</p>
<p><a href="../stories/app-development.html" class="read-more">Read more about the Ad Marketing Link Tracking Tool &rarr;</a></p>
</article>
<!-- NitricConcepts Project -->
<article class="project">
<h3><a href="../stories/nitric-leadership.html">DevOps & Co-Founder at Nitric Concepts</a></h3>
<p>
<strong>Timeframe:</strong> 2018-2021<br>
<strong>Overview:</strong> Co-founded and mentored Andrew Karvelis in building Nitric Concepts from a side project
to a $4M gross revenue company, providing executive mentorship and operational leadership.
</p>
<p><strong>Key Contributions:</strong></p>
<ul>
<li>Provided executive mentorship to CEO/Founder Andrew Karvelis, helping scale from side project earnings to significant revenue growth.</li>
<li>Managed 45 contractors worldwide, implementing Docker, Fail2Ban, and Salt Stack as part of a comprehensive toolchain.</li>
<li>Co-developed <em><a href="../stories/fawe-plotsquared.html">FastAsyncWorldEdit</a></em> and <em><a href="../stories/fawe-plotsquared.html">PlotSquared</a></em>, enabling billions of seamless edits for Minecraft creators.</li>
<li>Bootstrapped the company from zero to an estimated $4M gross revenue with strong profit margins.</li>
<li>Utilized Kanban/Trello to coordinate distributed teams and project workflows across multiple timezones.</li>
</ul>
<p><strong>Impact:</strong> Transformed Nitric Concepts into a thriving multinational entity through prolific and efficient development, while mentoring Andrew Karvelis from side project earnings to 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 &rarr;</a></p>
</article>
<!-- Entrepreneurial Ventures -->
<article class="project">
<h3>Entrepreneurial Ventures</h3>
<h4><a href="../stories/athion-turnaround.html">Athion.net</a> Turnaround</h4>
<p>
<strong>Timeframe:</strong> 2013-2017<br>
<strong>Overview:</strong> Revitalized a struggling business into a self-sustaining operation in two weeks,
developing revolutionary open source tooling with Athion to produce Nitric Concepts products.<br>
<strong>Key Contributions:</strong> Optimized systems and streamlined operations with rapid, effective solutions,
creating innovative tooling that would later power major open source projects.<br>
<strong>Impact:</strong> Created a profitable, independent venture while developing foundational technology
that would enable future open source success.
</p>
<p><a href="../stories/athion-turnaround.html" class="read-more">Read more about the Athion.net Turnaround &rarr;</a></p>
<h4><a href="../stories/motherboard-repair.html">MotherboardRepair.ca</a></h4>
<p>
<strong>Timeframe:</strong> 2019-Present<br>
<strong>Overview:</strong> Co-founded a company reducing e-waste through circuit board repairs.<br>
<strong>Key Contributions:</strong> Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.<br>
<strong>Impact:</strong> Promoted environmental responsibility in electronics.
</p>
<p><a href="../stories/motherboard-repair.html" class="read-more">Read more about MotherboardRepair.ca &rarr;</a></p>
</article>
</section>
<hr>
<!-- Additional Information -->
<section class="additional-info">
<h2>Additional Information</h2>
<h3>Personal Development</h3>
<p><strong>Timeframe:</strong> 2011-Present</p>
<ul>
<li>
<strong>Self-Taught Mastery:</strong> Continuously honed cybersecurity and systems management skills,
building a broad knowledge base to tackle unique challenges with a passion for innovation and problem-solving.
</li>
<li>
<strong>Open-Source Contributions:</strong> Actively maintain smaller self-run open-source projects;
previously led <em>OhMyForm</em> (sunset project now fully retired in favor of
<a href="https://formbricks.com/" target="_blank" rel="noopener noreferrer">Formbricks</a>),
an open source alternative to Google Forms and Office 365 that enabled data sovereignty
and privacy-focused form building, and contributed to <em><a href="../stories/fawe-plotsquared.html">PlotSquared</a></em>,
<em><a href="../stories/fawe-plotsquared.html">FastAsyncWorldEdit</a></em>, and <em>PlotHider</em>,
reflecting a prolific commitment to advancing technology.
</li>
<li>
<strong>Skill Maintenance:</strong> Regularly run Woodpecker CI and Gitea for on-premise source management,
testing, and deployment, employing security scanning and unit testing to ensure core functionality and
security baselines, alongside self-hosting exercises to sustain rapid, high-volume development capabilities
across a vast array of innovative projects.
</li>
</ul>
<p><a href="../stories/open-source-success.html" class="read-more">Read more about my open source contributions &rarr;</a></p>
<h3>Relevant Links & Web Impact</h3>
<ul>
<li>
<strong>Repositories:</strong>
<a href="https://github.com/IntellectualSites/PlotSquared">PlotSquared</a>,
<a href="https://github.com/IntellectualSites/FastAsyncWorldEdit">FastAsyncWorldEdit</a>,
<a href="https://github.com/OhMyForm/OhMyForm">OhMyForm</a>,
<a href="https://github.com/IntellectualSites/plothider">PlotHider</a>
</li>
<li>
<strong>Projects:</strong>
<a href="../stories/viperwire.html">ViperWire.ca</a>,
<a href="../stories/nitric-leadership.html">Nitric Concepts</a>,
<a href="https://showerloop.cc">ShowerLoop</a>
</li>
</ul>
</section>
<hr>
<!-- Open Source Section -->
<section class="open-source" role="region" aria-labelledby="open-source-heading">
<h2 id="open-source-heading"><a href="../stories/open-source-success.html">Open Source & Infrastructure</a></h2>
<div class="entry">
<h3><a href="../stories/fawe-plotsquared.html">PlotSquared & FastAsyncWorldEdit</a></h3>
<p class="date">2013-Present</p>
<p class="overview">Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.</p>
<ul>
<li>Contributed to <a href="../stories/fawe-plotsquared.html">PlotSquared</a>, a land management plugin with 572+ stars and 809+ forks</li>
<li>Enhanced <a href="../stories/fawe-plotsquared.html">FastAsyncWorldEdit</a>, improving world manipulation performance with 664+ stars</li>
<li>Implemented security improvements and performance optimizations for large-scale server operations</li>
</ul>
<p><a href="../stories/fawe-plotsquared.html" class="read-more">Read more about PlotSquared & FastAsyncWorldEdit &rarr;</a></p>
</div>
<div class="entry">
<h3>Athion.net Infrastructure</h3>
<p class="date">2013-Present</p>
<p class="overview">Established and maintained critical infrastructure supporting Nitric Concepts' official Minecraft Marketplace partnership, with PlotSquared and FastAsyncWorldEdit tooling used by content production companies in the $5 billion Minecraft gaming ecosystem.</p>
<ul>
<li>Set up and maintained <a href="https://ci.athion.net/" target="_blank">Jenkins CI/CD pipeline</a> since 2013, supporting Nitric Concepts' official marketplace partnership and the tooling used by content production companies</li>
<li>Hosted infrastructure enabling collaboration between developers and Microsoft for official Minecraft Marketplace content creation through Nitric Concepts</li>
<li>Implemented robust security measures and performance optimizations for high-traffic development environments supporting a $5 billion gaming brand</li>
</ul>
</div>
<div class="experience-item">
<h3><a href="../stories/open-source-success.html">Software Engineer</a></h3>
<p class="company">Oh My Form (Sunset Project)</p>
<p class="date">2020 - 2024</p>
<p class="achievement">Led development of Oh My Form (now sunset and succeeded by <a href="https://formbricks.com/" target="_blank" rel="noopener noreferrer">Formbricks</a>), an open source alternative to Google Forms and Office 365 that enabled data sovereignty and privacy-focused form building, achieving over 1.5 million Docker pulls as verified by <a href="https://hub.docker.com/u/ohmyform" target="_blank" rel="noopener noreferrer">Docker Hub</a> and <a href="https://archive.is/lZHAT" target="_blank" rel="noopener noreferrer">archived</a>.</p>
<ul>
<li>Developed and maintained a secure, high-performance form builder application focused on data sovereignty</li>
<li>Created an open source alternative to Google Forms and Office 365 for privacy-conscious organizations</li>
<li>Implemented robust security measures and best practices for data protection</li>
<li>Optimized application performance and user experience while maintaining privacy standards</li>
</ul>
<p><a href="../stories/open-source-success.html" class="read-more">Read more about my open source success &rarr;</a></p>
</div>
<div class="entry">
<h3>Home Infrastructure Cluster & WireGuard Mesh Networking</h3>
<p class="date">2020-Present</p>
<p class="overview">Built a comprehensive home infrastructure cluster using repurposed MacMini hardware for complete data sovereignty, including self-hosted email, DNS, and 100+ services, plus developed a WireGuard mesh networking tool for quantum-resistant networking deployment.</p>
<ul>
<li>Constructed a home cluster from repurposed MacMini hardware to maintain complete data sovereignty and avoid cloud dependencies</li>
<li>Self-hosted critical infrastructure including email, DNS, and over 100 additional services for complete data control</li>
<li>Developed a WireGuard mesh networking tool designed to simplify deployment of mesh networks as opposed to traditional hub-and-spoke architectures</li>
<li>Created infrastructure tooling that enables ease of deployment for quantum-resistant networking solutions</li>
<li>Implemented comprehensive self-hosting strategy to keep personal and business data out of cloud environments</li>
<li>Architected and deployed georedundant, nuclear war-resistant DNS cluster for clients</li>
</ul>
</div>
</section>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -0,0 +1,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 &rarr;</a></p>
</section>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

View File

@ -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 &rarr;</a></p>
</section>
</div>
<!-- Footer Include -->
<div id="footer-include"></div>
</body>
</html>

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

@ -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>

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="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>

View File

@ -0,0 +1,10 @@
# airport dns
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -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>

View File

@ -0,0 +1,10 @@
# app development
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -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>

View File

@ -0,0 +1,10 @@
# athion turnaround
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="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>

View File

@ -0,0 +1,10 @@
# fawe plotsquared
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="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>

View File

@ -0,0 +1,10 @@
# healthcare platform
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="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>

View File

@ -0,0 +1,10 @@
# home infrastructure
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,10 @@
# motherUoard repair
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="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>

View File

@ -0,0 +1,10 @@
# nitric leadership
> Draft placeholder. Content to be written.
- Summary: TBD
- Key outcomes: TBD
- Tech stack: TBD
- Challenges: TBD
- Results: TBD

View File

@ -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