Add Justfile and fix server implementation

Server improvements:
- Complete rewrite with robust IPv4/IPv6 support
- Graceful fallback from IPv6 dual-stack to IPv4-only
- Better error handling and informative startup messages
- Proper socket cleanup and server shutdown
- Custom logging with timestamps

Justfile additions:
- serve/serve-bg/stop - Server management
- lint-all/format-all/check-all - Code quality commands
- install/setup/clean - Project management
- validate/info/status - Development helpers
- icons-png/icons-browser - Icon generation
- commit/quick-commit - Git workflow helpers

The server now works reliably across different network configurations
and the justfile provides a comprehensive development workflow.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-18 20:13:04 +02:00
commit 749df446e9
2 changed files with 257 additions and 32 deletions

138
justfile Normal file
View file

@ -0,0 +1,138 @@
# GlitchCraft Development Commands
# Install just: https://github.com/casey/just
# Default recipe - show available commands
default:
@just --list
# Development server management
# Start the development server
serve:
python3 server.py
# Start server in background
serve-bg:
python3 server.py &
@echo "Server started in background"
@echo "Use 'just stop' to stop it"
# Stop background server
stop:
@echo "Stopping GlitchCraft server..."
pkill -f "python.*server.py" || echo "No server process found"
# Code quality and linting
# Lint all JavaScript files
lint-js:
bun run lint:js
# Lint and auto-fix JavaScript
fix-js:
bun run lint:js:fix
# Format all code with Prettier
format:
bun run format
# Check code formatting without modifying files
check-format:
bun run check
# Lint Python code
lint-python:
black --check server.py
pylint server.py
# Format Python code
format-python:
black server.py
# Lint and format everything
lint-all: lint-js lint-python
@echo "✓ All linting completed"
# Format everything
format-all: format-python format
@echo "✓ All formatting completed"
# Check everything (linting + formatting)
check-all: lint-all check-format
@echo "✓ All checks passed"
# PWA icon generation
# Generate PNG icons (requires canvas package)
icons-png:
bun add canvas
node app/create-icons.js
# Open browser-based icon generator
icons-browser:
@echo "Opening app/generate-icons.html..."
@echo "Right-click each canvas to save icons"
python3 -c "import webbrowser; webbrowser.open('app/generate-icons.html')"
# Development workflow
# Install dependencies
install:
bun install
# Clean up generated files
clean:
rm -rf node_modules/
rm -f bun.lockb
find . -name "*.pyc" -delete
find . -name "__pycache__" -type d -exec rm -rf {} +
# Full setup from scratch
setup: install icons-png
@echo "✓ GlitchCraft setup complete!"
@echo "Run 'just serve' to start development"
# Testing and validation
# Validate HTML files
validate-html:
bunx vnu-jar app/index.html app/generate-icons.html
# Test PWA manifest
validate-manifest:
python3 -m json.tool app/manifest.json > /dev/null && echo "✓ Manifest is valid JSON"
# Run all validations
validate: validate-html validate-manifest check-all
@echo "✓ All validations passed"
# Git helpers
# Create a commit with all changes
commit message:
git add -A
git commit -m "{{message}}\n\n🤖 Generated with [Claude Code](https://claude.ai/code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>"
# Quick commit with lint/format
quick-commit message: format-all lint-all
just commit "{{message}}"
# Development info
# Show project info
info:
@echo "🎨 GlitchCraft - Artisanal text corruption, served fresh!"
@echo ""
@echo "📁 Project structure:"
@echo " app/ - Main application (deploy this directory)"
@echo " server.py - Development server (IPv4/IPv6)"
@echo " justfile - Development commands"
@echo ""
@echo "🚀 Quick start:"
@echo " just install - Install dependencies"
@echo " just serve - Start development server"
@echo " just check-all - Run all linting and validation"
@echo ""
@echo "🔗 Server URLs:"
@echo " http://localhost:8000/"
@echo " http://127.0.0.1:8000/ (IPv4)"
@echo " http://[::1]:8000/ (IPv6)"
# Show server status
status:
@echo "Checking server status..."
@pgrep -f "python.*server.py" > /dev/null && echo "✓ Server is running" || echo "✗ Server is not running"
@echo "Use 'just serve' to start or 'just stop' to stop"

141
server.py
View file

@ -2,60 +2,147 @@
""" """
Simple HTTP server for testing the GlitchCraft PWA Simple HTTP server for testing the GlitchCraft PWA
Run with: python3 server.py Run with: python3 server.py
Then open http://localhost:8000/ in your browser Supports both IPv4 and IPv6 connections
""" """
import http.server import http.server
import socketserver import socketserver
import socket import socket
import os import os
import sys
import time
PORT = 8000 PORT = 8000
# Serve from the app directory # Serve from the app directory
DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app") DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app")
class DualStackServer(socketserver.TCPServer): class GlitchCraftHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""TCP server that supports both IPv4 and IPv6."""
address_family = socket.AF_INET6
def __init__(self, *args, **kwargs):
# Disable dual stack to make it work on all systems
# Some systems have it enabled by default, some don't
super().__init__(*args, **kwargs)
# Enable dual stack - listen on both IPv4 and IPv6
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""Custom HTTP request handler for serving the GlitchCraft PWA.""" """Custom HTTP request handler for serving the GlitchCraft PWA."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs) super().__init__(*args, directory=DIRECTORY, **kwargs)
def end_headers(self): def end_headers(self):
# Add headers for PWA and CORS # Add headers for PWA functionality
self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
self.send_header("Service-Worker-Allowed", "/") self.send_header("Service-Worker-Allowed", "/")
self.send_header("X-Content-Type-Options", "nosniff") self.send_header("X-Content-Type-Options", "nosniff")
# Add CORS headers for development
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "*")
super().end_headers() super().end_headers()
def log_message(self, fmt, *args):
# Custom log format with timestamp
timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"[{timestamp}] {fmt % args}")
if __name__ == "__main__":
# Listen on all interfaces (:: for IPv6, which also handles IPv4 with dual stack)
HOST = "::" # Listen on all interfaces (IPv6 and IPv4)
with DualStackServer((HOST, PORT), MyHTTPRequestHandler) as httpd: class DualStackTCPServer(socketserver.TCPServer):
print(f"Server running on all interfaces at port {PORT}") """TCP server that attempts to support both IPv4 and IPv6."""
print(f"Access at:")
allow_reuse_address = True
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
# Try IPv6 first (with dual-stack if available)
self.address_family = socket.AF_INET6
try:
super().__init__(
server_address, RequestHandlerClass, bind_and_activate=False
)
# Try to enable dual-stack (works on most modern systems)
try:
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
self._is_dual_stack = True
except (AttributeError, OSError):
# Dual-stack not supported, IPv6 only
self._is_dual_stack = False
if bind_and_activate:
self.server_bind()
self.server_activate()
except (OSError, socket.error):
# IPv6 failed, fall back to IPv4
self.server_close()
self.address_family = socket.AF_INET
self._is_dual_stack = False
# Convert IPv6 address to IPv4 if needed
if server_address[0] == "::":
server_address = ("0.0.0.0", server_address[1])
super().__init__(server_address, RequestHandlerClass, bind_and_activate)
def main():
"""Start the development server."""
print("Starting GlitchCraft development server...")
print(f"Serving files from: {DIRECTORY}")
print(f"Port: {PORT}")
# Check if app directory exists
if not os.path.exists(DIRECTORY):
print(f"Error: Directory {DIRECTORY} does not exist!")
sys.exit(1)
# Try different host configurations
hosts_to_try = [
("::", "IPv6 dual-stack"),
("0.0.0.0", "IPv4 all interfaces"),
("127.0.0.1", "IPv4 localhost only"),
]
server = None
for host, description in hosts_to_try:
try:
server = DualStackTCPServer((host, PORT), GlitchCraftHTTPRequestHandler)
print(f"✓ Server started using {description}")
# Print access URLs based on server configuration
print("\nAccess your app at:")
if server.address_family == socket.AF_INET6:
if hasattr(server, "_is_dual_stack") and getattr(
server, "_is_dual_stack", False
):
print(f" http://localhost:{PORT}/") print(f" http://localhost:{PORT}/")
print(f" http://127.0.0.1:{PORT}/ (IPv4)") print(f" http://127.0.0.1:{PORT}/ (IPv4)")
print(f" http://[::1]:{PORT}/ (IPv6)") print(f" http://[::1]:{PORT}/ (IPv6)")
print(f" http://<your-ip>:{PORT}/") print(f" http://<your-ip>:{PORT}/ (network)")
print(f"Serving files from: {DIRECTORY}") else:
print("Press Ctrl+C to stop the server") print(f" http://[::1]:{PORT}/ (IPv6 only)")
print(f" http://localhost:{PORT}/ (if supported)")
else:
if host == "0.0.0.0":
print(f" http://localhost:{PORT}/")
print(f" http://127.0.0.1:{PORT}/ (IPv4)")
print(f" http://<your-ip>:{PORT}/ (network)")
else:
print(f" http://localhost:{PORT}/ (localhost only)")
print("\nPress Ctrl+C to stop the server")
break
except OSError as e:
print(f"✗ Failed to start server on {host}: {e}")
if "Address already in use" in str(e):
print(
f" Port {PORT} is already in use. Try a different port or stop other servers."
)
continue
if server is None:
print("Failed to start server on any interface!")
sys.exit(1)
try: try:
httpd.serve_forever() server.serve_forever()
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nServer stopped.") print("\nShutting down server...")
finally:
server.server_close()
if __name__ == "__main__":
main()