commit 05e4a505b342e56691994004af6cbd996c4029f6 Author: sascha Date: Mon Oct 20 15:38:42 2025 +0200 Initial commit of step-competition project 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54204f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +step_competition.db +*.db +.DS_Store +*.log +npm-debug.log* diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..ad9dca8 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,132 @@ +# Quick Start Guide - Step Competition + +## Get Running in 2 Steps + +### 1. Start the Server +```bash +cd /home/sascha/gitea/step-competition +npm start +``` + +The server will start on **http://localhost:3060** + +### 2. Import Your 48 Participants + +**Prepare Your CSV File:** + +Create a CSV with 3 columns: `team_name`, `participant_name`, `email` + +Example (see `sample_import.csv` for 48-person template): +```csv +team_name,participant_name,email +Team Alpha,Alice Johnson,alice@example.com +Team Alpha,Bob Smith,bob@example.com +Team Beta,Carol Williams,carol@example.com +``` + +**Import Steps:** +1. Open http://localhost:3060 +2. On the "Log Your Steps" tab, scroll to "Admin: Bulk Import" +3. Select your CSV file +4. Click "Import CSV" +5. Done! All teams and participants are ready + +## Using the App + +### 3 Simple Tabs + +**1. Log Your Steps** +- Select your team from dropdown +- Select your name from filtered list (only shows your team) +- See your personal stats and history on the right +- Enter date and steps, click Submit + +**2. Team Standings** +- See team rankings (total steps, avg per person) +- See individual rankings (all 48 people) +- Gold/silver/bronze highlighting for top 3 + +**3. Competition Stats** +- Overall statistics cards +- Team progress chart (cumulative over time) +- Top 10 individuals bar chart +- Team comparison pie chart + +## Accessing via Tailscale + +Find your Tailscale IP: +```bash +tailscale ip -4 +``` + +Share this URL with all participants: +``` +http://YOUR_TAILSCALE_IP:3060 +``` + +For example: `http://100.64.1.2:3060` + +Everyone can access it from their phone or computer! + +## Accessing via Local Network + +Find your local IP: +```bash +hostname -I | awk '{print $1}' +``` + +Share this URL: +``` +http://YOUR_LOCAL_IP:3060 +``` + +For example: `http://192.168.1.100:3060` + +## Tips for Your 48-Person Competition + +1. **Pre-Import Everyone**: Use the CSV import before launch day +2. **Test First**: Try the sample_import.csv to see how it works +3. **Mobile-Friendly**: Participants can log from their phones +4. **Easy Name Finding**: Team dropdown first, then name dropdown shows only that team's 6 people +5. **Personal History**: Each person sees their entries immediately after selecting their name +6. **Daily Reminders**: Send daily reminders to log steps +7. **Display Leaderboard**: Show the "Team Standings" tab on a monitor or TV + +## Sample Data + +Want to test with fake data first? + +```bash +node sample-data.js +``` + +This creates 3 teams with 9 people and 7 days of step data. + +## Troubleshooting + +**CSV won't import:** +- Make sure first line has: `team_name,participant_name,email` +- No spaces in the header row +- Save as CSV, not Excel format + +**Can't find my name:** +- Did you select your team first? +- Names only show after team is selected +- Check spelling matches the import + +**Port 3060 in use:** +- Edit `src/server.js` line 8 to change port +- Or: `PORT=3070 npm start` + +## Resetting Everything + +```bash +rm step_competition.db +npm start +``` + +This deletes all data and creates a fresh database. + +## Need More Help? + +See the full [README.md](README.md) for complete documentation, API endpoints, and advanced features. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f89b0ad --- /dev/null +++ b/README.md @@ -0,0 +1,212 @@ +# Step Competition App + +A modern, streamlined web application for hosting workplace step competitions with 48+ participants across multiple teams. + +## Features + +**3 Simple Tabs:** +1. **Log Your Steps** - Easy 2-step selection (team → name) + instant personal history view +2. **Team Standings** - Combined team and individual leaderboards +3. **Competition Stats** - Overall metrics and visual progress charts + +**Key Capabilities:** +- CSV bulk import for quick setup (8 teams × 6 people = 48 participants) +- Team-first navigation makes finding your name fast +- Personal step history shown immediately after selecting your name +- Real-time leaderboards with gold/silver/bronze highlighting +- Visual charts showing team progress over time +- Mobile-friendly responsive design +- SQLite backend - no complex database setup + +## Quick Start + +### 1. Install and Start +```bash +cd /home/sascha/gitea/step-competition +npm install +npm start +``` + +### 2. Setup Teams & Participants + +**Option A: CSV Import (Recommended for 48 people)** + +1. Create a CSV file with 3 columns: `team_name`, `participant_name`, `email` +2. See `sample_import.csv` for reference format +3. Open the app and go to "Log Your Steps" tab +4. Scroll down to "Admin: Bulk Import" section +5. Select your CSV file and click "Import CSV" + +Example CSV format: +```csv +team_name,participant_name,email +Team Alpha,Alice Johnson,alice@example.com +Team Alpha,Bob Smith,bob@example.com +Team Beta,Carol Williams,carol@example.com +``` + +**Option B: Use Sample Data** +```bash +node sample-data.js +``` + +### 3. Start Logging Steps + +**For Participants:** +1. Open http://localhost:3060 (or your Tailscale URL) +2. Select your team from dropdown +3. Select your name from dropdown +4. See your personal stats and history on the right +5. Enter today's date and step count +6. Click "Submit Steps" + +**For Viewing Results:** +- **Team Standings** tab: See which teams and individuals are winning +- **Competition Stats** tab: View overall progress and charts + +## Accessing Remotely + +The server listens on port 3060 and is accessible via: + +**Via Tailscale:** +```bash +# Find your Tailscale IP +tailscale ip -4 + +# Share with participants +http://YOUR_TAILSCALE_IP:3060 +``` + +**Via Local Network:** +```bash +# Find your local IP +hostname -I | awk '{print $1}' + +# Share with participants +http://YOUR_LOCAL_IP:3060 +``` + +## CSV Import Format + +Your CSV must have these exact column headers: +- `team_name` - Name of the team +- `participant_name` - Person's full name +- `email` - Email address (optional, can be empty) + +The import will: +- Create teams automatically if they don't exist +- Add participants to their teams +- Skip duplicates (same name + team) +- Report how many teams and participants were created + +## Project Structure + +``` +step-competition/ +├── src/ +│ ├── server.js # Express server and API endpoints +│ └── database.js # SQLite database initialization +├── public/ +│ ├── index.html # 3-tab interface +│ ├── styles.css # Responsive styling +│ └── app.js # Frontend JavaScript +├── sample_import.csv # Template for bulk import (8 teams, 48 people) +├── step_competition.db # SQLite database (auto-created) +└── README.md +``` + +## API Endpoints + +### Teams & Participants +- `GET /api/teams` - Get all teams +- `POST /api/teams` - Create a team +- `GET /api/participants` - Get all participants +- `POST /api/participants` - Create a participant +- `POST /api/import/csv` - Bulk import from CSV +- `GET /api/participants/:id/history` - Get participant's step history + +### Steps +- `POST /api/steps` - Submit or update steps +- `GET /api/steps/:participantId/:date` - Get steps for specific date + +### Leaderboards +- `GET /api/leaderboard/individual` - Individual rankings +- `GET /api/leaderboard/team` - Team rankings +- `GET /api/progress/daily` - Daily progress data for charts + +## Configuration + +**Change Port:** +Edit `src/server.js` line 8: +```javascript +const PORT = process.env.PORT || 3060; +``` + +Or use environment variable: +```bash +PORT=8080 npm start +``` + +## Database Schema + +### teams +- `id`, `name`, `created_at` + +### participants +- `id`, `name`, `email`, `team_id`, `created_at` + +### daily_steps +- `id`, `participant_id`, `date`, `steps`, `created_at`, `updated_at` +- Unique constraint on (participant_id, date) + +## Tips for Running a Competition + +1. **Pre-populate**: Use CSV import to set up all 48 participants before launch +2. **Test First**: Import sample_import.csv and test with a few entries +3. **Share URL**: Send the Tailscale or local network URL to all participants +4. **Daily Reminders**: Encourage participants to log steps daily +5. **Showcase Leaderboards**: Display the Team Standings tab on a monitor +6. **Backup Data**: Periodically copy `step_competition.db` file + +## Troubleshooting + +**CSV import fails:** +- Check that your CSV has the exact column headers: `team_name,participant_name,email` +- Make sure there are no extra spaces or special characters +- Verify the file is saved as CSV format, not Excel + +**Can't find my name:** +- Make sure you select the correct team first +- Names are listed alphabetically within each team +- Check that your name was imported correctly + +**Steps not saving:** +- Verify you selected both team and name +- Check that you entered a valid number for steps +- Make sure the date is in the correct format + +**Port 3060 already in use:** +- Change the PORT in src/server.js to another number (e.g., 3070) +- Or stop the other service using that port + +## Resetting the Competition + +To start fresh: +```bash +rm step_competition.db +npm start +``` + +A new empty database will be created. You'll need to re-import your participants. + +## Browser Compatibility + +Works with all modern browsers: +- Chrome/Edge (recommended) +- Firefox +- Safari +- Mobile browsers (iOS/Android) + +## License + +ISC diff --git a/competition_participants.csv b/competition_participants.csv new file mode 100644 index 0000000..f5a4557 --- /dev/null +++ b/competition_participants.csv @@ -0,0 +1,49 @@ +team_name,participant_name,email +Van Helsing,Alexandra Perex,aperez@adaptivehealth.com +Van Helsing,Katrina Greene,kgreene@adaptivehealth.com +Van Helsing,Brittany Washington,bwashington@adaptivehealth.com +Van Helsing,Taylor Seay,taylor.seay@adaptivehealth.com +Van Helsing,Nicole Kane,nkane@adaptivehealth.com +Van Helsing,Mary Todd,mtodd@adaptivehealth.com +Mulder and Scully,Mikala Hukka,mhukka@adaptivehealth.com +Mulder and Scully,Tina Jackse,tjackse@wellful.com +Mulder and Scully,Alice Staehle,astaehle@adaptivehealth.com +Mulder and Scully,Amina Sarraf,asarraf@healthydirections.com +Mulder and Scully,Paula Harrell,pharrell@adaptivehealth.com +Mulder and Scully,Dianne Lidiak,dlidiak@healthydirections.com +Mystery Inc,Joe Robba,jrobba@adaptivehealth.com +Mystery Inc,Jayanth Chintireddy,jchintireddy@adaptivehealth.com +Mystery Inc,Renee Clark,rclark@adaptivehealth.com +Mystery Inc,Joel Clark,Jclark@adaptivehealth.com +Mystery Inc,Nicole Simpson,nsimpson@adaptivehealth.com +Mystery Inc,Izabela Kondracka,ikondracka@adaptivehealth.com +Blade,Ray Urrutia,rurrutia@adaptivehealth.com +Blade,Colleen Manner,cmanner@adaptivehealth.com +Blade,Taylor Wall,twall@adaptivehealth.com +Blade,Aaron Gentry,agentry@wellful.com +Blade,Samantha Downs,sdowns@biovationlabs.com +Blade,Megan McFadden,mmcfadden@adaptivehealth.com +Buffy,Jeanene Medley,jmedley@adaptivehealth.com +Buffy,Trevera Brathwaite,TBrathwaite@adaptivehealth.com +Buffy,Amol Pachpande,apachpande@adaptivehealth.com +Buffy,Anne Marie Kiesling,amkiesling@adaptivehealth.com +Buffy,Tatiana Nunes,tatiana@adaptivehealth.com +Buffy,Kathy Cadeaux,KCADEAUX@adaptivehealth.com +Ghostbusters,Gabriel McCann,gmccann@adaptivehealth.com +Ghostbusters,Emily Stillion,estillion@adaptivehealth.com +Ghostbusters,Andy Giroux,agiroux@adaptivehealth.com +Ghostbusters,Erica Bennerman,erica.bennerman@adaptivehealth.com +Ghostbusters,Orietta Santa Cruz,osantacruz@adaptivehealth.com +Ghostbusters,Tisha Verma,tverma@adaptivehealth.com +Winchesters,Sascha Maraj,smaraj@adaptivehealth.com +Winchesters,Lauren Whitehead,lwhitehead@adaptivehealth.com +Winchesters,Jenna Washington,jwashington@adaptivehealth.com +Winchesters,Erin Fusco,efusco@adaptivehealth.com +Winchesters,Ashley Delaney,adelaney@adaptivehealth.com +Winchesters,Kelsey Ford,kford@adaptivehealth.com +Daryl Dixon,Dimpal Ahir,dahir@adaptivehealth.com +Daryl Dixon,Carrie Flynn,cflynn@adaptivehealth.com +Daryl Dixon,Jackie Margolis,Jmargolis@adaptivehealth.com +Daryl Dixon,Hayley Chittum,hchittum@adaptivehealth.com +Daryl Dixon,Emily Bay,emily@adaptivehealth.com +Daryl Dixon,Scott Lewis,SLewis@adaptivehealth.com diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..05a13f8 --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,20 @@ +module.exports = { + apps: [{ + name: 'monster-dash', + script: './src/server.js', + cwd: '/home/sascha/gitea/step-competition', + exec_mode: 'fork', + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '500M', + env: { + NODE_ENV: 'production', + PORT: 3060 + }, + error_file: '/home/sascha/.pm2/logs/monster-dash-error.log', + out_file: '/home/sascha/.pm2/logs/monster-dash-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true + }] +}; diff --git a/monsters.csv b/monsters.csv new file mode 100644 index 0000000..e833629 --- /dev/null +++ b/monsters.csv @@ -0,0 +1,7 @@ +date,monster_name,monster_description,step_goal,monster_icon +2025-10-17,Test Monster,Can you catch this spooky creature?,30000,👻 +2025-10-20,Dracula,The Count demands your steps!,60000,🧛 +2025-10-21,Freddy Krueger,Don't fall asleep on your steps!,48000,🔪 +2025-10-22,Mummy,Unwrap your potential!,48000,🧟 +2025-10-23,Aliens,Take steps to another world!,72000,👽 +2025-10-24,Predator,Hunt down those steps!,66000,👹 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..224781f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2219 @@ +{ + "name": "step-competition", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "step-competition", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "sqlite3": "^5.1.7" + } + }, + "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", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "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", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "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", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "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", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "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", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "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", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "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", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "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", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "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", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.78.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz", + "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==", + "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", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "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", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "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", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "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", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "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", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "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", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "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", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "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/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "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/package.json b/package.json new file mode 100644 index 0000000..2164f53 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "step-competition", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "node src/server.js", + "dev": "node src/server.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "body-parser": "^2.2.0", + "cors": "^2.8.5", + "express": "^5.1.0", + "sqlite3": "^5.1.7" + } +} diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..26d730b --- /dev/null +++ b/public/app.js @@ -0,0 +1,888 @@ +const API_URL = window.location.origin; + +// State +let teams = []; +let participants = []; +let charts = {}; +let selectedParticipant = null; + +// Initialize app +document.addEventListener('DOMContentLoaded', () => { + initializeTabs(); + loadInitialData(); + setupEventListeners(); + setDefaultDate(); + handleRouting(); +}); + +// Routing +function handleRouting() { + // Handle initial load and hash changes + window.addEventListener('hashchange', navigateToTab); + navigateToTab(); +} + +function navigateToTab() { + const hash = window.location.hash.slice(1) || 'log'; // Default to 'log' tab + switchTab(hash); +} + +function switchTab(tabName) { + const tabButtons = document.querySelectorAll('.tab-btn'); + const tabContents = document.querySelectorAll('.tab-content'); + + // Remove active class from all + tabButtons.forEach(btn => btn.classList.remove('active')); + tabContents.forEach(content => content.classList.remove('active')); + + // Add active class to selected tab + const activeButton = document.querySelector(`[data-tab="${tabName}"]`); + const activeContent = document.getElementById(`${tabName}-tab`); + + if (activeButton && activeContent) { + activeButton.classList.add('active'); + activeContent.classList.add('active'); + + // Load data for specific tabs + if (tabName === 'teams') loadTeamStandings(); + if (tabName === 'stats') loadCompetitionStats(); + if (tabName === 'monsters') loadDailyMonsters(); + } +} + +// Tab management +function initializeTabs() { + const tabButtons = document.querySelectorAll('.tab-btn'); + + tabButtons.forEach(button => { + button.addEventListener('click', (e) => { + e.preventDefault(); + const tabName = button.dataset.tab; + window.location.hash = tabName; + }); + }); +} + +// Set default date to today +function setDefaultDate() { + const today = new Date().toISOString().split('T')[0]; + const dateInput = document.getElementById('step-date'); + + dateInput.value = today; + // Set max date to October 31, 2025 + dateInput.max = '2025-10-31'; + dateInput.min = '2025-10-15'; +} + +// Load initial data +async function loadInitialData() { + await loadTeams(); + await loadParticipants(); +} + +// Event listeners +function setupEventListeners() { + document.getElementById('team-select').addEventListener('change', onTeamSelect); + document.getElementById('name-select').addEventListener('change', onNameSelect); + document.getElementById('submit-steps').addEventListener('click', submitSteps); + document.getElementById('import-csv').addEventListener('click', importCSV); + document.getElementById('import-monsters').addEventListener('click', importMonsters); +} + +// API calls +async function apiCall(endpoint, method = 'GET', body = null) { + const options = { + method, + headers: { 'Content-Type': 'application/json' } + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(`${API_URL}${endpoint}`, options); + if (!response.ok) { + throw new Error(`API call failed: ${response.statusText}`); + } + return response.json(); +} + +// Teams +async function loadTeams() { + try { + teams = await apiCall('/api/teams'); + updateTeamSelect(); + } catch (error) { + console.error('Error loading teams:', error); + } +} + +function updateTeamSelect() { + const select = document.getElementById('team-select'); + select.innerHTML = ''; + + teams.forEach(team => { + const option = document.createElement('option'); + option.value = team.id; + option.textContent = team.name; + select.appendChild(option); + }); +} + +// Participants +async function loadParticipants() { + try { + participants = await apiCall('/api/participants'); + } catch (error) { + console.error('Error loading participants:', error); + } +} + +async function onTeamSelect(e) { + const teamId = parseInt(e.target.value); + const nameSelect = document.getElementById('name-select'); + + if (!teamId) { + nameSelect.disabled = true; + nameSelect.innerHTML = ''; + document.getElementById('team-stats').innerHTML = '

Select your team to see team stats

'; + return; + } + + // Filter participants by team + const teamParticipants = participants.filter(p => p.team_id === teamId); + + nameSelect.disabled = false; + nameSelect.innerHTML = ''; + + teamParticipants.forEach(participant => { + const option = document.createElement('option'); + option.value = participant.id; + option.textContent = participant.name; + nameSelect.appendChild(option); + }); + + // Load team stats + await loadTeamStats(teamId); +} + +async function onNameSelect(e) { + const participantId = parseInt(e.target.value); + + if (!participantId) { + selectedParticipant = null; + document.getElementById('personal-stats').innerHTML = '

Select your name to see your history

'; + document.getElementById('personal-history').innerHTML = ''; + return; + } + + selectedParticipant = participants.find(p => p.id === participantId); + await loadPersonalHistory(participantId); +} + +async function loadPersonalHistory(participantId) { + try { + const history = await apiCall(`/api/participants/${participantId}/history`); + const individualData = await apiCall('/api/leaderboard/individual'); + const personData = individualData.find(p => p.id === participantId); + + // Display stats + const statsBox = document.getElementById('personal-stats'); + if (personData) { + const avgSteps = personData.days_logged > 0 ? Math.round(personData.total_steps / personData.days_logged) : 0; + statsBox.innerHTML = ` +
+ Total Steps: + ${personData.total_steps.toLocaleString()} +
+
+ Days Logged: + ${personData.days_logged} +
+
+ Avg Steps/Day: + ${avgSteps.toLocaleString()} +
+ `; + } + + // Display history + const historyDiv = document.getElementById('personal-history'); + if (history.length === 0) { + historyDiv.innerHTML = '

No entries yet. Start logging your steps!

'; + } else { + historyDiv.innerHTML = history.map(entry => ` +
+ ${formatDate(entry.date)} + ${entry.steps.toLocaleString()} steps +
+ `).join(''); + } + } catch (error) { + console.error('Error loading personal history:', error); + } +} + +async function loadTeamStats(teamId) { + try { + const [individualData, teamData, timeline] = await Promise.all([ + apiCall('/api/leaderboard/individual'), + apiCall('/api/leaderboard/team'), + apiCall('/api/monsters/timeline') + ]); + + const team = teamData.find(t => t.id === teamId); + const teamMembers = individualData.filter(p => p.team_id === teamId); + + const teamStatsDiv = document.getElementById('team-stats'); + + if (team && teamMembers.length > 0) { + // Sort team members by total steps + teamMembers.sort((a, b) => b.total_steps - a.total_steps); + + // Find monsters caught by this team + const caughtMonsters = timeline.filter(monster => + monster.catches.some(c => c.team_id === teamId) + ); + + teamStatsDiv.innerHTML = ` +
+ Team Total: + ${team.total_steps.toLocaleString()} +
+ ${caughtMonsters.length > 0 ? ` +
+ Monsters Caught: + ${caughtMonsters.length} 🏆 +
+
+ ${caughtMonsters.map(m => ` + ${m.monster_icon} + `).join('')} +
+ ` : ''} +

Team Members:

+ ${teamMembers.map((member, index) => ` +
+ ${index + 1}. ${member.name} + ${member.total_steps.toLocaleString()} +
+ `).join('')} + `; + } + } catch (error) { + console.error('Error loading team stats:', error); + } +} + +// Step submission +async function submitSteps() { + const participantId = document.getElementById('name-select').value; + const date = document.getElementById('step-date').value; + const steps = parseInt(document.getElementById('step-count').value); + const messageDiv = document.getElementById('entry-message'); + + if (!participantId || !date || isNaN(steps)) { + showMessage(messageDiv, 'Please fill in all fields', 'error'); + return; + } + + // Validate date is not after October 31, 2025 + const contestEndDate = new Date('2025-10-31'); + const selectedDate = new Date(date + 'T00:00:00'); + + if (selectedDate > contestEndDate) { + showMessage(messageDiv, 'Contest ended on October 31, 2025. Cannot enter steps after this date.', 'error'); + return; + } + + try { + await apiCall('/api/steps', 'POST', { + participant_id: parseInt(participantId), + date, + steps + }); + + document.getElementById('step-count').value = ''; + showMessage(messageDiv, 'Steps submitted successfully!', 'success'); + + // Reload personal history and team stats + if (selectedParticipant) { + await loadPersonalHistory(selectedParticipant.id); + const teamId = document.getElementById('team-select').value; + if (teamId) { + await loadTeamStats(parseInt(teamId)); + } + } + } catch (error) { + showMessage(messageDiv, `Error: ${error.message}`, 'error'); + } +} + +// CSV Import +async function importCSV() { + const fileInput = document.getElementById('csv-file'); + const messageDiv = document.getElementById('import-message'); + + if (!fileInput.files || !fileInput.files[0]) { + showMessage(messageDiv, 'Please select a CSV file', 'error'); + return; + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + try { + const csvText = e.target.result; + const lines = csvText.split('\n').filter(line => line.trim()); + + // Skip header row + const dataLines = lines.slice(1); + + const data = dataLines.map(line => { + const [team_name, participant_name, email] = line.split(',').map(s => s.trim()); + return { team_name, participant_name, email }; + }); + + const result = await apiCall('/api/import/csv', 'POST', { data }); + + showMessage( + messageDiv, + `Success! Created ${result.teamsCreated} teams and ${result.participantsCreated} participants`, + 'success' + ); + + // Reload data + await loadTeams(); + await loadParticipants(); + + fileInput.value = ''; + } catch (error) { + showMessage(messageDiv, `Error: ${error.message}`, 'error'); + } + }; + + reader.readAsText(file); +} + +// Team Standings Tab +async function loadTeamStandings() { + try { + const [teamData, individualData, timeline] = await Promise.all([ + apiCall('/api/leaderboard/team'), + apiCall('/api/leaderboard/individual'), + apiCall('/api/monsters/timeline') + ]); + + // Team Leaderboard + const teamTbody = document.querySelector('#team-leaderboard tbody'); + teamTbody.innerHTML = ''; + + teamData.forEach((team, index) => { + const row = teamTbody.insertRow(); + const avgPerPerson = team.member_count > 0 ? Math.round(team.total_steps / team.member_count) : 0; + + // Find monsters caught by this team + const caughtMonsters = timeline.filter(monster => + monster.catches.some(c => c.team_id === team.id) + ); + + const monsterBadges = caughtMonsters.length > 0 + ? caughtMonsters.map(m => `${m.monster_icon}`).join(' ') + : '-'; + + row.innerHTML = ` + ${index + 1} + ${team.team_name} + ${team.total_steps.toLocaleString()} + ${team.active_count} + ${avgPerPerson.toLocaleString()} + ${monsterBadges} + `; + }); + + // Individual Leaderboard + const individualTbody = document.querySelector('#individual-leaderboard tbody'); + individualTbody.innerHTML = ''; + + individualData.forEach((person, index) => { + const row = individualTbody.insertRow(); + const avgSteps = person.days_logged > 0 ? Math.round(person.total_steps / person.days_logged) : 0; + row.innerHTML = ` + ${index + 1} + ${person.name} + ${person.team_name} + ${person.total_steps.toLocaleString()} + ${person.days_logged} + ${avgSteps.toLocaleString()} + `; + }); + } catch (error) { + console.error('Error loading team standings:', error); + } +} + +// Competition Stats Tab +async function loadCompetitionStats() { + try { + const [individualData, dailyProgress, teamData] = await Promise.all([ + apiCall('/api/leaderboard/individual'), + apiCall('/api/progress/daily'), + apiCall('/api/leaderboard/team') + ]); + + // Calculate overall stats + const totalSteps = individualData.reduce((sum, p) => sum + p.total_steps, 0); + const totalParticipants = individualData.length; + const totalDaysLogged = individualData.reduce((sum, p) => sum + p.days_logged, 0); + const avgDailySteps = totalDaysLogged > 0 ? Math.round(totalSteps / totalDaysLogged) : 0; + + // Get unique dates + const uniqueDates = [...new Set(dailyProgress.map(d => d.date))]; + const daysActive = uniqueDates.length; + + // Update stat cards + document.getElementById('total-steps').textContent = totalSteps.toLocaleString(); + document.getElementById('total-participants').textContent = totalParticipants; + document.getElementById('avg-daily-steps').textContent = avgDailySteps.toLocaleString(); + document.getElementById('days-active').textContent = daysActive; + + // Render charts + renderTeamProgressChart(dailyProgress); + renderTopIndividualsChart(individualData.slice(0, 10)); + await renderTeamComparisonChart(teamData); + } catch (error) { + console.error('Error loading competition stats:', error); + } +} + +function renderTeamProgressChart(data) { + const ctx = document.getElementById('team-progress-chart'); + + // Group by team + const teamData = {}; + data.forEach(entry => { + if (!teamData[entry.team_name]) { + teamData[entry.team_name] = { dates: [], steps: [] }; + } + teamData[entry.team_name].dates.push(entry.date); + teamData[entry.team_name].steps.push(entry.daily_steps); + }); + + // Get all unique dates sorted + const allDates = [...new Set(data.map(d => d.date))].sort(); + + // Create datasets + const colors = [ + 'rgba(102, 126, 234, 1)', + 'rgba(118, 75, 162, 1)', + 'rgba(237, 100, 166, 1)', + 'rgba(255, 154, 158, 1)', + 'rgba(250, 208, 196, 1)', + 'rgba(136, 140, 255, 1)', + 'rgba(72, 1, 255, 1)', + 'rgba(255, 193, 7, 1)' + ]; + + const datasets = Object.entries(teamData).map(([teamName, data], index) => { + const cumulativeSteps = []; + let total = 0; + + allDates.forEach(date => { + const dateIndex = data.dates.indexOf(date); + if (dateIndex !== -1) { + total += data.steps[dateIndex]; + } + cumulativeSteps.push(total); + }); + + return { + label: teamName, + data: cumulativeSteps, + borderColor: colors[index % colors.length], + backgroundColor: colors[index % colors.length].replace('1)', '0.1)'), + borderWidth: 3, + fill: true, + tension: 0.4 + }; + }); + + if (charts.teamProgress) { + charts.teamProgress.destroy(); + } + + charts.teamProgress = new Chart(ctx, { + type: 'line', + data: { + labels: allDates, + datasets: datasets + }, + options: { + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { + display: true, + position: 'top' + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: function(value) { + return value.toLocaleString(); + } + } + } + } + } + }); +} + +function renderTopIndividualsChart(data) { + const ctx = document.getElementById('top-individuals-chart'); + + if (charts.topIndividuals) { + charts.topIndividuals.destroy(); + } + + charts.topIndividuals = new Chart(ctx, { + type: 'bar', + data: { + labels: data.map(p => p.name), + datasets: [{ + label: 'Total Steps', + data: data.map(p => p.total_steps), + backgroundColor: 'rgba(102, 126, 234, 0.8)', + borderColor: 'rgba(102, 126, 234, 1)', + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { + display: false + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: function(value) { + return value.toLocaleString(); + } + } + } + } + } + }); +} + +async function renderTeamComparisonChart(teamData) { + const ctx = document.getElementById('team-comparison-chart'); + + if (charts.teamComparison) { + charts.teamComparison.destroy(); + } + + // Get individual data to break down each team + const individualData = await apiCall('/api/leaderboard/individual'); + + // Generate distinct colors for each person + const personColors = [ + 'rgba(102, 126, 234, 0.8)', + 'rgba(118, 75, 162, 0.8)', + 'rgba(237, 100, 166, 0.8)', + 'rgba(255, 154, 158, 0.8)', + 'rgba(250, 208, 196, 0.8)', + 'rgba(136, 140, 255, 0.8)', + 'rgba(72, 1, 255, 0.8)', + 'rgba(255, 193, 7, 0.8)', + 'rgba(0, 188, 212, 0.8)', + 'rgba(156, 39, 176, 0.8)', + 'rgba(255, 87, 34, 0.8)', + 'rgba(76, 175, 80, 0.8)' + ]; + + // Get team labels sorted by total steps + const sortedTeams = teamData.sort((a, b) => b.total_steps - a.total_steps); + const teamLabels = sortedTeams.map(t => t.team_name); + + // Create a dataset for each unique participant + const allParticipants = individualData.sort((a, b) => b.total_steps - a.total_steps); + const datasets = allParticipants.map((person, index) => { + // Create data array with this person's steps for their team, 0 for others + const data = teamLabels.map(teamName => { + return person.team_name === teamName ? person.total_steps : 0; + }); + + return { + label: person.name, + data: data, + backgroundColor: personColors[index % personColors.length], + borderWidth: 1, + borderColor: 'white' + }; + }); + + charts.teamComparison = new Chart(ctx, { + type: 'bar', + data: { + labels: teamLabels, + datasets: datasets + }, + options: { + indexAxis: 'y', // Makes it horizontal + responsive: true, + maintainAspectRatio: true, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + callbacks: { + label: function(context) { + const label = context.dataset.label || ''; + const value = context.parsed.x; + return value > 0 ? `${label}: ${value.toLocaleString()} steps` : null; + }, + afterBody: function(tooltipItems) { + // Calculate the team total by summing all items in this tooltip + const total = tooltipItems.reduce((sum, item) => sum + item.parsed.x, 0); + return `\nTeam Total: ${total.toLocaleString()} steps`; + } + } + } + }, + scales: { + x: { + stacked: true, + beginAtZero: true, + ticks: { + callback: function(value) { + return value.toLocaleString(); + } + } + }, + y: { + stacked: true + } + } + } + }); +} + +// Daily Monsters Tab +async function loadDailyMonsters() { + try { + const [todayStatus, timeline] = await Promise.all([ + apiCall('/api/monsters/status/today'), + apiCall('/api/monsters/timeline') + ]); + + // Render today's monster + renderTodaysMonster(todayStatus); + + // Render timeline + renderMonsterTimeline(timeline); + } catch (error) { + console.error('Error loading daily monsters:', error); + } +} + +function renderTodaysMonster(status) { + const cardDiv = document.getElementById('todays-monster-card'); + + if (!status.monster) { + cardDiv.innerHTML = '

No monster challenge available. Check back tomorrow!

'; + return; + } + + const { monster, progress } = status; + + // Calculate how many teams caught it + const caughtCount = progress.filter(p => p.caught).length; + + // Format the date nicely + const monsterDate = new Date(monster.date + 'T00:00:00'); + const formattedDate = monsterDate.toLocaleDateString('en-US', { + weekday: 'long', + month: 'short', + day: 'numeric' + }); + + cardDiv.innerHTML = ` +
+

+ 📅 Active Challenge: Log your steps from ${formattedDate} (yesterday) +

+
+
${monster.monster_icon}
+

${monster.monster_name}

+

${monster.monster_description || ''}

+
+ Goal: + ${monster.step_goal.toLocaleString()} steps +
+
${caughtCount} of ${progress.length} teams caught this monster!
+ +
+

Team Progress:

+ ${progress.map(team => { + const percentage = Math.min((team.total_steps / monster.step_goal) * 100, 100); + const caught = team.caught; + return ` +
+
+ ${team.team_name} + ${team.total_steps.toLocaleString()} steps ${caught ? '🏆' : ''} +
+
+
+
+
+ `; + }).join('')} +
+ `; +} + +function renderMonsterTimeline(timeline) { + const timelineDiv = document.getElementById('monster-timeline'); + + // Handle null, undefined, or empty array + if (!timeline || !Array.isArray(timeline) || timeline.length === 0) { + timelineDiv.innerHTML = '

No monsters have been configured yet.

'; + return; + } + + // Get yesterday's date (active challenge - enter yesterday's steps) + const now = new Date(); + now.setDate(now.getDate() - 1); + const yesterday = now.toISOString().split('T')[0]; + + const today = new Date().toISOString().split('T')[0]; + + timelineDiv.innerHTML = timeline.map(monster => { + const monsterDate = monster.date; + const isPast = monsterDate < yesterday; + const isYesterday = monsterDate === yesterday; // Active challenge - enter yesterday's steps + const isToday = monsterDate === today; // Next challenge (tomorrow) + const isFuture = monsterDate > today; + + let statusClass = 'future'; + let statusText = 'Upcoming'; + if (isPast) { + statusClass = 'past'; + statusText = monster.catches.length > 0 ? `${monster.catches.length} teams caught` : 'Escaped'; + } else if (isYesterday) { + statusClass = 'today'; + statusText = "Active - Enter Yesterday's Steps"; + } else if (isToday) { + statusClass = 'tomorrow'; + statusText = 'Next Challenge'; + } + + return ` +
+
+
${monster.monster_icon}
+
+

${monster.monster_name}

+
${formatDate(monster.date)}
+
+
${statusText}
+
+ +

${monster.monster_description || ''}

+ +
+ Goal: ${monster.step_goal.toLocaleString()} steps +
+ + ${monster.catches.length > 0 ? ` +
+ Caught by: +
    + ${monster.catches.map(c => ` +
  • ${c.team_name} - ${c.final_steps.toLocaleString()} steps
  • + `).join('')} +
+
+ ` : ''} +
+ `; + }).join(''); +} + +// Monster CSV Import +async function importMonsters() { + const fileInput = document.getElementById('monster-csv-file'); + const messageDiv = document.getElementById('monster-import-message'); + + if (!fileInput.files || !fileInput.files[0]) { + showMessage(messageDiv, 'Please select a CSV file', 'error'); + return; + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + try { + const csvText = e.target.result; + const lines = csvText.split('\n').filter(line => line.trim()); + + // Skip header row + const dataLines = lines.slice(1); + + const data = dataLines.map(line => { + const [date, monster_name, monster_description, step_goal, monster_icon] = line.split(',').map(s => s.trim()); + return { date, monster_name, monster_description, step_goal, monster_icon }; + }); + + const result = await apiCall('/api/monsters/import', 'POST', { data }); + + showMessage( + messageDiv, + `Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`, + 'success' + ); + + // Reload monsters + await loadDailyMonsters(); + + fileInput.value = ''; + } catch (error) { + showMessage(messageDiv, `Error: ${error.message}`, 'error'); + } + }; + + reader.readAsText(file); +} + +// Utility functions +function showMessage(element, message, type) { + element.textContent = message; + element.className = `message ${type}`; + element.style.display = 'block'; + + setTimeout(() => { + element.style.display = 'none'; + }, 5000); +} + +function formatDate(dateStr) { + const date = new Date(dateStr + 'T00:00:00'); + return date.toLocaleDateString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric' + }); +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..b18a64d --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..dfaf78c --- /dev/null +++ b/public/index.html @@ -0,0 +1,212 @@ + + + + + + Monster Dash Step Competition + + + + + +
+
+ + +
+ + +
+
+ +
+

Log Your Steps

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

Your Step History

+
+

Select your name to see your history

+
+
+ +

Your Team

+
+

Select your team to see team stats

+
+
+
+
+ + +
+

Daily Monster Challenge

+ + +
+

📖 How It Works

+

+ Each day, a new monster appears with a step goal. Your team must reach the goal to catch it! +

+

+ ⏰ Important: The "Active Challenge" shows yesterday's monster. + Log your steps from yesterday to see if your team caught it! +

+

+ 💡 You can enter steps for any past date, and catches will be updated automatically. +

+
+ + +
+

Admin: Import Monsters

+

Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon

+ + +
+
+ + +
+

Active Challenge

+

Enter your steps from yesterday to catch this monster!

+
+

Loading active challenge...

+
+
+ + +
+

Monster Timeline

+
+

Loading monsters...

+
+
+
+ + +
+

Team Standings

+ +
+

Team Leaderboard

+ + + + + + + + + + + + +
RankTeam NameTotal StepsActiveAvg Steps/PersonMonsters Caught
+
+ +
+

Individual Leaderboard

+ + + + + + + + + + + + +
RankNameTeamTotal StepsDays LoggedAvg Steps/Day
+
+
+ + +
+

Competition Statistics

+ +
+
+

Total Steps

+
-
+
+
+

Total Participants

+
-
+
+
+

Average Daily Steps

+
-
+
+
+

Days Active

+
-
+
+
+ +
+

Team Comparison

+ +
+ +
+

Top 10 Individuals

+ +
+ +
+

Team Progress Over Time

+ +
+
+
+ + + + diff --git a/public/monster-dash-logo.jpg b/public/monster-dash-logo.jpg new file mode 100644 index 0000000..dacf10c Binary files /dev/null and b/public/monster-dash-logo.jpg differ diff --git a/public/monster-dash-logo.png b/public/monster-dash-logo.png new file mode 100644 index 0000000..5bfe311 Binary files /dev/null and b/public/monster-dash-logo.png differ diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..5a78967 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,749 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #1a1a1a 0%, #2d1b4e 50%, #1a1a1a 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1400px; + margin: 0 auto; + background: #1a1a1a; + border-radius: 20px; + box-shadow: 0 20px 60px rgba(255, 119, 0, 0.3), 0 0 100px rgba(138, 43, 226, 0.2); + overflow: hidden; + border: 2px solid #ff7700; +} + +header { + background: #000; + color: white; + padding: 30px; + text-align: center; + position: relative; + box-shadow: 0 4px 20px rgba(255, 119, 0, 0.4); + border-bottom: 3px solid #ff7700; +} + +header h1 { + font-size: 2.5rem; + margin-bottom: 20px; + font-weight: 700; + text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5); + letter-spacing: 2px; +} + +.header-logo { + max-width: 600px; + width: 100%; + height: auto; + margin: 0 auto 0 auto; + display: block; +} + +nav { + display: flex; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} + +.tab-btn { + background: rgba(255, 255, 255, 0.2); + color: white; + border: 2px solid rgba(255, 255, 255, 0.3); + padding: 12px 30px; + border-radius: 25px; + cursor: pointer; + font-size: 1.1rem; + font-weight: 600; + transition: all 0.3s; +} + +.tab-btn:hover { + background: rgba(255, 255, 255, 0.3); + transform: translateY(-2px); +} + +.tab-btn.active { + background: #ff7700; + color: white; + border-color: #ff7700; + box-shadow: 0 0 20px rgba(255, 119, 0, 0.6); +} + +.tab-content { + display: none; + padding: 40px; + background: #1a1a1a; +} + +.tab-content.active { + display: block; +} + +.tab-content h2 { + color: #ff7700; + margin-bottom: 30px; + font-size: 2rem; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); +} + +.tab-content h3 { + color: #8a2be2; + margin-bottom: 20px; + font-size: 1.5rem; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); +} + +/* Two Column Layout */ +.two-column-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; +} + +.entry-column, .history-column { + min-height: 400px; +} + +/* Form Styles */ +.form-section { + background: #2a2a2a; + padding: 30px; + border-radius: 15px; + margin-bottom: 30px; + border: 2px solid #ff7700; + box-shadow: 0 4px 15px rgba(255, 119, 0, 0.2); +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 600; + color: #ff7700; + font-size: 1.05rem; +} + +.form-group input, +.form-group select { + width: 100%; + padding: 12px; + border: 2px solid #8a2be2; + border-radius: 8px; + font-size: 1rem; + transition: border 0.3s; + background: #1a1a1a; + color: #fff; +} + +.form-group input:focus, +.form-group select:focus { + outline: none; + border-color: #ff7700; + box-shadow: 0 0 10px rgba(255, 119, 0, 0.5); +} + +.form-group select:disabled { + background: #333; + cursor: not-allowed; + opacity: 0.6; +} + +.btn-primary, .btn-secondary { + color: white; + border: none; + padding: 14px 30px; + border-radius: 25px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + width: 100%; + margin-top: 10px; +} + +.btn-primary { + background: linear-gradient(135deg, #ff7700 0%, #8a2be2 100%); + box-shadow: 0 4px 15px rgba(255, 119, 0, 0.6); + border: 2px solid #ff7700; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(255, 119, 0, 0.8); +} + +.btn-secondary { + background: linear-gradient(135deg, #8a2be2 0%, #ff7700 100%); + box-shadow: 0 4px 15px rgba(138, 43, 226, 0.6); + border: 2px solid #8a2be2; +} + +.btn-secondary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(138, 43, 226, 0.8); +} + +.message { + margin-top: 15px; + padding: 12px; + border-radius: 8px; + display: none; +} + +.message.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + +.help-text { + color: #999; + font-size: 0.9rem; + font-style: italic; + margin-bottom: 10px; +} + +/* Import Section */ +.import-section { + background: #fff3cd; + padding: 20px; + border-radius: 15px; + border: 2px dashed #ffc107; +} + +.import-section h3 { + color: #856404; + font-size: 1.2rem; + margin-bottom: 10px; +} + +.import-section input[type="file"] { + display: block; + margin: 15px 0; + padding: 10px; + border: 2px solid #ffc107; + border-radius: 8px; + background: white; + width: 100%; +} + +/* Personal Stats Box */ +.stats-box { + background: #2a2a2a; + padding: 20px; + border-radius: 15px; + margin-bottom: 20px; + border: 2px solid #8a2be2; + box-shadow: 0 4px 15px rgba(138, 43, 226, 0.2); +} + +.stats-box .stat-item { + display: flex; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid #444; +} + +.stats-box .stat-item:last-child { + border-bottom: none; +} + +.stats-box .stat-label { + font-weight: 600; + color: #8a2be2; +} + +.stats-box .stat-value { + font-weight: 700; + color: #ff7700; + font-size: 1.2rem; +} + +/* History List */ +#personal-history { + max-height: 500px; + overflow-y: auto; +} + +.history-item { + background: #2a2a2a; + padding: 15px; + margin-bottom: 10px; + border-radius: 8px; + display: flex; + justify-content: space-between; + align-items: center; + border-left: 4px solid #ff7700; + box-shadow: 0 2px 8px rgba(255, 119, 0, 0.2); +} + +.history-item .date { + font-weight: 600; + color: #8a2be2; +} + +.history-item .steps { + font-size: 1.2rem; + font-weight: 700; + color: #ff7700; +} + +/* Leaderboard */ +.leaderboard-section { + margin-bottom: 40px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.leaderboard { + width: 100%; + border-collapse: collapse; + background: #2a2a2a; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 4px 15px rgba(255, 119, 0, 0.3); + border: 2px solid #8a2be2; +} + +.leaderboard th { + background: linear-gradient(135deg, #ff7700 0%, #8a2be2 100%); + color: white; + padding: 15px; + text-align: left; + font-weight: 600; +} + +.leaderboard td { + padding: 15px; + border-bottom: 1px solid #444; + color: #fff; +} + +.leaderboard tbody tr:hover { + background: #333; +} + +.leaderboard tbody tr:nth-child(1) { + background: linear-gradient(90deg, #ffd70020 0%, transparent 100%); +} + +.leaderboard tbody tr:nth-child(1) td:first-child { + color: #ffd700; + font-weight: bold; + font-size: 1.3rem; +} + +.leaderboard tbody tr:nth-child(2) { + background: linear-gradient(90deg, #c0c0c020 0%, transparent 100%); +} + +.leaderboard tbody tr:nth-child(2) td:first-child { + color: #c0c0c0; + font-weight: bold; + font-size: 1.2rem; +} + +.leaderboard tbody tr:nth-child(3) { + background: linear-gradient(90deg, #cd7f3220 0%, transparent 100%); +} + +.leaderboard tbody tr:nth-child(3) td:first-child { + color: #cd7f32; + font-weight: bold; + font-size: 1.1rem; +} + +.number-large { + font-size: 1.2rem; + font-weight: bold; + color: #ff7700; +} + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 20px; + margin-bottom: 40px; +} + +.stat-card { + background: linear-gradient(135deg, #ff7700 0%, #8a2be2 100%); + color: white; + padding: 30px; + border-radius: 15px; + text-align: center; + box-shadow: 0 4px 15px rgba(255, 119, 0, 0.6); + border: 2px solid #ff7700; +} + +.stat-card h3 { + color: white; + font-size: 1rem; + margin-bottom: 15px; + opacity: 0.9; +} + +.stat-number { + font-size: 2.5rem; + font-weight: 700; +} + +/* Charts */ +.chart-container { + background: #2a2a2a; + padding: 30px; + border-radius: 15px; + margin-bottom: 30px; + border: 2px solid #8a2be2; + box-shadow: 0 4px 15px rgba(138, 43, 226, 0.3); +} + +.chart-container canvas { + max-height: 400px; + width: 100%; + height: auto; +} + +/* Monster Cards */ +.monster-today { + margin-bottom: 40px; +} + +.monster-card-today { + background: linear-gradient(135deg, #2a2a2a 0%, #3a1a4a 100%); + padding: 40px; + border-radius: 20px; + border: 3px solid #ff7700; + box-shadow: 0 8px 30px rgba(255, 119, 0, 0.4); + text-align: center; +} + +.monster-icon-large { + font-size: 8rem; + margin-bottom: 20px; + filter: drop-shadow(0 0 20px rgba(255, 119, 0, 0.6)); +} + +.monster-name { + color: #ff7700; + font-size: 2.5rem; + margin-bottom: 15px; + text-shadow: 2px 2px 8px rgba(0, 0, 0, 0.7); +} + +.monster-description { + color: #bbb; + font-size: 1.2rem; + margin-bottom: 30px; + font-style: italic; +} + +.monster-goal { + background: rgba(138, 43, 226, 0.3); + padding: 20px; + border-radius: 15px; + margin-bottom: 20px; + border: 2px solid #8a2be2; +} + +.goal-label { + color: #8a2be2; + font-weight: 600; + font-size: 1.2rem; + margin-right: 10px; +} + +.goal-value { + color: #ff7700; + font-weight: 700; + font-size: 1.8rem; +} + +.caught-count { + color: #ff7700; + font-size: 1.3rem; + font-weight: 600; + margin-bottom: 30px; +} + +.team-progress-list { + text-align: left; + margin-top: 30px; +} + +.team-progress-list h4 { + color: #8a2be2; + margin-bottom: 15px; + font-size: 1.3rem; +} + +.team-progress-item { + background: rgba(42, 42, 42, 0.8); + padding: 15px; + border-radius: 10px; + margin-bottom: 15px; + border: 2px solid #444; + transition: all 0.3s; +} + +.team-progress-item.caught { + border-color: #ff7700; + background: rgba(255, 119, 0, 0.1); +} + +.team-progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.team-name { + color: #fff; + font-weight: 600; + font-size: 1.1rem; +} + +.team-steps { + color: #ff7700; + font-weight: 600; +} + +.progress-bar { + background: #1a1a1a; + height: 20px; + border-radius: 10px; + overflow: hidden; + border: 1px solid #444; +} + +.progress-fill { + background: linear-gradient(90deg, #ff7700 0%, #8a2be2 100%); + height: 100%; + transition: width 0.5s ease; + box-shadow: 0 0 10px rgba(255, 119, 0, 0.6); +} + +/* Monster Timeline */ +.monster-timeline { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 20px; +} + +.monster-card { + background: #2a2a2a; + border-radius: 15px; + padding: 20px; + border: 2px solid #444; + transition: all 0.3s; +} + +.monster-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5); +} + +.monster-card.today { + border-color: #ff7700; + box-shadow: 0 0 20px rgba(255, 119, 0, 0.4); +} + +.monster-card.past { + opacity: 0.8; +} + +.monster-card.future { + border-style: dashed; + opacity: 0.7; +} + +.monster-card-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 15px; +} + +.monster-icon { + font-size: 3rem; + flex-shrink: 0; +} + +.monster-info { + flex: 1; +} + +.monster-card-name { + color: #ff7700; + font-size: 1.3rem; + margin-bottom: 5px; +} + +.monster-date { + color: #999; + font-size: 0.9rem; +} + +.monster-status { + padding: 5px 15px; + border-radius: 20px; + font-size: 0.9rem; + font-weight: 600; +} + +.monster-status.today { + background: #ff7700; + color: white; +} + +.monster-status.past { + background: #444; + color: #999; +} + +.monster-status.future { + background: #8a2be2; + color: white; +} + +.monster-card-description { + color: #bbb; + font-size: 0.95rem; + margin-bottom: 15px; + font-style: italic; +} + +.monster-card-goal { + color: #ff7700; + font-weight: 600; + margin-bottom: 15px; +} + +.monster-catches { + background: rgba(255, 119, 0, 0.1); + padding: 15px; + border-radius: 10px; + border: 1px solid #ff7700; + margin-top: 15px; +} + +.monster-catches strong { + color: #ff7700; + display: block; + margin-bottom: 10px; +} + +.monster-catches ul { + list-style: none; + padding: 0; + margin: 0; +} + +.monster-catches li { + color: #fff; + padding: 5px 0; + border-bottom: 1px solid #444; +} + +.monster-catches li:last-child { + border-bottom: none; +} + +/* Caught Monsters List (Team Stats) */ +.caught-monsters-list { + display: flex; + flex-wrap: wrap; + gap: 10px; + padding: 15px 0; + border-bottom: 1px solid #444; +} + +.monster-badge { + font-size: 2rem; + cursor: pointer; + transition: transform 0.2s; + filter: drop-shadow(0 2px 4px rgba(255, 119, 0, 0.6)); +} + +.monster-badge:hover { + transform: scale(1.2); +} + +/* Responsive */ +@media (max-width: 1024px) { + .two-column-layout { + grid-template-columns: 1fr; + } + + .monster-timeline { + grid-template-columns: 1fr; + } +} + +@media (max-width: 768px) { + .header-logo { + max-width: 300px; + } + + header h1 { + font-size: 1.8rem; + } + + .tab-btn { + font-size: 0.9rem; + padding: 10px 20px; + } + + .tab-content { + padding: 20px; + } + + .stats-grid { + grid-template-columns: 1fr; + } + + .leaderboard { + font-size: 0.9rem; + min-width: 600px; + } + + .leaderboard th, + .leaderboard td { + padding: 10px; + white-space: nowrap; + } + + .chart-container { + padding: 15px; + } + + .chart-container canvas { + max-height: 300px; + } + + .chart-container h3 { + font-size: 1.2rem; + } +} diff --git a/sample-data.js b/sample-data.js new file mode 100644 index 0000000..588428e --- /dev/null +++ b/sample-data.js @@ -0,0 +1,81 @@ +// Sample data script to populate the database with test data +// Run with: node sample-data.js + +const { initializeDatabase, runQuery } = require('./src/database'); + +async function populateSampleData() { + console.log('Initializing database...'); + initializeDatabase(); + + // Wait a bit for database to initialize + await new Promise(resolve => setTimeout(resolve, 1000)); + + try { + // Create teams + console.log('Creating teams...'); + const team1 = await runQuery('INSERT INTO teams (name) VALUES (?)', ['Team Velocity']); + const team2 = await runQuery('INSERT INTO teams (name) VALUES (?)', ['Team Momentum']); + const team3 = await runQuery('INSERT INTO teams (name) VALUES (?)', ['Team Endurance']); + + console.log('Teams created!'); + + // Create participants + console.log('Creating participants...'); + const participants = [ + // Team Velocity + { name: 'Alice Johnson', email: 'alice@example.com', team_id: team1.id }, + { name: 'Bob Smith', email: 'bob@example.com', team_id: team1.id }, + { name: 'Carol Williams', email: 'carol@example.com', team_id: team1.id }, + // Team Momentum + { name: 'David Brown', email: 'david@example.com', team_id: team2.id }, + { name: 'Eve Davis', email: 'eve@example.com', team_id: team2.id }, + { name: 'Frank Miller', email: 'frank@example.com', team_id: team2.id }, + // Team Endurance + { name: 'Grace Wilson', email: 'grace@example.com', team_id: team3.id }, + { name: 'Henry Moore', email: 'henry@example.com', team_id: team3.id }, + { name: 'Iris Taylor', email: 'iris@example.com', team_id: team3.id }, + ]; + + const participantIds = []; + for (const p of participants) { + const result = await runQuery( + 'INSERT INTO participants (name, email, team_id) VALUES (?, ?, ?)', + [p.name, p.email, p.team_id] + ); + participantIds.push(result.id); + } + + console.log('Participants created!'); + + // Create sample step entries for the past week + console.log('Creating sample step entries...'); + const today = new Date(); + + for (let dayOffset = 0; dayOffset < 7; dayOffset++) { + const date = new Date(today); + date.setDate(date.getDate() - dayOffset); + const dateStr = date.toISOString().split('T')[0]; + + for (const participantId of participantIds) { + // Random steps between 5000 and 15000 + const steps = Math.floor(Math.random() * 10000) + 5000; + + await runQuery( + 'INSERT INTO daily_steps (participant_id, date, steps) VALUES (?, ?, ?)', + [participantId, dateStr, steps] + ); + } + } + + console.log('Sample data created successfully!'); + console.log('\nYou can now start the app with: npm start'); + console.log('Then visit: http://localhost:3060'); + + process.exit(0); + } catch (error) { + console.error('Error creating sample data:', error); + process.exit(1); + } +} + +populateSampleData(); diff --git a/sample_import.csv b/sample_import.csv new file mode 100644 index 0000000..2031e2e --- /dev/null +++ b/sample_import.csv @@ -0,0 +1,49 @@ +team_name,participant_name,email +Team Alpha,Alice Johnson,alice@example.com +Team Alpha,Bob Smith,bob@example.com +Team Alpha,Carol Williams,carol@example.com +Team Alpha,David Brown,david@example.com +Team Alpha,Eve Davis,eve@example.com +Team Alpha,Frank Miller,frank@example.com +Team Beta,Grace Wilson,grace@example.com +Team Beta,Henry Moore,henry@example.com +Team Beta,Iris Taylor,iris@example.com +Team Beta,Jack Anderson,jack@example.com +Team Beta,Karen Thomas,karen@example.com +Team Beta,Leo Jackson,leo@example.com +Team Gamma,Maria White,maria@example.com +Team Gamma,Nathan Harris,nathan@example.com +Team Gamma,Olivia Martin,olivia@example.com +Team Gamma,Paul Thompson,paul@example.com +Team Gamma,Quinn Garcia,quinn@example.com +Team Gamma,Rachel Martinez,rachel@example.com +Team Delta,Sam Robinson,sam@example.com +Team Delta,Tina Clark,tina@example.com +Team Delta,Uma Rodriguez,uma@example.com +Team Delta,Victor Lewis,victor@example.com +Team Delta,Wendy Lee,wendy@example.com +Team Delta,Xavier Walker,xavier@example.com +Team Epsilon,Yara Hall,yara@example.com +Team Epsilon,Zane Allen,zane@example.com +Team Epsilon,Amy Young,amy@example.com +Team Epsilon,Brian King,brian@example.com +Team Epsilon,Cathy Wright,cathy@example.com +Team Epsilon,Derek Lopez,derek@example.com +Team Zeta,Emma Hill,emma@example.com +Team Zeta,Felix Scott,felix@example.com +Team Zeta,Gina Green,gina@example.com +Team Zeta,Hugo Adams,hugo@example.com +Team Zeta,Ivy Baker,ivy@example.com +Team Zeta,James Nelson,james@example.com +Team Eta,Kelly Carter,kelly@example.com +Team Eta,Larry Mitchell,larry@example.com +Team Eta,Monica Perez,monica@example.com +Team Eta,Noah Roberts,noah@example.com +Team Eta,Opal Turner,opal@example.com +Team Eta,Peter Phillips,peter@example.com +Team Theta,Quincy Campbell,quincy@example.com +Team Theta,Rosa Parker,rosa@example.com +Team Theta,Steve Evans,steve@example.com +Team Theta,Tracy Edwards,tracy@example.com +Team Theta,Ursula Collins,ursula@example.com +Team Theta,Vincent Stewart,vincent@example.com diff --git a/src/database.js b/src/database.js new file mode 100644 index 0000000..9066cd0 --- /dev/null +++ b/src/database.js @@ -0,0 +1,118 @@ +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); + +const dbPath = path.join(__dirname, '..', 'step_competition.db'); +const db = new sqlite3.Database(dbPath); + +// Initialize database schema +function initializeDatabase() { + db.serialize(() => { + // Teams table + db.run(` + CREATE TABLE IF NOT EXISTS teams ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Participants table + db.run(` + CREATE TABLE IF NOT EXISTS participants ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + email TEXT UNIQUE, + team_id INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (team_id) REFERENCES teams(id) + ) + `); + + // Daily steps table + db.run(` + CREATE TABLE IF NOT EXISTS daily_steps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + participant_id INTEGER NOT NULL, + date DATE NOT NULL, + steps INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (participant_id) REFERENCES participants(id), + UNIQUE(participant_id, date) + ) + `); + + // Daily monsters table + db.run(` + CREATE TABLE IF NOT EXISTS daily_monsters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date DATE NOT NULL UNIQUE, + monster_name TEXT NOT NULL, + monster_description TEXT, + step_goal INTEGER NOT NULL, + monster_icon TEXT DEFAULT '👹', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `); + + // Monster catches table + db.run(` + CREATE TABLE IF NOT EXISTS monster_catches ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + team_id INTEGER NOT NULL, + monster_id INTEGER NOT NULL, + caught_at DATETIME DEFAULT CURRENT_TIMESTAMP, + final_steps INTEGER NOT NULL, + FOREIGN KEY (team_id) REFERENCES teams(id), + FOREIGN KEY (monster_id) REFERENCES daily_monsters(id), + UNIQUE(team_id, monster_id) + ) + `); + + // Create indexes for better query performance + db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_date ON daily_steps(date)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_participant ON daily_steps(participant_id)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_participants_team ON participants(team_id)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_daily_monsters_date ON daily_monsters(date)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_team ON monster_catches(team_id)`); + db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_monster ON monster_catches(monster_id)`); + + console.log('Database initialized successfully'); + }); +} + +// Helper function to run queries with promises +function runQuery(sql, params = []) { + return new Promise((resolve, reject) => { + db.run(sql, params, function(err) { + if (err) reject(err); + else resolve({ id: this.lastID, changes: this.changes }); + }); + }); +} + +function getQuery(sql, params = []) { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); +} + +function allQuery(sql, params = []) { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); +} + +module.exports = { + db, + initializeDatabase, + runQuery, + getQuery, + allQuery +}; diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..16244f4 --- /dev/null +++ b/src/server.js @@ -0,0 +1,581 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const cors = require('cors'); +const path = require('path'); +const { initializeDatabase, runQuery, getQuery, allQuery } = require('./database'); + +const app = express(); +const PORT = process.env.PORT || 3060; + +// Middleware +app.use(cors()); +app.use(bodyParser.json()); +app.use(express.static(path.join(__dirname, '..', 'public'))); + +// Initialize database +initializeDatabase(); + +// ===== TEAM ENDPOINTS ===== + +// Get all teams +app.get('/api/teams', async (req, res) => { + try { + const teams = await allQuery('SELECT * FROM teams ORDER BY name'); + res.json(teams); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Create a new team +app.post('/api/teams', async (req, res) => { + try { + const { name } = req.body; + if (!name) { + return res.status(400).json({ error: 'Team name is required' }); + } + const result = await runQuery('INSERT INTO teams (name) VALUES (?)', [name]); + res.json({ id: result.id, name }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ===== PARTICIPANT ENDPOINTS ===== + +// Get all participants +app.get('/api/participants', async (req, res) => { + try { + const participants = await allQuery(` + SELECT p.*, t.name as team_name + FROM participants p + JOIN teams t ON p.team_id = t.id + ORDER BY t.name, p.name + `); + res.json(participants); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get participants by team +app.get('/api/teams/:teamId/participants', async (req, res) => { + try { + const participants = await allQuery( + 'SELECT * FROM participants WHERE team_id = ? ORDER BY name', + [req.params.teamId] + ); + res.json(participants); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Create a new participant +app.post('/api/participants', async (req, res) => { + try { + const { name, email, team_id } = req.body; + if (!name || !team_id) { + return res.status(400).json({ error: 'Name and team_id are required' }); + } + const result = await runQuery( + 'INSERT INTO participants (name, email, team_id) VALUES (?, ?, ?)', + [name, email, team_id] + ); + res.json({ id: result.id, name, email, team_id }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Bulk import teams and participants from CSV data +app.post('/api/import/csv', async (req, res) => { + try { + const { data } = req.body; // Expected format: [{team_name, participant_name, email}] + + if (!data || !Array.isArray(data)) { + return res.status(400).json({ error: 'Invalid data format' }); + } + + const teamMap = new Map(); + let teamsCreated = 0; + let participantsCreated = 0; + let errors = []; + + // Process each row + for (let i = 0; i < data.length; i++) { + const row = data[i]; + + try { + // Get or create team + let teamId = teamMap.get(row.team_name); + + if (!teamId) { + // Check if team already exists + const existingTeam = await getQuery('SELECT id FROM teams WHERE name = ?', [row.team_name]); + + if (existingTeam) { + teamId = existingTeam.id; + } else { + // Create new team + const teamResult = await runQuery('INSERT INTO teams (name) VALUES (?)', [row.team_name]); + teamId = teamResult.id; + teamsCreated++; + } + + teamMap.set(row.team_name, teamId); + } + + // Create participant (skip if already exists) + const existingParticipant = await getQuery( + 'SELECT id FROM participants WHERE name = ? AND team_id = ?', + [row.participant_name, teamId] + ); + + if (!existingParticipant) { + await runQuery( + 'INSERT INTO participants (name, email, team_id) VALUES (?, ?, ?)', + [row.participant_name, row.email || null, teamId] + ); + participantsCreated++; + } + } catch (error) { + errors.push(`Row ${i + 1}: ${error.message}`); + } + } + + res.json({ + success: true, + teamsCreated, + participantsCreated, + errors: errors.length > 0 ? errors : null + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get participant's step history +app.get('/api/participants/:participantId/history', async (req, res) => { + try { + const history = await allQuery( + 'SELECT date, steps FROM daily_steps WHERE participant_id = ? ORDER BY date DESC', + [req.params.participantId] + ); + res.json(history); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ===== STEP TRACKING ENDPOINTS ===== + +// Get steps for a specific participant and date +app.get('/api/steps/:participantId/:date', async (req, res) => { + try { + const step = await getQuery( + 'SELECT * FROM daily_steps WHERE participant_id = ? AND date = ?', + [req.params.participantId, req.params.date] + ); + res.json(step || { steps: 0 }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Submit or update steps for a specific date +app.post('/api/steps', async (req, res) => { + try { + const { participant_id, date, steps } = req.body; + if (!participant_id || !date || steps === undefined) { + return res.status(400).json({ error: 'participant_id, date, and steps are required' }); + } + + // Check if entry exists + const existing = await getQuery( + 'SELECT id FROM daily_steps WHERE participant_id = ? AND date = ?', + [participant_id, date] + ); + + if (existing) { + // Update existing entry + await runQuery( + 'UPDATE daily_steps SET steps = ?, updated_at = CURRENT_TIMESTAMP WHERE participant_id = ? AND date = ?', + [steps, participant_id, date] + ); + res.json({ message: 'Steps updated', participant_id, date, steps }); + } else { + // Create new entry + const result = await runQuery( + 'INSERT INTO daily_steps (participant_id, date, steps) VALUES (?, ?, ?)', + [participant_id, date, steps] + ); + res.json({ id: result.id, participant_id, date, steps }); + } + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ===== LEADERBOARD ENDPOINTS ===== + +// Get individual leaderboard (total steps per person) +app.get('/api/leaderboard/individual', async (req, res) => { + try { + const startDate = req.query.start_date; + const endDate = req.query.end_date; + + let query = ` + SELECT + p.id, + p.name, + t.name as team_name, + t.id as team_id, + COALESCE(SUM(ds.steps), 0) as total_steps, + COUNT(DISTINCT ds.date) as days_logged + FROM participants p + LEFT JOIN daily_steps ds ON p.id = ds.participant_id + JOIN teams t ON p.team_id = t.id + `; + + const params = []; + if (startDate && endDate) { + query += ' WHERE ds.date BETWEEN ? AND ?'; + params.push(startDate, endDate); + } + + query += ' GROUP BY p.id, p.name, t.name, t.id ORDER BY total_steps DESC'; + + const leaderboard = await allQuery(query, params); + res.json(leaderboard); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get team leaderboard (total steps per team) +app.get('/api/leaderboard/team', async (req, res) => { + try { + const startDate = req.query.start_date; + const endDate = req.query.end_date; + + let query = ` + SELECT + t.id, + t.name as team_name, + COALESCE(SUM(ds.steps), 0) as total_steps, + COUNT(DISTINCT CASE WHEN COALESCE(ds.steps, 0) > 0 THEN p.id END) as active_count, + COUNT(DISTINCT p.id) as member_count, + COALESCE(AVG(ds.steps), 0) as avg_steps_per_entry + FROM teams t + LEFT JOIN participants p ON t.id = p.team_id + LEFT JOIN daily_steps ds ON p.id = ds.participant_id + `; + + const params = []; + if (startDate && endDate) { + query += ' WHERE ds.date BETWEEN ? AND ?'; + params.push(startDate, endDate); + } + + query += ' GROUP BY t.id, t.name ORDER BY total_steps DESC'; + + const leaderboard = await allQuery(query, params); + res.json(leaderboard); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get daily progress for all teams +app.get('/api/progress/daily', async (req, res) => { + try { + const startDate = req.query.start_date; + const endDate = req.query.end_date; + + let query = ` + SELECT + ds.date, + t.id as team_id, + t.name as team_name, + SUM(ds.steps) as daily_steps + FROM daily_steps ds + JOIN participants p ON ds.participant_id = p.id + JOIN teams t ON p.team_id = t.id + `; + + const params = []; + if (startDate && endDate) { + query += ' WHERE ds.date BETWEEN ? AND ?'; + params.push(startDate, endDate); + } + + query += ' GROUP BY ds.date, t.id, t.name ORDER BY ds.date, t.name'; + + const progress = await allQuery(query, params); + res.json(progress); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get date range of competition +app.get('/api/competition/dates', async (req, res) => { + try { + const result = await getQuery(` + SELECT + MIN(date) as start_date, + MAX(date) as end_date + FROM daily_steps + `); + res.json(result || { start_date: null, end_date: null }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// ===== DAILY MONSTERS ENDPOINTS ===== + +// Get all monsters (timeline view) +app.get('/api/monsters', async (req, res) => { + try { + const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date'); + res.json(monsters); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// IMPORTANT: Timeline route must come BEFORE the :date route to avoid conflicts +// Get all monsters with catch status +app.get('/api/monsters/timeline', async (req, res) => { + try { + const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date'); + + // For each monster, check if teams caught it and update catches table + const timeline = await Promise.all(monsters.map(async (monster) => { + // Get team totals for this monster's date + const teamTotals = await allQuery(` + SELECT + t.id as team_id, + t.name as team_name, + COALESCE(SUM(ds.steps), 0) as total_steps + FROM teams t + LEFT JOIN participants p ON t.id = p.team_id + LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = ? + GROUP BY t.id, t.name + `, [monster.date]); + + // Check and record catches for teams that met the goal + for (const team of teamTotals) { + if (team.total_steps >= monster.step_goal) { + // Check if catch already recorded + const existingCatch = await getQuery( + 'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?', + [team.team_id, monster.id] + ); + + if (!existingCatch) { + // Record the catch + await runQuery( + 'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)', + [team.team_id, monster.id, team.total_steps] + ); + } else { + // Update the final_steps if it changed + await runQuery( + 'UPDATE monster_catches SET final_steps = ? WHERE team_id = ? AND monster_id = ?', + [team.total_steps, team.team_id, monster.id] + ); + } + } + } + + // Now get the updated catches + const catches = await allQuery(` + SELECT + mc.*, + t.name as team_name + FROM monster_catches mc + JOIN teams t ON mc.team_id = t.id + WHERE mc.monster_id = ? + ORDER BY mc.caught_at + `, [monster.id]); + + return { + ...monster, + catches + }; + })); + + res.json(timeline); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get specific day's monster +app.get('/api/monsters/:date', async (req, res) => { + try { + const monster = await getQuery( + 'SELECT * FROM daily_monsters WHERE date = ?', + [req.params.date] + ); + res.json(monster || null); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Add/import monsters (bulk CSV import) +app.post('/api/monsters/import', async (req, res) => { + try { + const { data } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}] + + if (!data || !Array.isArray(data)) { + return res.status(400).json({ error: 'Invalid data format' }); + } + + let monstersCreated = 0; + let monstersUpdated = 0; + let errors = []; + + for (let i = 0; i < data.length; i++) { + const row = data[i]; + + try { + if (!row.date || !row.monster_name || !row.step_goal) { + errors.push(`Row ${i + 1}: Missing required fields (date, monster_name, step_goal)`); + continue; + } + + // Check if monster already exists for this date + const existing = await getQuery( + 'SELECT id FROM daily_monsters WHERE date = ?', + [row.date] + ); + + if (existing) { + // Update existing monster + await runQuery( + 'UPDATE daily_monsters SET monster_name = ?, monster_description = ?, step_goal = ?, monster_icon = ? WHERE date = ?', + [row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹', row.date] + ); + monstersUpdated++; + } else { + // Create new monster + await runQuery( + 'INSERT INTO daily_monsters (date, monster_name, monster_description, step_goal, monster_icon) VALUES (?, ?, ?, ?, ?)', + [row.date, row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹'] + ); + monstersCreated++; + } + } catch (error) { + errors.push(`Row ${i + 1}: ${error.message}`); + } + } + + res.json({ + success: true, + monstersCreated, + monstersUpdated, + errors: errors.length > 0 ? errors : null + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get which teams caught a specific monster +app.get('/api/monsters/:date/catches', async (req, res) => { + try { + const monster = await getQuery( + 'SELECT id FROM daily_monsters WHERE date = ?', + [req.params.date] + ); + + if (!monster) { + return res.json([]); + } + + const catches = await allQuery(` + SELECT + mc.*, + t.name as team_name + FROM monster_catches mc + JOIN teams t ON mc.team_id = t.id + WHERE mc.monster_id = ? + ORDER BY mc.caught_at + `, [monster.id]); + + res.json(catches); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Get current day's monster + team progress +// Note: Since people enter steps the following day, "today's challenge" is actually yesterday's date +app.get('/api/monsters/status/today', async (req, res) => { + try { + // Get yesterday's date (the challenge people are working on today) + const now = new Date(); + now.setDate(now.getDate() - 1); + const yesterday = now.toISOString().split('T')[0]; + + // Get yesterday's monster (today's challenge) + const monster = await getQuery( + 'SELECT * FROM daily_monsters WHERE date = ?', + [yesterday] + ); + + if (!monster) { + return res.json({ monster: null, progress: [] }); + } + + // Get team progress for yesterday's date + const progress = await allQuery(` + SELECT + t.id as team_id, + t.name as team_name, + COALESCE(SUM(ds.steps), 0) as total_steps, + CASE WHEN COALESCE(SUM(ds.steps), 0) >= ? THEN 1 ELSE 0 END as caught + FROM teams t + LEFT JOIN participants p ON t.id = p.team_id + LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = ? + GROUP BY t.id, t.name + ORDER BY total_steps DESC + `, [monster.step_goal, yesterday]); + + // Check and record new catches + for (const team of progress) { + if (team.caught) { + // Check if catch already recorded + const existingCatch = await getQuery( + 'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?', + [team.team_id, monster.id] + ); + + if (!existingCatch) { + // Record the catch + await runQuery( + 'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)', + [team.team_id, monster.id, team.total_steps] + ); + } + } + } + + res.json({ monster, progress }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// Start server on all interfaces (0.0.0.0) to allow Tailscale access +app.listen(PORT, '0.0.0.0', () => { + console.log(`Step Competition app running on:`); + console.log(` - Local: http://localhost:${PORT}`); + console.log(` - Network: http://0.0.0.0:${PORT}`); + console.log(` - Tailscale: Access via your Tailscale IP on port ${PORT}`); +});