Peacemanguy commited on
Commit
fe02ff1
·
1 Parent(s): 86e2a50

First commit

Browse files
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node.js
2
+ node_modules/
3
+ npm-debug.log
4
+ yarn-debug.log
5
+ yarn-error.log
6
+
7
+ # Environment variables
8
+ .env
9
+
10
+ # Data files
11
+ data/*.json
12
+
13
+ # Logs
14
+ logs
15
+ *.log
16
+
17
+ # Docker
18
+ .dockerignore
19
+
20
+ # OS specific
21
+ .DS_Store
22
+ Thumbs.db
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+
8
+ # Install dependencies
9
+ RUN npm ci --only=production
10
+
11
+ # Copy entrypoint script
12
+ COPY docker-entrypoint.sh /usr/local/bin/
13
+ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
14
+
15
+ # Copy application code
16
+ COPY . .
17
+
18
+ # Create data directory
19
+ RUN mkdir -p /app/data
20
+
21
+ # Expose ports
22
+ EXPOSE 3000 6969
23
+
24
+ # Set entrypoint
25
+ ENTRYPOINT ["docker-entrypoint.sh"]
26
+
27
+ # Command to run the application
28
+ CMD ["node", "start.js"]
README.md CHANGED
@@ -1,11 +1,159 @@
1
- ---
2
- title: LLMChoice
3
- emoji: 🚀
4
- colorFrom: green
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- short_description: What LLMs are you using this week?
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # GPU Leaderboard Application - Docker Setup
2
+
3
+ This repository contains a Node.js application for GPU leaderboards with Docker configuration for easy deployment.
4
+
5
+ ## Features
6
+
7
+ - Main server for user voting and interaction
8
+ - Admin server for managing entries
9
+ - Persistent data storage
10
+ - One vote per IP address per category
11
+ - Custom start script to run both servers in a single container
12
+ - **Weekly leaderboard archiving system**
13
+ - Automatic archiving at the end of each week
14
+ - Historical data stored in a catalogued format
15
+ - View archived results by week or date range
16
+
17
+ ## Docker Setup
18
+
19
+ ### Prerequisites
20
+
21
+ - Docker
22
+ - Docker Compose
23
+
24
+ ### Running the Application
25
+
26
+ 1. Clone this repository
27
+ 2. Navigate to the project directory
28
+ 3. Build and start the containers:
29
+
30
+ #### Option 1: Using the provided script
31
+
32
+ - Windows: Double-click the `docker-run.bat` file
33
+ - Linux/Mac: Run `./docker-run.sh` (make it executable first with `chmod +x docker-run.sh`)
34
+
35
+ #### Option 2: Using Docker Compose directly
36
+
37
+ ```bash
38
+ docker-compose up -d
39
+ ```
40
+
41
+ 4. Access the application:
42
+ - Main application: http://localhost:3000
43
+ - Admin interface: http://localhost:6969/admin
44
+ - Default admin credentials:
45
+ - Username: admin
46
+ - Password: secure_password123 (you should change this in admin_server.js)
47
+
48
+ ### Stopping the Application
49
+
50
+ ```bash
51
+ docker-compose down
52
+ ```
53
+
54
+ ## Data Persistence
55
+
56
+ All data is stored in the `./data` directory, which is mounted as a volume in the Docker container. This ensures that your data persists even if the container is removed.
57
+
58
+ ## Ports
59
+
60
+ - 3000: Main application server
61
+ - 6969: Admin server
62
+
63
+ ## Environment Variables
64
+
65
+ You can customize the application by modifying the `.env` file:
66
+
67
+ ```
68
+ # Main server configuration
69
+ PORT=3000
70
+ NODE_ENV=production
71
+
72
+ # Admin server configuration
73
+ ADMIN_PORT=6969
74
+ ```
75
+
76
+ These variables are used in the `docker-compose.yml` file and passed to the application.
77
+
78
+ ## Weekly Archiving System
79
+
80
+ The system automatically archives the current leaderboard data at the end of each week (Sunday at 23:59). Each archive includes:
81
+
82
+ - Week identifier (e.g., 2025-W17)
83
+ - Start and end dates of the week
84
+ - Timestamp when the archive was created
85
+ - Complete snapshot of the leaderboard data
86
+
87
+ ### Accessing Archived Data
88
+
89
+ #### User Interface
90
+
91
+ Users can access archived leaderboards through the main interface by clicking the "Archives" button. From there, they can:
92
+
93
+ 1. Select a specific week from the dropdown menu
94
+ 2. Search archives by date range
95
+ 3. View detailed results for each archived period
96
+
97
+ #### Admin Interface
98
+
99
+ Administrators have additional capabilities:
100
+
101
+ 1. View all archived weeks
102
+ 2. Search archives by date range
103
+ 3. Manually trigger archiving of the current leaderboard
104
+ 4. View detailed statistics for each archived period
105
+
106
+ ### Running the Archiving System
107
+
108
+ ```bash
109
+ # Start the weekly archiving scheduler
110
+ npm run start-scheduler
111
+
112
+ # Manually archive the current week
113
+ npm run archive-week
114
+ ```
115
+
116
+ ### API Endpoints for Archives
117
+
118
+ - `GET /api/archives/weeks` - Get list of all archived weeks
119
+ - `GET /api/archives/week/:weekId` - Get archived data for a specific week
120
+ - `GET /api/archives/week/:weekId/category/:category` - Get archived data for a specific week and category
121
+ - `GET /api/archives/range?startDate=<date>&endDate=<date>` - Get archived data for a date range
122
+
123
+ ## Security Notes
124
+
125
+ - For production use, consider changing the admin credentials in `admin_server.js`
126
+ - Consider adding HTTPS for secure connections
127
+ - Review the session configuration in `admin_server.js` for production use
128
+
129
+ ## Architecture
130
+
131
+ ### Docker Setup
132
+
133
+ The application is containerized using Docker with the following components:
134
+
135
+ - **Dockerfile**: Builds a Node.js Alpine container with the application code
136
+ - **docker-compose.yml**: Orchestrates the container and sets up networking and volumes
137
+ - **docker-entrypoint.sh**: Initializes the data directory and files before starting the application
138
+ - **start.js**: Custom Node.js script that runs both the main server and admin server in a single container
139
+
140
+ ### Custom Start Script
141
+
142
+ Instead of using npm scripts with concurrently, we use a custom Node.js script (`start.js`) to run both servers. This approach:
143
+
144
+ - Provides better process management in the Docker container
145
+ - Ensures proper handling of signals for graceful shutdown
146
+ - Simplifies logging by inheriting stdio from the parent process
147
+ - Avoids potential issues with npm scripts in containerized environments
148
+
149
+ ### Data Persistence
150
+
151
+ All data is stored in JSON files in the `./data` directory, which is mounted as a volume in the Docker container. This ensures that:
152
+
153
+ - Data persists across container restarts and rebuilds
154
+ - Files can be backed up easily from the host machine
155
+ - Multiple containers can share the same data if needed
156
+
157
+ ## License
158
+
159
+ ISC
admin_server.js ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const bodyParser = require('body-parser');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const session = require('express-session');
7
+ const moment = require('moment');
8
+ const archiver = require('./leaderboard_archiver');
9
+
10
+ // Constants
11
+ const app = express();
12
+ const PORT = process.env.ADMIN_PORT || 6969;
13
+ const DATA_FILE = path.join(__dirname, 'data', 'data.json');
14
+ const IP_FILE = path.join(__dirname, 'data', 'ips.json');
15
+ const CATEGORIES = ["6gb", "12gb", "16gb", "24gb", "48gb", "72gb", "96gb"];
16
+
17
+ // Admin credentials - in a real app, store these securely
18
+ const ADMIN_USER = 'admin';
19
+ const ADMIN_PASS = 'secure_password123'; // Change this to a strong password
20
+
21
+ // Middleware
22
+ app.use(bodyParser.urlencoded({ extended: true }));
23
+ app.use(bodyParser.json());
24
+ app.use(session({
25
+ secret: crypto.randomBytes(32).toString('hex'),
26
+ resave: false,
27
+ saveUninitialized: false,
28
+ cookie: {
29
+ secure: false, // Set to true if using HTTPS
30
+ httpOnly: true,
31
+ maxAge: 3600000 // 1 hour
32
+ }
33
+ }));
34
+
35
+ // --- Authentication ---
36
+ const authenticate = (req, res, next) => {
37
+ if (req.session && req.session.authenticated) {
38
+ return next();
39
+ }
40
+ res.redirect('/admin');
41
+ };
42
+
43
+ // --- Data Handling ---
44
+ function readJson(file, fallback) {
45
+ try {
46
+ if (fs.existsSync(file)) {
47
+ return JSON.parse(fs.readFileSync(file));
48
+ }
49
+ return fallback;
50
+ }
51
+ catch { return fallback; }
52
+ }
53
+
54
+ function writeJson(file, obj) {
55
+ fs.writeFileSync(file, JSON.stringify(obj, null, 2));
56
+ }
57
+
58
+ // --- Routes ---
59
+
60
+ // Simple root message
61
+ app.get('/', (req, res) => {
62
+ res.send('Admin Server is running. Access /admin for the interface.');
63
+ });
64
+
65
+ // Admin Login Page
66
+ app.get('/admin', (req, res) => {
67
+ if (req.session && req.session.authenticated) {
68
+ return res.redirect('/admin/dashboard');
69
+ }
70
+
71
+ res.send(`
72
+ <!DOCTYPE html>
73
+ <html>
74
+ <head>
75
+ <title>Poll Admin - Login</title>
76
+ <style>
77
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
78
+ .container { max-width: 500px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
79
+ h1 { color: #333; }
80
+ label { display: block; margin-bottom: 5px; }
81
+ input[type="text"], input[type="password"] { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 3px; }
82
+ button { background: #4CAF50; color: white; border: none; padding: 10px 15px; border-radius: 3px; cursor: pointer; }
83
+ button:hover { background: #45a049; }
84
+ .error { color: red; margin-bottom: 15px; }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div class="container">
89
+ <h1>Poll Admin Login</h1>
90
+ ${req.query.error ? '<p class="error">Invalid username or password</p>' : ''}
91
+ <form action="/admin/login" method="POST">
92
+ <label for="username">Username:</label>
93
+ <input type="text" id="username" name="username" required>
94
+
95
+ <label for="password">Password:</label>
96
+ <input type="password" id="password" name="password" required>
97
+
98
+ <button type="submit">Login</button>
99
+ </form>
100
+ </div>
101
+ </body>
102
+ </html>
103
+ `);
104
+ });
105
+
106
+ // Admin Login Handler
107
+ app.post('/admin/login', (req, res) => {
108
+ const { username, password } = req.body;
109
+
110
+ if (username === ADMIN_USER && password === ADMIN_PASS) {
111
+ req.session.authenticated = true;
112
+ req.session.username = username;
113
+ res.redirect('/admin/dashboard');
114
+ } else {
115
+ res.redirect('/admin?error=1');
116
+ }
117
+ });
118
+
119
+ // Admin Logout
120
+ app.get('/admin/logout', (req, res) => {
121
+ req.session.destroy();
122
+ res.redirect('/admin');
123
+ });
124
+
125
+ // Admin Dashboard (Protected)
126
+ app.get('/admin/dashboard', authenticate, (req, res) => {
127
+ const data = readJson(DATA_FILE, {});
128
+
129
+ let categoriesHtml = '';
130
+
131
+ CATEGORIES.forEach(category => {
132
+ const entries = data[category] || [];
133
+ const sortedEntries = [...entries].sort((a, b) => b.votes - a.votes);
134
+
135
+ let tableRows = sortedEntries.map((entry) => `
136
+ <tr>
137
+ <td>${escapeHtml(entry.id)}</td>
138
+ <td>${escapeHtml(entry.name)}</td>
139
+ <td>${entry.votes}</td>
140
+ <td>
141
+ <a href="/admin/edit/${category}/${entry.id}" class="btn btn-edit">Edit</a>
142
+ <form action="/admin/delete/${category}/${entry.id}" method="POST" style="display:inline;">
143
+ <button type="submit" class="btn btn-delete" onclick="return confirm('Are you sure you want to delete this entry?')">Delete</button>
144
+ </form>
145
+ </td>
146
+ </tr>
147
+ `).join('');
148
+
149
+ categoriesHtml += `
150
+ <div class="category-section">
151
+ <h2>${category} Category</h2>
152
+ ${sortedEntries.length > 0 ? `
153
+ <table>
154
+ <thead>
155
+ <tr>
156
+ <th>ID</th>
157
+ <th>Name</th>
158
+ <th>Votes</th>
159
+ <th>Actions</th>
160
+ </tr>
161
+ </thead>
162
+ <tbody>
163
+ ${tableRows}
164
+ </tbody>
165
+ </table>
166
+ ` : '<p>No entries in this category.</p>'}
167
+ </div>
168
+ `;
169
+ });
170
+
171
+ res.send(`
172
+ <!DOCTYPE html>
173
+ <html>
174
+ <head>
175
+ <title>Poll Admin Dashboard</title>
176
+ <style>
177
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
178
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
179
+ h1 { color: #333; margin: 0; }
180
+ table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
181
+ table, th, td { border: 1px solid #ddd; }
182
+ th { background-color: #f2f2f2; padding: 10px; text-align: left; }
183
+ td { padding: 10px; }
184
+ .category-section { margin-bottom: 30px; }
185
+ .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }
186
+ .btn-edit { background-color: #2196F3; }
187
+ .btn-delete { background-color: #f44336; }
188
+ .logout { text-decoration: none; color: #f44336; }
189
+ .nav-links { display: flex; gap: 15px; align-items: center; }
190
+ .nav-link { text-decoration: none; color: #2196F3; }
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <div class="header">
195
+ <h1>Poll Admin Dashboard</h1>
196
+ <div class="nav-links">
197
+ <a href="/admin/archives" class="nav-link">View Archives</a>
198
+ <a href="/admin/logout" class="logout">Logout</a>
199
+ </div>
200
+ </div>
201
+
202
+ ${categoriesHtml}
203
+ </body>
204
+ </html>
205
+ `);
206
+ });
207
+
208
+ // Edit Entry Form
209
+ app.get('/admin/edit/:category/:id', authenticate, (req, res) => {
210
+ const { category, id } = req.params;
211
+ const data = readJson(DATA_FILE, {});
212
+
213
+ if (!CATEGORIES.includes(category)) {
214
+ return res.status(400).send('Invalid category');
215
+ }
216
+
217
+ const entries = data[category] || [];
218
+ const entry = entries.find(e => e.id === id);
219
+
220
+ if (!entry) {
221
+ return res.status(404).send('Entry not found');
222
+ }
223
+
224
+ res.send(`
225
+ <!DOCTYPE html>
226
+ <html>
227
+ <head>
228
+ <title>Edit Entry</title>
229
+ <style>
230
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
231
+ .container { max-width: 600px; margin: 0 auto; }
232
+ h1 { color: #333; }
233
+ label { display: block; margin-bottom: 5px; }
234
+ input[type="text"], input[type="number"] { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 3px; }
235
+ .buttons { margin-top: 20px; }
236
+ button { padding: 8px 15px; margin-right: 10px; border: none; border-radius: 3px; cursor: pointer; }
237
+ .save { background-color: #4CAF50; color: white; }
238
+ .cancel { background-color: #f44336; color: white; }
239
+ </style>
240
+ </head>
241
+ <body>
242
+ <div class="container">
243
+ <h1>Edit Entry</h1>
244
+ <form action="/admin/edit/${category}/${id}" method="POST">
245
+ <label for="name">Name:</label>
246
+ <input type="text" id="name" name="name" value="${escapeHtml(entry.name)}" required>
247
+
248
+ <label for="votes">Votes:</label>
249
+ <input type="number" id="votes" name="votes" value="${entry.votes}" min="0" required>
250
+
251
+ <div class="buttons">
252
+ <button type="submit" class="save">Save Changes</button>
253
+ <a href="/admin/dashboard"><button type="button" class="cancel">Cancel</button></a>
254
+ </div>
255
+ </form>
256
+ </div>
257
+ </body>
258
+ </html>
259
+ `);
260
+ });
261
+
262
+ // Update Entry
263
+ app.post('/admin/edit/:category/:id', authenticate, (req, res) => {
264
+ const { category, id } = req.params;
265
+ const { name, votes } = req.body;
266
+ const data = readJson(DATA_FILE, {});
267
+
268
+ if (!CATEGORIES.includes(category)) {
269
+ return res.status(400).send('Invalid category');
270
+ }
271
+
272
+ const entries = data[category] || [];
273
+ const entryIndex = entries.findIndex(e => e.id === id);
274
+
275
+ if (entryIndex === -1) {
276
+ return res.status(404).send('Entry not found');
277
+ }
278
+
279
+ // Update entry
280
+ entries[entryIndex].name = name.trim();
281
+ entries[entryIndex].votes = parseInt(votes, 10);
282
+
283
+ // Save data
284
+ writeJson(DATA_FILE, data);
285
+ console.log(`Updated entry: ${category}/${id}`);
286
+
287
+ res.redirect('/admin/dashboard');
288
+ });
289
+
290
+ // Delete Entry
291
+ app.post('/admin/delete/:category/:id', authenticate, (req, res) => {
292
+ const { category, id } = req.params;
293
+ const data = readJson(DATA_FILE, {});
294
+
295
+ if (!CATEGORIES.includes(category)) {
296
+ return res.status(400).send('Invalid category');
297
+ }
298
+
299
+ const entries = data[category] || [];
300
+ const entryIndex = entries.findIndex(e => e.id === id);
301
+
302
+ if (entryIndex === -1) {
303
+ return res.status(404).send('Entry not found');
304
+ }
305
+
306
+ // Remove entry
307
+ entries.splice(entryIndex, 1);
308
+
309
+ // Save data
310
+ writeJson(DATA_FILE, data);
311
+ console.log(`Deleted entry: ${category}/${id}`);
312
+
313
+ // Also clean up any IP votes for this entry
314
+ const ips = readJson(IP_FILE, {});
315
+ let ipChanged = false;
316
+
317
+ // Check each IP entry
318
+ Object.keys(ips).forEach(ipKey => {
319
+ if (typeof ips[ipKey] === 'object' && ips[ipKey][category] === id) {
320
+ delete ips[ipKey][category];
321
+ ipChanged = true;
322
+ }
323
+ });
324
+
325
+ if (ipChanged) {
326
+ writeJson(IP_FILE, ips);
327
+ console.log('Updated IP tracking file after entry deletion');
328
+ }
329
+
330
+ res.redirect('/admin/dashboard');
331
+ });
332
+
333
+ // Archives Dashboard
334
+ app.get('/admin/archives', authenticate, (req, res) => {
335
+ const archivedWeeks = archiver.getArchivedWeeks();
336
+
337
+ let archivesHtml = '';
338
+
339
+ if (archivedWeeks.length === 0) {
340
+ archivesHtml = '<p>No archived data available yet.</p>';
341
+ } else {
342
+ let tableRows = archivedWeeks.map(weekId => {
343
+ const archive = archiver.getArchivedWeek(weekId);
344
+ if (!archive) return '';
345
+
346
+ return `
347
+ <tr>
348
+ <td>${escapeHtml(archive.weekId)}</td>
349
+ <td>${escapeHtml(archive.startDate)}</td>
350
+ <td>${escapeHtml(archive.endDate)}</td>
351
+ <td>${new Date(archive.archivedAt).toLocaleString()}</td>
352
+ <td>
353
+ <a href="/admin/archives/week/${archive.weekId}" class="btn btn-edit">View</a>
354
+ </td>
355
+ </tr>
356
+ `;
357
+ }).join('');
358
+
359
+ archivesHtml = `
360
+ <h2>Archived Leaderboards</h2>
361
+ <div class="archive-search">
362
+ <h3>Search Archives by Date Range</h3>
363
+ <form action="/admin/archives/search" method="GET">
364
+ <div class="form-group">
365
+ <label for="startDate">Start Date:</label>
366
+ <input type="date" id="startDate" name="startDate" required>
367
+ </div>
368
+ <div class="form-group">
369
+ <label for="endDate">End Date:</label>
370
+ <input type="date" id="endDate" name="endDate" required>
371
+ </div>
372
+ <button type="submit" class="btn btn-edit">Search</button>
373
+ </form>
374
+ </div>
375
+
376
+ <h3>All Archived Weeks</h3>
377
+ <table>
378
+ <thead>
379
+ <tr>
380
+ <th>Week ID</th>
381
+ <th>Start Date</th>
382
+ <th>End Date</th>
383
+ <th>Archived At</th>
384
+ <th>Actions</th>
385
+ </tr>
386
+ </thead>
387
+ <tbody>
388
+ ${tableRows}
389
+ </tbody>
390
+ </table>
391
+
392
+ <div class="archive-actions">
393
+ <form action="/admin/archives/create" method="POST" onsubmit="return confirm('Are you sure you want to archive the current leaderboard data?')">
394
+ <button type="submit" class="btn btn-edit">Archive Current Week</button>
395
+ </form>
396
+ </div>
397
+ `;
398
+ }
399
+
400
+ res.send(`
401
+ <!DOCTYPE html>
402
+ <html>
403
+ <head>
404
+ <title>Archived Leaderboards</title>
405
+ <style>
406
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
407
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
408
+ h1 { color: #333; margin: 0; }
409
+ h2 { color: #333; margin-top: 30px; }
410
+ h3 { color: #555; margin-top: 20px; }
411
+ table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
412
+ table, th, td { border: 1px solid #ddd; }
413
+ th { background-color: #f2f2f2; padding: 10px; text-align: left; }
414
+ td { padding: 10px; }
415
+ .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }
416
+ .btn-edit { background-color: #2196F3; }
417
+ .btn-delete { background-color: #f44336; }
418
+ .logout { text-decoration: none; color: #f44336; }
419
+ .nav-links { display: flex; gap: 15px; align-items: center; }
420
+ .nav-link { text-decoration: none; color: #2196F3; }
421
+ .archive-search { margin: 20px 0; padding: 15px; background-color: #f9f9f9; border-radius: 5px; }
422
+ .form-group { margin-bottom: 15px; }
423
+ .form-group label { display: block; margin-bottom: 5px; }
424
+ .form-group input { padding: 8px; width: 200px; }
425
+ .archive-actions { margin-top: 20px; }
426
+ </style>
427
+ </head>
428
+ <body>
429
+ <div class="header">
430
+ <h1>Archived Leaderboards</h1>
431
+ <div class="nav-links">
432
+ <a href="/admin/dashboard" class="nav-link">Back to Dashboard</a>
433
+ <a href="/admin/logout" class="logout">Logout</a>
434
+ </div>
435
+ </div>
436
+
437
+ ${archivesHtml}
438
+ </body>
439
+ </html>
440
+ `);
441
+ });
442
+
443
+ // View specific archived week
444
+ app.get('/admin/archives/week/:weekId', authenticate, (req, res) => {
445
+ const { weekId } = req.params;
446
+ const archive = archiver.getArchivedWeek(weekId);
447
+
448
+ if (!archive) {
449
+ return res.status(404).send('Archive not found');
450
+ }
451
+
452
+ let categoriesHtml = '';
453
+
454
+ CATEGORIES.forEach(category => {
455
+ const entries = archive.data[category] || [];
456
+ const sortedEntries = [...entries].sort((a, b) => b.votes - a.votes);
457
+
458
+ let tableRows = sortedEntries.map((entry) => `
459
+ <tr>
460
+ <td>${escapeHtml(entry.id)}</td>
461
+ <td>${escapeHtml(entry.name)}</td>
462
+ <td>${entry.votes}</td>
463
+ </tr>
464
+ `).join('');
465
+
466
+ categoriesHtml += `
467
+ <div class="category-section">
468
+ <h2>${category} Category</h2>
469
+ ${sortedEntries.length > 0 ? `
470
+ <table>
471
+ <thead>
472
+ <tr>
473
+ <th>ID</th>
474
+ <th>Name</th>
475
+ <th>Votes</th>
476
+ </tr>
477
+ </thead>
478
+ <tbody>
479
+ ${tableRows}
480
+ </tbody>
481
+ </table>
482
+ ` : '<p>No entries in this category.</p>'}
483
+ </div>
484
+ `;
485
+ });
486
+
487
+ res.send(`
488
+ <!DOCTYPE html>
489
+ <html>
490
+ <head>
491
+ <title>Archived Week: ${weekId}</title>
492
+ <style>
493
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
494
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
495
+ h1 { color: #333; margin: 0; }
496
+ h2 { color: #333; margin-top: 30px; }
497
+ table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
498
+ table, th, td { border: 1px solid #ddd; }
499
+ th { background-color: #f2f2f2; padding: 10px; text-align: left; }
500
+ td { padding: 10px; }
501
+ .category-section { margin-bottom: 30px; }
502
+ .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }
503
+ .btn-edit { background-color: #2196F3; }
504
+ .nav-links { display: flex; gap: 15px; align-items: center; }
505
+ .nav-link { text-decoration: none; color: #2196F3; }
506
+ .archive-meta { background-color: #f9f9f9; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
507
+ .archive-meta p { margin: 5px 0; }
508
+ </style>
509
+ </head>
510
+ <body>
511
+ <div class="header">
512
+ <h1>Archived Week: ${weekId}</h1>
513
+ <div class="nav-links">
514
+ <a href="/admin/archives" class="nav-link">Back to Archives</a>
515
+ <a href="/admin/dashboard" class="nav-link">Back to Dashboard</a>
516
+ <a href="/admin/logout" class="logout">Logout</a>
517
+ </div>
518
+ </div>
519
+
520
+ <div class="archive-meta">
521
+ <p><strong>Week ID:</strong> ${archive.weekId}</p>
522
+ <p><strong>Start Date:</strong> ${archive.startDate}</p>
523
+ <p><strong>End Date:</strong> ${archive.endDate}</p>
524
+ <p><strong>Archived At:</strong> ${new Date(archive.archivedAt).toLocaleString()}</p>
525
+ </div>
526
+
527
+ ${categoriesHtml}
528
+ </body>
529
+ </html>
530
+ `);
531
+ });
532
+
533
+ // Search archives by date range
534
+ app.get('/admin/archives/search', authenticate, (req, res) => {
535
+ const { startDate, endDate } = req.query;
536
+
537
+ if (!startDate || !endDate) {
538
+ return res.redirect('/admin/archives');
539
+ }
540
+
541
+ try {
542
+ const archives = archiver.getArchivedRange(startDate, endDate);
543
+
544
+ let resultsHtml = '';
545
+
546
+ if (archives.length === 0) {
547
+ resultsHtml = '<p>No archives found for the specified date range.</p>';
548
+ } else {
549
+ let tableRows = archives.map(archive => `
550
+ <tr>
551
+ <td>${escapeHtml(archive.weekId)}</td>
552
+ <td>${escapeHtml(archive.startDate)}</td>
553
+ <td>${escapeHtml(archive.endDate)}</td>
554
+ <td>${new Date(archive.archivedAt).toLocaleString()}</td>
555
+ <td>
556
+ <a href="/admin/archives/week/${archive.weekId}" class="btn btn-edit">View</a>
557
+ </td>
558
+ </tr>
559
+ `).join('');
560
+
561
+ resultsHtml = `
562
+ <h3>Search Results</h3>
563
+ <p>Found ${archives.length} archive(s) between ${startDate} and ${endDate}</p>
564
+ <table>
565
+ <thead>
566
+ <tr>
567
+ <th>Week ID</th>
568
+ <th>Start Date</th>
569
+ <th>End Date</th>
570
+ <th>Archived At</th>
571
+ <th>Actions</th>
572
+ </tr>
573
+ </thead>
574
+ <tbody>
575
+ ${tableRows}
576
+ </tbody>
577
+ </table>
578
+ `;
579
+ }
580
+
581
+ res.send(`
582
+ <!DOCTYPE html>
583
+ <html>
584
+ <head>
585
+ <title>Archive Search Results</title>
586
+ <style>
587
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
588
+ .header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
589
+ h1 { color: #333; margin: 0; }
590
+ h3 { color: #555; margin-top: 20px; }
591
+ table { width: 100%; border-collapse: collapse; margin-bottom: 30px; }
592
+ table, th, td { border: 1px solid #ddd; }
593
+ th { background-color: #f2f2f2; padding: 10px; text-align: left; }
594
+ td { padding: 10px; }
595
+ .btn { display: inline-block; padding: 5px 10px; margin-right: 5px; text-decoration: none; border-radius: 3px; color: white; border: none; cursor: pointer; }
596
+ .btn-edit { background-color: #2196F3; }
597
+ .nav-links { display: flex; gap: 15px; align-items: center; }
598
+ .nav-link { text-decoration: none; color: #2196F3; }
599
+ .search-form { margin: 20px 0; padding: 15px; background-color: #f9f9f9; border-radius: 5px; }
600
+ .form-group { margin-bottom: 15px; }
601
+ .form-group label { display: block; margin-bottom: 5px; }
602
+ .form-group input { padding: 8px; width: 200px; }
603
+ </style>
604
+ </head>
605
+ <body>
606
+ <div class="header">
607
+ <h1>Archive Search Results</h1>
608
+ <div class="nav-links">
609
+ <a href="/admin/archives" class="nav-link">Back to Archives</a>
610
+ <a href="/admin/dashboard" class="nav-link">Back to Dashboard</a>
611
+ <a href="/admin/logout" class="logout">Logout</a>
612
+ </div>
613
+ </div>
614
+
615
+ <div class="search-form">
616
+ <h3>Search Archives by Date Range</h3>
617
+ <form action="/admin/archives/search" method="GET">
618
+ <div class="form-group">
619
+ <label for="startDate">Start Date:</label>
620
+ <input type="date" id="startDate" name="startDate" value="${startDate}" required>
621
+ </div>
622
+ <div class="form-group">
623
+ <label for="endDate">End Date:</label>
624
+ <input type="date" id="endDate" name="endDate" value="${endDate}" required>
625
+ </div>
626
+ <button type="submit" class="btn btn-edit">Search</button>
627
+ </form>
628
+ </div>
629
+
630
+ ${resultsHtml}
631
+ </body>
632
+ </html>
633
+ `);
634
+ } catch (error) {
635
+ console.error('Error searching archives:', error);
636
+ res.redirect('/admin/archives?error=1');
637
+ }
638
+ });
639
+
640
+ // Manually create an archive
641
+ app.post('/admin/archives/create', authenticate, (req, res) => {
642
+ try {
643
+ const weekId = archiver.archiveCurrentWeek();
644
+ // Reset votes after archiving
645
+ archiver.resetLeaderboard();
646
+ res.redirect(`/admin/archives/week/${weekId}`);
647
+ } catch (error) {
648
+ console.error('Error creating archive:', error);
649
+ res.redirect('/admin/archives?error=1');
650
+ }
651
+ });
652
+
653
+ // Helper function to escape HTML (prevent XSS)
654
+ function escapeHtml(unsafe) {
655
+ if (typeof unsafe !== 'string') return '';
656
+ return unsafe
657
+ .replace(/&/g, "&amp;")
658
+ .replace(/</g, "&lt;")
659
+ .replace(/>/g, "&gt;")
660
+ .replace(/"/g, "&quot;")
661
+ .replace(/'/g, "&#039;");
662
+ }
663
+
664
+ // Start Server
665
+ app.listen(PORT, () => {
666
+ console.log(`Admin server listening on http://localhost:${PORT}`);
667
+ });
data.json ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "6gb": [
3
+ {
4
+ "id": "1745259026144",
5
+ "name": "delts",
6
+ "votes": 1
7
+ },
8
+ {
9
+ "id": "1745266909797",
10
+ "name": "test",
11
+ "votes": 1
12
+ }
13
+ ],
14
+ "12gb": [
15
+ {
16
+ "id": "1745259027766",
17
+ "name": "deltsRTX 3070",
18
+ "votes": 1210
19
+ },
20
+ {
21
+ "id": "1745259031820",
22
+ "name": "delts1",
23
+ "votes": 6
24
+ },
25
+ {
26
+ "id": "1745259051660",
27
+ "name": "sss",
28
+ "votes": 1
29
+ },
30
+ {
31
+ "id": "1745265231732",
32
+ "name": "RTX 4060",
33
+ "votes": 1
34
+ }
35
+ ],
36
+ "16gb": [
37
+ {
38
+ "id": "1745265403934",
39
+ "name": "RTX 3080",
40
+ "votes": 1
41
+ },
42
+ {
43
+ "id": "1745266940910",
44
+ "name": "test",
45
+ "votes": 1
46
+ }
47
+ ],
48
+ "24gb": [
49
+ {
50
+ "id": "1745265119271",
51
+ "name": "test",
52
+ "votes": 6
53
+ },
54
+ {
55
+ "id": "1745265122126",
56
+ "name": "ter",
57
+ "votes": 6
58
+ }
59
+ ],
60
+ "48gb": [
61
+ {
62
+ "id": "1745266632150",
63
+ "name": "gemma",
64
+ "votes": 15
65
+ },
66
+ {
67
+ "id": "1745266636155",
68
+ "name": "34",
69
+ "votes": 14
70
+ }
71
+ ]
72
+ }
data/archives/2025-W16.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "weekId": "2025-W16",
3
+ "startDate": "2025-04-13",
4
+ "endDate": "2025-04-19",
5
+ "archivedAt": "2025-04-22T11:55:25.370Z",
6
+ "data": {
7
+ "6gb": [
8
+ {
9
+ "id": "1745322881980",
10
+ "name": "Gemma",
11
+ "votes": 0
12
+ },
13
+ {
14
+ "id": "1745322885316",
15
+ "name": "phi",
16
+ "votes": 1
17
+ }
18
+ ],
19
+ "12gb": [],
20
+ "16gb": [],
21
+ "24gb": [],
22
+ "48gb": []
23
+ }
24
+ }
docker-compose.yml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ app:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ container_name: gpu-leaderboard
9
+ restart: unless-stopped
10
+ ports:
11
+ - "3000:3000" # Main server
12
+ - "6969:6969" # Admin server
13
+ volumes:
14
+ - ./data:/app/data
15
+ env_file:
16
+ - .env
17
+ environment:
18
+ - NODE_ENV=${NODE_ENV:-production}
19
+ - PORT=${PORT:-3000}
20
+ - ADMIN_PORT=${ADMIN_PORT:-6969}
21
+ networks:
22
+ - app-network
23
+
24
+ networks:
25
+ app-network:
26
+ driver: bridge
docker-entrypoint.sh ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ echo "Starting GPU Leaderboard application..."
5
+
6
+ # Create data directory if it doesn't exist
7
+ mkdir -p /app/data
8
+ echo "Ensuring data directory exists: /app/data"
9
+
10
+ # Initialize data.json if it doesn't exist
11
+ if [ ! -f /app/data/data.json ]; then
12
+ echo '{}' > /app/data/data.json
13
+ echo "Initialized empty data.json file"
14
+ fi
15
+
16
+ # Initialize ips.json if it doesn't exist
17
+ if [ ! -f /app/data/ips.json ]; then
18
+ echo '{}' > /app/data/ips.json
19
+ echo "Initialized empty ips.json file"
20
+ fi
21
+
22
+ echo "Data files initialized successfully"
23
+ echo "Starting application with: $@"
24
+
25
+ # Execute the provided command (node start.js)
26
+ exec "$@"
docker-run.bat ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo Stopping any running containers...
3
+ docker-compose down
4
+
5
+ echo Building and starting containers...
6
+ docker-compose up -d --build
7
+
8
+ echo Container status:
9
+ docker-compose ps
10
+
11
+ echo.
12
+ echo Application is now running with custom start script!
13
+ echo Main application: http://localhost:3000
14
+ echo Admin interface: http://localhost:6969/admin
15
+ echo.
16
+ echo Both servers are running in a single container.
17
+ echo.
18
+ echo To view logs: docker-compose logs -f
19
+ echo To stop: docker-compose down
20
+
21
+ pause
docker-run.sh ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Stop any running containers
4
+ echo "Stopping any running containers..."
5
+ docker-compose down
6
+
7
+ # Build and start the containers
8
+ echo "Building and starting containers..."
9
+ docker-compose up -d --build
10
+
11
+ # Show container status
12
+ echo "Container status:"
13
+ docker-compose ps
14
+
15
+ echo ""
16
+ echo "Application is now running with custom start script!"
17
+ echo "Main application: http://localhost:3000"
18
+ echo "Admin interface: http://localhost:6969/admin"
19
+ echo ""
20
+ echo "Both servers are running in a single container."
21
+ echo ""
22
+ echo "To view logs: docker-compose logs -f"
23
+ echo "To stop: docker-compose down"
ips.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "3e48ef9d22e096da6838540fb846999890462c8a32730a4f7a5eaee6945315f7": {
3
+ "6gb": "1745266909797",
4
+ "12gb": "1745259031820",
5
+ "16gb": "1745266940910"
6
+ },
7
+ "eff8e7ca506627fe15dda5e0e512fcaad70b6d520f37cc76597fdb4f2d83a1a3": {
8
+ "12gb": "1745265231732",
9
+ "16gb": "1745265403934"
10
+ }
11
+ }
leaderboard_archiver.js ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // leaderboard_archiver.js
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const moment = require('moment');
5
+
6
+ // Constants
7
+ const DATA_FILE = path.join(__dirname, 'data', 'data.json');
8
+ const ARCHIVES_DIR = path.join(__dirname, 'data', 'archives');
9
+ const WEEK_FORMAT = 'YYYY-[W]ww'; // Format: 2025-W17 (Year-Week number)
10
+
11
+ // Ensure archives directory exists
12
+ function ensureArchivesDir() {
13
+ if (!fs.existsSync(ARCHIVES_DIR)) {
14
+ fs.mkdirSync(ARCHIVES_DIR, { recursive: true });
15
+ }
16
+ }
17
+
18
+ // Generate a weekly snapshot and archive it
19
+ function archiveCurrentWeek() {
20
+ ensureArchivesDir();
21
+
22
+ try {
23
+ // Read current data
24
+ const data = JSON.parse(fs.readFileSync(DATA_FILE));
25
+
26
+ // Generate week identifier (e.g., 2025-W17)
27
+ const previousWeek = moment().subtract(1, 'week');
28
+ const weekId = previousWeek.format(WEEK_FORMAT);
29
+
30
+ // Create archive object with metadata
31
+ const archive = {
32
+ weekId,
33
+ startDate: previousWeek.startOf('week').format('YYYY-MM-DD'),
34
+ endDate: previousWeek.endOf('week').format('YYYY-MM-DD'),
35
+ archivedAt: new Date().toISOString(),
36
+ data
37
+ };
38
+
39
+ // Write to archive file
40
+ const archiveFile = path.join(ARCHIVES_DIR, `${weekId}.json`);
41
+ fs.writeFileSync(archiveFile, JSON.stringify(archive, null, 2));
42
+
43
+ console.log(`Archived leaderboard for week ${weekId}`);
44
+ return weekId;
45
+ } catch (err) {
46
+ console.error('Error archiving leaderboard:', err);
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ // Get list of all archived weeks
52
+ function getArchivedWeeks() {
53
+ ensureArchivesDir();
54
+
55
+ try {
56
+ const files = fs.readdirSync(ARCHIVES_DIR);
57
+ return files
58
+ .filter(file => file.endsWith('.json'))
59
+ .map(file => path.basename(file, '.json'))
60
+ .sort((a, b) => b.localeCompare(a)); // Sort in descending order (newest first)
61
+ } catch (err) {
62
+ console.error('Error getting archived weeks:', err);
63
+ return [];
64
+ }
65
+ }
66
+
67
+ // Get archived data for a specific week
68
+ function getArchivedWeek(weekId) {
69
+ const archiveFile = path.join(ARCHIVES_DIR, `${weekId}.json`);
70
+
71
+ try {
72
+ if (fs.existsSync(archiveFile)) {
73
+ return JSON.parse(fs.readFileSync(archiveFile));
74
+ }
75
+ return null;
76
+ } catch (err) {
77
+ console.error(`Error reading archive for week ${weekId}:`, err);
78
+ return null;
79
+ }
80
+ }
81
+
82
+ // Get archived data for a date range
83
+ function getArchivedRange(startDate, endDate) {
84
+ try {
85
+ const start = moment(startDate);
86
+ const end = moment(endDate);
87
+
88
+ if (!start.isValid() || !end.isValid()) {
89
+ throw new Error('Invalid date format');
90
+ }
91
+
92
+ const weeks = getArchivedWeeks();
93
+ const result = [];
94
+
95
+ for (const weekId of weeks) {
96
+ const archive = getArchivedWeek(weekId);
97
+ if (archive) {
98
+ const archiveStart = moment(archive.startDate);
99
+ const archiveEnd = moment(archive.endDate);
100
+
101
+ // Check if this archive falls within the requested range
102
+ if ((archiveStart.isSameOrAfter(start) && archiveStart.isSameOrBefore(end)) ||
103
+ (archiveEnd.isSameOrAfter(start) && archiveEnd.isSameOrBefore(end)) ||
104
+ (archiveStart.isBefore(start) && archiveEnd.isAfter(end))) {
105
+ result.push(archive);
106
+ }
107
+ }
108
+ }
109
+
110
+ return result;
111
+ } catch (err) {
112
+ console.error('Error getting archived range:', err);
113
+ return [];
114
+ }
115
+ }
116
+
117
+ // Reset leaderboard (optional, if you want to reset votes after archiving)
118
+ function resetLeaderboard() {
119
+ try {
120
+ const data = JSON.parse(fs.readFileSync(DATA_FILE));
121
+
122
+ // Clear all entries in all categories
123
+ Object.keys(data).forEach(category => {
124
+ data[category] = [];
125
+ });
126
+
127
+ // Save the reset data
128
+ fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
129
+ console.log('Leaderboard entries cleared successfully');
130
+ } catch (err) {
131
+ console.error('Error resetting leaderboard:', err);
132
+ throw err;
133
+ }
134
+ }
135
+
136
+ module.exports = {
137
+ archiveCurrentWeek,
138
+ getArchivedWeeks,
139
+ getArchivedWeek,
140
+ getArchivedRange,
141
+ resetLeaderboard,
142
+ WEEK_FORMAT
143
+ };
package-lock.json ADDED
@@ -0,0 +1,1149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "leader",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "leader",
9
+ "version": "1.0.0",
10
+ "license": "ISC",
11
+ "dependencies": {
12
+ "body-parser": "^2.2.0",
13
+ "express": "^5.1.0",
14
+ "express-session": "^1.17.3",
15
+ "moment": "^2.30.1",
16
+ "node-cron": "^3.0.3",
17
+ "request-ip": "^3.3.0"
18
+ },
19
+ "devDependencies": {
20
+ "concurrently": "^9.1.2"
21
+ }
22
+ },
23
+ "node_modules/accepts": {
24
+ "version": "2.0.0",
25
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
26
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
27
+ "dependencies": {
28
+ "mime-types": "^3.0.0",
29
+ "negotiator": "^1.0.0"
30
+ },
31
+ "engines": {
32
+ "node": ">= 0.6"
33
+ }
34
+ },
35
+ "node_modules/ansi-regex": {
36
+ "version": "5.0.1",
37
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
38
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
39
+ "dev": true,
40
+ "engines": {
41
+ "node": ">=8"
42
+ }
43
+ },
44
+ "node_modules/ansi-styles": {
45
+ "version": "4.3.0",
46
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
47
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
48
+ "dev": true,
49
+ "dependencies": {
50
+ "color-convert": "^2.0.1"
51
+ },
52
+ "engines": {
53
+ "node": ">=8"
54
+ },
55
+ "funding": {
56
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
57
+ }
58
+ },
59
+ "node_modules/body-parser": {
60
+ "version": "2.2.0",
61
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
62
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
63
+ "dependencies": {
64
+ "bytes": "^3.1.2",
65
+ "content-type": "^1.0.5",
66
+ "debug": "^4.4.0",
67
+ "http-errors": "^2.0.0",
68
+ "iconv-lite": "^0.6.3",
69
+ "on-finished": "^2.4.1",
70
+ "qs": "^6.14.0",
71
+ "raw-body": "^3.0.0",
72
+ "type-is": "^2.0.0"
73
+ },
74
+ "engines": {
75
+ "node": ">=18"
76
+ }
77
+ },
78
+ "node_modules/bytes": {
79
+ "version": "3.1.2",
80
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
81
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
82
+ "engines": {
83
+ "node": ">= 0.8"
84
+ }
85
+ },
86
+ "node_modules/call-bind-apply-helpers": {
87
+ "version": "1.0.2",
88
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
89
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
90
+ "dependencies": {
91
+ "es-errors": "^1.3.0",
92
+ "function-bind": "^1.1.2"
93
+ },
94
+ "engines": {
95
+ "node": ">= 0.4"
96
+ }
97
+ },
98
+ "node_modules/call-bound": {
99
+ "version": "1.0.4",
100
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
101
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
102
+ "dependencies": {
103
+ "call-bind-apply-helpers": "^1.0.2",
104
+ "get-intrinsic": "^1.3.0"
105
+ },
106
+ "engines": {
107
+ "node": ">= 0.4"
108
+ },
109
+ "funding": {
110
+ "url": "https://github.com/sponsors/ljharb"
111
+ }
112
+ },
113
+ "node_modules/chalk": {
114
+ "version": "4.1.2",
115
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
116
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
117
+ "dev": true,
118
+ "dependencies": {
119
+ "ansi-styles": "^4.1.0",
120
+ "supports-color": "^7.1.0"
121
+ },
122
+ "engines": {
123
+ "node": ">=10"
124
+ },
125
+ "funding": {
126
+ "url": "https://github.com/chalk/chalk?sponsor=1"
127
+ }
128
+ },
129
+ "node_modules/chalk/node_modules/supports-color": {
130
+ "version": "7.2.0",
131
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
132
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
133
+ "dev": true,
134
+ "dependencies": {
135
+ "has-flag": "^4.0.0"
136
+ },
137
+ "engines": {
138
+ "node": ">=8"
139
+ }
140
+ },
141
+ "node_modules/cliui": {
142
+ "version": "8.0.1",
143
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
144
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
145
+ "dev": true,
146
+ "dependencies": {
147
+ "string-width": "^4.2.0",
148
+ "strip-ansi": "^6.0.1",
149
+ "wrap-ansi": "^7.0.0"
150
+ },
151
+ "engines": {
152
+ "node": ">=12"
153
+ }
154
+ },
155
+ "node_modules/color-convert": {
156
+ "version": "2.0.1",
157
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
158
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
159
+ "dev": true,
160
+ "dependencies": {
161
+ "color-name": "~1.1.4"
162
+ },
163
+ "engines": {
164
+ "node": ">=7.0.0"
165
+ }
166
+ },
167
+ "node_modules/color-name": {
168
+ "version": "1.1.4",
169
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
170
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
171
+ "dev": true
172
+ },
173
+ "node_modules/concurrently": {
174
+ "version": "9.1.2",
175
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz",
176
+ "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==",
177
+ "dev": true,
178
+ "dependencies": {
179
+ "chalk": "^4.1.2",
180
+ "lodash": "^4.17.21",
181
+ "rxjs": "^7.8.1",
182
+ "shell-quote": "^1.8.1",
183
+ "supports-color": "^8.1.1",
184
+ "tree-kill": "^1.2.2",
185
+ "yargs": "^17.7.2"
186
+ },
187
+ "bin": {
188
+ "conc": "dist/bin/concurrently.js",
189
+ "concurrently": "dist/bin/concurrently.js"
190
+ },
191
+ "engines": {
192
+ "node": ">=18"
193
+ },
194
+ "funding": {
195
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
196
+ }
197
+ },
198
+ "node_modules/content-disposition": {
199
+ "version": "1.0.0",
200
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
201
+ "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
202
+ "dependencies": {
203
+ "safe-buffer": "5.2.1"
204
+ },
205
+ "engines": {
206
+ "node": ">= 0.6"
207
+ }
208
+ },
209
+ "node_modules/content-type": {
210
+ "version": "1.0.5",
211
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
212
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
213
+ "engines": {
214
+ "node": ">= 0.6"
215
+ }
216
+ },
217
+ "node_modules/cookie": {
218
+ "version": "0.7.2",
219
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
220
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
221
+ "engines": {
222
+ "node": ">= 0.6"
223
+ }
224
+ },
225
+ "node_modules/cookie-signature": {
226
+ "version": "1.2.2",
227
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
228
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
229
+ "engines": {
230
+ "node": ">=6.6.0"
231
+ }
232
+ },
233
+ "node_modules/debug": {
234
+ "version": "4.4.0",
235
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
236
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
237
+ "dependencies": {
238
+ "ms": "^2.1.3"
239
+ },
240
+ "engines": {
241
+ "node": ">=6.0"
242
+ },
243
+ "peerDependenciesMeta": {
244
+ "supports-color": {
245
+ "optional": true
246
+ }
247
+ }
248
+ },
249
+ "node_modules/depd": {
250
+ "version": "2.0.0",
251
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
252
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
253
+ "engines": {
254
+ "node": ">= 0.8"
255
+ }
256
+ },
257
+ "node_modules/dunder-proto": {
258
+ "version": "1.0.1",
259
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
260
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
261
+ "dependencies": {
262
+ "call-bind-apply-helpers": "^1.0.1",
263
+ "es-errors": "^1.3.0",
264
+ "gopd": "^1.2.0"
265
+ },
266
+ "engines": {
267
+ "node": ">= 0.4"
268
+ }
269
+ },
270
+ "node_modules/ee-first": {
271
+ "version": "1.1.1",
272
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
273
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
274
+ },
275
+ "node_modules/emoji-regex": {
276
+ "version": "8.0.0",
277
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
278
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
279
+ "dev": true
280
+ },
281
+ "node_modules/encodeurl": {
282
+ "version": "2.0.0",
283
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
284
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
285
+ "engines": {
286
+ "node": ">= 0.8"
287
+ }
288
+ },
289
+ "node_modules/es-define-property": {
290
+ "version": "1.0.1",
291
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
292
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
293
+ "engines": {
294
+ "node": ">= 0.4"
295
+ }
296
+ },
297
+ "node_modules/es-errors": {
298
+ "version": "1.3.0",
299
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
300
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
301
+ "engines": {
302
+ "node": ">= 0.4"
303
+ }
304
+ },
305
+ "node_modules/es-object-atoms": {
306
+ "version": "1.1.1",
307
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
308
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
309
+ "dependencies": {
310
+ "es-errors": "^1.3.0"
311
+ },
312
+ "engines": {
313
+ "node": ">= 0.4"
314
+ }
315
+ },
316
+ "node_modules/escalade": {
317
+ "version": "3.2.0",
318
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
319
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
320
+ "dev": true,
321
+ "engines": {
322
+ "node": ">=6"
323
+ }
324
+ },
325
+ "node_modules/escape-html": {
326
+ "version": "1.0.3",
327
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
328
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
329
+ },
330
+ "node_modules/etag": {
331
+ "version": "1.8.1",
332
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
333
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
334
+ "engines": {
335
+ "node": ">= 0.6"
336
+ }
337
+ },
338
+ "node_modules/express": {
339
+ "version": "5.1.0",
340
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
341
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
342
+ "dependencies": {
343
+ "accepts": "^2.0.0",
344
+ "body-parser": "^2.2.0",
345
+ "content-disposition": "^1.0.0",
346
+ "content-type": "^1.0.5",
347
+ "cookie": "^0.7.1",
348
+ "cookie-signature": "^1.2.1",
349
+ "debug": "^4.4.0",
350
+ "encodeurl": "^2.0.0",
351
+ "escape-html": "^1.0.3",
352
+ "etag": "^1.8.1",
353
+ "finalhandler": "^2.1.0",
354
+ "fresh": "^2.0.0",
355
+ "http-errors": "^2.0.0",
356
+ "merge-descriptors": "^2.0.0",
357
+ "mime-types": "^3.0.0",
358
+ "on-finished": "^2.4.1",
359
+ "once": "^1.4.0",
360
+ "parseurl": "^1.3.3",
361
+ "proxy-addr": "^2.0.7",
362
+ "qs": "^6.14.0",
363
+ "range-parser": "^1.2.1",
364
+ "router": "^2.2.0",
365
+ "send": "^1.1.0",
366
+ "serve-static": "^2.2.0",
367
+ "statuses": "^2.0.1",
368
+ "type-is": "^2.0.1",
369
+ "vary": "^1.1.2"
370
+ },
371
+ "engines": {
372
+ "node": ">= 18"
373
+ },
374
+ "funding": {
375
+ "type": "opencollective",
376
+ "url": "https://opencollective.com/express"
377
+ }
378
+ },
379
+ "node_modules/express-session": {
380
+ "version": "1.18.1",
381
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz",
382
+ "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==",
383
+ "dependencies": {
384
+ "cookie": "0.7.2",
385
+ "cookie-signature": "1.0.7",
386
+ "debug": "2.6.9",
387
+ "depd": "~2.0.0",
388
+ "on-headers": "~1.0.2",
389
+ "parseurl": "~1.3.3",
390
+ "safe-buffer": "5.2.1",
391
+ "uid-safe": "~2.1.5"
392
+ },
393
+ "engines": {
394
+ "node": ">= 0.8.0"
395
+ }
396
+ },
397
+ "node_modules/express-session/node_modules/cookie-signature": {
398
+ "version": "1.0.7",
399
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
400
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
401
+ },
402
+ "node_modules/express-session/node_modules/debug": {
403
+ "version": "2.6.9",
404
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
405
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
406
+ "dependencies": {
407
+ "ms": "2.0.0"
408
+ }
409
+ },
410
+ "node_modules/express-session/node_modules/ms": {
411
+ "version": "2.0.0",
412
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
413
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
414
+ },
415
+ "node_modules/finalhandler": {
416
+ "version": "2.1.0",
417
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
418
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
419
+ "dependencies": {
420
+ "debug": "^4.4.0",
421
+ "encodeurl": "^2.0.0",
422
+ "escape-html": "^1.0.3",
423
+ "on-finished": "^2.4.1",
424
+ "parseurl": "^1.3.3",
425
+ "statuses": "^2.0.1"
426
+ },
427
+ "engines": {
428
+ "node": ">= 0.8"
429
+ }
430
+ },
431
+ "node_modules/forwarded": {
432
+ "version": "0.2.0",
433
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
434
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
435
+ "engines": {
436
+ "node": ">= 0.6"
437
+ }
438
+ },
439
+ "node_modules/fresh": {
440
+ "version": "2.0.0",
441
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
442
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
443
+ "engines": {
444
+ "node": ">= 0.8"
445
+ }
446
+ },
447
+ "node_modules/function-bind": {
448
+ "version": "1.1.2",
449
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
450
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
451
+ "funding": {
452
+ "url": "https://github.com/sponsors/ljharb"
453
+ }
454
+ },
455
+ "node_modules/get-caller-file": {
456
+ "version": "2.0.5",
457
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
458
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
459
+ "dev": true,
460
+ "engines": {
461
+ "node": "6.* || 8.* || >= 10.*"
462
+ }
463
+ },
464
+ "node_modules/get-intrinsic": {
465
+ "version": "1.3.0",
466
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
467
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
468
+ "dependencies": {
469
+ "call-bind-apply-helpers": "^1.0.2",
470
+ "es-define-property": "^1.0.1",
471
+ "es-errors": "^1.3.0",
472
+ "es-object-atoms": "^1.1.1",
473
+ "function-bind": "^1.1.2",
474
+ "get-proto": "^1.0.1",
475
+ "gopd": "^1.2.0",
476
+ "has-symbols": "^1.1.0",
477
+ "hasown": "^2.0.2",
478
+ "math-intrinsics": "^1.1.0"
479
+ },
480
+ "engines": {
481
+ "node": ">= 0.4"
482
+ },
483
+ "funding": {
484
+ "url": "https://github.com/sponsors/ljharb"
485
+ }
486
+ },
487
+ "node_modules/get-proto": {
488
+ "version": "1.0.1",
489
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
490
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
491
+ "dependencies": {
492
+ "dunder-proto": "^1.0.1",
493
+ "es-object-atoms": "^1.0.0"
494
+ },
495
+ "engines": {
496
+ "node": ">= 0.4"
497
+ }
498
+ },
499
+ "node_modules/gopd": {
500
+ "version": "1.2.0",
501
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
502
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
503
+ "engines": {
504
+ "node": ">= 0.4"
505
+ },
506
+ "funding": {
507
+ "url": "https://github.com/sponsors/ljharb"
508
+ }
509
+ },
510
+ "node_modules/has-flag": {
511
+ "version": "4.0.0",
512
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
513
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
514
+ "dev": true,
515
+ "engines": {
516
+ "node": ">=8"
517
+ }
518
+ },
519
+ "node_modules/has-symbols": {
520
+ "version": "1.1.0",
521
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
522
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
523
+ "engines": {
524
+ "node": ">= 0.4"
525
+ },
526
+ "funding": {
527
+ "url": "https://github.com/sponsors/ljharb"
528
+ }
529
+ },
530
+ "node_modules/hasown": {
531
+ "version": "2.0.2",
532
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
533
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
534
+ "dependencies": {
535
+ "function-bind": "^1.1.2"
536
+ },
537
+ "engines": {
538
+ "node": ">= 0.4"
539
+ }
540
+ },
541
+ "node_modules/http-errors": {
542
+ "version": "2.0.0",
543
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
544
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
545
+ "dependencies": {
546
+ "depd": "2.0.0",
547
+ "inherits": "2.0.4",
548
+ "setprototypeof": "1.2.0",
549
+ "statuses": "2.0.1",
550
+ "toidentifier": "1.0.1"
551
+ },
552
+ "engines": {
553
+ "node": ">= 0.8"
554
+ }
555
+ },
556
+ "node_modules/iconv-lite": {
557
+ "version": "0.6.3",
558
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
559
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
560
+ "dependencies": {
561
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
562
+ },
563
+ "engines": {
564
+ "node": ">=0.10.0"
565
+ }
566
+ },
567
+ "node_modules/inherits": {
568
+ "version": "2.0.4",
569
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
570
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
571
+ },
572
+ "node_modules/ipaddr.js": {
573
+ "version": "1.9.1",
574
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
575
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
576
+ "engines": {
577
+ "node": ">= 0.10"
578
+ }
579
+ },
580
+ "node_modules/is-fullwidth-code-point": {
581
+ "version": "3.0.0",
582
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
583
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
584
+ "dev": true,
585
+ "engines": {
586
+ "node": ">=8"
587
+ }
588
+ },
589
+ "node_modules/is-promise": {
590
+ "version": "4.0.0",
591
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
592
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
593
+ },
594
+ "node_modules/lodash": {
595
+ "version": "4.17.21",
596
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
597
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
598
+ "dev": true
599
+ },
600
+ "node_modules/math-intrinsics": {
601
+ "version": "1.1.0",
602
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
603
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
604
+ "engines": {
605
+ "node": ">= 0.4"
606
+ }
607
+ },
608
+ "node_modules/media-typer": {
609
+ "version": "1.1.0",
610
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
611
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
612
+ "engines": {
613
+ "node": ">= 0.8"
614
+ }
615
+ },
616
+ "node_modules/merge-descriptors": {
617
+ "version": "2.0.0",
618
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
619
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
620
+ "engines": {
621
+ "node": ">=18"
622
+ },
623
+ "funding": {
624
+ "url": "https://github.com/sponsors/sindresorhus"
625
+ }
626
+ },
627
+ "node_modules/mime-db": {
628
+ "version": "1.54.0",
629
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
630
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
631
+ "engines": {
632
+ "node": ">= 0.6"
633
+ }
634
+ },
635
+ "node_modules/mime-types": {
636
+ "version": "3.0.1",
637
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
638
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
639
+ "dependencies": {
640
+ "mime-db": "^1.54.0"
641
+ },
642
+ "engines": {
643
+ "node": ">= 0.6"
644
+ }
645
+ },
646
+ "node_modules/moment": {
647
+ "version": "2.30.1",
648
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
649
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
650
+ "engines": {
651
+ "node": "*"
652
+ }
653
+ },
654
+ "node_modules/ms": {
655
+ "version": "2.1.3",
656
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
657
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
658
+ },
659
+ "node_modules/negotiator": {
660
+ "version": "1.0.0",
661
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
662
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
663
+ "engines": {
664
+ "node": ">= 0.6"
665
+ }
666
+ },
667
+ "node_modules/node-cron": {
668
+ "version": "3.0.3",
669
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
670
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
671
+ "dependencies": {
672
+ "uuid": "8.3.2"
673
+ },
674
+ "engines": {
675
+ "node": ">=6.0.0"
676
+ }
677
+ },
678
+ "node_modules/object-inspect": {
679
+ "version": "1.13.4",
680
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
681
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
682
+ "engines": {
683
+ "node": ">= 0.4"
684
+ },
685
+ "funding": {
686
+ "url": "https://github.com/sponsors/ljharb"
687
+ }
688
+ },
689
+ "node_modules/on-finished": {
690
+ "version": "2.4.1",
691
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
692
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
693
+ "dependencies": {
694
+ "ee-first": "1.1.1"
695
+ },
696
+ "engines": {
697
+ "node": ">= 0.8"
698
+ }
699
+ },
700
+ "node_modules/on-headers": {
701
+ "version": "1.0.2",
702
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
703
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
704
+ "engines": {
705
+ "node": ">= 0.8"
706
+ }
707
+ },
708
+ "node_modules/once": {
709
+ "version": "1.4.0",
710
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
711
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
712
+ "dependencies": {
713
+ "wrappy": "1"
714
+ }
715
+ },
716
+ "node_modules/parseurl": {
717
+ "version": "1.3.3",
718
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
719
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
720
+ "engines": {
721
+ "node": ">= 0.8"
722
+ }
723
+ },
724
+ "node_modules/path-to-regexp": {
725
+ "version": "8.2.0",
726
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
727
+ "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
728
+ "engines": {
729
+ "node": ">=16"
730
+ }
731
+ },
732
+ "node_modules/proxy-addr": {
733
+ "version": "2.0.7",
734
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
735
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
736
+ "dependencies": {
737
+ "forwarded": "0.2.0",
738
+ "ipaddr.js": "1.9.1"
739
+ },
740
+ "engines": {
741
+ "node": ">= 0.10"
742
+ }
743
+ },
744
+ "node_modules/qs": {
745
+ "version": "6.14.0",
746
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
747
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
748
+ "dependencies": {
749
+ "side-channel": "^1.1.0"
750
+ },
751
+ "engines": {
752
+ "node": ">=0.6"
753
+ },
754
+ "funding": {
755
+ "url": "https://github.com/sponsors/ljharb"
756
+ }
757
+ },
758
+ "node_modules/random-bytes": {
759
+ "version": "1.0.0",
760
+ "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
761
+ "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
762
+ "engines": {
763
+ "node": ">= 0.8"
764
+ }
765
+ },
766
+ "node_modules/range-parser": {
767
+ "version": "1.2.1",
768
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
769
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
770
+ "engines": {
771
+ "node": ">= 0.6"
772
+ }
773
+ },
774
+ "node_modules/raw-body": {
775
+ "version": "3.0.0",
776
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
777
+ "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
778
+ "dependencies": {
779
+ "bytes": "3.1.2",
780
+ "http-errors": "2.0.0",
781
+ "iconv-lite": "0.6.3",
782
+ "unpipe": "1.0.0"
783
+ },
784
+ "engines": {
785
+ "node": ">= 0.8"
786
+ }
787
+ },
788
+ "node_modules/request-ip": {
789
+ "version": "3.3.0",
790
+ "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz",
791
+ "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA=="
792
+ },
793
+ "node_modules/require-directory": {
794
+ "version": "2.1.1",
795
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
796
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
797
+ "dev": true,
798
+ "engines": {
799
+ "node": ">=0.10.0"
800
+ }
801
+ },
802
+ "node_modules/router": {
803
+ "version": "2.2.0",
804
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
805
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
806
+ "dependencies": {
807
+ "debug": "^4.4.0",
808
+ "depd": "^2.0.0",
809
+ "is-promise": "^4.0.0",
810
+ "parseurl": "^1.3.3",
811
+ "path-to-regexp": "^8.0.0"
812
+ },
813
+ "engines": {
814
+ "node": ">= 18"
815
+ }
816
+ },
817
+ "node_modules/rxjs": {
818
+ "version": "7.8.2",
819
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
820
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
821
+ "dev": true,
822
+ "dependencies": {
823
+ "tslib": "^2.1.0"
824
+ }
825
+ },
826
+ "node_modules/safe-buffer": {
827
+ "version": "5.2.1",
828
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
829
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
830
+ "funding": [
831
+ {
832
+ "type": "github",
833
+ "url": "https://github.com/sponsors/feross"
834
+ },
835
+ {
836
+ "type": "patreon",
837
+ "url": "https://www.patreon.com/feross"
838
+ },
839
+ {
840
+ "type": "consulting",
841
+ "url": "https://feross.org/support"
842
+ }
843
+ ]
844
+ },
845
+ "node_modules/safer-buffer": {
846
+ "version": "2.1.2",
847
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
848
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
849
+ },
850
+ "node_modules/send": {
851
+ "version": "1.2.0",
852
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
853
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
854
+ "dependencies": {
855
+ "debug": "^4.3.5",
856
+ "encodeurl": "^2.0.0",
857
+ "escape-html": "^1.0.3",
858
+ "etag": "^1.8.1",
859
+ "fresh": "^2.0.0",
860
+ "http-errors": "^2.0.0",
861
+ "mime-types": "^3.0.1",
862
+ "ms": "^2.1.3",
863
+ "on-finished": "^2.4.1",
864
+ "range-parser": "^1.2.1",
865
+ "statuses": "^2.0.1"
866
+ },
867
+ "engines": {
868
+ "node": ">= 18"
869
+ }
870
+ },
871
+ "node_modules/serve-static": {
872
+ "version": "2.2.0",
873
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
874
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
875
+ "dependencies": {
876
+ "encodeurl": "^2.0.0",
877
+ "escape-html": "^1.0.3",
878
+ "parseurl": "^1.3.3",
879
+ "send": "^1.2.0"
880
+ },
881
+ "engines": {
882
+ "node": ">= 18"
883
+ }
884
+ },
885
+ "node_modules/setprototypeof": {
886
+ "version": "1.2.0",
887
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
888
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
889
+ },
890
+ "node_modules/shell-quote": {
891
+ "version": "1.8.2",
892
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
893
+ "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
894
+ "dev": true,
895
+ "engines": {
896
+ "node": ">= 0.4"
897
+ },
898
+ "funding": {
899
+ "url": "https://github.com/sponsors/ljharb"
900
+ }
901
+ },
902
+ "node_modules/side-channel": {
903
+ "version": "1.1.0",
904
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
905
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
906
+ "dependencies": {
907
+ "es-errors": "^1.3.0",
908
+ "object-inspect": "^1.13.3",
909
+ "side-channel-list": "^1.0.0",
910
+ "side-channel-map": "^1.0.1",
911
+ "side-channel-weakmap": "^1.0.2"
912
+ },
913
+ "engines": {
914
+ "node": ">= 0.4"
915
+ },
916
+ "funding": {
917
+ "url": "https://github.com/sponsors/ljharb"
918
+ }
919
+ },
920
+ "node_modules/side-channel-list": {
921
+ "version": "1.0.0",
922
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
923
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
924
+ "dependencies": {
925
+ "es-errors": "^1.3.0",
926
+ "object-inspect": "^1.13.3"
927
+ },
928
+ "engines": {
929
+ "node": ">= 0.4"
930
+ },
931
+ "funding": {
932
+ "url": "https://github.com/sponsors/ljharb"
933
+ }
934
+ },
935
+ "node_modules/side-channel-map": {
936
+ "version": "1.0.1",
937
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
938
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
939
+ "dependencies": {
940
+ "call-bound": "^1.0.2",
941
+ "es-errors": "^1.3.0",
942
+ "get-intrinsic": "^1.2.5",
943
+ "object-inspect": "^1.13.3"
944
+ },
945
+ "engines": {
946
+ "node": ">= 0.4"
947
+ },
948
+ "funding": {
949
+ "url": "https://github.com/sponsors/ljharb"
950
+ }
951
+ },
952
+ "node_modules/side-channel-weakmap": {
953
+ "version": "1.0.2",
954
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
955
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
956
+ "dependencies": {
957
+ "call-bound": "^1.0.2",
958
+ "es-errors": "^1.3.0",
959
+ "get-intrinsic": "^1.2.5",
960
+ "object-inspect": "^1.13.3",
961
+ "side-channel-map": "^1.0.1"
962
+ },
963
+ "engines": {
964
+ "node": ">= 0.4"
965
+ },
966
+ "funding": {
967
+ "url": "https://github.com/sponsors/ljharb"
968
+ }
969
+ },
970
+ "node_modules/statuses": {
971
+ "version": "2.0.1",
972
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
973
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
974
+ "engines": {
975
+ "node": ">= 0.8"
976
+ }
977
+ },
978
+ "node_modules/string-width": {
979
+ "version": "4.2.3",
980
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
981
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
982
+ "dev": true,
983
+ "dependencies": {
984
+ "emoji-regex": "^8.0.0",
985
+ "is-fullwidth-code-point": "^3.0.0",
986
+ "strip-ansi": "^6.0.1"
987
+ },
988
+ "engines": {
989
+ "node": ">=8"
990
+ }
991
+ },
992
+ "node_modules/strip-ansi": {
993
+ "version": "6.0.1",
994
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
995
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
996
+ "dev": true,
997
+ "dependencies": {
998
+ "ansi-regex": "^5.0.1"
999
+ },
1000
+ "engines": {
1001
+ "node": ">=8"
1002
+ }
1003
+ },
1004
+ "node_modules/supports-color": {
1005
+ "version": "8.1.1",
1006
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
1007
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
1008
+ "dev": true,
1009
+ "dependencies": {
1010
+ "has-flag": "^4.0.0"
1011
+ },
1012
+ "engines": {
1013
+ "node": ">=10"
1014
+ },
1015
+ "funding": {
1016
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
1017
+ }
1018
+ },
1019
+ "node_modules/toidentifier": {
1020
+ "version": "1.0.1",
1021
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1022
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1023
+ "engines": {
1024
+ "node": ">=0.6"
1025
+ }
1026
+ },
1027
+ "node_modules/tree-kill": {
1028
+ "version": "1.2.2",
1029
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
1030
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
1031
+ "dev": true,
1032
+ "bin": {
1033
+ "tree-kill": "cli.js"
1034
+ }
1035
+ },
1036
+ "node_modules/tslib": {
1037
+ "version": "2.8.1",
1038
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1039
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1040
+ "dev": true
1041
+ },
1042
+ "node_modules/type-is": {
1043
+ "version": "2.0.1",
1044
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
1045
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
1046
+ "dependencies": {
1047
+ "content-type": "^1.0.5",
1048
+ "media-typer": "^1.1.0",
1049
+ "mime-types": "^3.0.0"
1050
+ },
1051
+ "engines": {
1052
+ "node": ">= 0.6"
1053
+ }
1054
+ },
1055
+ "node_modules/uid-safe": {
1056
+ "version": "2.1.5",
1057
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
1058
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
1059
+ "dependencies": {
1060
+ "random-bytes": "~1.0.0"
1061
+ },
1062
+ "engines": {
1063
+ "node": ">= 0.8"
1064
+ }
1065
+ },
1066
+ "node_modules/unpipe": {
1067
+ "version": "1.0.0",
1068
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1069
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1070
+ "engines": {
1071
+ "node": ">= 0.8"
1072
+ }
1073
+ },
1074
+ "node_modules/uuid": {
1075
+ "version": "8.3.2",
1076
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
1077
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
1078
+ "bin": {
1079
+ "uuid": "dist/bin/uuid"
1080
+ }
1081
+ },
1082
+ "node_modules/vary": {
1083
+ "version": "1.1.2",
1084
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1085
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1086
+ "engines": {
1087
+ "node": ">= 0.8"
1088
+ }
1089
+ },
1090
+ "node_modules/wrap-ansi": {
1091
+ "version": "7.0.0",
1092
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1093
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1094
+ "dev": true,
1095
+ "dependencies": {
1096
+ "ansi-styles": "^4.0.0",
1097
+ "string-width": "^4.1.0",
1098
+ "strip-ansi": "^6.0.0"
1099
+ },
1100
+ "engines": {
1101
+ "node": ">=10"
1102
+ },
1103
+ "funding": {
1104
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1105
+ }
1106
+ },
1107
+ "node_modules/wrappy": {
1108
+ "version": "1.0.2",
1109
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1110
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
1111
+ },
1112
+ "node_modules/y18n": {
1113
+ "version": "5.0.8",
1114
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
1115
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
1116
+ "dev": true,
1117
+ "engines": {
1118
+ "node": ">=10"
1119
+ }
1120
+ },
1121
+ "node_modules/yargs": {
1122
+ "version": "17.7.2",
1123
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
1124
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
1125
+ "dev": true,
1126
+ "dependencies": {
1127
+ "cliui": "^8.0.1",
1128
+ "escalade": "^3.1.1",
1129
+ "get-caller-file": "^2.0.5",
1130
+ "require-directory": "^2.1.1",
1131
+ "string-width": "^4.2.3",
1132
+ "y18n": "^5.0.5",
1133
+ "yargs-parser": "^21.1.1"
1134
+ },
1135
+ "engines": {
1136
+ "node": ">=12"
1137
+ }
1138
+ },
1139
+ "node_modules/yargs-parser": {
1140
+ "version": "21.1.1",
1141
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
1142
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
1143
+ "dev": true,
1144
+ "engines": {
1145
+ "node": ">=12"
1146
+ }
1147
+ }
1148
+ }
1149
+ }
package.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "leader",
3
+ "version": "1.0.0",
4
+ "main": "server.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1",
7
+ "start": "node server.js",
8
+ "start-admin": "node admin_server.js",
9
+ "start-both": "concurrently \"npm run start\" \"npm run start-admin\"",
10
+ "archive-week": "node scripts/archive_week.js",
11
+ "start-scheduler": "node scheduler.js"
12
+ },
13
+ "keywords": [],
14
+ "author": "",
15
+ "license": "ISC",
16
+ "dependencies": {
17
+ "body-parser": "^2.2.0",
18
+ "express": "^5.1.0",
19
+ "express-session": "^1.17.3",
20
+ "moment": "^2.30.1",
21
+ "node-cron": "^3.0.3",
22
+ "request-ip": "^3.3.0"
23
+ },
24
+ "description": "",
25
+ "devDependencies": {
26
+ "concurrently": "^9.1.2"
27
+ }
28
+ }
public/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ node_modules/
public/archives.js ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // archives.js - Handles the archived leaderboards functionality
2
+
3
+ document.addEventListener('DOMContentLoaded', () => {
4
+ // DOM Elements
5
+ const currentViewBtn = document.getElementById('current-view-btn');
6
+ const archivesViewBtn = document.getElementById('archives-view-btn');
7
+ const currentView = document.getElementById('current-view');
8
+ const archivesView = document.getElementById('archives-view');
9
+ const weekSelect = document.getElementById('week-select');
10
+ const startDateInput = document.getElementById('start-date');
11
+ const endDateInput = document.getElementById('end-date');
12
+ const searchArchivesBtn = document.getElementById('search-archives-btn');
13
+ const archiveResults = document.getElementById('archive-results');
14
+
15
+ // View toggle functionality
16
+ currentViewBtn.addEventListener('click', () => {
17
+ currentViewBtn.classList.add('active');
18
+ archivesViewBtn.classList.remove('active');
19
+ currentView.classList.remove('hidden');
20
+ archivesView.classList.add('hidden');
21
+ });
22
+
23
+ archivesViewBtn.addEventListener('click', () => {
24
+ archivesViewBtn.classList.add('active');
25
+ currentViewBtn.classList.remove('active');
26
+ archivesView.classList.remove('hidden');
27
+ currentView.classList.add('hidden');
28
+
29
+ // Load archived weeks when switching to archives view
30
+ loadArchivedWeeks();
31
+ });
32
+
33
+ // Load archived weeks for the dropdown
34
+ async function loadArchivedWeeks() {
35
+ try {
36
+ const response = await fetch('/api/archives/weeks');
37
+ if (!response.ok) throw new Error('Failed to fetch archived weeks');
38
+
39
+ const weeks = await response.json();
40
+
41
+ // Clear previous options except the default one
42
+ weekSelect.innerHTML = '<option value="">Select a week...</option>';
43
+
44
+ if (weeks.length === 0) {
45
+ const option = document.createElement('option');
46
+ option.disabled = true;
47
+ option.textContent = 'No archives available';
48
+ weekSelect.appendChild(option);
49
+ } else {
50
+ weeks.forEach(weekId => {
51
+ const option = document.createElement('option');
52
+ option.value = weekId;
53
+ option.textContent = weekId;
54
+ weekSelect.appendChild(option);
55
+ });
56
+ }
57
+ } catch (error) {
58
+ console.error('Error loading archived weeks:', error);
59
+ archiveResults.innerHTML = `<p class="error">Error loading archived weeks: ${error.message}</p>`;
60
+ }
61
+ }
62
+
63
+ // Load archived data for a specific week
64
+ async function loadArchivedWeek(weekId) {
65
+ try {
66
+ const response = await fetch(`/api/archives/week/${weekId}`);
67
+ if (!response.ok) throw new Error('Failed to fetch archived data');
68
+
69
+ const archive = await response.json();
70
+ displayArchivedData(archive);
71
+ } catch (error) {
72
+ console.error('Error loading archived week:', error);
73
+ archiveResults.innerHTML = `<p class="error">Error loading archived data: ${error.message}</p>`;
74
+ }
75
+ }
76
+
77
+ // Load archived data for a date range
78
+ async function loadArchivedRange(startDate, endDate) {
79
+ try {
80
+ const response = await fetch(`/api/archives/range?startDate=${startDate}&endDate=${endDate}`);
81
+ if (!response.ok) throw new Error('Failed to fetch archived data');
82
+
83
+ const archives = await response.json();
84
+
85
+ if (archives.length === 0) {
86
+ archiveResults.innerHTML = '<p class="no-archives">No archives found for the specified date range</p>';
87
+ return;
88
+ }
89
+
90
+ // Display the first archive in the range
91
+ displayArchivedData(archives[0], archives.length > 1 ? archives.length : null);
92
+ } catch (error) {
93
+ console.error('Error loading archived range:', error);
94
+ archiveResults.innerHTML = `<p class="error">Error loading archived data: ${error.message}</p>`;
95
+ }
96
+ }
97
+
98
+ // Display archived data
99
+ function displayArchivedData(archive, totalArchives = null) {
100
+ // Create archive info section
101
+ let html = `
102
+ <div class="archive-week-info">
103
+ <p><strong>Week ID:</strong> ${archive.weekId}</p>
104
+ <p><strong>Period:</strong> ${archive.startDate} to ${archive.endDate}</p>
105
+ <p><strong>Archived:</strong> ${new Date(archive.archivedAt).toLocaleString()}</p>
106
+ ${totalArchives ? `<p><strong>Note:</strong> Showing 1 of ${totalArchives} archives in the selected range</p>` : ''}
107
+ </div>
108
+ `;
109
+
110
+ // Create sections for each category
111
+ const categories = [
112
+ { key: "6gb", label: "6GB VRAM" },
113
+ { key: "12gb", label: "12GB VRAM" },
114
+ { key: "16gb", label: "16GB VRAM" },
115
+ { key: "24gb", label: "24GB VRAM" },
116
+ { key: "48gb", label: "48GB VRAM" },
117
+ { key: "72gb", label: "72GB VRAM" },
118
+ { key: "96gb", label: "96GB VRAM" }
119
+ ];
120
+
121
+ categories.forEach(category => {
122
+ const entries = archive.data[category.key] || [];
123
+
124
+ if (entries.length === 0) return; // Skip empty categories
125
+
126
+ const sortedEntries = [...entries].sort((a, b) => b.votes - a.votes);
127
+ const totalVotes = sortedEntries.reduce((sum, entry) => sum + entry.votes, 0);
128
+
129
+ html += `<div class="archive-category">
130
+ <h3>${category.label}</h3>
131
+ <div class="archive-entries">`;
132
+
133
+ sortedEntries.forEach((entry, index) => {
134
+ const percentage = totalVotes > 0 ? Math.round((entry.votes / totalVotes) * 100) : 0;
135
+ const rankClass = index < 3 ? `rank-${index + 1}` : '';
136
+
137
+ html += `
138
+ <div class="poll-item">
139
+ <div class="poll-item-header">
140
+ <div class="poll-item-name">
141
+ ${rankClass ? `<span class="rank ${rankClass}">${index + 1}</span>` : `<span class="rank">${index + 1}</span>`}
142
+ ${entry.name}
143
+ </div>
144
+ <div class="poll-item-votes">${formatNumber(entry.votes)} votes</div>
145
+ </div>
146
+ <div class="progress-container">
147
+ <div class="progress-bar" style="width: ${percentage}%"></div>
148
+ </div>
149
+ <div class="poll-item-footer">
150
+ <span class="vote-percentage">${percentage}%</span>
151
+ </div>
152
+ </div>
153
+ `;
154
+ });
155
+
156
+ html += `</div></div>`;
157
+ });
158
+
159
+ archiveResults.innerHTML = html;
160
+ }
161
+
162
+ // Format number helper (same as in script.js)
163
+ function formatNumber(num) {
164
+ if (num >= 1000000) {
165
+ return (num / 1000000).toFixed(1) + 'M';
166
+ } else if (num >= 1000) {
167
+ return (num / 1000).toFixed(1) + 'K';
168
+ }
169
+ return num.toString();
170
+ }
171
+
172
+ // Event listeners
173
+ weekSelect.addEventListener('change', () => {
174
+ const weekId = weekSelect.value;
175
+ if (weekId) {
176
+ loadArchivedWeek(weekId);
177
+ } else {
178
+ archiveResults.innerHTML = '<p class="no-archives">Select a week or date range to view archived leaderboards</p>';
179
+ }
180
+ });
181
+
182
+ searchArchivesBtn.addEventListener('click', () => {
183
+ const startDate = startDateInput.value;
184
+ const endDate = endDateInput.value;
185
+
186
+ if (!startDate || !endDate) {
187
+ alert('Please select both start and end dates');
188
+ return;
189
+ }
190
+
191
+ if (new Date(startDate) > new Date(endDate)) {
192
+ alert('Start date must be before end date');
193
+ return;
194
+ }
195
+
196
+ loadArchivedRange(startDate, endDate);
197
+ });
198
+ });
public/index.html ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GPU Leaderboards by VRAM</title>
7
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
8
+ <link rel="stylesheet" href="style.css" />
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <header>
13
+ <h1>LLM Leaderboards by VRAM</h1>
14
+ <p class="subtitle">Vote for the best LLMs in each VRAM category</p>
15
+ <div class="view-toggle">
16
+ <button id="current-view-btn" class="view-btn active">Current Leaderboard</button>
17
+ <button id="archives-view-btn" class="view-btn">Archives</button>
18
+ </div>
19
+ </header>
20
+
21
+ <div id="current-view">
22
+ <div class="tabs-container">
23
+ <div class="tabs" id="category-tabs">
24
+ <!-- Category tabs will be rendered here by script.js -->
25
+ </div>
26
+ </div>
27
+
28
+ <div id="leaderboards">
29
+ <!-- Leaderboards will be rendered here by script.js -->
30
+ </div>
31
+ </div>
32
+
33
+ <div id="archives-view" class="hidden">
34
+ <div class="archives-container">
35
+ <h2>Archived Leaderboards</h2>
36
+
37
+ <div class="archives-controls">
38
+ <div class="archives-selector">
39
+ <label for="week-select">Select Week:</label>
40
+ <select id="week-select">
41
+ <option value="">Select a week...</option>
42
+ <!-- Options will be populated by JavaScript -->
43
+ </select>
44
+ </div>
45
+
46
+ <div class="date-range-selector">
47
+ <h3>Search by Date Range</h3>
48
+ <div class="date-inputs">
49
+ <div class="date-input">
50
+ <label for="start-date">Start Date:</label>
51
+ <input type="date" id="start-date">
52
+ </div>
53
+ <div class="date-input">
54
+ <label for="end-date">End Date:</label>
55
+ <input type="date" id="end-date">
56
+ </div>
57
+ <button id="search-archives-btn" class="search-btn">Search</button>
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <div id="archive-results">
63
+ <!-- Archive results will be displayed here -->
64
+ <p class="no-archives">Select a week or date range to view archived leaderboards</p>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="info-panel">
70
+ <div class="info-item">
71
+ <i class="fas fa-info-circle"></i>
72
+ <span>One vote per IP address per category</span>
73
+ </div>
74
+ <div class="info-item">
75
+ <i class="fas fa-sync-alt"></i>
76
+ <span>Real-time updates every 10 seconds</span>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <script src="script.js"></script>
82
+ <script src="archives.js"></script>
83
+ </body>
84
+ </html>
public/script.js ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ---- VRAM categories ----
2
+ const CATEGORIES = [
3
+ { key: "6gb", label: "6GB VRAM" },
4
+ { key: "12gb", label: "12GB VRAM" },
5
+ { key: "16gb", label: "16GB VRAM" },
6
+ { key: "24gb", label: "24GB VRAM" },
7
+ { key: "48gb", label: "48GB VRAM" },
8
+ { key: "72gb", label: "72GB VRAM" },
9
+ { key: "96gb", label: "96GB VRAM" }
10
+ ];
11
+
12
+ // ---- State management ----
13
+ const state = {
14
+ activeCategory: CATEGORIES[0].key,
15
+ sortOption: 'votes', // 'votes', 'newest', 'oldest'
16
+ data: {},
17
+ lastVotedIds: {},
18
+ refreshInterval: null,
19
+ pollInterval: 10000, // 10 seconds
20
+ };
21
+
22
+ // ---- helpers ----
23
+ async function api(url, data) {
24
+ const opts = data ? {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(data)
28
+ } : {};
29
+ const res = await fetch(url, opts);
30
+ const json = await res.json();
31
+ if (!res.ok) throw new Error(json.error || 'Server error');
32
+ return json;
33
+ }
34
+
35
+ function calculatePercentage(votes, totalVotes) {
36
+ if (totalVotes === 0) return 0;
37
+ return Math.round((votes / totalVotes) * 100);
38
+ }
39
+
40
+ function getTotalVotes(entries) {
41
+ return entries.reduce((sum, entry) => sum + entry.votes, 0);
42
+ }
43
+
44
+ function formatNumber(num) {
45
+ if (num >= 1000000) {
46
+ return (num / 1000000).toFixed(1) + 'M';
47
+ } else if (num >= 1000) {
48
+ return (num / 1000).toFixed(1) + 'K';
49
+ }
50
+ return num.toString();
51
+ }
52
+
53
+ function sortEntries(entries, sortOption) {
54
+ const entriesCopy = [...entries];
55
+
56
+ if (sortOption === 'votes') {
57
+ return entriesCopy.sort((a, b) => {
58
+ // First sort by votes (descending)
59
+ const votesDiff = b.votes - a.votes;
60
+ if (votesDiff !== 0) return votesDiff;
61
+
62
+ // If votes are equal, sort by id (newest first)
63
+ return parseInt(b.id) - parseInt(a.id);
64
+ });
65
+ } else if (sortOption === 'newest') {
66
+ return entriesCopy.sort((a, b) => parseInt(b.id) - parseInt(a.id));
67
+ } else if (sortOption === 'oldest') {
68
+ return entriesCopy.sort((a, b) => parseInt(a.id) - parseInt(b.id));
69
+ }
70
+
71
+ // Default to votes sorting
72
+ return entriesCopy.sort((a, b) => b.votes - a.votes);
73
+ }
74
+
75
+ // ---- rendering ----
76
+ function createCategoryTabs() {
77
+ const tabsContainer = document.getElementById('category-tabs');
78
+ tabsContainer.innerHTML = '';
79
+
80
+ CATEGORIES.forEach(category => {
81
+ const tab = document.createElement('div');
82
+ tab.className = `tab ${category.key === state.activeCategory ? 'active' : ''}`;
83
+ tab.setAttribute('data-category', category.key);
84
+ tab.textContent = category.label;
85
+ tabsContainer.appendChild(tab);
86
+ });
87
+ }
88
+
89
+ function createLeaderboardSection(category) {
90
+ const section = document.createElement('section');
91
+ section.className = `leaderboard-section ${category.key === state.activeCategory ? 'active' : ''}`;
92
+ section.id = `section-${category.key}`;
93
+
94
+ section.innerHTML = `
95
+ <div class="section-header">
96
+ <h2 class="section-title">${category.label} Leaderboard</h2>
97
+ <div class="sort-options">
98
+ <button class="sort-option ${state.sortOption === 'votes' ? 'active' : ''}" data-sort="votes">Most Votes</button>
99
+ <button class="sort-option ${state.sortOption === 'newest' ? 'active' : ''}" data-sort="newest">Newest</button>
100
+ <button class="sort-option ${state.sortOption === 'oldest' ? 'active' : ''}" data-sort="oldest">Oldest</button>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="poll-items" id="poll-items-${category.key}">
105
+ <!-- Poll items will be rendered here -->
106
+ </div>
107
+
108
+ <div class="add-form-container">
109
+ <form class="add-form" data-category="${category.key}">
110
+ <div class="input-container">
111
+ <input class="add-input" type="text" placeholder="Add a new entry..." required autocomplete="off" />
112
+ <div class="validation-indicator"></div>
113
+ <div class="dropdown-container">
114
+ <div class="dropdown-loading hidden">
115
+ <div class="spinner"></div>
116
+ <span>Loading results...</span>
117
+ </div>
118
+ <ul class="dropdown-results hidden"></ul>
119
+ </div>
120
+ </div>
121
+ <button type="submit" class="add-btn" disabled>Add & Vote</button>
122
+ <span class="error add-error"></span>
123
+ </form>
124
+ </div>
125
+ `;
126
+
127
+ return section;
128
+ }
129
+
130
+ function renderPollItems(category) {
131
+ const container = document.getElementById(`poll-items-${category}`);
132
+ if (!container) return;
133
+
134
+ container.innerHTML = '';
135
+ const entries = state.data[category] || [];
136
+ if (entries.length === 0) {
137
+ container.innerHTML = '<p class="no-entries">No entries yet. Be the first to add one!</p>';
138
+ return;
139
+ }
140
+
141
+ const sortedEntries = sortEntries(entries, state.sortOption);
142
+ const totalVotes = getTotalVotes(sortedEntries);
143
+
144
+ sortedEntries.forEach((entry, index) => {
145
+ const percentage = calculatePercentage(entry.votes, totalVotes);
146
+ const isVoted = state.lastVotedIds[category] === entry.id;
147
+ const rankClass = index < 3 ? `rank-${index + 1}` : '';
148
+
149
+ const pollItem = document.createElement('div');
150
+ pollItem.className = `poll-item ${isVoted ? 'voted' : ''}`;
151
+ pollItem.setAttribute('data-id', entry.id);
152
+
153
+ if (state.lastVotedIds[category] === entry.id) {
154
+ pollItem.classList.add('highlight');
155
+ // Remove highlight class after animation completes
156
+ setTimeout(() => {
157
+ pollItem.classList.remove('highlight');
158
+ }, 1000);
159
+ }
160
+
161
+ pollItem.innerHTML = `
162
+ <div class="poll-item-header">
163
+ <div class="poll-item-name">
164
+ ${rankClass ? `<span class="rank ${rankClass}">${index + 1}</span>` : `<span class="rank">${index + 1}</span>`}
165
+ ${entry.name}
166
+ </div>
167
+ <div class="poll-item-votes">${formatNumber(entry.votes)} votes</div>
168
+ </div>
169
+ <div class="progress-container">
170
+ <div class="progress-bar" style="width: ${percentage}%"></div>
171
+ </div>
172
+ <div class="poll-item-footer">
173
+ <span class="vote-percentage">${percentage}%</span>
174
+ <button class="vote-btn ${isVoted ? 'voted' : ''}"
175
+ data-id="${entry.id}"
176
+ data-category="${category}"
177
+ ${isVoted ? 'disabled' : ''}>
178
+ ${isVoted ? 'Voted' : 'Vote'}
179
+ </button>
180
+ </div>
181
+ `;
182
+
183
+ container.appendChild(pollItem);
184
+ });
185
+ }
186
+
187
+ async function refreshData(category, highlightChanges = false) {
188
+ try {
189
+ const entries = await api(`/api/entries?category=${category}`);
190
+
191
+ // Store previous data for comparison if highlighting changes
192
+ const prevEntries = state.data[category] || [];
193
+
194
+ // Update state
195
+ state.data[category] = entries;
196
+
197
+ // Render the updated data
198
+ renderPollItems(category);
199
+
200
+ // Highlight changes if needed
201
+ if (highlightChanges && prevEntries.length > 0) {
202
+ entries.forEach(entry => {
203
+ const prevEntry = prevEntries.find(e => e.id === entry.id);
204
+ if (prevEntry && prevEntry.votes !== entry.votes) {
205
+ const pollItem = document.querySelector(`.poll-item[data-id="${entry.id}"]`);
206
+ if (pollItem) {
207
+ pollItem.classList.add('highlight');
208
+ setTimeout(() => {
209
+ pollItem.classList.remove('highlight');
210
+ }, 1000);
211
+ }
212
+ }
213
+ });
214
+ }
215
+ } catch (err) {
216
+ console.error(`Error refreshing ${category}:`, err);
217
+ }
218
+ }
219
+
220
+ function setupPolling() {
221
+ // Clear any existing interval
222
+ if (state.refreshInterval) {
223
+ clearInterval(state.refreshInterval);
224
+ }
225
+
226
+ // Set up new polling interval
227
+ state.refreshInterval = setInterval(() => {
228
+ refreshData(state.activeCategory, true);
229
+ }, state.pollInterval);
230
+ }
231
+
232
+ // ---- event handlers ----
233
+ function handleCategoryChange(category) {
234
+ // Update active category
235
+ state.activeCategory = category;
236
+
237
+ // Update UI
238
+ document.querySelectorAll('.tab').forEach(tab => {
239
+ tab.classList.toggle('active', tab.getAttribute('data-category') === category);
240
+ });
241
+
242
+ document.querySelectorAll('.leaderboard-section').forEach(section => {
243
+ section.classList.toggle('active', section.id === `section-${category}`);
244
+ });
245
+
246
+ // Refresh data for the new category
247
+ refreshData(category);
248
+ }
249
+
250
+ function handleSortChange(sortOption) {
251
+ // Update sort option
252
+ state.sortOption = sortOption;
253
+
254
+ // Update UI
255
+ document.querySelectorAll('.sort-option').forEach(btn => {
256
+ btn.classList.remove('active');
257
+ if (btn.getAttribute('data-sort') === sortOption) {
258
+ btn.classList.add('active');
259
+ }
260
+ });
261
+
262
+ // Re-render with new sort
263
+ refreshData(state.activeCategory, false).then(() => {
264
+ renderPollItems(state.activeCategory);
265
+ });
266
+ }
267
+
268
+ // Hugging Face API validation
269
+ let debounceTimer;
270
+ let selectedModel = null;
271
+
272
+ async function validateWithHuggingFace(query) {
273
+ if (!query || query.length < 2) return [];
274
+
275
+ try {
276
+ // Use our server-side proxy endpoint to avoid CORS issues
277
+ const response = await fetch(`/api/huggingface/models?query=${encodeURIComponent(query)}`);
278
+
279
+ if (!response.ok) {
280
+ const errorData = await response.json();
281
+ throw new Error(errorData.error || 'Failed to fetch from Hugging Face API');
282
+ }
283
+
284
+ return await response.json();
285
+ } catch (error) {
286
+ console.error('Hugging Face API error:', error);
287
+ return [];
288
+ }
289
+ }
290
+
291
+ function setupModelValidation(form) {
292
+ const input = form.querySelector('.add-input');
293
+ const dropdownContainer = form.querySelector('.dropdown-container');
294
+ const dropdownResults = form.querySelector('.dropdown-results');
295
+ const dropdownLoading = form.querySelector('.dropdown-loading');
296
+ const submitBtn = form.querySelector('.add-btn');
297
+ const validationIndicator = form.querySelector('.validation-indicator');
298
+
299
+ input.addEventListener('input', function() {
300
+ const query = this.value.trim();
301
+ selectedModel = null;
302
+
303
+ // Reset validation state
304
+ validationIndicator.className = 'validation-indicator';
305
+ submitBtn.disabled = true;
306
+
307
+ // Clear previous results
308
+ dropdownResults.innerHTML = '';
309
+ dropdownResults.classList.add('hidden');
310
+
311
+ if (query.length < 2) return;
312
+
313
+ // Show loading indicator
314
+ dropdownLoading.classList.remove('hidden');
315
+
316
+ // Debounce API calls
317
+ clearTimeout(debounceTimer);
318
+ debounceTimer = setTimeout(async () => {
319
+ try {
320
+ const results = await validateWithHuggingFace(query);
321
+
322
+ // Hide loading indicator
323
+ dropdownLoading.classList.add('hidden');
324
+
325
+ if (results.length === 0) {
326
+ dropdownResults.innerHTML = '<li class="no-results">No matching models found</li>';
327
+ dropdownResults.classList.remove('hidden');
328
+ return;
329
+ }
330
+
331
+ // Populate dropdown with results
332
+ results.forEach(model => {
333
+ const li = document.createElement('li');
334
+ li.className = 'dropdown-item';
335
+
336
+ // Create a more informative display with model name and author
337
+ const displayName = model.modelId || model.id || model.name;
338
+ const authorInfo = model.author && model.author !== 'Unknown' ? ` by ${model.author}` : '';
339
+
340
+ li.innerHTML = `
341
+ <div class="dropdown-item-name">${displayName}</div>
342
+ ${authorInfo ? `<div class="dropdown-item-author">${authorInfo}</div>` : ''}
343
+ `;
344
+
345
+ li.addEventListener('click', () => {
346
+ input.value = displayName;
347
+ selectedModel = model;
348
+ dropdownResults.classList.add('hidden');
349
+
350
+ // Show validation success
351
+ validationIndicator.className = 'validation-indicator valid';
352
+ submitBtn.disabled = false;
353
+ });
354
+ dropdownResults.appendChild(li);
355
+ });
356
+
357
+ dropdownResults.classList.remove('hidden');
358
+ } catch (error) {
359
+ console.error('Validation error:', error);
360
+ dropdownLoading.classList.add('hidden');
361
+
362
+ // Show validation error
363
+ validationIndicator.className = 'validation-indicator invalid';
364
+ }
365
+ }, 300);
366
+ });
367
+
368
+ // Hide dropdown when clicking outside
369
+ document.addEventListener('click', (e) => {
370
+ if (!dropdownContainer.contains(e.target)) {
371
+ dropdownResults.classList.add('hidden');
372
+ }
373
+ });
374
+
375
+ // Prevent form submission when pressing Enter in the input field
376
+ input.addEventListener('keydown', (e) => {
377
+ if (e.key === 'Enter' && !selectedModel) {
378
+ e.preventDefault();
379
+ }
380
+ });
381
+ }
382
+
383
+ async function handleAddEntry(form) {
384
+ const category = form.getAttribute('data-category');
385
+ const input = form.querySelector('.add-input');
386
+ const errorSpan = form.querySelector('.error');
387
+ const name = input.value.trim();
388
+
389
+ errorSpan.textContent = '';
390
+
391
+ if (!name) {
392
+ errorSpan.textContent = 'Please enter a name';
393
+ return;
394
+ }
395
+
396
+ if (!selectedModel) {
397
+ errorSpan.textContent = 'Please select a validated model from the dropdown';
398
+ return;
399
+ }
400
+
401
+ try {
402
+ const entry = await api('/api/add', { name, category });
403
+ input.value = '';
404
+ selectedModel = null;
405
+
406
+ // Reset validation state
407
+ form.querySelector('.validation-indicator').className = 'validation-indicator';
408
+ form.querySelector('.add-btn').disabled = true;
409
+
410
+ // Update state
411
+ state.lastVotedIds[category] = entry.id;
412
+
413
+ // Refresh data
414
+ await refreshData(category);
415
+ } catch (err) {
416
+ errorSpan.textContent = err.message;
417
+ }
418
+ }
419
+
420
+ async function handleVote(btn) {
421
+ const id = btn.getAttribute('data-id');
422
+ const category = btn.getAttribute('data-category');
423
+
424
+ try {
425
+ await api('/api/vote', { id, category });
426
+
427
+ // Update state
428
+ state.lastVotedIds[category] = id;
429
+
430
+ // Refresh data
431
+ await refreshData(category);
432
+ } catch (err) {
433
+ alert(err.message);
434
+ }
435
+ }
436
+
437
+ // ---- main ----
438
+ window.addEventListener('DOMContentLoaded', () => {
439
+ const leaderboardsDiv = document.getElementById('leaderboards');
440
+ leaderboardsDiv.innerHTML = '';
441
+
442
+ // Create category tabs
443
+ createCategoryTabs();
444
+
445
+ // Render all leaderboard sections
446
+ CATEGORIES.forEach(cat => {
447
+ const section = createLeaderboardSection(cat);
448
+ leaderboardsDiv.appendChild(section);
449
+ });
450
+
451
+ // Set up model validation for all forms
452
+ document.querySelectorAll('.add-form').forEach(form => {
453
+ setupModelValidation(form);
454
+ });
455
+
456
+ // Initial data load
457
+ CATEGORIES.forEach(cat => {
458
+ refreshData(cat.key);
459
+ });
460
+
461
+ // Set up polling for real-time updates
462
+ setupPolling();
463
+
464
+ // Tab click handler
465
+ document.getElementById('category-tabs').addEventListener('click', e => {
466
+ if (e.target.classList.contains('tab')) {
467
+ const category = e.target.getAttribute('data-category');
468
+ handleCategoryChange(category);
469
+ }
470
+ });
471
+
472
+ // Sort option click handler
473
+ leaderboardsDiv.addEventListener('click', e => {
474
+ if (e.target.classList.contains('sort-option')) {
475
+ const sortOption = e.target.getAttribute('data-sort');
476
+ if (sortOption && sortOption !== state.sortOption) {
477
+ handleSortChange(sortOption);
478
+ }
479
+ }
480
+ });
481
+
482
+ // Add entry form handler
483
+ leaderboardsDiv.addEventListener('submit', async e => {
484
+ if (e.target.classList.contains('add-form')) {
485
+ e.preventDefault();
486
+ await handleAddEntry(e.target);
487
+ }
488
+ });
489
+
490
+ // Vote button handler
491
+ leaderboardsDiv.addEventListener('click', async e => {
492
+ if (e.target.classList.contains('vote-btn') && !e.target.disabled) {
493
+ await handleVote(e.target);
494
+ }
495
+ });
496
+ });
497
+
498
+ // Clean up polling on page unload
499
+ window.addEventListener('beforeunload', () => {
500
+ if (state.refreshInterval) {
501
+ clearInterval(state.refreshInterval);
502
+ }
503
+ });
public/style.css ADDED
@@ -0,0 +1,642 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #065fd4;
3
+ --primary-hover: #0356c3;
4
+ --secondary-color: #f8f8f8;
5
+ --text-color: #0f0f0f;
6
+ --text-secondary: #606060;
7
+ --border-color: #e0e0e0;
8
+ --gold: #ffd700;
9
+ --silver: #c0c0c0;
10
+ --bronze: #cd7f32;
11
+ --background: #f9f9f9;
12
+ --card-bg: #ffffff;
13
+ --shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
14
+ --animation-speed: 0.3s;
15
+ }
16
+
17
+ * {
18
+ box-sizing: border-box;
19
+ margin: 0;
20
+ padding: 0;
21
+ }
22
+
23
+ body {
24
+ font-family: 'Roboto', Arial, sans-serif;
25
+ background: var(--background);
26
+ color: var(--text-color);
27
+ line-height: 1.6;
28
+ }
29
+
30
+ .container {
31
+ max-width: 1200px;
32
+ margin: 40px auto;
33
+ background: var(--card-bg);
34
+ padding: 24px 32px;
35
+ border-radius: 12px;
36
+ box-shadow: var(--shadow);
37
+ }
38
+
39
+ header {
40
+ text-align: center;
41
+ margin-bottom: 24px;
42
+ padding-bottom: 16px;
43
+ border-bottom: 1px solid var(--border-color);
44
+ }
45
+
46
+ h1 {
47
+ font-size: 28px;
48
+ margin-bottom: 8px;
49
+ color: var(--text-color);
50
+ }
51
+
52
+ .subtitle {
53
+ color: var(--text-secondary);
54
+ font-size: 16px;
55
+ }
56
+
57
+ /* Tabs styling */
58
+ .tabs-container {
59
+ margin-bottom: 24px;
60
+ overflow-x: auto;
61
+ }
62
+
63
+ .tabs {
64
+ display: flex;
65
+ border-bottom: 1px solid var(--border-color);
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .tab {
70
+ padding: 12px 16px;
71
+ cursor: pointer;
72
+ font-weight: 500;
73
+ color: var(--text-secondary);
74
+ position: relative;
75
+ transition: color var(--animation-speed);
76
+ }
77
+
78
+ .tab:hover {
79
+ color: var(--primary-color);
80
+ }
81
+
82
+ .tab.active {
83
+ color: var(--primary-color);
84
+ }
85
+
86
+ .tab.active::after {
87
+ content: '';
88
+ position: absolute;
89
+ bottom: -1px;
90
+ left: 0;
91
+ width: 100%;
92
+ height: 3px;
93
+ background-color: var(--primary-color);
94
+ }
95
+
96
+ /* Leaderboard styling */
97
+ .leaderboard-section {
98
+ display: none;
99
+ margin-bottom: 32px;
100
+ }
101
+
102
+ .leaderboard-section.active {
103
+ display: block;
104
+ animation: fadeIn 0.3s ease-in-out;
105
+ }
106
+
107
+ @keyframes fadeIn {
108
+ from { opacity: 0; }
109
+ to { opacity: 1; }
110
+ }
111
+
112
+ .section-header {
113
+ display: flex;
114
+ justify-content: space-between;
115
+ align-items: center;
116
+ margin-bottom: 16px;
117
+ }
118
+
119
+ .section-title {
120
+ font-size: 20px;
121
+ font-weight: 500;
122
+ }
123
+
124
+ .sort-options {
125
+ display: flex;
126
+ gap: 8px;
127
+ }
128
+
129
+ .sort-option {
130
+ background: none;
131
+ border: none;
132
+ color: var(--text-secondary);
133
+ cursor: pointer;
134
+ font-size: 14px;
135
+ padding: 4px 8px;
136
+ border-radius: 16px;
137
+ transition: all var(--animation-speed);
138
+ }
139
+
140
+ .sort-option:hover {
141
+ background: var(--secondary-color);
142
+ }
143
+
144
+ .sort-option.active {
145
+ background: var(--secondary-color);
146
+ color: var(--primary-color);
147
+ font-weight: 500;
148
+ }
149
+
150
+ /* Poll items */
151
+ .poll-items {
152
+ margin-top: 16px;
153
+ }
154
+
155
+ .poll-item {
156
+ background: var(--card-bg);
157
+ border-radius: 8px;
158
+ padding: 16px;
159
+ margin-bottom: 12px;
160
+ border: 1px solid var(--border-color);
161
+ transition: transform 0.2s, box-shadow 0.2s;
162
+ }
163
+
164
+ .poll-item:hover {
165
+ transform: translateY(-2px);
166
+ box-shadow: var(--shadow);
167
+ }
168
+
169
+ .poll-item.highlight {
170
+ animation: highlight 1s ease-in-out;
171
+ }
172
+
173
+ @keyframes highlight {
174
+ 0%, 100% { background-color: var(--card-bg); }
175
+ 50% { background-color: rgba(6, 95, 212, 0.1); }
176
+ }
177
+
178
+ .poll-item-header {
179
+ display: flex;
180
+ justify-content: space-between;
181
+ align-items: center;
182
+ margin-bottom: 8px;
183
+ }
184
+
185
+ .poll-item-name {
186
+ font-weight: 500;
187
+ font-size: 16px;
188
+ display: flex;
189
+ align-items: center;
190
+ }
191
+
192
+ .rank {
193
+ display: inline-block;
194
+ width: 24px;
195
+ height: 24px;
196
+ border-radius: 50%;
197
+ text-align: center;
198
+ line-height: 24px;
199
+ font-size: 14px;
200
+ margin-right: 8px;
201
+ font-weight: bold;
202
+ }
203
+
204
+ .rank-1 {
205
+ background-color: var(--gold);
206
+ color: #000;
207
+ }
208
+
209
+ .rank-2 {
210
+ background-color: var(--silver);
211
+ color: #000;
212
+ }
213
+
214
+ .rank-3 {
215
+ background-color: var(--bronze);
216
+ color: #000;
217
+ }
218
+
219
+ .poll-item-votes {
220
+ font-size: 14px;
221
+ color: var(--text-secondary);
222
+ }
223
+
224
+ .progress-container {
225
+ height: 8px;
226
+ background-color: var(--secondary-color);
227
+ border-radius: 4px;
228
+ overflow: hidden;
229
+ margin-bottom: 8px;
230
+ }
231
+
232
+ .progress-bar {
233
+ height: 100%;
234
+ background-color: var(--primary-color);
235
+ border-radius: 4px;
236
+ transition: width 0.5s ease-in-out;
237
+ }
238
+
239
+ .poll-item-footer {
240
+ display: flex;
241
+ justify-content: space-between;
242
+ align-items: center;
243
+ margin-top: 8px;
244
+ }
245
+
246
+ .vote-percentage {
247
+ font-size: 14px;
248
+ font-weight: 500;
249
+ }
250
+
251
+ .vote-btn {
252
+ background-color: var(--primary-color);
253
+ color: white;
254
+ border: none;
255
+ border-radius: 18px;
256
+ padding: 6px 16px;
257
+ font-size: 14px;
258
+ font-weight: 500;
259
+ cursor: pointer;
260
+ transition: background-color var(--animation-speed);
261
+ }
262
+
263
+ .vote-btn:hover {
264
+ background-color: var(--primary-hover);
265
+ }
266
+
267
+ .vote-btn:disabled {
268
+ background-color: var(--text-secondary);
269
+ cursor: not-allowed;
270
+ opacity: 0.7;
271
+ }
272
+
273
+ .vote-btn.voted {
274
+ background-color: var(--text-secondary);
275
+ }
276
+
277
+ /* Add form */
278
+ .add-form-container {
279
+ margin-top: 24px;
280
+ padding-top: 16px;
281
+ border-top: 1px solid var(--border-color);
282
+ }
283
+
284
+ .add-form {
285
+ display: flex;
286
+ gap: 8px;
287
+ }
288
+
289
+ .input-container {
290
+ flex: 1;
291
+ position: relative;
292
+ }
293
+
294
+ .add-input {
295
+ width: 100%;
296
+ padding: 10px 12px;
297
+ padding-right: 36px; /* Space for validation indicator */
298
+ border: 1px solid var(--border-color);
299
+ border-radius: 4px;
300
+ font-size: 14px;
301
+ }
302
+
303
+ .add-input:focus {
304
+ outline: none;
305
+ border-color: var(--primary-color);
306
+ }
307
+
308
+ /* Validation indicator */
309
+ .validation-indicator {
310
+ position: absolute;
311
+ right: 10px;
312
+ top: 50%;
313
+ transform: translateY(-50%);
314
+ width: 20px;
315
+ height: 20px;
316
+ border-radius: 50%;
317
+ display: none;
318
+ }
319
+
320
+ .validation-indicator.valid {
321
+ display: block;
322
+ background-color: #4CAF50;
323
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
324
+ background-size: 14px;
325
+ background-position: center;
326
+ background-repeat: no-repeat;
327
+ }
328
+
329
+ .validation-indicator.invalid {
330
+ display: block;
331
+ background-color: #F44336;
332
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z'/%3E%3C/svg%3E");
333
+ background-size: 14px;
334
+ background-position: center;
335
+ background-repeat: no-repeat;
336
+ }
337
+
338
+ /* Dropdown styles */
339
+ .dropdown-container {
340
+ position: absolute;
341
+ width: 100%;
342
+ z-index: 10;
343
+ margin-top: 2px;
344
+ }
345
+
346
+ .dropdown-results {
347
+ max-height: 200px;
348
+ overflow-y: auto;
349
+ background-color: white;
350
+ border: 1px solid var(--border-color);
351
+ border-radius: 4px;
352
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
353
+ }
354
+
355
+ .dropdown-item {
356
+ padding: 10px 12px;
357
+ cursor: pointer;
358
+ transition: background-color 0.2s;
359
+ list-style: none;
360
+ border-bottom: 1px solid var(--border-color);
361
+ }
362
+
363
+ .dropdown-item:last-child {
364
+ border-bottom: none;
365
+ }
366
+
367
+ .dropdown-item:hover {
368
+ background-color: var(--secondary-color);
369
+ }
370
+
371
+ .dropdown-item-name {
372
+ font-weight: 500;
373
+ margin-bottom: 2px;
374
+ }
375
+
376
+ .dropdown-item-author {
377
+ font-size: 12px;
378
+ color: var(--text-secondary);
379
+ }
380
+
381
+ .no-results {
382
+ padding: 10px 12px;
383
+ color: var(--text-secondary);
384
+ font-style: italic;
385
+ list-style: none;
386
+ }
387
+
388
+ /* Loading indicator */
389
+ .dropdown-loading {
390
+ display: flex;
391
+ align-items: center;
392
+ padding: 10px 12px;
393
+ background-color: white;
394
+ border: 1px solid var(--border-color);
395
+ border-radius: 4px;
396
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
397
+ }
398
+
399
+ .spinner {
400
+ width: 16px;
401
+ height: 16px;
402
+ border: 2px solid var(--border-color);
403
+ border-top: 2px solid var(--primary-color);
404
+ border-radius: 50%;
405
+ margin-right: 8px;
406
+ animation: spin 1s linear infinite;
407
+ }
408
+
409
+ @keyframes spin {
410
+ 0% { transform: rotate(0deg); }
411
+ 100% { transform: rotate(360deg); }
412
+ }
413
+
414
+ .dropdown-loading span {
415
+ font-size: 14px;
416
+ color: var(--text-secondary);
417
+ }
418
+
419
+ .add-btn {
420
+ background-color: var(--primary-color);
421
+ color: white;
422
+ border: none;
423
+ border-radius: 4px;
424
+ padding: 10px 16px;
425
+ font-size: 14px;
426
+ font-weight: 500;
427
+ cursor: pointer;
428
+ transition: background-color var(--animation-speed), opacity var(--animation-speed);
429
+ }
430
+
431
+ .add-btn:hover:not(:disabled) {
432
+ background-color: var(--primary-hover);
433
+ }
434
+
435
+ .add-btn:disabled {
436
+ opacity: 0.6;
437
+ cursor: not-allowed;
438
+ }
439
+
440
+ .error {
441
+ color: #d93025;
442
+ font-size: 14px;
443
+ margin-top: 8px;
444
+ display: block;
445
+ }
446
+
447
+ /* Info panel */
448
+ .info-panel {
449
+ margin-top: 32px;
450
+ padding-top: 16px;
451
+ border-top: 1px solid var(--border-color);
452
+ display: flex;
453
+ flex-wrap: wrap;
454
+ gap: 16px;
455
+ }
456
+
457
+ .info-item {
458
+ display: flex;
459
+ align-items: center;
460
+ color: var(--text-secondary);
461
+ font-size: 14px;
462
+ }
463
+
464
+ .info-item i {
465
+ margin-right: 8px;
466
+ }
467
+
468
+ /* View toggle */
469
+ .view-toggle {
470
+ display: flex;
471
+ justify-content: center;
472
+ margin-top: 15px;
473
+ }
474
+
475
+ .view-btn {
476
+ background-color: var(--secondary-color);
477
+ border: none;
478
+ padding: 8px 16px;
479
+ margin: 0 5px;
480
+ border-radius: 4px;
481
+ cursor: pointer;
482
+ font-weight: 500;
483
+ transition: all 0.2s ease;
484
+ }
485
+
486
+ .view-btn.active {
487
+ background-color: var(--primary-color);
488
+ color: white;
489
+ }
490
+
491
+ .view-btn:hover:not(.active) {
492
+ background-color: var(--border-color);
493
+ }
494
+
495
+ /* Archives view */
496
+ #archives-view {
497
+ padding: 20px;
498
+ }
499
+
500
+ .archives-container {
501
+ background-color: var(--card-bg);
502
+ border-radius: 8px;
503
+ box-shadow: var(--shadow);
504
+ padding: 20px;
505
+ margin-bottom: 20px;
506
+ }
507
+
508
+ .archives-controls {
509
+ margin-bottom: 20px;
510
+ }
511
+
512
+ .archives-selector {
513
+ margin-bottom: 20px;
514
+ }
515
+
516
+ .archives-selector label {
517
+ display: block;
518
+ margin-bottom: 5px;
519
+ font-weight: 500;
520
+ }
521
+
522
+ .archives-selector select {
523
+ width: 100%;
524
+ padding: 8px;
525
+ border: 1px solid var(--border-color);
526
+ border-radius: 4px;
527
+ background-color: white;
528
+ }
529
+
530
+ .date-range-selector {
531
+ background-color: var(--secondary-color);
532
+ padding: 15px;
533
+ border-radius: 4px;
534
+ margin-bottom: 20px;
535
+ }
536
+
537
+ .date-range-selector h3 {
538
+ margin-top: 0;
539
+ margin-bottom: 10px;
540
+ font-size: 16px;
541
+ }
542
+
543
+ .date-inputs {
544
+ display: flex;
545
+ flex-wrap: wrap;
546
+ gap: 10px;
547
+ align-items: flex-end;
548
+ }
549
+
550
+ .date-input {
551
+ flex: 1;
552
+ min-width: 200px;
553
+ }
554
+
555
+ .date-input label {
556
+ display: block;
557
+ margin-bottom: 5px;
558
+ font-weight: 500;
559
+ }
560
+
561
+ .date-input input {
562
+ width: 100%;
563
+ padding: 8px;
564
+ border: 1px solid var(--border-color);
565
+ border-radius: 4px;
566
+ }
567
+
568
+ .search-btn {
569
+ background-color: var(--primary-color);
570
+ color: white;
571
+ border: none;
572
+ padding: 9px 16px;
573
+ border-radius: 4px;
574
+ cursor: pointer;
575
+ font-weight: 500;
576
+ }
577
+
578
+ .search-btn:hover {
579
+ background-color: var(--primary-hover);
580
+ }
581
+
582
+ #archive-results {
583
+ background-color: white;
584
+ border-radius: 4px;
585
+ padding: 15px;
586
+ border: 1px solid var(--border-color);
587
+ }
588
+
589
+ .archive-week-info {
590
+ background-color: var(--secondary-color);
591
+ padding: 10px 15px;
592
+ border-radius: 4px;
593
+ margin-bottom: 15px;
594
+ }
595
+
596
+ .archive-week-info p {
597
+ margin: 5px 0;
598
+ }
599
+
600
+ .archive-category {
601
+ margin-bottom: 20px;
602
+ }
603
+
604
+ .archive-category h3 {
605
+ border-bottom: 1px solid var(--border-color);
606
+ padding-bottom: 5px;
607
+ margin-bottom: 10px;
608
+ }
609
+
610
+ .no-archives {
611
+ text-align: center;
612
+ color: var(--text-secondary);
613
+ padding: 20px;
614
+ }
615
+
616
+ .hidden {
617
+ display: none;
618
+ }
619
+
620
+ /* Responsive */
621
+ @media (max-width: 768px) {
622
+ .container {
623
+ margin: 20px 16px;
624
+ padding: 16px;
625
+ }
626
+
627
+ .add-form {
628
+ flex-direction: column;
629
+ }
630
+
631
+ .section-header {
632
+ flex-direction: column;
633
+ align-items: flex-start;
634
+ gap: 8px;
635
+ }
636
+
637
+ .sort-options {
638
+ width: 100%;
639
+ overflow-x: auto;
640
+ padding-bottom: 8px;
641
+ }
642
+ }
scheduler.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // scheduler.js
2
+ // This script sets up a cron job to automatically archive the leaderboard at the end of each week
3
+ const cron = require('node-cron');
4
+ const archiver = require('./leaderboard_archiver');
5
+ const moment = require('moment');
6
+
7
+ console.log('Starting leaderboard archiving scheduler...');
8
+
9
+ // Schedule the archiving task to run at 23:59 on Sunday (end of the week)
10
+ // Cron format: second(optional) minute hour day-of-month month day-of-week
11
+ cron.schedule('59 23 * * 0', () => {
12
+ console.log(`Running weekly archiving task at ${moment().format('YYYY-MM-DD HH:mm:ss')}`);
13
+
14
+ try {
15
+ // Archive the current week's data
16
+ const weekId = archiver.archiveCurrentWeek();
17
+ console.log(`Successfully archived leaderboard data for week ${weekId}`);
18
+
19
+ // Reset votes after archiving
20
+ archiver.resetLeaderboard();
21
+
22
+ console.log('Weekly archiving completed successfully');
23
+ } catch (error) {
24
+ console.error('Error during weekly archiving:', error);
25
+ }
26
+ });
27
+
28
+ console.log('Scheduler started. Waiting for scheduled time to run archiving task...');
29
+ console.log('Press Ctrl+C to stop the scheduler');
30
+
31
+ // Keep the process running
32
+ process.stdin.resume();
33
+
34
+ // Handle graceful shutdown
35
+ process.on('SIGINT', () => {
36
+ console.log('Scheduler stopped');
37
+ process.exit(0);
38
+ });
scripts/archive_week.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // scripts/archive_week.js
2
+ // This script archives the current leaderboard data for the previous week
3
+ // It can be run manually or scheduled to run automatically
4
+
5
+ const archiver = require('../leaderboard_archiver');
6
+ const moment = require('moment');
7
+
8
+ console.log('Starting weekly leaderboard archiving process...');
9
+ console.log(`Current time: ${moment().format('YYYY-MM-DD HH:mm:ss')}`);
10
+
11
+ try {
12
+ // Archive the previous week's data
13
+ const weekId = archiver.archiveCurrentWeek();
14
+ console.log(`Successfully archived leaderboard data for week ${weekId}`);
15
+
16
+ // Reset votes after archiving
17
+ archiver.resetLeaderboard();
18
+
19
+ console.log('Archiving process completed successfully');
20
+ } catch (error) {
21
+ console.error('Error during archiving process:', error);
22
+ process.exit(1);
23
+ }
server.js ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // server.js — one‑vote‑per‑IP edition
2
+ const express = require('express');
3
+ const bodyParser = require('body-parser');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const requestIp = require('request-ip'); // NEW
7
+ const crypto = require('crypto'); // we'll hash IPs before saving
8
+ const archiver = require('./leaderboard_archiver');
9
+ const https = require('https'); // For Hugging Face API requests
10
+
11
+ const PORT = process.env.PORT || 3000;
12
+ const DATA_FILE = path.join(__dirname, 'data', 'data.json');
13
+ const IP_FILE = path.join(__dirname, 'data', 'ips.json');
14
+
15
+ const CATEGORIES = ["6gb", "12gb", "16gb", "24gb", "48gb", "72gb", "96gb"];
16
+ function validateCategory(cat) {
17
+ return CATEGORIES.includes(cat);
18
+ }
19
+
20
+ const app = express();
21
+ app.use(bodyParser.json());
22
+ app.use(requestIp.mw()); // adds req.clientIp
23
+ app.use(express.static(path.join(__dirname, 'public')));
24
+
25
+ /* ---------- tiny helpers ---------- */
26
+ function readJson(file, fallback) {
27
+ try { return JSON.parse(fs.readFileSync(file)); }
28
+ catch { return fallback; }
29
+ }
30
+ function writeJson(file, obj) {
31
+ fs.writeFileSync(file, JSON.stringify(obj, null, 2));
32
+ }
33
+ function hash(ip) { // do not store raw IP
34
+ return crypto.createHash('sha256').update(ip).digest('hex');
35
+ }
36
+
37
+ /* ---------- IP‑limit middleware ---------- */
38
+ function oneVotePerIP(req, res, next) {
39
+ const ipList = readJson(IP_FILE, {});
40
+ const key = hash(req.clientIp || 'unknown');
41
+ if (ipList[key]) return res.status(409)
42
+ .json({ error: 'You have already voted from this IP' });
43
+ req._ipKey = key; // remember for later
44
+ next();
45
+ }
46
+
47
+ /* ---------- Ensure IP tracking is properly formatted ---------- */
48
+ function ensureValidIpTracking() {
49
+ const ips = readJson(IP_FILE, {});
50
+ let changed = false;
51
+
52
+ // Convert any string values to objects
53
+ Object.keys(ips).forEach(key => {
54
+ if (typeof ips[key] === 'string') {
55
+ ips[key] = {};
56
+ changed = true;
57
+ }
58
+ });
59
+
60
+ if (changed) {
61
+ writeJson(IP_FILE, ips);
62
+ }
63
+
64
+ return ips;
65
+ }
66
+
67
+ /* ---------- API ---------- */
68
+ app.get('/api/entries', (req, res) => {
69
+ const category = req.query.category;
70
+ const data = readJson(DATA_FILE, {});
71
+ if (!validateCategory(category)) {
72
+ return res.status(400).json({ error: 'Invalid category' });
73
+ }
74
+ const entries = (data[category] || []).sort((a, b) => b.votes - a.votes);
75
+ res.json(entries);
76
+ });
77
+
78
+ /* Add new entry + cast initial vote */
79
+ app.post('/api/add', (req, res) => {
80
+ const name = (req.body.name || '').trim();
81
+ const category = req.body.category;
82
+ if (!name) return res.status(400).json({ error: 'Name required' });
83
+ if (!validateCategory(category)) return res.status(400).json({ error: 'Invalid category' });
84
+
85
+ const data = readJson(DATA_FILE, {});
86
+ const list = data[category] = data[category] || [];
87
+ if (list.find(e => e.name.toLowerCase() === name.toLowerCase()))
88
+ return res.status(400).json({ error: 'Entry already exists' });
89
+
90
+ const ips = ensureValidIpTracking();
91
+ const ipKey = hash(req.clientIp || 'unknown');
92
+ if (!ips[ipKey] || typeof ips[ipKey] !== 'object') ips[ipKey] = {};
93
+ const prevVotedId = ips[ipKey][category];
94
+
95
+ // If user has already voted for another entry, decrement its votes
96
+ if (prevVotedId) {
97
+ const prevItem = list.find(e => e.id === prevVotedId);
98
+ if (prevItem && prevItem.votes > 0) prevItem.votes -= 1;
99
+ }
100
+
101
+ // Add new entry with 1 vote
102
+ const entry = { id: Date.now().toString(), name, votes: 1 };
103
+ list.push(entry);
104
+ writeJson(DATA_FILE, data);
105
+
106
+ // Update IP record to new entry id for this category
107
+ ips[ipKey][category] = entry.id;
108
+ writeJson(IP_FILE, ips);
109
+
110
+ res.json(entry);
111
+ });
112
+
113
+ /* Vote for existing entry */
114
+ app.post('/api/vote', (req, res) => {
115
+ const { id, category } = req.body;
116
+ if (!validateCategory(category)) return res.status(400).json({ error: 'Invalid category' });
117
+
118
+ const data = readJson(DATA_FILE, {});
119
+ const list = data[category] = data[category] || [];
120
+ const item = list.find(e => e.id === id);
121
+ if (!item) return res.status(404).json({ error: 'Entry not found' });
122
+
123
+ const ips = ensureValidIpTracking();
124
+ const ipKey = hash(req.clientIp || 'unknown');
125
+ if (!ips[ipKey] || typeof ips[ipKey] !== 'object') ips[ipKey] = {};
126
+ const prevVotedId = ips[ipKey][category];
127
+
128
+ if (prevVotedId === id) {
129
+ // Already voted for this option
130
+ return res.status(409).json({ error: 'You have already voted for this option' });
131
+ }
132
+
133
+ // If user has voted for a different option, decrement that vote
134
+ if (prevVotedId) {
135
+ const prevItem = list.find(e => e.id === prevVotedId);
136
+ if (prevItem && prevItem.votes > 0) prevItem.votes -= 1;
137
+ }
138
+
139
+ // Increment vote for the new option
140
+ item.votes += 1;
141
+ writeJson(DATA_FILE, data);
142
+
143
+ // Update IP record to new voted id for this category
144
+ ips[ipKey][category] = id;
145
+ writeJson(IP_FILE, ips);
146
+
147
+ res.json(item);
148
+ });
149
+
150
+ /* ---------- Archive API ---------- */
151
+ // Get list of archived weeks
152
+ app.get('/api/archives/weeks', (req, res) => {
153
+ try {
154
+ const weeks = archiver.getArchivedWeeks();
155
+ res.json(weeks);
156
+ } catch (error) {
157
+ console.error('Error getting archived weeks:', error);
158
+ res.status(500).json({ error: 'Failed to retrieve archived weeks' });
159
+ }
160
+ });
161
+
162
+ // Get archived data for a specific week
163
+ app.get('/api/archives/week/:weekId', (req, res) => {
164
+ try {
165
+ const { weekId } = req.params;
166
+ const archive = archiver.getArchivedWeek(weekId);
167
+
168
+ if (!archive) {
169
+ return res.status(404).json({ error: 'Archive not found for the specified week' });
170
+ }
171
+
172
+ res.json(archive);
173
+ } catch (error) {
174
+ console.error('Error getting archived week:', error);
175
+ res.status(500).json({ error: 'Failed to retrieve archived data' });
176
+ }
177
+ });
178
+
179
+ // Get archived data for a specific week and category
180
+ app.get('/api/archives/week/:weekId/category/:category', (req, res) => {
181
+ try {
182
+ const { weekId, category } = req.params;
183
+ const archive = archiver.getArchivedWeek(weekId);
184
+
185
+ if (!archive) {
186
+ return res.status(404).json({ error: 'Archive not found for the specified week' });
187
+ }
188
+
189
+ if (!validateCategory(category)) {
190
+ return res.status(400).json({ error: 'Invalid category' });
191
+ }
192
+
193
+ const entries = (archive.data[category] || []).sort((a, b) => b.votes - a.votes);
194
+ res.json(entries);
195
+ } catch (error) {
196
+ console.error('Error getting archived category:', error);
197
+ res.status(500).json({ error: 'Failed to retrieve archived data' });
198
+ }
199
+ });
200
+
201
+ // Get archived data for a date range
202
+ app.get('/api/archives/range', (req, res) => {
203
+ try {
204
+ const { startDate, endDate } = req.query;
205
+
206
+ if (!startDate || !endDate) {
207
+ return res.status(400).json({ error: 'Both startDate and endDate are required' });
208
+ }
209
+
210
+ const archives = archiver.getArchivedRange(startDate, endDate);
211
+ res.json(archives);
212
+ } catch (error) {
213
+ console.error('Error getting archived range:', error);
214
+ res.status(500).json({ error: 'Failed to retrieve archived data for the specified range' });
215
+ }
216
+ });
217
+
218
+ /* ---------- Hugging Face API Proxy ---------- */
219
+ app.get('/api/huggingface/models', (req, res) => {
220
+ const query = req.query.query;
221
+
222
+ if (!query || query.length < 2) {
223
+ return res.status(400).json({ error: 'Query must be at least 2 characters' });
224
+ }
225
+
226
+ const options = {
227
+ hostname: 'huggingface.co',
228
+ path: `/api/models?search=${encodeURIComponent(query)}`,
229
+ method: 'GET',
230
+ headers: {
231
+ 'Accept': 'application/json'
232
+ }
233
+ };
234
+
235
+ const hfRequest = https.request(options, (hfResponse) => {
236
+ let data = '';
237
+
238
+ hfResponse.on('data', (chunk) => {
239
+ data += chunk;
240
+ });
241
+
242
+ hfResponse.on('end', () => {
243
+ try {
244
+ const parsedData = JSON.parse(data);
245
+
246
+ // Format the response to include only necessary information
247
+ const formattedResults = parsedData.map(model => ({
248
+ id: model.id,
249
+ modelId: model.modelId,
250
+ name: model.name || model.id,
251
+ author: model.author?.name || 'Unknown',
252
+ downloads: model.downloads || 0,
253
+ likes: model.likes || 0
254
+ })).slice(0, 10); // Limit to 10 results
255
+
256
+ res.json(formattedResults);
257
+ } catch (error) {
258
+ console.error('Error parsing Hugging Face API response:', error);
259
+ res.status(500).json({ error: 'Failed to parse Hugging Face API response' });
260
+ }
261
+ });
262
+ });
263
+
264
+ hfRequest.on('error', (error) => {
265
+ console.error('Error fetching from Hugging Face API:', error);
266
+ res.status(500).json({ error: 'Failed to fetch from Hugging Face API' });
267
+ });
268
+
269
+ hfRequest.end();
270
+ });
271
+
272
+ /* ---------- start ---------- */
273
+ app.listen(PORT, () => console.log('Leaderboard running on', PORT));
start.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // start.js - Custom script to run both servers
2
+ const { spawn } = require('child_process');
3
+ const path = require('path');
4
+
5
+ // Function to start a server process
6
+ function startServer(scriptPath, name) {
7
+ console.log(`Starting ${name} server...`);
8
+
9
+ const server = spawn('node', [scriptPath], {
10
+ stdio: 'inherit',
11
+ env: process.env
12
+ });
13
+
14
+ server.on('close', (code) => {
15
+ console.log(`${name} server process exited with code ${code}`);
16
+ process.exit(code);
17
+ });
18
+
19
+ return server;
20
+ }
21
+
22
+ // Start both servers
23
+ const mainServer = startServer(path.join(__dirname, 'server.js'), 'main');
24
+ const adminServer = startServer(path.join(__dirname, 'admin_server.js'), 'admin');
25
+
26
+ // Handle process termination
27
+ process.on('SIGINT', () => {
28
+ console.log('Received SIGINT. Shutting down servers...');
29
+ process.exit(0);
30
+ });
31
+
32
+ process.on('SIGTERM', () => {
33
+ console.log('Received SIGTERM. Shutting down servers...');
34
+ process.exit(0);
35
+ });
36
+
37
+ console.log('Both servers are running. Press Ctrl+C to stop.');