diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..8156ab7
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,27 @@
+# Security Headers
+Header always set X-Content-Type-Options nosniff
+Header always set X-Frame-Options DENY
+Header always set X-XSS-Protection "1; mode=block"
+Header always set Referrer-Policy "strict-origin-when-cross-origin"
+Header always set Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; script-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self';"
+
+# Disable server signature
+ServerTokens Prod
+ServerSignature Off
+
+# Prevent access to sensitive files
+
+ Order allow,deny
+ Deny from all
+
+
+# Prevent access to backup files
+
+ Order allow,deny
+ Deny from all
+
+
+# Enable HTTPS redirect (uncomment when using HTTPS)
+# RewriteEngine On
+# RewriteCond %{HTTPS} off
+# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
diff --git a/coolify-nginx.conf b/coolify-nginx.conf
new file mode 100644
index 0000000..e312604
--- /dev/null
+++ b/coolify-nginx.conf
@@ -0,0 +1,56 @@
+# Coolify Static Site Security Configuration
+# This file provides nginx security headers for static sites deployed on Coolify
+# Place this in your project root or reference it in your nixpacks.toml
+
+# Security headers for static sites served by nginx in Coolify
+add_header X-Content-Type-Options nosniff always;
+add_header X-Frame-Options DENY always;
+add_header X-XSS-Protection "1; mode=block" always;
+add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; script-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self';" always;
+
+# Additional security measures
+add_header X-Permitted-Cross-Domain-Policies none always;
+add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+# Hide nginx version
+server_tokens off;
+
+# Prevent access to hidden files
+location ~ /\. {
+ deny all;
+ access_log off;
+ log_not_found off;
+}
+
+# Prevent access to backup and temporary files
+location ~* \.(bak|backup|old|tmp|swp|swo|log)$ {
+ deny all;
+ access_log off;
+ log_not_found off;
+}
+
+# Security for common files
+location ~* \.(htaccess|htpasswd|ini|log|sh|sql|conf)$ {
+ deny all;
+ access_log off;
+ log_not_found off;
+}
+
+# Optimize static file serving
+location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ add_header X-Content-Type-Options nosniff always;
+ add_header X-Frame-Options DENY always;
+}
+
+# Main location for HTML files
+location / {
+ try_files $uri $uri/ /index.html;
+ add_header X-Content-Type-Options nosniff always;
+ add_header X-Frame-Options DENY always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self'; script-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self';" always;
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..66296a8
--- /dev/null
+++ b/index.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+ Chemex Pour-Over Guide
+
+
+
+
+
+
+
+
+
+
Get Ready
+
+ - Coffee: 21g (Medium-fine grind)
+ - Water: 340g at 94-96°C
+ - Ratio: 1:16
+ - Total Time: ~4:00 min
+
+
Preheat your Chemex and metal filter with hot water before you begin.
+
+
+
+
+
+
+
+ 0:00
+ 0g
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nixpacks.toml b/nixpacks.toml
new file mode 100644
index 0000000..71019c1
--- /dev/null
+++ b/nixpacks.toml
@@ -0,0 +1,25 @@
+# Nixpacks configuration for Coolify static site deployment
+# This ensures your Coffee website is deployed securely with nginx
+
+[variables]
+# Enable static site serving with nginx
+NIXPACKS_NO_CACHE = "1"
+
+[phases.setup]
+# Install nginx and required packages
+nixPkgs = ["nginx"]
+
+[phases.build]
+# Copy security configuration
+cmds = [
+ "mkdir -p /etc/nginx/conf.d",
+ "cp coolify-nginx.conf /etc/nginx/conf.d/security.conf"
+]
+
+[phases.start]
+# Start nginx with security configuration
+cmd = "nginx -g 'daemon off;'"
+
+[staticAssets]
+# Configure static asset serving
+"/" = "."
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..e678dd1
--- /dev/null
+++ b/script.js
@@ -0,0 +1,316 @@
+// Security utility functions
+function sanitizeText(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+}
+
+function validateElement(element) {
+ return element && element.nodeType === Node.ELEMENT_NODE;
+}
+
+// Secure DOM manipulation
+function secureSetTextContent(element, text) {
+ if (!validateElement(element)) {
+ console.error('Invalid element provided');
+ return false;
+ }
+ element.textContent = sanitizeText(text);
+ return true;
+}
+
+// Audio notification function
+function playTimerCompleteSound() {
+ try {
+ // Create audio context
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+
+ // Create a pleasant chime sound using multiple tones
+ const frequencies = [523.25, 659.25, 783.99]; // C5, E5, G5 - major chord
+ const duration = 0.8; // seconds
+
+ frequencies.forEach((freq, index) => {
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.frequency.setValueAtTime(freq, audioContext.currentTime);
+ oscillator.type = 'sine';
+
+ // Create a gentle fade-in and fade-out
+ const startTime = audioContext.currentTime + (index * 0.1);
+ const endTime = startTime + duration;
+
+ gainNode.gain.setValueAtTime(0, startTime);
+ gainNode.gain.linearRampToValueAtTime(0.1, startTime + 0.1);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, endTime);
+
+ oscillator.start(startTime);
+ oscillator.stop(endTime);
+ });
+
+ console.log('Timer complete sound played');
+ } catch (error) {
+ console.log('Could not play sound:', error);
+ // Fallback: try to use a simple beep
+ try {
+ const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+
+ oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
+ oscillator.type = 'sine';
+
+ gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
+ gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
+
+ oscillator.start();
+ oscillator.stop(audioContext.currentTime + 0.5);
+ } catch (fallbackError) {
+ console.log('Audio not supported in this browser');
+ }
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ const setupCard = document.getElementById('setup');
+ const brewingCard = document.getElementById('brewing-process');
+ const startBtn = document.getElementById('start-btn');
+ const nextBtn = document.getElementById('next-btn');
+ const resetBtn = document.getElementById('reset-btn');
+ const pauseBtn = document.getElementById('pause-btn');
+
+ // Debug: Check if pause button exists
+ if (!pauseBtn) {
+ console.error('Pause button not found!');
+ } else {
+ console.log('Pause button found successfully');
+ }
+
+ const timerDisplay = document.getElementById('timer-display');
+ const totalWaterDisplay = document.getElementById('total-water');
+ const stepTitle = document.getElementById('step-title');
+ const stepInstruction = document.getElementById('step-instruction');
+
+ const progressRing = document.querySelector('.progress-ring__circle');
+ const radius = progressRing.r.baseVal.value;
+ const circumference = 2 * Math.PI * radius;
+ progressRing.style.strokeDasharray = `${circumference} ${circumference}`;
+ progressRing.style.strokeDashoffset = circumference;
+
+ const brewSteps = [
+ {
+ title: 'Bloom',
+ instruction: 'Pour 50g of water evenly over the coffee grounds. Wait for the coffee to degas.',
+ duration: 45, // seconds
+ water: 50, // grams
+ },
+ {
+ title: 'Second Pour',
+ instruction: 'Slowly pour in circles until the total weight reaches 200g. Avoid pouring on the edges.',
+ duration: 60,
+ water: 200,
+ },
+ {
+ title: 'Final Pour',
+ instruction: 'Continue pouring in slow circles until you reach the final weight of 340g.',
+ duration: 60,
+ water: 340,
+ },
+ {
+ title: 'Let it Drain',
+ instruction: 'Allow all the water to drain through the coffee bed. This should take about 30-60 seconds.',
+ duration: 60,
+ water: 340,
+ },
+ {
+ title: 'Enjoy!',
+ instruction: 'Your delicious Chemex coffee is ready. Remove the filter, swirl, and serve.',
+ duration: 0,
+ water: 340,
+ }
+ ];
+
+ let currentStepIndex = 0;
+ let timerInterval;
+ let secondsElapsed = 0;
+ let totalSeconds = 0;
+
+ // Step-specific timer states - each step has its own pause state
+ let stepTimerStates = brewSteps.map(() => ({
+ isPaused: false,
+ pausedTime: 0,
+ secondsElapsed: 0,
+ isCompleted: false
+ }));
+
+ function setProgress(percent) {
+ const offset = circumference - (percent / 100) * circumference;
+ progressRing.style.strokeDashoffset = offset;
+ }
+
+ function formatTime(seconds) {
+ const minutes = Math.floor(seconds / 60);
+ const remainingSeconds = seconds % 60;
+ return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
+ }
+
+ function startTimer() {
+ const step = brewSteps[currentStepIndex];
+ const stepState = stepTimerStates[currentStepIndex];
+ totalSeconds = step.duration;
+
+ // Use step-specific timer state
+ if (!stepState.isPaused) {
+ // Starting fresh or step not paused
+ secondsElapsed = stepState.secondsElapsed;
+ } else {
+ // Resuming from pause
+ secondsElapsed = stepState.pausedTime;
+ stepState.isPaused = false;
+ pauseBtn.classList.remove('paused');
+ secureSetTextContent(pauseBtn, 'Pause');
+ }
+
+ updateTimerDisplay();
+ const percent = (secondsElapsed / totalSeconds) * 100;
+ setProgress(percent);
+
+ if (totalSeconds === 0) return;
+
+ timerInterval = setInterval(() => {
+ const currentStepState = stepTimerStates[currentStepIndex];
+ if (!currentStepState.isPaused) {
+ secondsElapsed++;
+ currentStepState.secondsElapsed = secondsElapsed;
+ updateTimerDisplay();
+ const percent = (secondsElapsed / totalSeconds) * 100;
+ setProgress(percent);
+
+ if (secondsElapsed >= totalSeconds) {
+ clearInterval(timerInterval);
+ currentStepState.isCompleted = true;
+ // Play pleasant sound when timer completes
+ playTimerCompleteSound();
+ }
+ }
+ }, 1000);
+ }
+
+ function updateTimerDisplay() {
+ secureSetTextContent(timerDisplay, formatTime(secondsElapsed));
+ }
+
+ function updateUIForStep() {
+ const step = brewSteps[currentStepIndex];
+ const stepState = stepTimerStates[currentStepIndex];
+
+ if (!step) {
+ console.error('Invalid step index');
+ return;
+ }
+
+ secureSetTextContent(stepTitle, step.title);
+ secureSetTextContent(stepInstruction, step.instruction);
+ secureSetTextContent(totalWaterDisplay, `${step.water}g`);
+
+ if (currentStepIndex === brewSteps.length - 1) {
+ secureSetTextContent(nextBtn, 'Brew Again');
+ } else {
+ secureSetTextContent(nextBtn, 'Next Step');
+ }
+
+ // Reset pause button state for the new step
+ if (stepState.isPaused) {
+ pauseBtn.classList.add('paused');
+ secureSetTextContent(pauseBtn, 'Resume');
+ } else {
+ pauseBtn.classList.remove('paused');
+ secureSetTextContent(pauseBtn, 'Pause');
+ }
+
+ clearInterval(timerInterval);
+ startTimer();
+ }
+
+ startBtn.addEventListener('click', () => {
+ setupCard.classList.remove('active');
+ brewingCard.style.display = 'block';
+ // A little trick to trigger the animation
+ setTimeout(() => brewingCard.classList.add('active'), 10);
+ updateUIForStep();
+ });
+
+ nextBtn.addEventListener('click', () => {
+ // Bounds checking for security
+ if (currentStepIndex < 0 || currentStepIndex >= brewSteps.length - 1) {
+ if (currentStepIndex >= brewSteps.length - 1) {
+ reset();
+ return;
+ }
+ }
+ currentStepIndex = Math.max(0, Math.min(currentStepIndex + 1, brewSteps.length - 1));
+ updateUIForStep();
+ });
+
+ resetBtn.addEventListener('click', reset);
+
+ // Pause/Resume functionality - now step-specific
+ function togglePause() {
+ const currentStepState = stepTimerStates[currentStepIndex];
+ console.log('Pause button clicked! Current step state:', currentStepState.isPaused);
+
+ if (!currentStepState.isPaused) {
+ // Pause the current step's timer
+ currentStepState.isPaused = true;
+ currentStepState.pausedTime = secondsElapsed;
+ pauseBtn.classList.add('paused');
+ secureSetTextContent(pauseBtn, 'Resume');
+ console.log('Step', currentStepIndex, 'timer paused at:', currentStepState.pausedTime, 'seconds');
+ } else {
+ // Resume the current step's timer
+ currentStepState.isPaused = false;
+ pauseBtn.classList.remove('paused');
+ secureSetTextContent(pauseBtn, 'Pause');
+ console.log('Step', currentStepIndex, 'timer resumed from:', currentStepState.pausedTime, 'seconds');
+ }
+ }
+
+ if (pauseBtn) {
+ pauseBtn.addEventListener('click', togglePause);
+ console.log('Pause button event listener added');
+ } else {
+ console.error('Cannot add event listener - pause button not found');
+ }
+
+ function reset() {
+ clearInterval(timerInterval);
+ currentStepIndex = 0;
+ secondsElapsed = 0;
+ totalSeconds = 0;
+
+ // Reset all step-specific timer states
+ stepTimerStates = brewSteps.map(() => ({
+ isPaused: false,
+ pausedTime: 0,
+ secondsElapsed: 0,
+ isCompleted: false
+ }));
+
+ // Reset pause button state
+ pauseBtn.classList.remove('paused');
+ secureSetTextContent(pauseBtn, 'Pause');
+
+ brewingCard.classList.remove('active');
+ setupCard.classList.add('active');
+ setTimeout(() => brewingCard.style.display = 'none', 500); // Match CSS transition
+
+ updateTimerDisplay();
+ setProgress(0);
+ }
+});
diff --git a/security.md b/security.md
new file mode 100644
index 0000000..984c839
--- /dev/null
+++ b/security.md
@@ -0,0 +1,67 @@
+# Security Implementation Report
+
+## Security Measures Implemented
+
+### 1. HTTP Security Headers
+- **X-Content-Type-Options**: Prevents MIME type sniffing attacks
+- **X-Frame-Options**: Prevents clickjacking attacks by denying iframe embedding
+- **X-XSS-Protection**: Enables browser XSS filtering
+- **Referrer-Policy**: Controls referrer information sent with requests
+- **Content-Security-Policy**: Comprehensive CSP to prevent XSS and data injection
+
+### 2. JavaScript Security Enhancements
+- **Input Sanitization**: Added `sanitizeText()` function to prevent XSS
+- **DOM Validation**: Added `validateElement()` to ensure safe DOM manipulation
+- **Secure Text Setting**: Created `secureSetTextContent()` for safe content updates
+- **Bounds Checking**: Added array bounds validation to prevent out-of-bounds access
+- **Error Handling**: Improved error handling and logging
+
+### 3. Server Configuration
+- **Apache .htaccess**: Security headers and file access restrictions
+- **Hidden Files Protection**: Prevents access to sensitive configuration files
+- **Backup Files Protection**: Blocks access to backup and temporary files
+- **HTTPS Redirect**: Ready-to-enable HTTPS enforcement
+
+### 4. Development Security
+- **CSP Compliance**: All external resources properly whitelisted
+- **No Inline Scripts**: All JavaScript moved to external files
+- **Font Security**: Secure loading of Google Fonts with proper CSP
+
+### Web Server Compatibility
+
+### Apache
+- Use the `.htaccess` file for Apache web servers
+- Place it in your website's root directory
+- Requires mod_headers module to be enabled
+
+### Nginx
+- Use the `nginx.conf` configuration for nginx servers
+- Include the configuration in your nginx server block
+- Update the `root` path to match your website directory
+- Restart nginx after applying changes
+
+### Coolify (Recommended)
+- **Uses nginx by default** for static sites via Nixpacks
+- Use `coolify-nginx.conf` for security headers
+- Use `nixpacks.toml` to configure the build process
+- Automatically handles SSL/TLS certificates via Traefik proxy
+- No manual server configuration required
+
+### Development Server (Python)
+- The security headers are already implemented in the HTML meta tags
+- Additional server-level security requires Apache or nginx in production
+- **No Inline Scripts**: All JavaScript moved to external files
+- **Font Security**: Secure loading of Google Fonts with proper CSP
+
+## Security Best Practices Applied
+- Defense in depth approach
+- Input validation and sanitization
+- Secure coding practices
+- Proper error handling
+- Content Security Policy implementation
+
+## Next Steps for Production
+1. Enable HTTPS and update CSP accordingly
+2. Implement proper logging and monitoring
+3. Regular security audits and updates
+4. Consider implementing Subresource Integrity (SRI) for external resources
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..3c7c88b
--- /dev/null
+++ b/style.css
@@ -0,0 +1,202 @@
+:root {
+ --bg-color: #f4f1eb;
+ --text-color: #4a4a4a;
+ --accent-color: #6f4e37;
+ --accent-light: #a08a7b;
+ --card-bg: #ffffff;
+ --border-color: #e0e0e0;
+}
+
+body {
+ font-family: 'Poppins', sans-serif;
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ margin: 0;
+}
+
+.container {
+ width: 100%;
+ max-width: 420px;
+ padding: 2rem;
+ text-align: center;
+}
+
+header h1 {
+ font-size: 2rem;
+ color: var(--accent-color);
+ font-weight: 600;
+ margin-bottom: 0.25rem;
+}
+
+header p {
+ font-size: 1rem;
+ color: var(--accent-light);
+ margin-top: 0;
+}
+
+.step-card {
+ background-color: var(--card-bg);
+ border-radius: 15px;
+ padding: 2rem;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
+ display: none; /* Hidden by default */
+}
+
+.step-card.active {
+ display: block;
+}
+
+#setup h2 {
+ margin-top: 0;
+ font-weight: 400;
+ color: var(--accent-color);
+}
+
+.recipe-overview {
+ list-style: none;
+ padding: 0;
+ margin: 1.5rem 0;
+ text-align: left;
+}
+
+.recipe-overview li {
+ padding: 0.5rem 0;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.recipe-overview li:last-child {
+ border-bottom: none;
+}
+
+.recipe-overview strong {
+ color: var(--accent-color);
+}
+
+.tip {
+ font-size: 0.9rem;
+ color: var(--accent-light);
+ background-color: var(--bg-color);
+ padding: 0.75rem;
+ border-radius: 8px;
+}
+
+.btn {
+ background-color: var(--accent-color);
+ color: white;
+ border: none;
+ padding: 0.8rem 1.5rem;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.3s, transform 0.2s;
+ width: 100%;
+ margin-top: 1rem;
+}
+
+.btn:hover {
+ background-color: #5a3f2e;
+ transform: translateY(-2px);
+}
+
+.btn-secondary {
+ background-color: transparent;
+ color: var(--accent-light);
+ border: 1px solid var(--border-color);
+}
+
+.btn-secondary:hover {
+ background-color: var(--bg-color);
+ color: var(--accent-color);
+}
+
+.btn-pause {
+ background-color: #f39c12;
+ color: white;
+}
+
+.btn-pause:hover {
+ background-color: #e67e22;
+}
+
+.btn-pause.paused {
+ background-color: #27ae60;
+}
+
+.btn-pause.paused:hover {
+ background-color: #229954;
+}
+
+#brewing-process {
+ display: none; /* Initially hidden */
+}
+
+.timer-container {
+ position: relative;
+ margin: 0 auto 2rem;
+ width: 200px;
+ height: 200px;
+}
+
+.progress-ring__circle {
+ transform-origin: center;
+ transform: rotate(-90deg);
+ stroke: var(--accent-color);
+ transition: stroke-dashoffset 0.5s;
+}
+
+.progress-ring__bg {
+ stroke: var(--border-color);
+}
+
+.timer-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+#timer-display {
+ font-size: 3rem;
+ font-weight: 600;
+ color: var(--accent-color);
+}
+
+#total-water {
+ font-size: 1.2rem;
+ color: var(--accent-light);
+}
+
+.instructions h2 {
+ font-size: 1.5rem;
+ font-weight: 400;
+ color: var(--accent-color);
+ margin-bottom: 0.5rem;
+}
+
+.instructions p {
+ font-size: 1rem;
+ line-height: 1.6;
+ margin-top: 0;
+}
+
+.controls {
+ margin-top: 2rem;
+}
+
+@media (max-width: 480px) {
+ .container {
+ padding: 1rem;
+ }
+
+ .step-card {
+ padding: 1.5rem;
+ }
+}