Initial commit for Start9 packaging

This commit is contained in:
MacPro
2026-02-28 09:27:26 -06:00
commit 1b64c45c52
124 changed files with 15671 additions and 0 deletions
@@ -0,0 +1,113 @@
# PWA Icon Generator
This script generates PWA (Progressive Web App) icon PNG files for the Workout Planner app using only Node.js built-in modules.
## Features
- **No external dependencies**: Uses only Node.js built-in modules (`fs`, `zlib`, `path`)
- **PNG format with proper compression**: Generates valid PNG files with zlib compression
- **Multiple sizes**: Creates icons for all common PWA sizes:
- 72x72, 96x96, 128x128, 144x144, 152x152 (small devices)
- 192x192, 384x384, 512x512 (larger devices)
- **Maskable icons**: Creates adaptive icon variants (192x192 and 512x512) for Android
- **SVG favicon**: Generates a bonus SVG favicon for browsers
## Design
The icons feature:
- **Dark luxury aesthetic**: #0A0A0A (near-black) background
- **Stylized "W" letter**: White (#FFFFFF) geometric design representing "Workout"
- **Clean, bold design**: Perfect for a gym/fitness application
## Usage
```bash
node scripts/generate-icons.js
```
This will generate all icon files in `public/icons/` directory:
- `icon-{size}x{size}.png` - Regular icons for various sizes
- `icon-{size}x{size}-maskable.png` - Maskable variants for adaptive icons
- `favicon.svg` - SVG favicon for browsers
## Integration with PWA
The generated icons are automatically referenced in `public/manifest.json`. The manifest includes:
- All standard icon sizes for different devices
- Maskable icons for Android adaptive icon support
- Proper MIME types and purposes
### HTML Integration
Add this to your `<head>`:
```html
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/icons/favicon.svg">
<link rel="icon" type="image/png" sizes="192x192" href="/icons/icon-192x192.png">
<link rel="apple-touch-icon" href="/icons/icon-192x192.png">
<!-- Theme color -->
<meta name="theme-color" content="#FFFFFF">
<meta name="background-color" content="#0A0A0A">
```
## Technical Details
### PNG Generation
The script uses raw PNG format generation:
1. Creates pixel buffer with RGBA color values
2. Generates PNG header (IHDR chunk) with image dimensions and color info
3. Prepares image data with filter bytes for each row
4. Compresses data using zlib deflate algorithm
5. Generates IDAT chunk with compressed data
6. Calculates CRC32 checksums for data integrity
7. Outputs valid PNG file with proper structure
### Custom Drawing
The `drawW()` function:
- Fills background with #0A0A0A
- Draws a stylized "W" using vertical bars/rectangles
- Proportionally scales to any icon size
- Uses simple, fast rectangle fill operations
## Regenerating Icons
If you modify the design colors or want to regenerate:
1. Edit the color constants in `generate-icons.js`:
- `BACKGROUND_COLOR` - Icon background (default: #0A0A0A)
- `TEXT_COLOR` - "W" letter color (default: #FFFFFF)
2. Modify the `drawW()` function to change the letter design
3. Run the script again:
```bash
node scripts/generate-icons.js
```
All icons will be regenerated with the new design.
## File Sizes
The generated PNG files are highly optimized:
- Small sizes (72-152px): ~250-1000 bytes
- Medium size (192px): ~1.3 KB
- Large size (384px): ~4.2 KB
- Extra large (512px): ~7.2 KB
The compact file sizes are due to zlib compression of the simple color scheme.
## Browser Support
These icons work on:
- Chrome/Edge PWA installation
- Firefox PWA installation
- Safari iOS add-to-home-screen
- Android adaptive icons (maskable variants)
- Desktop browser tabs (favicon)
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env node
/**
* PWA Icon Generator for Workout App
* Generates PNG icons without external dependencies
* Uses only Node.js built-in modules (fs, zlib, path)
*/
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
// Configuration
const BACKGROUND_COLOR = { r: 10, g: 10, b: 10 }; // #0A0A0A
const TEXT_COLOR = { r: 255, g: 255, b: 255 }; // White
const ICON_SIZES = [72, 96, 128, 144, 152, 192, 384, 512];
const MASKABLE_SIZES = [192, 512];
const OUTPUT_DIR = path.join(__dirname, '..', 'public', 'icons');
/**
* Create output directory if it doesn't exist
*/
function ensureOutputDir() {
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
}
/**
* Draw a stylized "W" character on a pixel buffer
* Uses simple geometric shapes to create the letter
*/
function drawW(buffer, width, height) {
const canvasWidth = width;
const canvasHeight = height;
// Helper to set a pixel
const setPixel = (x, y, r, g, b, a = 255) => {
if (x < 0 || x >= canvasWidth || y < 0 || y >= canvasHeight) return;
const idx = (y * canvasWidth + x) * 4;
buffer[idx] = r;
buffer[idx + 1] = g;
buffer[idx + 2] = b;
buffer[idx + 3] = a;
};
// Helper to draw a filled rectangle (fast)
const fillRect = (x, y, w, h, r, g, b) => {
const x0 = Math.max(0, Math.floor(x));
const y0 = Math.max(0, Math.floor(y));
const x1 = Math.min(canvasWidth, Math.ceil(x + w));
const y1 = Math.min(canvasHeight, Math.ceil(y + h));
for (let yi = y0; yi < y1; yi++) {
for (let xi = x0; xi < x1; xi++) {
setPixel(xi, yi, r, g, b);
}
}
};
// Fill background
fillRect(0, 0, canvasWidth, canvasHeight, BACKGROUND_COLOR.r, BACKGROUND_COLOR.g, BACKGROUND_COLOR.b);
// Calculate dimensions for the "W"
const padding = Math.floor(canvasHeight * 0.1);
const letterWidth = canvasWidth - padding * 2;
const letterHeight = canvasHeight - padding * 2;
const startX = padding;
const startY = padding;
// Draw a stylized "W" using rectangles (five vertical bars forming W shape)
const barWidth = letterWidth * 0.12;
const spacing = letterWidth / 4;
// Create a V-V-V pattern (W shape)
// First V: two diagonal bars meeting at a point
const v1LeftX = startX + spacing * 0.2;
const v1RightX = startX + spacing * 0.8;
const v1MidX = (v1LeftX + v1RightX) / 2;
const v1BottomY = startY + letterHeight;
const v1TopY = startY;
// Left bar of first V
fillRect(v1LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
// Right bar of first V
fillRect(v1RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
// Second V
const v2LeftX = startX + spacing * 0.9;
const v2RightX = startX + spacing * 1.5;
fillRect(v2LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
fillRect(v2RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
// Third V (right side)
const v3LeftX = startX + spacing * 1.6;
const v3RightX = startX + spacing * 2.2;
fillRect(v3LeftX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
fillRect(v3RightX, v1TopY, barWidth, letterHeight, TEXT_COLOR.r, TEXT_COLOR.g, TEXT_COLOR.b);
}
/**
* Create a PNG file from raw pixel data
* Uses PNG file format with zlib compression
*/
function createPNG(width, height, pixelBuffer, outputPath) {
// PNG signature
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
// IHDR chunk (image header)
const ihdrData = Buffer.alloc(13);
ihdrData.writeUInt32BE(width, 0);
ihdrData.writeUInt32BE(height, 4);
ihdrData[8] = 8; // bit depth
ihdrData[9] = 6; // color type (RGBA)
ihdrData[10] = 0; // compression method
ihdrData[11] = 0; // filter method
ihdrData[12] = 0; // interlace method
const ihdr = createChunk('IHDR', ihdrData);
// IDAT chunk (image data)
// Each row needs a filter byte (0 = None)
const rawData = Buffer.alloc(height * (1 + width * 4));
for (let y = 0; y < height; y++) {
rawData[y * (1 + width * 4)] = 0; // filter type
const rowStart = y * (1 + width * 4) + 1;
pixelBuffer.copy(rawData, rowStart, y * width * 4, (y + 1) * width * 4);
}
const compressed = zlib.deflateSync(rawData);
const idat = createChunk('IDAT', compressed);
// IEND chunk (image end)
const iend = createChunk('IEND', Buffer.alloc(0));
// Combine all chunks
const png = Buffer.concat([pngSignature, ihdr, idat, iend]);
fs.writeFileSync(outputPath, png);
}
/**
* Create a PNG chunk with proper CRC
*/
function createChunk(type, data) {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length, 0);
const typeBuffer = Buffer.from(type, 'ascii');
const chunkData = Buffer.concat([typeBuffer, data]);
// Calculate CRC32
const crc = calculateCRC32(chunkData);
const crcBuffer = Buffer.alloc(4);
crcBuffer.writeUInt32BE(crc, 0);
return Buffer.concat([length, chunkData, crcBuffer]);
}
/**
* Calculate CRC32 checksum for PNG chunks
*/
function calculateCRC32(data) {
const table = [];
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[n] = c >>> 0;
}
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = table[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
/**
* Generate all icon sizes
*/
function generateIcons() {
ensureOutputDir();
console.log('Generating PWA icons for Workout App...\n');
ICON_SIZES.forEach((size) => {
// Regular icon
const buffer = Buffer.alloc(size * size * 4);
drawW(buffer, size, size);
const outputPath = path.join(OUTPUT_DIR, `icon-${size}x${size}.png`);
createPNG(size, size, buffer, outputPath);
console.log(`✓ Created ${path.basename(outputPath)}`);
// Maskable variant (for adaptive icons on Android)
if (MASKABLE_SIZES.includes(size)) {
const maskableBuffer = Buffer.alloc(size * size * 4);
drawW(maskableBuffer, size, size);
const maskablePath = path.join(OUTPUT_DIR, `icon-${size}x${size}-maskable.png`);
createPNG(size, size, maskableBuffer, maskablePath);
console.log(`✓ Created ${path.basename(maskablePath)}`);
}
});
// Create a favicon SVG as well
const svgPath = path.join(OUTPUT_DIR, 'favicon.svg');
const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192">
<rect width="192" height="192" fill="#0A0A0A"/>
<text x="96" y="132" font-size="120" font-weight="bold" text-anchor="middle" fill="white" font-family="Arial, sans-serif">W</text>
</svg>`;
fs.writeFileSync(svgPath, svgContent);
console.log(`✓ Created ${path.basename(svgPath)}`);
console.log(`\nAll icons generated successfully in ${OUTPUT_DIR}`);
}
// Run the generator
generateIcons();
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# Rebuild and restart the Workout Planner
# Usage: ./scripts/rebuild.sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
echo "Stopping server..."
"$SCRIPT_DIR/stop.sh"
echo ""
echo "Applying database migrations..."
npx prisma db push
echo ""
echo "Building production bundle..."
npm run build
echo ""
echo "Starting server..."
"$SCRIPT_DIR/start.sh"
+111
View File
@@ -0,0 +1,111 @@
#!/bin/bash
# Set up macOS Launch Agent so the Workout Planner starts automatically on login.
# Usage: ./scripts/setup-autostart.sh
#
# This creates a Launch Agent plist that runs scripts/start.sh on login.
# To remove: launchctl unload ~/Library/LaunchAgents/com.workout-planner.plist
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
PLIST_NAME="com.workout-planner"
PLIST_DIR="$HOME/Library/LaunchAgents"
PLIST_PATH="$PLIST_DIR/$PLIST_NAME.plist"
LOG_DIR="$PROJECT_DIR/logs"
NODE_PATH="$(which node)"
echo "=== Workout Planner — Auto-Start Setup ==="
echo ""
echo "Project: $PROJECT_DIR"
echo "Node: $NODE_PATH"
echo "Logs: $LOG_DIR"
echo ""
# Check node exists
if [ -z "$NODE_PATH" ]; then
echo "ERROR: node not found in PATH."
echo "Make sure Node.js is installed (e.g. via nvm, homebrew, or nodejs.org)."
exit 1
fi
# Build first if needed
if [ ! -d "$PROJECT_DIR/.next" ]; then
echo "No production build found. Building now..."
cd "$PROJECT_DIR"
npm run build
echo ""
fi
# Apply prisma migrations
echo "Applying database schema..."
cd "$PROJECT_DIR"
npx prisma db push --accept-data-loss 2>/dev/null || npx prisma db push
echo ""
mkdir -p "$PLIST_DIR"
mkdir -p "$LOG_DIR"
# Create the plist
cat > "$PLIST_PATH" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$PLIST_NAME</string>
<key>ProgramArguments</key>
<array>
<string>$NODE_PATH</string>
<string>$PROJECT_DIR/node_modules/.bin/next</string>
<string>start</string>
<string>-p</string>
<string>3000</string>
</array>
<key>WorkingDirectory</key>
<string>$PROJECT_DIR</string>
<key>EnvironmentVariables</key>
<dict>
<key>NODE_ENV</key>
<string>production</string>
<key>PATH</key>
<string>$(dirname "$NODE_PATH"):/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>$LOG_DIR/server.log</string>
<key>StandardErrorPath</key>
<string>$LOG_DIR/server-error.log</string>
</dict>
</plist>
EOF
echo "Created: $PLIST_PATH"
echo ""
# Unload if already loaded, then load
launchctl unload "$PLIST_PATH" 2>/dev/null || true
launchctl load "$PLIST_PATH"
echo "=== Done! ==="
echo ""
echo "The server will now start automatically when you log in."
echo "It's also running right now at: http://localhost:3000"
echo ""
echo "Useful commands:"
echo " Stop: launchctl unload ~/Library/LaunchAgents/$PLIST_NAME.plist"
echo " Start: launchctl load ~/Library/LaunchAgents/$PLIST_NAME.plist"
echo " Logs: tail -f $LOG_DIR/server.log"
echo " Rebuild: ./scripts/rebuild.sh"
echo " Remove: launchctl unload ~/Library/LaunchAgents/$PLIST_NAME.plist && rm ~/Library/LaunchAgents/$PLIST_NAME.plist"
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
# Start the Workout Planner production server
# Usage: ./scripts/start.sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="$PROJECT_DIR/logs"
PID_FILE="$PROJECT_DIR/.server.pid"
mkdir -p "$LOG_DIR"
# Check if already running
if [ -f "$PID_FILE" ]; then
EXISTING_PID=$(cat "$PID_FILE")
if kill -0 "$EXISTING_PID" 2>/dev/null; then
echo "Server already running (PID $EXISTING_PID)"
echo "http://localhost:3000"
exit 0
fi
rm -f "$PID_FILE"
fi
cd "$PROJECT_DIR"
# Build if no .next directory exists
if [ ! -d ".next" ]; then
echo "Building production bundle..."
npm run build 2>&1 | tee "$LOG_DIR/build.log"
fi
# Start production server in background
echo "Starting server..."
NODE_ENV=production nohup npx next start -p 3000 \
> "$LOG_DIR/server.log" 2>&1 &
echo $! > "$PID_FILE"
echo "Server started (PID $(cat "$PID_FILE"))"
echo "http://localhost:3000"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
# Stop the Workout Planner server
# Usage: ./scripts/stop.sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
PID_FILE="$PROJECT_DIR/.server.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "Stopping server (PID $PID)..."
kill "$PID"
rm -f "$PID_FILE"
echo "Server stopped."
else
echo "Server not running (stale PID file)."
rm -f "$PID_FILE"
fi
else
echo "No PID file found. Server may not be running."
# Try to find and kill any next server on port 3000
PIDS=$(lsof -ti:3000 2>/dev/null)
if [ -n "$PIDS" ]; then
echo "Found process(es) on port 3000: $PIDS"
echo "Kill them? (y/n)"
read -r REPLY
if [ "$REPLY" = "y" ]; then
echo "$PIDS" | xargs kill
echo "Killed."
fi
fi
fi