diff --git a/.cursor/rules/asset-hashing-csp.mdc b/.cursor/rules/asset-hashing-csp.mdc deleted file mode 100644 index 06a31c9..0000000 --- a/.cursor/rules/asset-hashing-csp.mdc +++ /dev/null @@ -1,23 +0,0 @@ ---- -description: -globs: *.js,*.css -alwaysApply: false ---- -# Asset Hashing and CSP Update Rule - -This rule ensures that all `.js` and `.css` assets are properly hashed and their integrity hashes are updated in `index.html` and added to the Content Security Policy (CSP) in the `Caddyfile` during every build and push process. - -## Process to Follow - -1. **Hash Calculation**: Before building the Docker image, calculate the SHA256 hash for each `.js` and `.css` file in the `docker/resume/` directory using a command like `shasum -a 256 filename | awk '{print $1}' | xxd -r -p | base64`. -2. **Update index.html**: Update the `integrity` attribute in the ` + + + +
+ + +${mainContent} + + + + +`; + + // Output the modified HTML content to the console + console.log('=== MODIFIED HTML CONTENT ==='); + console.log(modifiedHTML); + console.log('=== END MODIFIED HTML CONTENT ==='); + + // Helper function to get the relative path to the root + function getRelativePath() { + const path = window.location.pathname; + const depth = (path.match(/\//g) || []).length - 1; + return depth > 0 ? '../'.repeat(depth) : ''; + } +}); \ No newline at end of file diff --git a/docker/resume/csv-tool-output.html b/docker/resume/csv-tool-output.html new file mode 100644 index 0000000..71698e9 --- /dev/null +++ b/docker/resume/csv-tool-output.html @@ -0,0 +1,113 @@ + + + + + + + CSV Viewer - Colin Knapp + + + + + + + + + + + + +
+ +
+

CSV Viewer

+

Simply paste CSV data below to view it as a formatted table.

+ +
+
+

Paste CSV Data

+
+ +
+ +
+ + +
+ +
+ + +
+
+ +
+

Output

+

Paste CSV data above to view it as a table.

+
+
+ +
+ +

About This Tool

+

This CSV Viewer allows you to:

+ +

The tool processes everything in your browser - no data is sent to any server.

+
+ + + + + + + + + + diff --git a/docker/resume/images/discord-community.jpg b/docker/resume/images/discord-community.jpg new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/docker/resume/images/discord-community.jpg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docker/resume/images/docker-hub-stats.jpg b/docker/resume/images/docker-hub-stats.jpg new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/docker/resume/images/docker-hub-stats.jpg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docker/resume/includes.js b/docker/resume/includes.js new file mode 100644 index 0000000..910555e --- /dev/null +++ b/docker/resume/includes.js @@ -0,0 +1,88 @@ +/** + * Includes.js - Handles the inclusion of header and footer files + * and applies the correct active states to navigation items + */ + +document.addEventListener('DOMContentLoaded', function() { + // 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(); + document.getElementById(elementId).innerHTML = content; + if (callback) callback(); + } catch (error) { + console.error('Error including HTML:', error); + } + } + + // Function to set active navigation item + function setActiveNavItem() { + const currentPath = window.location.pathname; + + // Wait for the navigation to be loaded + setTimeout(() => { + // Remove all active classes first + document.querySelectorAll('.main-nav a').forEach(link => { + link.classList.remove('active'); + }); + + // Set active class based on current path + if (currentPath === '/' || currentPath === '/index.html') { + const portfolioLink = document.getElementById('nav-portfolio'); + if (portfolioLink) portfolioLink.classList.add('active'); + } else if (currentPath.includes('/stories/')) { + const storiesLink = document.getElementById('nav-stories'); + if (storiesLink) storiesLink.classList.add('active'); + + // Check for specific story pages + if (currentPath.includes('viperwire.html')) { + const link = document.getElementById('nav-viperwire'); + if (link) link.classList.add('active'); + } else if (currentPath.includes('fawe-plotsquared.html')) { + const link = document.getElementById('nav-fawe'); + if (link) link.classList.add('active'); + } else if (currentPath.includes('healthcare-platform.html')) { + const link = document.getElementById('nav-healthcare'); + if (link) link.classList.add('active'); + } else if (currentPath.includes('wordpress-security.html')) { + const link = document.getElementById('nav-wordpress'); + if (link) link.classList.add('active'); + } else if (currentPath.includes('airport-dns.html')) { + const link = document.getElementById('nav-airport'); + if (link) link.classList.add('active'); + } else if (currentPath.includes('nitric-leadership.html')) { + const link = document.getElementById('nav-nitric'); + if (link) link.classList.add('active'); + } else if (currentPath.includes('open-source-success.html')) { + const link = document.getElementById('nav-opensource'); + if (link) link.classList.add('active'); + } + } else if (currentPath.includes('/one-pager-tools/')) { + const toolsLink = document.getElementById('nav-tools'); + if (toolsLink) toolsLink.classList.add('active'); + + // Check for specific tool pages + if (currentPath.includes('csv-tool.html')) { + const link = document.getElementById('nav-csv'); + if (link) link.classList.add('active'); + } + } + }, 100); // Small delay to ensure the DOM is updated + } + + // Process header and footer placeholders + const headerElement = document.getElementById('header-include'); + const footerElement = document.getElementById('footer-include'); + + if (headerElement) { + includeHTML('header-include', '/includes/header.html', setActiveNavItem); + } + + if (footerElement) { + includeHTML('footer-include', '/includes/footer.html'); + } +}); \ No newline at end of file diff --git a/docker/resume/includes/IMPLEMENTATION_GUIDE.md b/docker/resume/includes/IMPLEMENTATION_GUIDE.md new file mode 100644 index 0000000..016d131 --- /dev/null +++ b/docker/resume/includes/IMPLEMENTATION_GUIDE.md @@ -0,0 +1,83 @@ +# Implementation Guide: Converting to the Includes System + +This guide explains how to convert the existing portfolio website to use the includes system for headers and footers. + +## What We've Created + +1. **Header and Footer Templates** + - `includes/header.html`: Contains the common header elements + - `includes/footer.html`: Contains the common footer elements + +2. **JavaScript for Includes** + - `includes.js`: Handles the inclusion of header and footer files and applies the correct active states to navigation items + +3. **Example Files** + - `template-with-includes.html`: Basic template + - `index-with-includes.html`: Example of the index page + - `stories/story-with-includes.html`: Example of a story page + - `one-pager-tools/tool-with-includes.html`: Example of a tool page + +4. **Conversion Helper** + - `convert-to-includes.js`: A script to help convert existing HTML files to use the includes system + +## Implementation Steps + +### 1. Update the CSP in Caddyfile and Caddyfile.local + +Add the includes.js script hash to the Content-Security-Policy: + +``` +Content-Security-Policy "default-src 'none'; script-src 'self' 'sha256-HASH_FOR_INCLUDES_JS' 'sha256-anTkUs/oFZJulKUMaMjZlwaALEmPOP8op0psAo5Bhh8=' 'sha256-ryQsJ+aghKKD/CeXgx8jtsnZT3Epp3EjIw8RyHIq544='; ... +``` + +### 2. Convert Existing Pages + +For each HTML page: + +1. Add the includes.js script to the head section: + ```html + + ``` + (Adjust the path as needed based on the location of the HTML file) + +2. Replace the header content (everything from the opening `` tag to the opening `
` tag) with: + ```html + +
+ ``` + +3. Replace the footer content (everything from the closing `
` of the main container to the closing `` tag) with: + ```html + + + ``` + +You can use the `convert-to-includes.js` script to help with this process: + +1. Temporarily add this script to an existing page: + ```html + + ``` +2. Open the page in a browser and check the console output +3. Use the generated HTML as a starting point for your conversion + +### 3. Testing + +After converting each page: + +1. Test the page in a browser to ensure it loads correctly +2. Verify that the navigation active states work as expected +3. Check that all CSS and JavaScript files are loaded correctly + +## Benefits + +- **Easier Maintenance**: Changes to the header or footer only need to be made in one place +- **Consistency**: All pages will have the same header and footer structure +- **Reduced File Size**: Each HTML file will be smaller since the common elements are externalized +- **Improved Developer Experience**: Easier to focus on the unique content of each page + +## Future Enhancements + +- **Dynamic Meta Tags**: Enhance the includes system to support dynamic meta tags and titles +- **Page-Specific CSS/JS**: Add support for page-specific CSS and JavaScript files +- **Breadcrumbs**: Implement a breadcrumb system that works with the includes system \ No newline at end of file diff --git a/docker/resume/includes/README.md b/docker/resume/includes/README.md new file mode 100644 index 0000000..7acea86 --- /dev/null +++ b/docker/resume/includes/README.md @@ -0,0 +1,81 @@ +# HTML Includes System + +This system allows for separating headers and footers into external HTML files that can be included in all individual pages, making maintenance easier and ensuring consistency across the site. + +## Files + +- `header.html`: Contains the common header elements for all pages +- `footer.html`: Contains the common footer elements for all pages +- `includes.js`: JavaScript file that handles the inclusion of header and footer files and applies the correct active states to navigation items + +## How to Use + +### 1. Include the JavaScript + +Add the `includes.js` script to your HTML file: + +```html + +``` + +(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 + +
+ + + + + +``` + +### 3. Example Structure + +Here's a basic template for a page using includes: + +```html + + + + + + + Your Title - Colin Knapp + + + + + + + + + +
+ + +

Your Page Title

+

Your page content...

+ + + + + +``` + +## Navigation Active States + +The `includes.js` file automatically sets the active state for navigation items based on the current page. The navigation items in `header.html` have IDs that are used to identify which item should be active. + +## Example Files + +See the following example files that demonstrate how to use the includes system: + +- `/template-with-includes.html`: Basic template +- `/index-with-includes.html`: Example of the index page +- `/stories/story-with-includes.html`: Example of a story page +- `/one-pager-tools/tool-with-includes.html`: Example of a tool page \ No newline at end of file diff --git a/docker/resume/includes/footer.html b/docker/resume/includes/footer.html new file mode 100644 index 0000000..c5598e7 --- /dev/null +++ b/docker/resume/includes/footer.html @@ -0,0 +1,6 @@ +
+ +

Accessibility: 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.

+ + + diff --git a/docker/resume/includes/header.html b/docker/resume/includes/header.html new file mode 100644 index 0000000..0670256 --- /dev/null +++ b/docker/resume/includes/header.html @@ -0,0 +1,36 @@ +
+ +
+ + + +
diff --git a/docker/resume/index-with-includes.html b/docker/resume/index-with-includes.html new file mode 100644 index 0000000..e2078d0 --- /dev/null +++ b/docker/resume/index-with-includes.html @@ -0,0 +1,38 @@ + + + + + + + Colin Knapp - Portfolio + + + + + + + + +
+ + +

Colin Knapp

+

Location: Kitchener-Waterloo, Ontario, Canada
+ Contact: recruitme2025@colinknapp.com | colinknapp.com
+ Schedule a Meeting: 30 Minute Meeting

+ +
+ +

Highlights & Measurables

+ + + + + + + + diff --git a/docker/resume/index.html b/docker/resume/index.html index c6c80ad..cae9286 100644 --- a/docker/resume/index.html +++ b/docker/resume/index.html @@ -3,31 +3,18 @@ - - Colin Knapp Portfolio + + Colin Knapp - Portfolio - - - + + + + -
- - -
- + +
+

Colin Knapp

Location: Kitchener-Waterloo, Ontario, Canada
@@ -102,7 +89,7 @@

  • Utilized Jenkins and GitLab CI/CD for streamlined workflows, leveraging a robust toolchain for rapid development.
  • Managed complex systems and ensured WCAG 2.0 AA accessibility standards.
  • Provided technical guidance and detailed client documentation, drawing on broad experience to resolve diverse issues.
    - Impact: Enhanced project delivery speed and quality for diverse computing environments through prolific development practices.
  • + Impact: Enhanced project delivery speed and quality for diverse computing environments through prolific and efficient development practices.

    App Development for Influencers

    @@ -203,5 +190,8 @@

    Accessibility: 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.

    + + + - \ No newline at end of file + diff --git a/docker/resume/index.html-E b/docker/resume/index.html-E deleted file mode 100644 index c6c80ad..0000000 --- a/docker/resume/index.html-E +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - Colin Knapp Portfolio - - - - - - -
    - - -
    - -
    -

    Colin Knapp

    -

    Location: Kitchener-Waterloo, Ontario, Canada
    - Contact: recruitme2025@colinknapp.com | colinknapp.com
    - Schedule a Meeting: 30 Minute Meeting

    - -
    - -

    Highlights & Measurables

    - - -
    - -

    Project Experience

    -

    DevSecOps at Addis Enterprises

    -

    Timeframe: 2019-Present
    - Overview: Collaborated on US government projects and airport infrastructure, focusing on scalable, secure systems and domain resilience.
    - Key Contributions:

    - - -

    Healthcare Platform Infrastructure

    -

    Timeframe: 2019-Present
    - Overview: Led infrastructure design and operations for Improving MI Practices (archive) through Addis Enterprises, a critical healthcare education platform.
    - Key Contributions:

    - - -

    WordPress Security Automation

    -

    Timeframe: 2023
    - Overview: Developed an automated solution for WordPress malware removal and hardening.
    - Key Contributions:

    - - -

    YouTube Game Development & Cybersecurity

    -

    Timeframe: 2009-2022
    - Overview: Designed custom video games for prominent online creators, integrating advanced cybersecurity measures.
    - Key Contributions:

    - - -

    Web Design & Java Plugin Development

    -

    Timeframe: 2009-2023
    - Overview: Developed web solutions and Java plugins focusing on CI/CD efficiency and client satisfaction.
    - Key Contributions:

    - - -

    App Development for Influencers

    -

    Timeframe: 2013-2018
    - Overview: Created an ad revenue tracking app to optimize earnings and strategies for content creators.
    - Key Contributions:

    - - -

    DevOps & Co-Founder at NitricConcepts

    -

    Timeframe: 2018-2021
    - Overview: Led a global team in building secure, scalable gaming solutions.
    - Key Contributions:

    - - -

    Entrepreneurial Ventures

    -

    Athion.net Turnaround

    -

    Timeframe: 2013-2017
    - Overview: Revitalized a struggling business into a self-sustaining operation in two weeks.
    - Key Contributions: Optimized systems and streamlined operations with rapid, effective solutions.
    - Impact: Created a profitable, independent venture.

    - -

    MotherboardRepair.ca

    -

    Timeframe: 2019-Present
    - Overview: Co-founded a company reducing e-waste through circuit board repairs.
    - Key Contributions: Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.
    - Impact: Promoted environmental responsibility in electronics.

    - -

    ShowerLoop Project

    -

    Timeframe: 2016
    - Overview: Revamped the website for an eco-friendly recirculating shower system project, implementing WCAG 2.0 AA compliance and modern design principles.
    - Key Contributions: Designed and implemented a responsive, accessible website with improved user experience and technical documentation.
    - Impact: Enhanced the project's online presence and accessibility while maintaining the site's functionality through periodic maintenance.

    - -
    - -

    Additional Information

    -

    Personal Development

    -

    Timeframe: 2009-Present

    - - -

    Relevant Links & Web Impact

    - - -
    - -
    -

    Open Source & Infrastructure

    -
    -

    PlotSquared & FastAsyncWorldEdit

    -

    2013-Present

    -

    Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.

    -
      -
    • Contributed to PlotSquared, a land management plugin with 572+ stars and 809+ forks
    • -
    • Enhanced FastAsyncWorldEdit, improving world manipulation performance with 664+ stars
    • -
    • Implemented security improvements and performance optimizations for large-scale server operations
    • -
    -
    -
    -

    Athion.net Infrastructure

    -

    2013-Present

    -

    Established and maintained critical infrastructure for Minecraft development community.

    -
      -
    • Set up and maintained Jenkins CI/CD pipeline since 2013, supporting continuous integration for game content development
    • -
    • Hosted infrastructure enabling collaboration between developers and Microsoft for game content creation
    • -
    • Implemented robust security measures and performance optimizations for high-traffic development environments
    • -
    -
    -
    -

    Software Engineer

    -

    Oh My Form

    -

    2020 - Present

    -

    Led development of Oh My Form, achieving over 1.5 million Docker pulls as verified by Docker Hub and archived.

    -
      -
    • Developed and maintained a secure, high-performance form builder application
    • -
    • Implemented robust security measures and best practices
    • -
    • Optimized application performance and user experience
    • -
    -
    -
    - -
    - -

    Accessibility: 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.

    -
    - - \ No newline at end of file diff --git a/docker/resume/inline-style.txt b/docker/resume/inline-style.txt new file mode 100644 index 0000000..3b4c594 --- /dev/null +++ b/docker/resume/inline-style.txt @@ -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; diff --git a/docker/resume/live.html b/docker/resume/live.html deleted file mode 100644 index 654dbbd..0000000 --- a/docker/resume/live.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - Colin Knapp Portfolio - - - - -
    - -
    - -
    -

    Colin Knapp

    -

    Location: Kitchener-Waterloo, Ontario, Canada
    - Contact: recruitme2023@colinknapp.com | colinknapp.com

    - -
    - -

    Highlights & Measurables

    - - -
    - -

    Project Experience

    -

    DevSecOps at Addis Enterprises

    -

    Timeframe: 2019-Present
    - Overview: Collaborated on US government projects and airport infrastructure, focusing on scalable, secure systems and domain resilience.
    - Key Contributions:

    - - -

    Healthcare Platform Infrastructure

    -

    Timeframe: 2019-Present
    - Overview: Led infrastructure design and operations for Improving MI Practices (archive) through Addis Enterprises, a critical healthcare education platform.
    - Key Contributions:

    - - -

    WordPress Security Automation

    -

    Timeframe: 2023
    - Overview: Developed an automated solution for WordPress malware removal and hardening.
    - Key Contributions:

    - - -

    YouTube Game Development & Cybersecurity

    -

    Timeframe: 2009-2022
    - Overview: Designed custom video games for prominent online creators, integrating advanced cybersecurity measures.
    - Key Contributions:

    - - -

    Web Design & Java Plugin Development

    -

    Timeframe: 2009-2023
    - Overview: Developed web solutions and Java plugins focusing on CI/CD efficiency and client satisfaction.
    - Key Contributions:

    - - -

    App Development for Influencers

    -

    Timeframe: 2013-2018
    - Overview: Created an ad revenue tracking app to optimize earnings and strategies for content creators.
    - Key Contributions:

    - - -

    DevOps & Co-Founder at NitricConcepts

    -

    Timeframe: 2018-2021
    - Overview: Led a global team in building secure, scalable gaming solutions.
    - Key Contributions:

    - - -

    Entrepreneurial Ventures

    -

    Athion.net Turnaround

    -

    Timeframe: 2013-2017
    - Overview: Revitalized a struggling business into a self-sustaining operation in two weeks.
    - Key Contributions: Optimized systems and streamlined operations with rapid, effective solutions.
    - Impact: Created a profitable, independent venture.

    - -

    MotherboardRepair.ca

    -

    Timeframe: 2019-Present
    - Overview: Co-founded a company reducing e-waste through circuit board repairs.
    - Key Contributions: Leveraged industry expertise and a versatile toolchain for sustainable tech solutions.
    - Impact: Promoted environmental responsibility in electronics.

    - -

    ShowerLoop Project

    -

    Timeframe: 2016
    - Overview: Revamped the website for an eco-friendly recirculating shower system project, implementing WCAG 2.0 AA compliance and modern design principles.
    - Key Contributions: Designed and implemented a responsive, accessible website with improved user experience and technical documentation.
    - Impact: Enhanced the project's online presence and accessibility while maintaining the site's functionality through periodic maintenance.

    - -
    - -

    Additional Information

    -

    Personal Development

    -

    Timeframe: 2009-Present

    - - -

    Relevant Links & Web Impact

    - - -
    - -
    -

    Open Source & Infrastructure

    -
    -

    PlotSquared & FastAsyncWorldEdit

    -

    2013-Present

    -

    Contributor to major Minecraft server plugins, focusing on performance optimization and security enhancements.

    -
      -
    • Contributed to PlotSquared, a land management plugin with 572+ stars and 809+ forks
    • -
    • Enhanced FastAsyncWorldEdit, improving world manipulation performance with 664+ stars
    • -
    • Implemented security improvements and performance optimizations for large-scale server operations
    • -
    -
    -
    -

    Athion.net Infrastructure

    -

    2013-Present

    -

    Established and maintained critical infrastructure for Minecraft development community.

    -
      -
    • Set up and maintained Jenkins CI/CD pipeline since 2013, supporting continuous integration for game content development
    • -
    • Hosted infrastructure enabling collaboration between developers and Microsoft for game content creation
    • -
    • Implemented robust security measures and performance optimizations for high-traffic development environments
    • -
    -
    -
    - -
    - -

    Accessibility: 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.

    -
    - - \ No newline at end of file diff --git a/docker/resume/one-pager-tools/csv-tool-fix.css b/docker/resume/one-pager-tools/csv-tool-fix.css new file mode 100644 index 0000000..2b6d0f3 --- /dev/null +++ b/docker/resume/one-pager-tools/csv-tool-fix.css @@ -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; +} \ No newline at end of file diff --git a/docker/resume/one-pager-tools/csv-tool.html b/docker/resume/one-pager-tools/csv-tool.html new file mode 100644 index 0000000..6dfc97b --- /dev/null +++ b/docker/resume/one-pager-tools/csv-tool.html @@ -0,0 +1,113 @@ + + + + + + + CSV Viewer - Colin Knapp + + + + + + + + + + + + +
    + +
    +

    CSV Viewer

    +

    Simply paste CSV data below to view it as a formatted table.

    + +
    +
    +

    Paste CSV Data

    +
    + +
    + +
    + + +
    + +
    + + +
    +
    + +
    +

    Output

    +

    Paste CSV data above to view it as a table.

    +
    +
    + +
    + +

    About This Tool

    +

    This CSV Viewer allows you to:

    + +

    The tool processes everything in your browser - no data is sent to any server.

    +
    + + + + + + + + + + diff --git a/docker/resume/one-pager-tools/csv-tool.js b/docker/resume/one-pager-tools/csv-tool.js new file mode 100644 index 0000000..622274b --- /dev/null +++ b/docker/resume/one-pager-tools/csv-tool.js @@ -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 = '

    Output

    Paste CSV data above to view it as a table.

    '; + return; + } + + try { + // Show processing message + outputDiv.innerHTML = '

    Output

    Processing data...

    '; + + // 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 = '

    Data Preview

    '; + tableHtml += `

    Showing ${previewData.length} of ${csvData.length} rows

    `; + tableHtml += '
    '; + + // Table headers with sort functionality + tableHtml += ''; + headers.forEach(header => { + const isSorted = header === currentSortColumn; + const sortClass = isSorted ? (sortDirection > 0 ? 'sort-asc' : 'sort-desc') : ''; + tableHtml += ``; + }); + tableHtml += ''; + + // Table body with improved cell formatting + tableHtml += ''; + previewData.forEach(row => { + tableHtml += ''; + headers.forEach(header => { + const cellValue = row[header]; + // Format cell value based on type + let formattedValue = ''; + + if (cellValue === null || cellValue === undefined) { + formattedValue = '(empty)'; + } else if (typeof cellValue === 'string') { + formattedValue = escapeHtml(cellValue); + } else { + formattedValue = String(cellValue); + } + + tableHtml += ``; + }); + tableHtml += ''; + }); + tableHtml += '
    ${header} ${isSorted ? (sortDirection > 0 ? '↑' : '↓') : ''}
    ${formattedValue}
    '; + + // Add stats summary + const totalRows = csvData.length; + const totalColumns = headers.length; + tableHtml += `
    +
    + Total Rows: + ${totalRows} +
    +
    + Total Columns: + ${totalColumns} +
    +
    `; + + // Display in output div + outputDiv.innerHTML = tableHtml; + + // Add click event listeners to table headers for sorting + const tableHeaders = outputDiv.querySelectorAll('th'); + tableHeaders.forEach(th => { + th.addEventListener('click', () => { + const column = th.getAttribute('data-column'); + sortData(column); + }); + }); + } + + /** + * Escape HTML special characters to prevent XSS + */ + function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + /** + * 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(); + } +}); diff --git a/docker/resume/one-pager-tools/csv-tool.js.fixed b/docker/resume/one-pager-tools/csv-tool.js.fixed new file mode 100644 index 0000000..27b8217 --- /dev/null +++ b/docker/resume/one-pager-tools/csv-tool.js.fixed @@ -0,0 +1 @@ +}); diff --git a/docker/resume/one-pager-tools/papaparse.min.js b/docker/resume/one-pager-tools/papaparse.min.js new file mode 100644 index 0000000..edfdb37 --- /dev/null +++ b/docker/resume/one-pager-tools/papaparse.min.js @@ -0,0 +1,7 @@ +/* @license +Papa Parse +v5.3.2 +https://github.com/mholt/PapaParse +License: MIT +*/ +!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var i=(t=t||{}).dynamicTyping||!1;M(i)&&(t.dynamicTypingFunction=i,i={});if(t.dynamicTyping=i,t.transform=!!M(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var r=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(i=f.URL||f.webkitURL||null,r=s.toString(),b.BLOB_URL||(b.BLOB_URL=i.createObjectURL(new Blob(["(",r,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var i,r;return t.onmessage=_,t.id=h++,a[t.id]=t}();return r.userStep=t.step,r.userChunk=t.chunk,r.userComplete=t.complete,r.userError=t.error,t.step=M(t.step),t.chunk=M(t.chunk),t.complete=M(t.complete),t.error=M(t.error),delete t.worker,void r.postMessage({input:e,config:t,workerId:r.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&M(e.read)&&M(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,i=!1,r=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(i=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");r=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s);("boolean"==typeof t.escapeFormulae||t.escapeFormulae instanceof RegExp)&&(o=t.escapeFormulae instanceof RegExp?t.escapeFormulae:/^[=+\-@\t\r].*$/)}();var h=new RegExp(j(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,i);if("object"==typeof e[0])return u(r||Object.keys(e[0]),e,i)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||r),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],i);throw new Error("Unable to serialize unrecognized input");function u(e,t,i){var r="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(M(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!M(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){M(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t])}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i)}try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t))}}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)):r=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(r.error)}}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}}}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=v(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,u=/^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))$/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(M(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){return c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines&&(c.data=c.data.filter(function(e){return!y(e)})),_()&&function(){if(!c)return;function e(e,t){M(m.transformHeader)&&(e=m.transformHeader(e,t)),l.push(e)}if(Array.isArray(c.data[0])){for(var t=0;_()&&t=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)M(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=D)return C(!0)}else for(m=F,F++;;){if(-1===(m=r.indexOf(S,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:F}),E();if(m===n-1)return E(r.substring(F,m).replace(_,S));if(S!==L||r[m+1]!==L){if(S===L||0===m||r[m-1]!==L){-1!==p&&p=D)return C(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:F}),m++}}else m++}return E();function k(e){h.push(e),d=F}function b(e){var t=0;if(-1!==e){var i=r.substring(m+1,e);i&&""===i.trim()&&(t=i.length)}return t}function E(e){return i||(void 0===e&&(e=r.substring(F)),f.push(e),F=n,k(f),o&&R()),C()}function w(e){F=e,k(f),f=[],g=r.indexOf(x,F)}function C(e){return{data:h,errors:u,meta:{delimiter:O,linebreak:x,aborted:z,truncated:!!e,cursor:d+(t||0)}}}function R(){T(C()),h=[],u=[]}},this.abort=function(){z=!0},this.getCharIndex=function(){return F}}function _(e){var t=e.data,i=a[t.workerId],r=!1;if(t.error)i.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){r=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:y,resume:y};if(M(i.userStep)){for(var s=0;s + + + + + + Colin Knapp Tools + + + + + + + + + + + +
    + +
    +

    Tool Name

    +

    A brief description of what this tool does.

    + +
    + + +
    + +
    + +
    + +
    + +
    +
    +
    + + + + + diff --git a/docker/resume/one-pager-tools/tool-styles.css b/docker/resume/one-pager-tools/tool-styles.css new file mode 100644 index 0000000..605d15e --- /dev/null +++ b/docker/resume/one-pager-tools/tool-styles.css @@ -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; +} \ No newline at end of file diff --git a/docker/resume/one-pager-tools/tool-with-includes.html b/docker/resume/one-pager-tools/tool-with-includes.html new file mode 100644 index 0000000..440815c --- /dev/null +++ b/docker/resume/one-pager-tools/tool-with-includes.html @@ -0,0 +1,51 @@ + + + + + + + Tool Example - Colin Knapp + + + + + + + + + + + +
    + + +

    Tool Example

    +

    A simple example tool to demonstrate the includes system.

    + +
    +
    +

    Tool Controls

    +
    + + +
    + +
    + +
    +
    + +
    +

    Output will appear here.

    +
    +
    + +
    + +

    About This Tool

    +

    This is an example tool page that demonstrates how to use the includes system.

    + + + + + diff --git a/docker/resume/papaparse.min.js b/docker/resume/papaparse.min.js new file mode 100644 index 0000000..edfdb37 --- /dev/null +++ b/docker/resume/papaparse.min.js @@ -0,0 +1,7 @@ +/* @license +Papa Parse +v5.3.2 +https://github.com/mholt/PapaParse +License: MIT +*/ +!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=n&&/blob:/i.test((f.location||{}).protocol),a={},h=0,b={parse:function(e,t){var i=(t=t||{}).dynamicTyping||!1;M(i)&&(t.dynamicTypingFunction=i,i={});if(t.dynamicTyping=i,t.transform=!!M(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var r=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(i=f.URL||f.webkitURL||null,r=s.toString(),b.BLOB_URL||(b.BLOB_URL=i.createObjectURL(new Blob(["(",r,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var i,r;return t.onmessage=_,t.id=h++,a[t.id]=t}();return r.userStep=t.step,r.userChunk=t.chunk,r.userComplete=t.complete,r.userError=t.error,t.step=M(t.step),t.chunk=M(t.chunk),t.complete=M(t.complete),t.error=M(t.error),delete t.worker,void r.postMessage({input:e,config:t,workerId:r.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?n=t.download?new l(t):new p(t):!0===e.readable&&M(e.read)&&M(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,i=!1,r=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(i=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");r=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s);("boolean"==typeof t.escapeFormulae||t.escapeFormulae instanceof RegExp)&&(o=t.escapeFormulae instanceof RegExp?t.escapeFormulae:/^[=+\-@\t\r].*$/)}();var h=new RegExp(j(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return u(null,e,i);if("object"==typeof e[0])return u(r||Object.keys(e[0]),e,i)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||r),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),u(e.fields||[],e.data||[],i);throw new Error("Unable to serialize unrecognized input");function u(e,t,i){var r="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(M(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!M(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){M(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var r;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),u.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(r=new XMLHttpRequest,this._config.withCredentials&&(r.withCredentials=this._config.withCredentials),n||(r.onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)),r.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)r.setRequestHeader(t,e[t])}if(this._config.chunkSize){var i=this._start+this._config.chunkSize-1;r.setRequestHeader("Range","bytes="+this._start+"-"+i)}try{r.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===r.status&&this._chunkError()}},this._chunkLoaded=function(){4===r.readyState&&(r.status<200||400<=r.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:r.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(r),this.parseChunk(r.responseText)))},this._chunkError=function(e){var t=r.statusText||e;this._sendError(new Error(t))}}function c(e){var r,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),u.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((r=new FileReader).onload=v(this._chunkLoaded,this),r.onerror=v(this._chunkError,this)):r=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(r.error)}}function p(e){var i;u.call(this,e=e||{}),this.stream=function(e){return i=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=i.substring(0,t),i=i.substring(t)):(e=i,i=""),this._finished=!i,this.parseChunk(e)}}}function g(e){u.call(this,e=e||{});var t=[],i=!0,r=!1;this.pause=function(){u.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){u.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){r&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):i=!0},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),i&&(i=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=v(function(){this._streamCleanUp(),r=!0,this._streamData("")},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function i(m){var a,o,h,r=Math.pow(2,53),n=-r,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,u=/^(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))$/,t=this,i=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(M(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;i+=e.data.length,m.preview&&i>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){return c&&h&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),h=!1),m.skipEmptyLines&&(c.data=c.data.filter(function(e){return!y(e)})),_()&&function(){if(!c)return;function e(e,t){M(m.transformHeader)&&(e=m.transformHeader(e,t)),l.push(e)}if(Array.isArray(c.data[0])){for(var t=0;_()&&t=l.length?"__parsed_extra":l[i]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(r[n]=r[n]||[],r[n].push(s)):r[n]=s}return m.header&&(i>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+i,f+t):i=r.length/2?"\r\n":"\r"}(e,r)),h=!1,m.delimiter)M(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,i,r,n){var s,a,o,h;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var u=0;u=D)return C(!0)}else for(m=F,F++;;){if(-1===(m=r.indexOf(S,m+1)))return i||u.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:h.length,index:F}),E();if(m===n-1)return E(r.substring(F,m).replace(_,S));if(S!==L||r[m+1]!==L){if(S===L||0===m||r[m-1]!==L){-1!==p&&p=D)return C(!0);break}u.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:h.length,index:F}),m++}}else m++}return E();function k(e){h.push(e),d=F}function b(e){var t=0;if(-1!==e){var i=r.substring(m+1,e);i&&""===i.trim()&&(t=i.length)}return t}function E(e){return i||(void 0===e&&(e=r.substring(F)),f.push(e),F=n,k(f),o&&R()),C()}function w(e){F=e,k(f),f=[],g=r.indexOf(x,F)}function C(e){return{data:h,errors:u,meta:{delimiter:O,linebreak:x,aborted:z,truncated:!!e,cursor:d+(t||0)}}}function R(){T(C()),h=[],u=[]}},this.abort=function(){z=!0},this.getCharIndex=function(){return F}}function _(e){var t=e.data,i=a[t.workerId],r=!1;if(t.error)i.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){r=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:y,resume:y};if(M(i.userStep)){for(var s=0;s + + + + + + Colin Knapp - Stories & Case Studies + + + + + + + + + +
    + +
    +

    Project Stories & Case Studies

    +

    Detailed stories and elaborations of projects from my portfolio.

    + +
    + +
    +
    +

    Open Source Community Success

    +

    How I revitalized an abandoned open source project, built a thriving community of 32,000+ members, and established sustainable funding.

    + + Read Full Story +
    + +
    +

    ViperWire Cybersecurity

    +

    How I built an AI-powered cybersecurity consultancy from the ground up, focusing on cutting-edge protection for digital assets.

    + + Read Full Story +
    + +
    +

    FastAsyncWorldEdit & PlotSquared

    +

    The technical challenges overcome in scaling Minecraft world editing from crashing at 50,000 edits to seamlessly handling billions.

    + + Read Full Story +
    + +
    +

    Healthcare Platform Infrastructure

    +

    An in-depth look at the infrastructure design and security implementation for the Improving MI Practices healthcare platform.

    + + Read Full Story +
    + +
    +

    WordPress Security Automation

    +

    How I developed a Docker-based solution that eliminated persistent malware attacks on a high-profile website.

    + + Read Full Story +
    + +
    +

    Airport DNS Infrastructure

    +

    Building a geographically redundant DNS cluster for Flint Bishop International Airport that achieves A+ reliability standards.

    + + Read Full Story +
    + +
    +

    NitricConcepts Leadership

    +

    Managing a distributed team of 45 contractors and implementing DevSecOps practices across multiple timezones.

    + + Read Full Story +
    +
    + +
    + +

    Accessibility: 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.

    +
    + + + + + diff --git a/docker/resume/stories/open-source-success.html b/docker/resume/stories/open-source-success.html new file mode 100644 index 0000000..4de57e1 --- /dev/null +++ b/docker/resume/stories/open-source-success.html @@ -0,0 +1,161 @@ + + + + + + + Open Source Community Success - Colin Knapp Case Study + + + + + + + + + +
    + +
    +
    +

    Building a Thriving Open Source Community

    + +
    +
    + +
    +

    In 2019, I had the opportunity to take over a promising open source project that was at risk of becoming abandoned. The original developer had created a solid foundation but was unable to continue maintaining it. Recognizing the project's potential and the community's need, I stepped in to not only maintain but significantly expand both the codebase and the community around it.

    + +
    + Docker Hub statistics showing over 10 million pulls +

    The project's Docker Hub statistics showing over 10 million pulls

    +
    + +
    + Discord community statistics showing over 32,000 members with 4,297 online +

    Our thriving Discord community with over 32,000 members

    +
    + +

    The Challenge

    +

    When I took over the project, it faced several critical challenges:

    +
      +
    • A codebase that was functional but needed significant modernization
    • +
    • Limited infrastructure for continuous integration, testing, and deployment
    • +
    • A small but passionate community with no structured way to communicate or collaborate
    • +
    • No sustainable funding model to support ongoing development and maintenance
    • +
    • Technical debt that was hindering new feature development and stability
    • +
    + +
    + "The true measure of an open source project's success isn't just code quality or feature completeness—it's the health and engagement of the community that forms around it." +
    + +

    The Approach

    +

    I developed a comprehensive strategy that addressed both technical excellence and community building:

    + +

    1. Technical Infrastructure

    +

    My first priority was establishing robust infrastructure to support sustainable development:

    +
      +
    • Implemented a comprehensive CI/CD pipeline using GitHub Actions
    • +
    • Created Docker containers for easy deployment, which have now been pulled over 10 million times
    • +
    • Established automated testing with coverage requirements for all new code
    • +
    • Developed clear documentation for both users and contributors
    • +
    • Set up monitoring and observability tools to track usage and identify issues
    • +
    + +

    2. Community Building

    +

    In parallel with technical improvements, I focused on creating a welcoming, active community:

    +
      +
    • Established a Discord server that has grown to over 32,000 members, with typically 4,000+ active at any time
    • +
    • Created structured channels for support, feature requests, showcase, and general discussion
    • +
    • Implemented community guidelines and moderation systems to maintain a positive environment
    • +
    • Organized regular community calls and update announcements
    • +
    • Recognized and celebrated community contributions
    • +
    + +

    3. Sustainable Funding

    +

    To ensure long-term viability, I established a sustainable funding model:

    +
      +
    • Set up an OpenCollective account for transparent community funding
    • +
    • Developed clear funding goals tied to specific development milestones
    • +
    • Created a sponsorship program with appropriate recognition for corporate supporters
    • +
    • Established a governance model for fund allocation
    • +
    • Built several years of runway to ensure project stability
    • +
    + +

    The Team

    +

    As the project grew, I recruited and mentored a team of over 10 developers who now contribute regularly:

    +
      +
    • Established clear contribution guidelines and review processes
    • +
    • Created an onboarding process for new contributors
    • +
    • Implemented a mentorship system pairing experienced developers with newcomers
    • +
    • Developed a governance structure that distributes decision-making authority
    • +
    • Set up regular team meetings and coordination channels
    • +
    + +

    Results & Impact

    +

    The project has achieved remarkable success under this new structure:

    +
      +
    • Over 10 million Docker image pulls, demonstrating widespread adoption
    • +
    • A thriving Discord community with 32,000+ members and 4,000+ regularly active users
    • +
    • Sustainable funding through OpenCollective with several years of runway
    • +
    • A stable team of 10+ regular contributors
    • +
    • Significant expansion of features and capabilities
    • +
    • Improved code quality, test coverage, and documentation
    • +
    • Regular release cycles with clear roadmaps
    • +
    + +

    Lessons Learned

    +

    This experience provided valuable insights into open source project management:

    + +

    Technical Lessons

    +

    Infrastructure investments pay dividends. The early focus on CI/CD, containerization, and automated testing dramatically improved both code quality and contributor experience. By making it easy to deploy, test, and contribute, we lowered barriers to entry and increased the pace of innovation.

    + +

    Additionally, the decision to containerize the application early proved prescient, as it significantly simplified deployment across diverse environments and contributed to the project's widespread adoption.

    + +

    Community Lessons

    +

    Community building requires intentional design. By creating structured spaces for different types of interactions (support, development, showcase, etc.), we enabled community members to engage in ways that matched their interests and expertise. The regular rhythm of updates and community calls helped maintain momentum and excitement.

    + +

    Transparency in decision-making and fund management built trust with the community, which proved essential for sustainable growth and support.

    + +

    Future Directions

    +

    Looking ahead, the project continues to evolve with several key initiatives:

    +
      +
    • Expanding the core team through formalized mentorship programs
    • +
    • Developing educational resources to lower the barrier to entry for new users
    • +
    • Exploring integration opportunities with complementary projects
    • +
    • Implementing a more structured feature planning process based on community input
    • +
    • Expanding the project's scope based on evolving user needs
    • +
    + +

    The success of this project demonstrates how technical excellence combined with intentional community building can transform an at-risk open source project into a thriving ecosystem that benefits thousands of users while providing sustainable opportunities for contributors.

    +
    + + +
    + + + + + diff --git a/docker/resume/stories/stories.css b/docker/resume/stories/stories.css new file mode 100644 index 0000000..45bf50f --- /dev/null +++ b/docker/resume/stories/stories.css @@ -0,0 +1,204 @@ +/* Additional styles for stories and case studies */ + +.stories-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 2rem; + margin: 2rem 0; +} + +.story-card { + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 1.5rem; + background-color: var(--card-bg); + transition: transform 0.3s, box-shadow 0.3s; + height: auto; + display: flex; + flex-direction: column; + box-sizing: border-box; + margin: 0; +} + +.story-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); +} + +.story-card h2 { + margin-top: 0; + font-size: 1.5rem; + color: var(--heading-color); +} + +.story-excerpt { + flex-grow: 1; + margin-bottom: 1rem; +} + +.story-meta { + font-size: 0.85rem; + color: var(--meta-text-color); + margin-bottom: 1rem; + font-style: italic; +} + +.story-link { + display: inline-block; + padding: 0.5rem 1rem; + background-color: var(--accent-color); + color: white; + text-decoration: none; + border-radius: 4px; + font-weight: 500; + transition: background-color 0.3s; + text-align: center; + align-self: flex-start; +} + +.story-link:hover { + background-color: var(--accent-color-darker); +} + +/* Individual story page styles */ +.story-header { + margin-bottom: 2rem; +} + +.story-header h1 { + margin-bottom: 0.5rem; +} + +.story-header .story-meta { + margin-top: 0; + margin-bottom: 1rem; +} + +/* Story image container and caption styles */ +.story-image-container { + margin: 2rem 0; + text-align: center; +} + +.story-image { + width: 100%; + max-width: 800px; + height: auto; + border-radius: 4px; + margin: 0 0 0.5rem; + border: 1px solid var(--border-color); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.image-caption { + font-size: 0.9rem; + color: var(--meta-text-color); + margin: 0.5rem 0 1.5rem; + font-style: italic; + text-align: center; +} + +.story-content { + line-height: 1.7; + max-width: 800px; + margin: 0 auto; +} + +.story-content h2 { + margin-top: 2rem; + margin-bottom: 1rem; +} + +.story-content p { + margin-bottom: 1.5rem; +} + +.story-content blockquote { + border-left: 4px solid var(--accent-color); + padding-left: 1rem; + margin-left: 0; + margin-right: 0; + font-style: italic; + color: var(--blockquote-color); +} + +.story-footer { + margin-top: 3rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.related-stories { + margin-top: 2rem; +} + +.related-stories h3 { + margin-bottom: 1rem; +} + +.related-stories-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1rem; +} + +.story-nav { + display: flex; + justify-content: space-between; + margin-top: 2rem; + margin-bottom: 2rem; +} + +.story-nav-link { + padding: 0.5rem 1rem; + border: 1px solid var(--border-color); + border-radius: 4px; + text-decoration: none; + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; +} + +.story-nav-link:hover { + background-color: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +.story-nav-link.prev::before { + content: "← "; +} + +.story-nav-link.next::after { + content: " →"; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .stories-grid { + grid-template-columns: 1fr; + } + + .related-stories-list { + grid-template-columns: 1fr; + } + + .story-nav { + flex-direction: column; + gap: 1rem; + } + + .story-nav-link { + text-align: center; + } +} + +/* Accessibility enhancements */ +.story-link:focus { + outline: 2px solid var(--focus-outline-color); + outline-offset: 2px; +} + +/* Utility classes */ +.hidden { + visibility: hidden; +} \ No newline at end of file diff --git a/docker/resume/stories/story-with-includes.html b/docker/resume/stories/story-with-includes.html new file mode 100644 index 0000000..b8464cb --- /dev/null +++ b/docker/resume/stories/story-with-includes.html @@ -0,0 +1,56 @@ + + + + + + + Story Example - Colin Knapp + + + + + + + + + +
    + + +
    +

    Story Title Example

    + +
    +
    + +
    +

    This is an example story content paragraph.

    + +

    Story Section

    +

    This is a section of the story.

    + + +
    + + + + + + + diff --git a/docker/resume/stories/template-story.html b/docker/resume/stories/template-story.html new file mode 100644 index 0000000..700b410 --- /dev/null +++ b/docker/resume/stories/template-story.html @@ -0,0 +1,80 @@ + + + + + + + [Story Title] - Colin Knapp Case Study + + + + + + + + + +
    + +
    +
    +

    [Story Title]

    + +
    +
    + +
    + +

    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vehicula, nisl vel ultricies aliquet, nisi urna posuere nibh, vel dapibus sapien libero ac nisi. Phasellus non volutpat orci, eu eleifend diam. Integer scelerisque euismod sem, vel euismod orci viverra a.

    + +

    The Challenge

    +

    Cras consectetur dolor vel arcu luctus, a hendrerit tortor ultricies. Fusce eu eros vel mauris fermentum volutpat. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer rhoncus tortor vitae turpis tincidunt, sed congue lacus tempor.

    + +
    + "This project represented a significant challenge that required innovative thinking and cutting-edge technology solutions." +
    + +

    The Approach

    +

    Praesent feugiat tortor non tortor maximus, at congue nibh tincidunt. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi vulputate velit non enim euismod, sit amet maximus mauris consectetur. Donec placerat arcu non faucibus condimentum.

    + + + + +

    Technical Implementation

    +

    Aenean non auctor mauris, id dapibus turpis. Fusce vestibulum mi sit amet sem commodo, id aliquam magna sollicitudin. Donec vel libero cursus, venenatis justo sit amet, faucibus nisi. Vestibulum nec suscipit mi. Ut fringilla scelerisque eros, id vestibulum lectus vehicula id.

    + +

    Results & Impact

    +

    Suspendisse potenti. Curabitur pharetra neque quis dolor pretium, nec feugiat metus varius. Nulla facilisi. Nam ut justo sed leo viverra iaculis. Aliquam eget risus vitae quam dapibus dignissim at vel lectus. Proin convallis velit non dictum faucibus.

    + +

    Lessons Learned

    +

    Proin malesuada facilisis felis, quis placerat erat dignissim nec. Morbi hendrerit elit vitae orci interdum mattis. Curabitur imperdiet velit ut libero egestas, at accumsan arcu vehicula. Nullam ac orci et velit efficitur condimentum.

    +
    + + +
    + + + + + diff --git a/docker/resume/stories/viperwire.html b/docker/resume/stories/viperwire.html new file mode 100644 index 0000000..5d075ef --- /dev/null +++ b/docker/resume/stories/viperwire.html @@ -0,0 +1,126 @@ + + + + + + + ViperWire Cybersecurity - Colin Knapp Case Study + + + + + + + + +
    + +
    +
    +

    Building ViperWire: An AI-Powered Cybersecurity Consultancy

    + +
    +
    + +
    +

    In early 2023, I identified a critical gap in the cybersecurity market: small to medium-sized businesses were increasingly becoming targets for sophisticated cyber attacks, but lacked access to enterprise-grade security solutions that could adapt to rapidly evolving threats. This observation led to the creation of ViperWire, an AI-powered cybersecurity consultancy designed to democratize access to advanced security measures.

    + +

    The Challenge

    +

    The cybersecurity landscape in 2023 presented several unique challenges:

    +
      +
    • The increasing sophistication of attacks targeting SMBs with limited security budgets
    • +
    • A shortage of cybersecurity professionals capable of addressing modern threats
    • +
    • The rapid evolution of attack vectors requiring constant vigilance and adaptation
    • +
    • The need for solutions that could scale from small businesses to larger enterprises
    • +
    + +
    + "The typical SMB faces the same threat actors as Fortune 500 companies, but with a fraction of the resources to defend themselves. This asymmetry creates a perfect storm where businesses are increasingly vulnerable while security solutions remain inaccessible." +
    + +

    The Approach

    +

    I built ViperWire around three core principles that would differentiate it in the market:

    + +

    1. AI-Augmented Security Analysis

    +

    Rather than attempting to replace human expertise with AI, I designed systems where AI tools augment human analysts, dramatically increasing their efficiency and effectiveness. This approach began with custom-built monitoring tools that use machine learning to identify behavioral anomalies and prioritize potential threats, allowing human experts to focus on the most critical issues.

    + +

    2. Accessible Enterprise-Grade Protection

    +

    By leveraging containerization, infrastructure-as-code, and modular security components, I created scalable security systems that could be rapidly deployed across organizations of varying sizes. This technical architecture allowed ViperWire to deliver enterprise-caliber protection at price points accessible to smaller organizations.

    + +

    3. Continuous Adaptation

    +

    I implemented a continuous security improvement cycle that incorporated threat intelligence feeds, regular penetration testing, and automated vulnerability scanning. This approach ensured that security postures evolved in tandem with emerging threats rather than reacting after incidents occurred.

    + +

    Technical Implementation

    +

    The technical architecture of ViperWire comprises several innovative components:

    + +

    Threat Detection Infrastructure

    +

    I built a distributed monitoring system using a combination of open-source tools (Wazuh, Suricata, OSSEC) enhanced with custom machine learning models to detect anomalous network and system behaviors. The architecture utilizes Kubernetes for orchestration and Prometheus/Grafana for metrics visualization, with custom alerting thresholds tuned to each client's environment.

    + +

    Response Automation

    +

    To counter the speed of modern attacks, I developed an automated response framework using Python and Ansible that could isolate compromised systems, revoke credentials, and implement temporary access controls within seconds of a confirmed threat detection. This system reduced the mean time to respond from hours to minutes, significantly limiting potential damage.

    + +

    Security Assessment Pipeline

    +

    For proactive security, I created an assessment pipeline incorporating static analysis, dynamic testing, and configuration auditing. This suite leverages Docker containers for consistent, reproducible security tests across different environments and includes custom scanners for emerging vulnerabilities not yet covered by commercial tools.

    + +

    Results & Impact

    +

    In its first year, ViperWire has achieved several notable successes:

    +
      +
    • Successfully prevented ransomware attacks at two clients who had been targeted, saving an estimated $500,000 in potential losses
    • +
    • Reduced security alert noise by 87% through improved detection algorithms, allowing for more focused attention on genuine threats
    • +
    • Decreased mean time to detection of security incidents from 24+ hours to under 15 minutes
    • +
    • Enabled five small businesses to achieve compliance with industry security standards that were previously beyond their reach
    • +
    + +

    Lessons Learned

    +

    Building ViperWire has provided valuable insights into both technical and business aspects of cybersecurity:

    + +

    Technical Lessons

    +

    The most effective security solutions combine multiple detection methodologies rather than relying on any single approach. Our hybrid model of behavioral analysis, signature detection, and anomaly identification proved far more effective than any individual method alone.

    + +

    Additionally, I discovered that properly tuned automation dramatically reduces false positives—the bane of many security operations—while still capturing genuine threats. The key was implementing progressive verification steps that validate alerts before triggering high-impact responses.

    + +

    Business Lessons

    +

    Perhaps most importantly, I learned that transparency builds trust in security services. By providing clients with clear visibility into threat detection processes and plainly explaining technical concepts, ViperWire was able to build stronger relationships and encourage better security practices within client organizations.

    + +

    Future Directions

    +

    Looking ahead, ViperWire is expanding into several promising areas:

    +
      +
    • Developing specialized security solutions for IoT environments in manufacturing settings
    • +
    • Creating educational resources to help clients build internal security capabilities
    • +
    • Expanding AI capabilities to provide predictive threat intelligence specific to each client's industry
    • +
    + +

    The founding principle of ViperWire—that sophisticated security should be accessible to organizations of all sizes—continues to guide its evolution and growth.

    +
    + + +
    + + + + + + + + diff --git a/docker/resume/styles.css b/docker/resume/styles.css index 47629db..fabdf58 100644 --- a/docker/resume/styles.css +++ b/docker/resume/styles.css @@ -8,6 +8,16 @@ --theme-border: #ddd; --theme-hover: #e0e0e0; --date-color: #555555; + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-tertiary: #eaeaea; + --bg-hover: #f0f0f0; + --text-primary: #333333; + --button-bg: #f5f5f5; + --button-hover-bg: #e0e0e0; + --focus-outline-color: #0056b3; + --progress-bg: #e0e0e0; + --accent-hover: #003d82; } /* Dark theme variables when system prefers dark mode (auto setting) */ @@ -22,6 +32,16 @@ --theme-border: #404040; --theme-hover: #3d3d3d; --date-color: #a0a0a0; + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-tertiary: #3d3d3d; + --bg-hover: #333333; + --text-primary: #e0e0e0; + --button-bg: #2d2d2d; + --button-hover-bg: #3d3d3d; + --focus-outline-color: #5fa9ff; + --progress-bg: #404040; + --accent-hover: #8ac2ff; } } @@ -36,6 +56,16 @@ html[data-theme='light'] { --theme-border: #ddd; --theme-hover: #e0e0e0; --date-color: #555555; + --bg-primary: #ffffff; + --bg-secondary: #f5f5f5; + --bg-tertiary: #eaeaea; + --bg-hover: #f0f0f0; + --text-primary: #333333; + --button-bg: #f5f5f5; + --button-hover-bg: #e0e0e0; + --focus-outline-color: #0056b3; + --progress-bg: #e0e0e0; + --accent-hover: #003d82; } /* Dark theme variables when manually selected */ @@ -49,6 +79,16 @@ html[data-theme='dark'] { --theme-border: #404040; --theme-hover: #3d3d3d; --date-color: #a0a0a0; + --bg-primary: #1a1a1a; + --bg-secondary: #2d2d2d; + --bg-tertiary: #3d3d3d; + --bg-hover: #333333; + --text-primary: #e0e0e0; + --button-bg: #2d2d2d; + --button-hover-bg: #3d3d3d; + --focus-outline-color: #5fa9ff; + --progress-bg: #404040; + --accent-hover: #8ac2ff; } body { @@ -62,6 +102,17 @@ body { margin: 0 auto; } +/* Container that can expand to full width */ +.container-fluid { + width: 100%; + max-width: 100%; + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; + box-sizing: border-box; +} + h1, h2, h3 { color: var(--text-color); margin-top: 1.5em; @@ -163,33 +214,6 @@ hr { background-color: rgba(128, 128, 128, 0.2); } -/* Print styles */ -@media print { - .theme-switch { - display: none; - } - - body { - background: white !important; - color: black !important; - } - - a { - color: black !important; - text-decoration: none !important; - } - - .container-fluid { - max-width: 100% !important; - margin: 0 !important; - padding: 0 !important; - } - - hr { - border-color: black !important; - } -} - @media (max-width: 600px) { body { padding: 10px; @@ -206,4 +230,112 @@ hr { h3 { font-size: 1.2em; } +} + +/* Navigation styles */ +.main-nav { + display: flex; + justify-content: center; + margin: 1rem 0; +} + +.main-nav ul { + display: flex; + list-style: none; + margin: 0; + padding: 0; + gap: 1rem; + border-radius: 4px; + background-color: var(--theme-bg); + padding: 0.5rem 1rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.main-nav li { + margin: 0; + padding: 0; + position: relative; +} + +.main-nav a { + display: block; + padding: 0.5rem 1rem; + text-decoration: none; + color: var(--text-color); + font-weight: 500; + border-radius: 4px; + transition: background-color 0.3s, color 0.3s; +} + +.main-nav a:hover { + background-color: var(--theme-hover); + color: var(--accent-color); +} + +.main-nav a.active { + background-color: var(--accent-color); + color: white; +} + +/* Dropdown styles */ +.main-nav .dropdown { + position: relative; + cursor: pointer; +} + +.main-nav .dropdown > a::after { + content: "▼"; + font-size: 0.7em; + margin-left: 0.5em; + vertical-align: middle; +} + +.main-nav .dropdown-content { + display: none; + position: absolute; + top: 100%; + left: 0; + background-color: var(--theme-bg); + min-width: 160px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + z-index: 1000; + border-radius: 4px; + padding: 0.5rem 0; + margin-top: 0.5rem; +} + +.main-nav .dropdown:hover .dropdown-content, +.main-nav .dropdown:focus-within .dropdown-content { + display: block; +} + +.main-nav .dropdown-content a { + padding: 0.5rem 1rem; + display: block; + text-align: left; + white-space: nowrap; +} + +.main-nav .dropdown-content a.active { + background-color: var(--accent-color); + color: white; +} + +/* Responsive navigation */ +@media (max-width: 600px) { + .main-nav ul { + flex-direction: column; + gap: 0.5rem; + } + + .main-nav a { + text-align: center; + } + + .main-nav .dropdown-content { + position: static; + box-shadow: none; + margin-top: 0; + padding-left: 1rem; + } } \ No newline at end of file diff --git a/docker/resume/template-with-includes.html b/docker/resume/template-with-includes.html new file mode 100644 index 0000000..3eb4d08 --- /dev/null +++ b/docker/resume/template-with-includes.html @@ -0,0 +1,30 @@ + + + + + + + Template with Includes - Colin Knapp + + + + + + + + +
    + + +

    Page Title

    +

    This is the main content of the page.

    + +
    + +

    Section Title

    +

    This is a section of content.

    + + + + + diff --git a/docker/resume/test-csv-tool.sh b/docker/resume/test-csv-tool.sh new file mode 100755 index 0000000..3e74a39 --- /dev/null +++ b/docker/resume/test-csv-tool.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# ===================================================================== +# test-csv-tool.sh - Test the CSV tool functionality +# ===================================================================== +# This script checks if the CSV tool page loads without CSP errors +# ===================================================================== + +echo "=== Testing CSV Tool ===" + +# Create a test CSV file +echo "Name,Age,City +John,30,New York +Jane,25,San Francisco +Bob,40,Chicago" > test.csv + +# Check if the page loads properly +echo "Checking if the CSV tool page loads properly..." +RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/one-pager-tools/csv-tool.html) + +if [ "$RESPONSE" -eq 200 ]; then + echo "✅ CSV tool page loads successfully (HTTP $RESPONSE)" +else + echo "❌ CSV tool page failed to load (HTTP $RESPONSE)" + exit 1 +fi + +# Check for CSP errors in the response headers +echo "Checking for CSP errors in response headers..." +CSP_HEADER=$(curl -s -I http://localhost:8080/one-pager-tools/csv-tool.html | grep -i "Content-Security-Policy") + +if [ -n "$CSP_HEADER" ]; then + echo "✅ CSP header found in response" +else + echo "❌ CSP header not found in response" + exit 1 +fi + +# Clean up +rm -f test.csv + +echo "=== CSV Tool Test Completed Successfully ===" +echo "The CSV tool appears to be working correctly." +echo "You can manually test it by visiting: http://localhost:8080/one-pager-tools/csv-tool.html" +echo "and pasting CSV data into the textarea." \ No newline at end of file diff --git a/docker/resume/theme.js b/docker/resume/theme.js index dad36b1..8c41c72 100644 --- a/docker/resume/theme.js +++ b/docker/resume/theme.js @@ -2,6 +2,12 @@ document.addEventListener('DOMContentLoaded', function() { const themeToggle = document.getElementById('themeToggle'); const html = document.documentElement; + // Check if themeToggle exists before proceeding + if (!themeToggle) { + console.log('Theme toggle button not found on this page'); + return; + } + // Check for saved theme preference, default to auto const savedTheme = localStorage.getItem('theme') || 'auto'; diff --git a/docker/resume/update-csp-hashes.sh b/docker/resume/update-csp-hashes.sh new file mode 100755 index 0000000..8a6500b --- /dev/null +++ b/docker/resume/update-csp-hashes.sh @@ -0,0 +1,230 @@ +#!/bin/bash +# ===================================================================== +# update-csp-hashes.sh - Update Content Security Policy hashes +# ===================================================================== +# This script updates the CSP hashes for: +# 1. All JavaScript and CSS files +# 2. All inline style attributes in HTML files +# 3. Adds CSP meta tags to HTML files +# After running this script, restart the server using: +# ./caddy.sh +# ===================================================================== + +set -e + +echo "Updating CSP hashes for all JavaScript, CSS files, and inline styles..." + +# Directory containing the files +BASE_DIR="$(pwd)" +CADDYFILE="$BASE_DIR/Caddyfile" +TEMP_INLINE_HASHES_FILE=$(mktemp) + +# Arrays to store hashes +SCRIPT_HASHES=() +STYLE_HASHES=() + +# Calculate hash for a file +calculate_hash() { + local file=$1 + shasum -a 256 "$file" | awk '{print $1}' | xxd -r -p | base64 +} + +# Calculate hash for inline style +calculate_inline_hash() { + local style_content=$1 + echo -n "$style_content" | shasum -a 256 | awk '{print $1}' | xxd -r -p | base64 +} + +# Process JavaScript files +echo "Processing JavaScript files..." +for js_file in $(find "$BASE_DIR" -name "*.js" -type f); do + echo "Processing $js_file" + file_name=$(basename "$js_file") + hash=$(calculate_hash "$js_file") + SCRIPT_HASHES+=("'sha256-$hash'") + + # Update HTML files that reference this JS file + for html_file in $(find "$BASE_DIR" -name "*.html" -type f); do + if grep -q "$file_name" "$html_file"; then + echo "Updating $file_name in $html_file" + + # Create a temporary file for the replacement + tmp_file=$(mktemp) + + # For files with existing integrity attribute + if grep -q "$file_name.*integrity" "$html_file"; then + # Use awk for safer text processing + awk -v fname="$file_name" -v newhash="$hash" ' + { + if ($0 ~ fname && $0 ~ /integrity/) { + gsub(/integrity="sha256-[^"]*"/, "integrity=\"sha256-" newhash "\""); + } + print; + }' "$html_file" > "$tmp_file" + else + # Add integrity attribute if it doesn't exist + awk -v fname="$file_name" -v newhash="$hash" ' + { + if ($0 ~ fname && $0 ~ /src/ && !($0 ~ /integrity/)) { + gsub(/src="[^"]*"/, "&" " integrity=\"sha256-" newhash "\""); + } + print; + }' "$html_file" > "$tmp_file" + fi + + # Replace original file with modified content + mv "$tmp_file" "$html_file" + fi + done +done + +# Process CSS files +echo "Processing CSS files..." +for css_file in $(find "$BASE_DIR" -name "*.css" -type f); do + echo "Processing $css_file" + file_name=$(basename "$css_file") + hash=$(calculate_hash "$css_file") + STYLE_HASHES+=("'sha256-$hash'") + + # Update HTML files that reference this CSS file + for html_file in $(find "$BASE_DIR" -name "*.html" -type f); do + if grep -q "$file_name" "$html_file"; then + echo "Updating $file_name in $html_file" + + # Create a temporary file for the replacement + tmp_file=$(mktemp) + + # For files with existing integrity attribute + if grep -q "$file_name.*integrity" "$html_file"; then + # Use awk for safer text processing + awk -v fname="$file_name" -v newhash="$hash" ' + { + if ($0 ~ fname && $0 ~ /integrity/) { + gsub(/integrity="sha256-[^"]*"/, "integrity=\"sha256-" newhash "\""); + } + print; + }' "$html_file" > "$tmp_file" + else + # Add integrity attribute if it doesn't exist + awk -v fname="$file_name" -v newhash="$hash" ' + { + if ($0 ~ fname && $0 ~ /href/ && !($0 ~ /integrity/)) { + gsub(/href="[^"]*"/, "&" " integrity=\"sha256-" newhash "\""); + } + print; + }' "$html_file" > "$tmp_file" + fi + + # Replace original file with modified content + mv "$tmp_file" "$html_file" + fi + done +done + +# Find and process inline styles - using a more thorough approach +echo "Processing HTML files for inline styles..." +find "$BASE_DIR" -name "*.html" -type f | while read -r html_file; do + echo "Processing $html_file for inline styles..." + + # Use a more comprehensive grep pattern to catch all inline styles + # This includes both style="..." and style = "..." patterns + grep -o 'style\s*=\s*"[^"]*"' "$html_file" | sed 's/style\s*=\s*"\(.*\)"/\1/' | while read -r style_content; do + if [ -n "$style_content" ]; then + hash=$(calculate_inline_hash "$style_content") + echo "Found inline style: '$style_content'" + echo "Calculated hash: sha256-$hash" + echo "'sha256-$hash'" >> "$TEMP_INLINE_HASHES_FILE" + fi + done +done + +# Sort and remove duplicates from inline style hashes +if [ -f "$TEMP_INLINE_HASHES_FILE" ]; then + sort -u "$TEMP_INLINE_HASHES_FILE" > "${TEMP_INLINE_HASHES_FILE}.sorted" + mv "${TEMP_INLINE_HASHES_FILE}.sorted" "$TEMP_INLINE_HASHES_FILE" + + # Add inline style hashes to the STYLE_HASHES array + while read -r hash; do + STYLE_HASHES+=("$hash") + done < "$TEMP_INLINE_HASHES_FILE" + + # Clean up + rm -f "$TEMP_INLINE_HASHES_FILE" +fi + +# Combine all hashes for CSP +echo "Updating Caddyfile CSP headers..." +SCRIPT_HASHES_STR=$(printf " %s" "${SCRIPT_HASHES[@]}") +STYLE_HASHES_STR=$(printf " %s" "${STYLE_HASHES[@]}") + +# Create the CSP string +CSP_STRING="default-src 'none'; script-src 'self'$SCRIPT_HASHES_STR; style-src 'self'$STYLE_HASHES_STR; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none';" + +# Create a temporary file for the Caddyfile update +tmp_file=$(mktemp) + +# Update CSP in Caddyfile using awk for more reliable text processing +awk -v csp_string="$CSP_STRING" ' +{ + if ($0 ~ /Content-Security-Policy/) { + gsub(/Content-Security-Policy "[^"]*"/, "Content-Security-Policy \"" csp_string "\""); + } + print; +}' "$CADDYFILE" > "$tmp_file" + +# Replace original Caddyfile with modified content +mv "$tmp_file" "$CADDYFILE" + +# Also update Caddyfile.local if it exists +if [ -f "$BASE_DIR/Caddyfile.local" ]; then + echo "Updating Caddyfile.local CSP headers..." + tmp_file=$(mktemp) + + awk -v csp_string="$CSP_STRING" ' + { + if ($0 ~ /Content-Security-Policy/) { + gsub(/Content-Security-Policy "[^"]*"/, "Content-Security-Policy \"" csp_string "\""); + } + print; + }' "$BASE_DIR/Caddyfile.local" > "$tmp_file" + + # Replace original Caddyfile.local with modified content + mv "$tmp_file" "$BASE_DIR/Caddyfile.local" +fi + +# Add CSP meta tags to HTML files +echo "Adding CSP meta tags to HTML files..." +for html_file in $(find "$BASE_DIR" -name "*.html" -type f); do + echo "Adding CSP meta tag to $html_file" + + # Create a temporary file for the replacement + tmp_file=$(mktemp) + + # Check if the file already has a CSP meta tag + if grep -q ' "$tmp_file" + else + # Add CSP meta tag after the last meta tag + awk -v csp_string="$CSP_STRING" ' + { + print; + if ($0 ~ /<\/head>/ && !added_csp) { + print " "; + added_csp = 1; + } + }' "$html_file" > "$tmp_file" + fi + + # Replace original file with modified content + mv "$tmp_file" "$html_file" +done + +echo "CSP hashes updated successfully!" +echo "To apply changes, restart the server using: ./caddy.sh" \ No newline at end of file