From efa7ff1d39c6dfdaefd2984e8c37b2f38067b947 Mon Sep 17 00:00:00 2001 From: Roger Oriol Date: Thu, 9 Oct 2025 20:19:20 +0200 Subject: [PATCH] frontend implementation --- .dockerignore | 37 + Dockerfile | 66 ++ backend/database.js | 29 +- backend/package-lock.json | 1392 ++++++++++++++++++++++++- backend/package.json | 7 +- backend/server.js | 8 +- data/gym-tracker.db | Bin 0 -> 12288 bytes frontend/api.js | 145 +++ frontend/app.js | 247 +++++ frontend/heatmap.js | 132 +++ frontend/index.html | 145 ++- frontend/muscleGroups.js | 166 +++ frontend/styles.css | 411 ++++++++ kubernetes/README.md | 158 +++ kubernetes/deployment.yaml | 62 ++ kubernetes/ingress.yaml | 30 + kubernetes/persistentvolumeclaim.yaml | 13 + kubernetes/service.yaml | 15 + nginx/nginx.conf | 80 ++ 19 files changed, 3072 insertions(+), 71 deletions(-) create mode 100644 .dockerignore create mode 100644 data/gym-tracker.db create mode 100644 frontend/heatmap.js create mode 100644 frontend/muscleGroups.js create mode 100644 kubernetes/README.md create mode 100644 kubernetes/deployment.yaml create mode 100644 kubernetes/ingress.yaml create mode 100644 kubernetes/persistentvolumeclaim.yaml create mode 100644 kubernetes/service.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c4e2350 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,37 @@ +# Git files +.git +.gitignore +.jj + +# Node modules (will be installed in container) +node_modules +backend/node_modules + +# Database files (use volume mount for persistence) +data/ +*.db +*.db-journal + +# Documentation and planning +README.md +PLAN.md +CLAUDE.md + +# Development files +.vscode +.idea +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# OS files +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp +*.swp +*.swo +*~ diff --git a/Dockerfile b/Dockerfile index e69de29..5175845 100644 --- a/Dockerfile +++ b/Dockerfile @@ -0,0 +1,66 @@ +# Multi-stage build for Gym Tracker application +# Stage 1: Dependencies +FROM node:20-alpine AS dependencies + +WORKDIR /app + +# Copy backend package files +COPY backend/package*.json ./backend/ + +# Install backend dependencies +RUN cd backend && npm ci --only=production + +# Stage 2: Final image +FROM node:20-alpine + +# Install nginx +RUN apk add --no-cache nginx + +# Create app directory +WORKDIR /app + +# Copy backend files and dependencies +COPY --from=dependencies /app/backend/node_modules ./backend/node_modules +COPY backend/package*.json ./backend/ +COPY backend/*.js ./backend/ + +# Copy frontend files to nginx html directory +COPY frontend/ /usr/share/nginx/html/ + +# Copy nginx configuration +COPY nginx/nginx.conf /etc/nginx/nginx.conf + +# Create data directory for SQLite database +RUN mkdir -p /app/data && chmod 755 /app/data + +# Create nginx directories and set permissions +RUN mkdir -p /var/log/nginx /var/lib/nginx /run/nginx && \ + chown -R nginx:nginx /var/log/nginx /var/lib/nginx /run/nginx + +# Create startup script +RUN echo '#!/bin/sh' > /app/start.sh && \ + echo 'echo "Starting Gym Tracker application..."' >> /app/start.sh && \ + echo '' >> /app/start.sh && \ + echo '# Start nginx in background' >> /app/start.sh && \ + echo 'echo "Starting nginx..."' >> /app/start.sh && \ + echo 'nginx' >> /app/start.sh && \ + echo '' >> /app/start.sh && \ + echo '# Start backend server' >> /app/start.sh && \ + echo 'echo "Starting backend server on port 3000..."' >> /app/start.sh && \ + echo 'cd /app/backend' >> /app/start.sh && \ + echo 'exec node server.js' >> /app/start.sh && \ + chmod +x /app/start.sh + +# Expose ports +EXPOSE 80 3000 + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost/health || exit 1 + +# Run startup script +CMD ["/app/start.sh"] diff --git a/backend/database.js b/backend/database.js index 74253af..a6ec446 100644 --- a/backend/database.js +++ b/backend/database.js @@ -1,8 +1,8 @@ -const sqlite3 = require('sqlite3').verbose(); -const path = require('path'); +import sqlite3 from 'sqlite3'; +import path from 'path'; // Database file path -const DB_PATH = path.join(__dirname, '../data/gym-tracker.db'); +const DB_PATH = path.join('../data/gym-tracker.db'); // Initialize database connection const db = new sqlite3.Database(DB_PATH, (err) => { @@ -15,7 +15,7 @@ const db = new sqlite3.Database(DB_PATH, (err) => { }); // Create tables if they don't exist -function initializeDatabase() { +export function initializeDatabase() { const createTableSQL = ` CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, @@ -34,7 +34,7 @@ function initializeDatabase() { } // Get all sessions -function getAllSessions(callback) { +export function getAllSessions(callback) { const sql = 'SELECT * FROM sessions ORDER BY date DESC'; db.all(sql, [], (err, rows) => { if (err) { @@ -52,7 +52,7 @@ function getAllSessions(callback) { } // Get session by ID -function getSessionById(id, callback) { +export function getSessionById(id, callback) { const sql = 'SELECT * FROM sessions WHERE id = ?'; db.get(sql, [id], (err, row) => { if (err) { @@ -71,7 +71,7 @@ function getSessionById(id, callback) { } // Create new session -function createSession(id, date, muscleGroups, callback) { +export function createSession(id, date, muscleGroups, callback) { const sql = 'INSERT INTO sessions (id, date, muscle_groups) VALUES (?, ?, ?)'; const muscleGroupsJSON = JSON.stringify(muscleGroups); @@ -85,7 +85,7 @@ function createSession(id, date, muscleGroups, callback) { } // Update existing session -function updateSession(id, date, muscleGroups, callback) { +export function updateSession(id, date, muscleGroups, callback) { const sql = 'UPDATE sessions SET date = ?, muscle_groups = ? WHERE id = ?'; const muscleGroupsJSON = JSON.stringify(muscleGroups); @@ -101,7 +101,7 @@ function updateSession(id, date, muscleGroups, callback) { } // Delete session -function deleteSession(id, callback) { +export function deleteSession(id, callback) { const sql = 'DELETE FROM sessions WHERE id = ?'; db.run(sql, [id], function(err) { @@ -116,7 +116,7 @@ function deleteSession(id, callback) { } // Close database connection -function closeDatabase() { +export function closeDatabase() { db.close((err) => { if (err) { console.error('Error closing database:', err.message); @@ -125,12 +125,3 @@ function closeDatabase() { } }); } - -module.exports = { - getAllSessions, - getSessionById, - createSession, - updateSession, - deleteSession, - closeDatabase -}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 5b53b99..1315c02 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,9 +9,62 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "express": "^5.1.0" + "cors": "^2.8.5", + "express": "^5.1.0", + "sqlite3": "^5.1.7", + "uuid": "^13.0.0" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -25,6 +78,125 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", @@ -45,6 +217,41 @@ "node": ">=18" } }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -54,6 +261,36 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -83,6 +320,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -122,6 +402,19 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -139,6 +432,37 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -148,6 +472,15 @@ "node": ">= 0.8" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -168,6 +501,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -177,6 +517,42 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -222,6 +598,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", @@ -264,6 +649,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", @@ -299,6 +690,31 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -308,6 +724,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -345,6 +782,34 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -357,6 +822,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -369,6 +841,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -381,6 +860,13 @@ "node": ">= 0.4" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -406,6 +892,45 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -418,12 +943,87 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -433,12 +1033,87 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -490,12 +1165,165 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -505,6 +1333,91 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.77.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz", + "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -538,6 +1451,22 @@ "wrappy": "1" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -547,6 +1476,16 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", @@ -557,6 +1496,53 @@ "url": "https://opencollective.com/express" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -570,6 +1556,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -625,6 +1621,62 @@ "url": "https://opencollective.com/express" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -667,6 +1719,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -704,6 +1768,13 @@ "node": ">= 18" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -782,6 +1853,136 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -791,6 +1992,112 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -800,6 +2107,18 @@ "node": ">=0.6" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -814,6 +2133,26 @@ "node": ">= 0.6" } }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -823,6 +2162,25 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -832,11 +2190,43 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/backend/package.json b/backend/package.json index 0fa7f3c..8cdedc0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,13 +4,16 @@ "description": "Gym tracker application back-end", "license": "ISC", "author": "Roger Oriol", - "type": "commonjs", + "type": "module", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "dependencies": { - "express": "^5.1.0" + "cors": "^2.8.5", + "express": "^5.1.0", + "sqlite3": "^5.1.7", + "uuid": "^13.0.0" } } diff --git a/backend/server.js b/backend/server.js index e6a16da..8bd7b42 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,7 +1,7 @@ -const express = require('express'); -const cors = require('cors'); -const { v4: uuidv4 } = require('uuid'); -const db = require('./database'); +import express from 'express'; +import cors from 'cors'; +import { v4 as uuidv4 } from 'uuid'; +import * as db from './database.js'; const app = express(); const PORT = process.env.PORT || 3000; diff --git a/data/gym-tracker.db b/data/gym-tracker.db new file mode 100644 index 0000000000000000000000000000000000000000..1906df0501d5dad9aeef1ade84cb825d99ff4fbc GIT binary patch literal 12288 zcmeI$&rZTH90%}r5dV;n>B$pG-9=594kkEmPO^h>qT)y-#*hj{mjFY$C0=XSZol2$_+c#AFq(~d%xtnnC?%(i5kfRIb82Qm)gqs5 zR9@_tg+>l%Z$H1EoMCVD& zA4ft8DZ^+Yv#d4So*Q^9aL;^?Wl!0Dfu#`+Sm50S?7Dq^>9)Jh)+^JgCnpv?-+qyTJ&qBsV+3umnzAHg?gvS!5q literal 0 HcmV?d00001 diff --git a/frontend/api.js b/frontend/api.js index e69de29..e4715ae 100644 --- a/frontend/api.js +++ b/frontend/api.js @@ -0,0 +1,145 @@ +// API client for backend communication + +const API_BASE_URL = 'http://localhost:3000/api'; + +/** + * Fetch all gym sessions from the backend + * @returns {Promise} Array of session objects + */ +async function fetchSessions() { + try { + const response = await fetch(`${API_BASE_URL}/sessions`); + + if (!response.ok) { + throw new Error(`Failed to fetch sessions: ${response.statusText}`); + } + + const sessions = await response.json(); + return sessions; + } catch (error) { + console.error('Error fetching sessions:', error); + throw error; + } +} + +/** + * Create a new gym session + * @param {Object} session - Session object with date and muscle_groups + * @param {string} session.date - Date in ISO format (YYYY-MM-DD) + * @param {Array} session.muscle_groups - Array of muscle group names + * @returns {Promise} Created session object + */ +async function createSession(session) { + try { + const response = await fetch(`${API_BASE_URL}/sessions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(session), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `Failed to create session: ${response.statusText}`); + } + + const createdSession = await response.json(); + return createdSession; + } catch (error) { + console.error('Error creating session:', error); + throw error; + } +} + +/** + * Update an existing gym session + * @param {string} id - Session ID + * @param {Object} session - Updated session object + * @param {string} session.date - Date in ISO format (YYYY-MM-DD) + * @param {Array} session.muscle_groups - Array of muscle group names + * @returns {Promise} Updated session object + */ +async function updateSession(id, session) { + try { + const response = await fetch(`${API_BASE_URL}/sessions/${id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(session), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `Failed to update session: ${response.statusText}`); + } + + const updatedSession = await response.json(); + return updatedSession; + } catch (error) { + console.error('Error updating session:', error); + throw error; + } +} + +/** + * Delete a gym session + * @param {string} id - Session ID + * @returns {Promise} + */ +async function deleteSession(id) { + try { + const response = await fetch(`${API_BASE_URL}/sessions/${id}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.error || `Failed to delete session: ${response.statusText}`); + } + + return; + } catch (error) { + console.error('Error deleting session:', error); + throw error; + } +} + +/** + * Show loading indicator + */ +function showLoading() { + const loadingIndicator = document.getElementById('loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.classList.add('active'); + } +} + +/** + * Hide loading indicator + */ +function hideLoading() { + const loadingIndicator = document.getElementById('loadingIndicator'); + if (loadingIndicator) { + loadingIndicator.classList.remove('active'); + } +} + +/** + * Show error message to user + * @param {string} message - Error message to display + */ +function showError(message) { + alert(`Error: ${message}`); +} + +/** + * Show success message to user + * @param {string} message - Success message to display + */ +function showSuccess(message) { + // For now, we'll use console.log + // In a production app, you might use a toast notification + console.log(`Success: ${message}`); +} diff --git a/frontend/app.js b/frontend/app.js index e69de29..0445db2 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -0,0 +1,247 @@ +// Main Application Logic + +// Application state +let sessions = []; +let currentEditingSessionId = null; + +/** + * Initialize the application + */ +async function init() { + try { + showLoading(); + + // Load sessions from backend + await loadSessions(); + + // Set up event listeners + setupEventListeners(); + + // Initial render + updateUI(); + + hideLoading(); + } catch (error) { + hideLoading(); + showError('Failed to initialize application: ' + error.message); + } +} + +/** + * Load all sessions from the backend + */ +async function loadSessions() { + try { + sessions = await fetchSessions(); + } catch (error) { + console.error('Error loading sessions:', error); + throw error; + } +} + +/** + * Set up all event listeners + */ +function setupEventListeners() { + // Add Workout button + const addWorkoutBtn = document.getElementById('addWorkoutBtn'); + if (addWorkoutBtn) { + addWorkoutBtn.addEventListener('click', openAddSessionModal); + } + + // Modal close button + const closeModalBtn = document.getElementById('closeModal'); + if (closeModalBtn) { + closeModalBtn.addEventListener('click', closeModal); + } + + // Cancel button + const cancelBtn = document.getElementById('cancelBtn'); + if (cancelBtn) { + cancelBtn.addEventListener('click', closeModal); + } + + // Session form submit + const sessionForm = document.getElementById('sessionForm'); + if (sessionForm) { + sessionForm.addEventListener('submit', handleSessionFormSubmit); + } + + // Close modal when clicking outside + const modal = document.getElementById('sessionModal'); + if (modal) { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeModal(); + } + }); + } +} + +/** + * Open modal for adding a new session + */ +function openAddSessionModal() { + currentEditingSessionId = null; + + const modalTitle = document.getElementById('modalTitle'); + if (modalTitle) { + modalTitle.textContent = 'Add Workout Session'; + } + + // Set default date to today + const dateInput = document.getElementById('sessionDate'); + if (dateInput) { + const today = new Date().toISOString().split('T')[0]; + dateInput.value = today; + } + + // Clear all checkboxes + const checkboxes = document.querySelectorAll('input[name="muscleGroup"]'); + checkboxes.forEach(cb => cb.checked = false); + + showModal(); +} + +/** + * Open modal for editing an existing session + * @param {string} sessionId - ID of the session to edit + */ +function openEditSessionModal(sessionId) { + const session = sessions.find(s => s.id === sessionId); + if (!session) { + showError('Session not found'); + return; + } + + currentEditingSessionId = sessionId; + + const modalTitle = document.getElementById('modalTitle'); + if (modalTitle) { + modalTitle.textContent = 'Edit Workout Session'; + } + + // Set date + const dateInput = document.getElementById('sessionDate'); + if (dateInput) { + dateInput.value = session.date; + } + + // Set checkboxes + const checkboxes = document.querySelectorAll('input[name="muscleGroup"]'); + checkboxes.forEach(cb => { + cb.checked = session.muscle_groups.includes(cb.value); + }); + + showModal(); +} + +/** + * Show the modal + */ +function showModal() { + const modal = document.getElementById('sessionModal'); + if (modal) { + modal.classList.add('active'); + } +} + +/** + * Close the modal + */ +function closeModal() { + const modal = document.getElementById('sessionModal'); + if (modal) { + modal.classList.remove('active'); + } + currentEditingSessionId = null; +} + +/** + * Handle session form submission + * @param {Event} e - Form submit event + */ +async function handleSessionFormSubmit(e) { + e.preventDefault(); + + const dateInput = document.getElementById('sessionDate'); + const checkboxes = document.querySelectorAll('input[name="muscleGroup"]:checked'); + + const date = dateInput.value; + const muscle_groups = Array.from(checkboxes).map(cb => cb.value); + + // Validate that at least one muscle group is selected + if (muscle_groups.length === 0) { + showError('Please select at least one muscle group'); + return; + } + + const sessionData = { + date, + muscle_groups + }; + + try { + showLoading(); + + if (currentEditingSessionId) { + // Update existing session + await updateSession(currentEditingSessionId, sessionData); + showSuccess('Session updated successfully'); + } else { + // Create new session + await createSession(sessionData); + showSuccess('Session added successfully'); + } + + // Reload sessions and update UI + await loadSessions(); + updateUI(); + + closeModal(); + hideLoading(); + } catch (error) { + hideLoading(); + showError('Failed to save session: ' + error.message); + } +} + +/** + * Delete a session + * @param {string} sessionId - ID of the session to delete + */ +async function handleDeleteSession(sessionId) { + if (!confirm('Are you sure you want to delete this session?')) { + return; + } + + try { + showLoading(); + await deleteSession(sessionId); + await loadSessions(); + updateUI(); + hideLoading(); + showSuccess('Session deleted successfully'); + } catch (error) { + hideLoading(); + showError('Failed to delete session: ' + error.message); + } +} + +/** + * Update the entire UI with current sessions data + */ +function updateUI() { + // Update muscle groups dashboard + updateMuscleGroupsDashboard(sessions); + + // Update heatmap + updateHeatmap(sessions); +} + +// Initialize the application when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} diff --git a/frontend/heatmap.js b/frontend/heatmap.js new file mode 100644 index 0000000..b65d66c --- /dev/null +++ b/frontend/heatmap.js @@ -0,0 +1,132 @@ +// Heatmap Component using Cal-Heatmap library + +let cal = null; + +/** + * Initialize the Cal-Heatmap component + * @param {Array} sessions - Array of session objects + */ +function initHeatmap(sessions) { + const container = document.getElementById('heatmap'); + if (!container) return; + + // Transform sessions data for Cal-Heatmap + const heatmapData = transformSessionsForHeatmap(sessions); + + // Initialize Cal-Heatmap + cal = new CalHeatmap(); + + const currentYear = new Date().getFullYear(); + + cal.paint({ + itemSelector: '#heatmap', + domain: { + type: 'month', + label: { + position: 'bottom' + }, + }, + subDomain: { + type: 'day' + }, + data: { + source: heatmapData, + type: 'json', + x: 'date', + y: 'value' + }, + date: { + start: new Date(currentYear, 0, 1), + max: new Date(currentYear, 11, 31) + }, + range: 12, + scale: { + color: { + type: 'threshold', + range: ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'], + domain: [1, 2, 3, 4] + } + }, + itemName: ['workout', 'workouts'], + subDomainTextFormat: '%d', + tooltip: true + }); + + // Set up navigation controls + setupHeatmapControls(); +} + +/** + * Transform sessions array into format suitable for Cal-Heatmap + * @param {Array} sessions - Array of session objects + * @returns {Array} Transformed data for Cal-Heatmap + */ +function transformSessionsForHeatmap(sessions) { + const dateCounts = {}; + + sessions.forEach(session => { + const date = session.date; + // Count number of muscle groups trained (as measure of intensity) + const value = session.muscle_groups.length; + + if (dateCounts[date]) { + dateCounts[date] += value; + } else { + dateCounts[date] = value; + } + }); + + // Convert to array format expected by Cal-Heatmap + return Object.entries(dateCounts).map(([date, value]) => ({ + date, + value + })); +} + +/** + * Update heatmap with new sessions data + * @param {Array} sessions - Array of session objects + */ +function updateHeatmap(sessions) { + if (!cal) { + initHeatmap(sessions); + return; + } + + const heatmapData = transformSessionsForHeatmap(sessions); + + // Update the heatmap data + cal.fill(heatmapData); +} + +/** + * Set up navigation controls for the heatmap (previous/next) + */ +function setupHeatmapControls() { + const prevBtn = document.getElementById('heatmapPrev'); + const nextBtn = document.getElementById('heatmapNext'); + + if (prevBtn && cal) { + prevBtn.addEventListener('click', () => { + cal.previous(); + }); + } + + if (nextBtn && cal) { + nextBtn.addEventListener('click', () => { + cal.next(); + }); + } +} + +/** + * Destroy and reinitialize the heatmap + * @param {Array} sessions - Array of session objects + */ +function reinitializeHeatmap(sessions) { + if (cal) { + cal.destroy(); + cal = null; + } + initHeatmap(sessions); +} diff --git a/frontend/index.html b/frontend/index.html index 1243fdf..873eeb3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,55 +2,110 @@ - - Gym tracker - - - - - - - - - - - - - - + + + Gym Tracker + + - - - - - - - - - - + + + +
+ +
+

Gym Tracker

+
+ 😐 + Neutral Balance +
+
+ +
+ +
+ + +
+

Training Calendar

+
+ + +
+
+
+ + +
+

Muscle Groups

+
+ +
+
+
+ + + + + +
+
+
+ + + + + + diff --git a/frontend/muscleGroups.js b/frontend/muscleGroups.js new file mode 100644 index 0000000..6acb2d6 --- /dev/null +++ b/frontend/muscleGroups.js @@ -0,0 +1,166 @@ +// Muscle Groups Dashboard Component + +// Configuration for the 6 muscle groups +const MUSCLE_GROUPS = ['Chest', 'Legs', 'Delts', 'Lats', 'Triceps', 'Biceps']; + +/** + * Calculate statistics for each muscle group based on sessions + * @param {Array} sessions - Array of session objects + * @returns {Object} Statistics for each muscle group + */ +function calculateMuscleGroupStats(sessions) { + const now = new Date(); + const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + + const stats = {}; + + MUSCLE_GROUPS.forEach(muscle => { + // Filter sessions that include this muscle group + const muscleSessions = sessions.filter(session => + session.muscle_groups.includes(muscle) + ); + + // Sort by date descending + const sortedSessions = muscleSessions + .map(s => ({ ...s, dateObj: new Date(s.date) })) + .sort((a, b) => b.dateObj - a.dateObj); + + // Calculate days since last trained + let daysSince = null; + if (sortedSessions.length > 0) { + const lastDate = sortedSessions[0].dateObj; + const diffTime = now - lastDate; + daysSince = Math.floor(diffTime / (1000 * 60 * 60 * 24)); + } + + // Count sessions in last 7 days + const last7Days = sortedSessions.filter(s => s.dateObj >= sevenDaysAgo).length; + + // Count sessions in last 30 days + const last30Days = sortedSessions.filter(s => s.dateObj >= thirtyDaysAgo).length; + + // Determine status (good, warning, bad) + let status; + if (daysSince === null) { + status = 'bad'; // Never trained + } else if (daysSince <= 3) { + status = 'good'; + } else if (daysSince <= 7) { + status = 'warning'; + } else { + status = 'bad'; + } + + stats[muscle] = { + daysSince, + last7Days, + last30Days, + status + }; + }); + + return stats; +} + +/** + * Render muscle group cards in the dashboard + * @param {Object} stats - Statistics for each muscle group + */ +function renderMuscleGroups(stats) { + const container = document.getElementById('muscleGroupsGrid'); + if (!container) return; + + container.innerHTML = ''; + + MUSCLE_GROUPS.forEach(muscle => { + const muscleStats = stats[muscle]; + + const card = document.createElement('div'); + card.className = `muscle-card status-${muscleStats.status}`; + + const daysSinceText = muscleStats.daysSince === null + ? 'Never trained' + : muscleStats.daysSince === 0 + ? 'Trained today' + : `${muscleStats.daysSince} day${muscleStats.daysSince === 1 ? '' : 's'} ago`; + + card.innerHTML = ` +
+

${muscle}

+
+
+
+
+ Last trained: + ${daysSinceText} +
+
+ Last 7 days: + ${muscleStats.last7Days}x +
+
+ Last 30 days: + ${muscleStats.last30Days}x +
+
+ `; + + container.appendChild(card); + }); +} + +/** + * Calculate and update balance indicator + * @param {Object} stats - Statistics for each muscle group + */ +function updateBalanceIndicator(stats) { + const emojiElement = document.getElementById('balanceEmoji'); + const textElement = document.getElementById('balanceText'); + + if (!emojiElement || !textElement) return; + + // Check if all muscle groups meet certain criteria + const allTrainedLast7Days = MUSCLE_GROUPS.every(muscle => + stats[muscle].last7Days >= 1 + ); + + const allTrainedTwiceLast7Days = MUSCLE_GROUPS.every(muscle => + stats[muscle].last7Days >= 2 + ); + + let emoji, text, status; + + if (allTrainedTwiceLast7Days) { + emoji = '=+'; + text = 'Excellent Balance'; + status = 'happy'; + } else if (allTrainedLast7Days) { + emoji = '='; + text = 'Neutral Balance'; + status = 'neutral'; + } else { + emoji = '= '; + text = 'Poor Balance'; + status = 'angry'; + } + + emojiElement.textContent = emoji; + textElement.textContent = text; + + // Add status class for potential styling + const indicator = document.getElementById('balanceIndicator'); + if (indicator) { + indicator.className = `balance-indicator balance-${status}`; + } +} + +/** + * Update the entire muscle groups dashboard + * @param {Array} sessions - Array of session objects + */ +function updateMuscleGroupsDashboard(sessions) { + const stats = calculateMuscleGroupStats(sessions); + renderMuscleGroups(stats); + updateBalanceIndicator(stats); +} diff --git a/frontend/styles.css b/frontend/styles.css index e69de29..dd47e9c 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -0,0 +1,411 @@ +/* Reset and Base Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #f5f5f5; + color: #333; + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 20px; +} + +/* Header */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; + padding: 20px 0; + border-bottom: 2px solid #e0e0e0; +} + +.header h1 { + font-size: 2rem; + font-weight: 700; + color: #2c3e50; +} + +.balance-indicator { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 20px; + background: white; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.balance-emoji { + font-size: 2rem; +} + +.balance-text { + font-size: 1rem; + font-weight: 500; + color: #555; +} + +/* Action Bar */ +.action-bar { + margin-bottom: 30px; + text-align: center; +} + +/* Buttons */ +.btn-primary { + background: #4CAF50; + color: white; + border: none; + padding: 12px 24px; + font-size: 1rem; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s ease; +} + +.btn-primary:hover { + background: #45a049; +} + +.btn-secondary { + background: #6c757d; + color: white; + border: none; + padding: 8px 16px; + font-size: 0.9rem; + font-weight: 500; + border-radius: 6px; + cursor: pointer; + transition: background 0.3s ease; +} + +.btn-secondary:hover { + background: #5a6268; +} + +/* Heatmap Section */ +.heatmap-section { + background: white; + padding: 30px; + margin-bottom: 30px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.heatmap-section h2 { + font-size: 1.5rem; + margin-bottom: 20px; + color: #2c3e50; +} + +.heatmap-controls { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +.heatmap-container { + overflow-x: auto; +} + +/* Muscle Groups Section */ +.muscle-groups-section { + background: white; + padding: 30px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.muscle-groups-section h2 { + font-size: 1.5rem; + margin-bottom: 20px; + color: #2c3e50; +} + +.muscle-groups-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 20px; +} + +/* Muscle Group Card */ +.muscle-card { + background: #f9f9f9; + padding: 20px; + border-radius: 8px; + border-left: 4px solid #ccc; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.muscle-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.muscle-card.status-good { + border-left-color: #4CAF50; +} + +.muscle-card.status-warning { + border-left-color: #FFC107; +} + +.muscle-card.status-bad { + border-left-color: #F44336; +} + +.muscle-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.muscle-name { + font-size: 1.2rem; + font-weight: 600; + color: #2c3e50; +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.status-indicator.good { + background: #4CAF50; +} + +.status-indicator.warning { + background: #FFC107; +} + +.status-indicator.bad { + background: #F44336; +} + +.muscle-card-stats { + display: flex; + flex-direction: column; + gap: 8px; +} + +.stat-row { + display: flex; + justify-content: space-between; + font-size: 0.9rem; +} + +.stat-label { + color: #666; +} + +.stat-value { + font-weight: 600; + color: #333; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal.active { + display: flex; + justify-content: center; + align-items: center; +} + +.modal-content { + background: white; + padding: 0; + border-radius: 8px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #e0e0e0; +} + +.modal-header h3 { + font-size: 1.3rem; + color: #2c3e50; +} + +.close-btn { + background: none; + border: none; + font-size: 1.8rem; + color: #999; + cursor: pointer; + line-height: 1; +} + +.close-btn:hover { + color: #333; +} + +/* Form */ +#sessionForm { + padding: 20px; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #555; +} + +.form-group input[type="date"] { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; +} + +.checkbox-group { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 8px; + padding: 10px; + background: #f5f5f5; + border-radius: 4px; + cursor: pointer; + transition: background 0.2s ease; +} + +.checkbox-label:hover { + background: #e8e8e8; +} + +.checkbox-label input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; +} + +.checkbox-label span { + font-size: 0.95rem; + color: #333; +} + +.form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 24px; +} + +/* Loading Indicator */ +.loading-indicator { + display: none; + position: fixed; + z-index: 2000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.3); + justify-content: center; + align-items: center; +} + +.loading-indicator.active { + display: flex; +} + +.spinner { + border: 4px solid #f3f3f3; + border-top: 4px solid #4CAF50; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .header { + flex-direction: column; + gap: 15px; + text-align: center; + } + + .header h1 { + font-size: 1.5rem; + } + + .muscle-groups-grid { + grid-template-columns: 1fr; + } + + .checkbox-group { + grid-template-columns: 1fr; + } + + .heatmap-section, + .muscle-groups-section { + padding: 15px; + } +} + +/* Cal-Heatmap Overrides */ +.cal-heatmap-container { + font-family: inherit; +} + +.cal-heatmap-container .graph-label { + fill: #666; + font-size: 12px; +} + +.cal-heatmap-container rect.highlight { + stroke: #4CAF50; + stroke-width: 2; +} diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 0000000..e8c8d16 --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,158 @@ +# Kubernetes Deployment + +This directory contains Kubernetes manifests for deploying the Gym Tracker application. + +## Prerequisites + +- Kubernetes cluster (1.19+) +- kubectl configured to access your cluster +- Docker registry (optional, for remote deployments) + +## Files + +- **persistentvolumeclaim.yaml** - PVC for SQLite database storage +- **deployment.yaml** - Application deployment +- **service.yaml** - ClusterIP service for internal access +- **ingress.yaml** - Optional ingress for external access + +## Deployment Steps + +### 1. Build and Push Docker Image + +Build the Docker image: +```bash +docker build -t gym-tracker:latest . +``` + +If deploying to a remote cluster, tag and push to your registry: +```bash +docker tag gym-tracker:latest your-registry/gym-tracker:latest +docker push your-registry/gym-tracker:latest +``` + +Update the image name in `deployment.yaml` accordingly. + +### 2. Deploy to Kubernetes + +Apply all manifests: +```bash +kubectl apply -f kubernetes/ +``` + +Or apply individually in order: +```bash +kubectl apply -f kubernetes/persistentvolumeclaim.yaml +kubectl apply -f kubernetes/deployment.yaml +kubectl apply -f kubernetes/service.yaml +# Optional: kubectl apply -f kubernetes/ingress.yaml +``` + +### 3. Verify Deployment + +Check the deployment status: +```bash +kubectl get pods -l app=gym-tracker +kubectl get svc gym-tracker +kubectl get pvc gym-tracker-data +``` + +View logs: +```bash +kubectl logs -l app=gym-tracker -f +``` + +### 4. Access the Application + +#### Port Forward (for testing) +```bash +kubectl port-forward svc/gym-tracker 8080:80 +``` +Then access at http://localhost:8080 + +#### Using Ingress (for production) +1. Ensure an Ingress controller is installed in your cluster +2. Update the host in `ingress.yaml` with your domain +3. Apply the ingress: `kubectl apply -f kubernetes/ingress.yaml` +4. Access at http://your-domain.com + +#### Using LoadBalancer +Change the service type in `service.yaml` from `ClusterIP` to `LoadBalancer`: +```yaml +spec: + type: LoadBalancer +``` +Then get the external IP: +```bash +kubectl get svc gym-tracker +``` + +## Configuration + +### Storage + +The default PVC requests 1Gi of storage. Adjust in `persistentvolumeclaim.yaml`: +```yaml +resources: + requests: + storage: 5Gi # Increase as needed +``` + +### Replicas + +The application uses SQLite, which is file-based. Keep replicas at 1 to avoid database locking issues: +```yaml +spec: + replicas: 1 +``` + +For high availability, consider migrating to PostgreSQL or MySQL. + +### Resources + +Adjust resource limits in `deployment.yaml` based on your needs: +```yaml +resources: + requests: + memory: "256Mi" + cpu: "200m" + limits: + memory: "1Gi" + cpu: "1000m" +``` + +## Backup + +To backup the SQLite database: +```bash +kubectl exec -it -- sqlite3 /app/data/gym-tracker.db ".backup /app/data/backup.db" +kubectl cp :/app/data/backup.db ./backup.db +``` + +## Troubleshooting + +### Pod not starting +```bash +kubectl describe pod -l app=gym-tracker +kubectl logs -l app=gym-tracker +``` + +### Database issues +Check volume mount: +```bash +kubectl exec -it -- ls -la /app/data +``` + +### Health check failures +Test health endpoint: +```bash +kubectl exec -it -- wget -O- http://localhost/health +``` + +## Cleanup + +Remove all resources: +```bash +kubectl delete -f kubernetes/ +``` + +**Warning:** This will delete the PVC and all data. Backup first if needed. diff --git a/kubernetes/deployment.yaml b/kubernetes/deployment.yaml new file mode 100644 index 0000000..f428f0d --- /dev/null +++ b/kubernetes/deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gym-tracker + labels: + app: gym-tracker +spec: + replicas: 1 # Single replica since we're using SQLite with file-based storage + selector: + matchLabels: + app: gym-tracker + template: + metadata: + labels: + app: gym-tracker + spec: + containers: + - name: gym-tracker + image: gym-tracker:latest # Update with your registry/image name + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + protocol: TCP + - name: api + containerPort: 3000 + protocol: TCP + env: + - name: NODE_ENV + value: "production" + - name: PORT + value: "3000" + volumeMounts: + - name: data + mountPath: /app/data + livenessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /health + port: 80 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + volumes: + - name: data + persistentVolumeClaim: + claimName: gym-tracker-data diff --git a/kubernetes/ingress.yaml b/kubernetes/ingress.yaml new file mode 100644 index 0000000..d8c9f07 --- /dev/null +++ b/kubernetes/ingress.yaml @@ -0,0 +1,30 @@ +# Optional: Ingress for external access +# Requires an Ingress controller (e.g., nginx-ingress, traefik) installed in your cluster +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: gym-tracker + labels: + app: gym-tracker + annotations: + # Uncomment and adjust based on your ingress controller + # nginx.ingress.kubernetes.io/rewrite-target: / + # cert-manager.io/cluster-issuer: letsencrypt-prod # For HTTPS with cert-manager +spec: + ingressClassName: nginx # Adjust based on your ingress controller + rules: + - host: gym-tracker.example.com # Update with your domain + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: gym-tracker + port: + number: 80 + # Uncomment for HTTPS + # tls: + # - hosts: + # - gym-tracker.example.com + # secretName: gym-tracker-tls diff --git a/kubernetes/persistentvolumeclaim.yaml b/kubernetes/persistentvolumeclaim.yaml new file mode 100644 index 0000000..917abfe --- /dev/null +++ b/kubernetes/persistentvolumeclaim.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: gym-tracker-data + labels: + app: gym-tracker +spec: + accessModes: + - ReadWriteOnce # Single node access for SQLite + resources: + requests: + storage: 1Gi # Adjust size as needed + # storageClassName: standard # Uncomment and adjust based on your cluster's storage classes diff --git a/kubernetes/service.yaml b/kubernetes/service.yaml new file mode 100644 index 0000000..6ae10c7 --- /dev/null +++ b/kubernetes/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: gym-tracker + labels: + app: gym-tracker +spec: + type: ClusterIP # Change to LoadBalancer or NodePort if external access is needed + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: http + selector: + app: gym-tracker diff --git a/nginx/nginx.conf b/nginx/nginx.conf index e69de29..4e508c7 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -0,0 +1,80 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss; + + server { + listen 80; + server_name localhost; + + # Root directory for static files + root /usr/share/nginx/html; + index index.html; + + # Serve static frontend files + location / { + try_files $uri $uri/ /index.html; + + # Cache control for static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + + # Headers + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Disable buffering for real-time responses + proxy_buffering off; + } + + # Health check endpoint (proxy to backend) + location /health { + proxy_pass http://localhost:3000/health; + proxy_http_version 1.1; + proxy_set_header Host $host; + access_log off; + } + + # Error pages + error_page 404 /index.html; + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + } +}