Compare commits

...

2 Commits

Author SHA1 Message Date
a26c92ff2b Resync 2025-10-21 22:29:44 +02:00
04891bf449 Refactor monster_catches from table to view and add overwrite option
- Convert monster_catches from table to view for automatic calculation
- Add overwrite checkbox to monster import UI
- Remove manual INSERT/UPDATE logic for catches (now handled by view)
- Simplify API endpoints to query view instead of managing state
- Add confirmation dialog for overwrite operation

Benefits:
- No data duplication
- Always accurate catch status based on current steps
- Simpler codebase with less state management
- Easier to reset steps without orphaned catch records

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 16:08:02 +02:00
5 changed files with 132 additions and 118 deletions

View File

@@ -1,7 +1,11 @@
date,monster_name,monster_description,step_goal,monster_icon date,monster_name,monster_description,step_goal,monster_icon
2025-10-17,Test Monster,Can you catch this spooky creature?,30000,👻
2025-10-20,Dracula,The Count demands your steps!,60000,🧛 2025-10-20,Dracula,The Count demands your steps!,60000,🧛
2025-10-21,Freddy Krueger,Don't fall asleep on your steps!,48000,🔪 2025-10-21,Freddy Krueger,Don't fall asleep on your steps!,48000,🔪
2025-10-22,Mummy,Unwrap your potential!,48000,🧟 2025-10-22,Mummy,Unwrap your potential!,48000,🧟
2025-10-23,Aliens,Take steps to another world!,72000,👽 2025-10-23,Xenomorph,The perfect organism hunts in the shadows!,72000,👽
2025-10-24,Predator,Hunt down those steps!,66000,👹 2025-10-24,Predator,Hunt down those steps!,66000,👹
2025-10-27,Blob,This oozing creature is slow and easy to catch!,36000,🟢
2025-10-28,Godzilla,Each massive stride covers miles!,72000,🦖
2025-10-29,Werewolf,Fast and agile under the full moon!,78000,🐺
2025-10-30,Frankenstein's Monster,A lumbering creation seeks companionship!,48000,
2025-10-31,Dr. Jekyll/Mr. Hyde,Two personalities - one fast chase!,66000,🧪
1 date monster_name monster_description step_goal monster_icon
2025-10-17 Test Monster Can you catch this spooky creature? 30000 👻
2 2025-10-20 Dracula The Count demands your steps! 60000 🧛
3 2025-10-21 Freddy Krueger Don't fall asleep on your steps! 48000 🔪
4 2025-10-22 Mummy Unwrap your potential! 48000 🧟
5 2025-10-23 Aliens Xenomorph Take steps to another world! The perfect organism hunts in the shadows! 72000 👽
6 2025-10-24 Predator Hunt down those steps! 66000 👹
7 2025-10-27 Blob This oozing creature is slow and easy to catch! 36000 🟢
8 2025-10-28 Godzilla Each massive stride covers miles! 72000 🦖
9 2025-10-29 Werewolf Fast and agile under the full moon! 78000 🐺
10 2025-10-30 Frankenstein's Monster A lumbering creation seeks companionship! 48000
11 2025-10-31 Dr. Jekyll/Mr. Hyde Two personalities - one fast chase! 66000 🧪

View File

@@ -13,8 +13,19 @@ document.addEventListener('DOMContentLoaded', () => {
setupEventListeners(); setupEventListeners();
setDefaultDate(); setDefaultDate();
handleRouting(); handleRouting();
checkAdminMode();
}); });
// Admin mode
function checkAdminMode() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('admin') === 'true') {
document.querySelectorAll('.import-section').forEach(el => {
el.style.display = 'block';
});
}
}
// Routing // Routing
function handleRouting() { function handleRouting() {
// Handle initial load and hash changes // Handle initial load and hash changes
@@ -63,15 +74,17 @@ function initializeTabs() {
}); });
} }
// Set default date to today // Set default date to yesterday (since we enter steps the day after)
function setDefaultDate() { function setDefaultDate() {
const today = new Date().toISOString().split('T')[0]; const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
const dateInput = document.getElementById('step-date'); const dateInput = document.getElementById('step-date');
dateInput.value = today; dateInput.value = yesterdayStr;
// Set max date to October 31, 2025 // Set max date to October 31, 2025
dateInput.max = '2025-10-31'; dateInput.max = '2025-10-31';
dateInput.min = '2025-10-15'; dateInput.min = '2025-10-20';
} }
// Load initial data // Load initial data
@@ -823,6 +836,7 @@ function renderMonsterTimeline(timeline) {
// Monster CSV Import // Monster CSV Import
async function importMonsters() { async function importMonsters() {
const fileInput = document.getElementById('monster-csv-file'); const fileInput = document.getElementById('monster-csv-file');
const overwriteCheckbox = document.getElementById('monster-overwrite');
const messageDiv = document.getElementById('monster-import-message'); const messageDiv = document.getElementById('monster-import-message');
if (!fileInput.files || !fileInput.files[0]) { if (!fileInput.files || !fileInput.files[0]) {
@@ -830,6 +844,20 @@ async function importMonsters() {
return; return;
} }
const overwrite = overwriteCheckbox.checked;
console.log('Overwrite checkbox checked:', overwrite);
// Confirm if overwrite is selected
if (overwrite) {
const confirmed = confirm(
'WARNING: This will DELETE all existing monsters and monster catches!\n\n' +
'Are you sure you want to proceed?'
);
if (!confirmed) {
return;
}
}
const file = fileInput.files[0]; const file = fileInput.files[0];
const reader = new FileReader(); const reader = new FileReader();
@@ -846,18 +874,22 @@ async function importMonsters() {
return { date, monster_name, monster_description, step_goal, monster_icon }; return { date, monster_name, monster_description, step_goal, monster_icon };
}); });
const result = await apiCall('/api/monsters/import', 'POST', { data }); const result = await apiCall('/api/monsters/import', 'POST', { data, overwrite });
showMessage( let successMessage;
messageDiv, if (overwrite) {
`Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`, successMessage = `Success! Deleted ${result.monstersDeleted} old monsters, created ${result.monstersCreated} new monsters`;
'success' } else {
); successMessage = `Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`;
}
showMessage(messageDiv, successMessage, 'success');
// Reload monsters // Reload monsters
await loadDailyMonsters(); await loadDailyMonsters();
fileInput.value = ''; fileInput.value = '';
overwriteCheckbox.checked = false;
} catch (error) { } catch (error) {
showMessage(messageDiv, `Error: ${error.message}`, 'error'); showMessage(messageDiv, `Error: ${error.message}`, 'error');
} }

View File

@@ -96,16 +96,19 @@
<strong>⏰ Important:</strong> The <strong>"Active Challenge"</strong> shows yesterday's monster. <strong>⏰ Important:</strong> The <strong>"Active Challenge"</strong> shows yesterday's monster.
Log your steps from yesterday to see if your team caught it! Log your steps from yesterday to see if your team caught it!
</p> </p>
<p style="margin: 8px 0; line-height: 1.6; font-size: 14px; color: #aaa;">
💡 You can enter steps for any past date, and catches will be updated automatically.
</p>
</div> </div>
<!-- Monster CSV Import Section --> <!-- Monster CSV Import Section -->
<div class="import-section"> <div class="import-section" style="display: none;">
<h3>Admin: Import Monsters</h3> <h3>Admin: Import Monsters</h3>
<p class="help-text">Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon</p> <p class="help-text">Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon</p>
<input type="file" id="monster-csv-file" accept=".csv"> <input type="file" id="monster-csv-file" accept=".csv">
<div style="margin: 10px 0;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
<input type="checkbox" id="monster-overwrite" style="cursor: pointer;">
<span style="color: #ff7700;">Overwrite all existing monsters (deletes all current monsters and catches)</span>
</label>
</div>
<button id="import-monsters" class="btn-secondary">Import Monsters</button> <button id="import-monsters" class="btn-secondary">Import Monsters</button>
<div id="monster-import-message" class="message"></div> <div id="monster-import-message" class="message"></div>
</div> </div>

View File

@@ -55,18 +55,25 @@ function initializeDatabase() {
) )
`); `);
// Monster catches table // Monster catches view - derived from actual step data
// Drop the old table/view if it exists and recreate as a view
db.run(`DROP VIEW IF EXISTS monster_catches`);
db.run(`DROP TABLE IF EXISTS monster_catches`);
db.run(` db.run(`
CREATE TABLE IF NOT EXISTS monster_catches ( CREATE VIEW IF NOT EXISTS monster_catches AS
id INTEGER PRIMARY KEY AUTOINCREMENT, SELECT
team_id INTEGER NOT NULL, dm.id as monster_id,
monster_id INTEGER NOT NULL, dm.date,
caught_at DATETIME DEFAULT CURRENT_TIMESTAMP, t.id as team_id,
final_steps INTEGER NOT NULL, t.name as team_name,
FOREIGN KEY (team_id) REFERENCES teams(id), COALESCE(SUM(ds.steps), 0) as final_steps,
FOREIGN KEY (monster_id) REFERENCES daily_monsters(id), dm.step_goal
UNIQUE(team_id, monster_id) FROM daily_monsters dm
) CROSS JOIN teams t
LEFT JOIN participants p ON t.id = p.team_id
LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = dm.date
GROUP BY dm.id, dm.date, t.id, t.name, dm.step_goal
HAVING final_steps >= dm.step_goal
`); `);
// Create indexes for better query performance // Create indexes for better query performance
@@ -74,8 +81,6 @@ function initializeDatabase() {
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_participant ON daily_steps(participant_id)`); db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_participant ON daily_steps(participant_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_participants_team ON participants(team_id)`); db.run(`CREATE INDEX IF NOT EXISTS idx_participants_team ON participants(team_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_monsters_date ON daily_monsters(date)`); db.run(`CREATE INDEX IF NOT EXISTS idx_daily_monsters_date ON daily_monsters(date)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_team ON monster_catches(team_id)`);
db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_monster ON monster_catches(monster_id)`);
console.log('Database initialized successfully'); console.log('Database initialized successfully');
}); });

View File

@@ -352,54 +352,17 @@ app.get('/api/monsters/timeline', async (req, res) => {
try { try {
const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date'); const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date');
// For each monster, check if teams caught it and update catches table // For each monster, get catches from the view
const timeline = await Promise.all(monsters.map(async (monster) => { const timeline = await Promise.all(monsters.map(async (monster) => {
// Get team totals for this monster's date
const teamTotals = await allQuery(`
SELECT
t.id as team_id,
t.name as team_name,
COALESCE(SUM(ds.steps), 0) as total_steps
FROM teams t
LEFT JOIN participants p ON t.id = p.team_id
LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = ?
GROUP BY t.id, t.name
`, [monster.date]);
// Check and record catches for teams that met the goal
for (const team of teamTotals) {
if (team.total_steps >= monster.step_goal) {
// Check if catch already recorded
const existingCatch = await getQuery(
'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?',
[team.team_id, monster.id]
);
if (!existingCatch) {
// Record the catch
await runQuery(
'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)',
[team.team_id, monster.id, team.total_steps]
);
} else {
// Update the final_steps if it changed
await runQuery(
'UPDATE monster_catches SET final_steps = ? WHERE team_id = ? AND monster_id = ?',
[team.total_steps, team.team_id, monster.id]
);
}
}
}
// Now get the updated catches
const catches = await allQuery(` const catches = await allQuery(`
SELECT SELECT
mc.*, monster_id,
t.name as team_name team_id,
FROM monster_catches mc team_name,
JOIN teams t ON mc.team_id = t.id final_steps
WHERE mc.monster_id = ? FROM monster_catches
ORDER BY mc.caught_at WHERE monster_id = ?
ORDER BY final_steps DESC
`, [monster.id]); `, [monster.id]);
return { return {
@@ -430,7 +393,7 @@ app.get('/api/monsters/:date', async (req, res) => {
// Add/import monsters (bulk CSV import) // Add/import monsters (bulk CSV import)
app.post('/api/monsters/import', async (req, res) => { app.post('/api/monsters/import', async (req, res) => {
try { try {
const { data } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}] const { data, overwrite } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}]
if (!data || !Array.isArray(data)) { if (!data || !Array.isArray(data)) {
return res.status(400).json({ error: 'Invalid data format' }); return res.status(400).json({ error: 'Invalid data format' });
@@ -438,8 +401,16 @@ app.post('/api/monsters/import', async (req, res) => {
let monstersCreated = 0; let monstersCreated = 0;
let monstersUpdated = 0; let monstersUpdated = 0;
let monstersDeleted = 0;
let errors = []; let errors = [];
// If overwrite is true, delete all existing monsters first
// Note: monster_catches is now a view, so no need to delete from it
if (overwrite === true) {
const deleteMonstersResult = await runQuery('DELETE FROM daily_monsters');
monstersDeleted = deleteMonstersResult.changes || 0;
}
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const row = data[i]; const row = data[i];
@@ -449,6 +420,14 @@ app.post('/api/monsters/import', async (req, res) => {
continue; continue;
} }
if (overwrite) {
// When overwriting, always insert (since we deleted everything)
await runQuery(
'INSERT INTO daily_monsters (date, monster_name, monster_description, step_goal, monster_icon) VALUES (?, ?, ?, ?, ?)',
[row.date, row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹']
);
monstersCreated++;
} else {
// Check if monster already exists for this date // Check if monster already exists for this date
const existing = await getQuery( const existing = await getQuery(
'SELECT id FROM daily_monsters WHERE date = ?', 'SELECT id FROM daily_monsters WHERE date = ?',
@@ -470,17 +449,25 @@ app.post('/api/monsters/import', async (req, res) => {
); );
monstersCreated++; monstersCreated++;
} }
}
} catch (error) { } catch (error) {
errors.push(`Row ${i + 1}: ${error.message}`); errors.push(`Row ${i + 1}: ${error.message}`);
} }
} }
res.json({ const result = {
success: true, success: true,
monstersCreated, monstersCreated,
monstersUpdated,
errors: errors.length > 0 ? errors : null errors: errors.length > 0 ? errors : null
}); };
if (overwrite) {
result.monstersDeleted = monstersDeleted;
} else {
result.monstersUpdated = monstersUpdated;
}
res.json(result);
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
@@ -500,12 +487,13 @@ app.get('/api/monsters/:date/catches', async (req, res) => {
const catches = await allQuery(` const catches = await allQuery(`
SELECT SELECT
mc.*, monster_id,
t.name as team_name team_id,
FROM monster_catches mc team_name,
JOIN teams t ON mc.team_id = t.id final_steps
WHERE mc.monster_id = ? FROM monster_catches
ORDER BY mc.caught_at WHERE monster_id = ?
ORDER BY final_steps DESC
`, [monster.id]); `, [monster.id]);
res.json(catches); res.json(catches);
@@ -534,6 +522,7 @@ app.get('/api/monsters/status/today', async (req, res) => {
} }
// Get team progress for yesterday's date // Get team progress for yesterday's date
// The view automatically calculates which teams caught it
const progress = await allQuery(` const progress = await allQuery(`
SELECT SELECT
t.id as team_id, t.id as team_id,
@@ -547,25 +536,6 @@ app.get('/api/monsters/status/today', async (req, res) => {
ORDER BY total_steps DESC ORDER BY total_steps DESC
`, [monster.step_goal, yesterday]); `, [monster.step_goal, yesterday]);
// Check and record new catches
for (const team of progress) {
if (team.caught) {
// Check if catch already recorded
const existingCatch = await getQuery(
'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?',
[team.team_id, monster.id]
);
if (!existingCatch) {
// Record the catch
await runQuery(
'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)',
[team.team_id, monster.id, team.total_steps]
);
}
}
}
res.json({ monster, progress }); res.json({ monster, progress });
} catch (error) { } catch (error) {
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });