feat(api): kill and delete endpoints for the Chaos Button demo
CI/CD Pipeline / test (push) Successful in 9s
CI/CD Pipeline / build-push-deploy (push) Successful in 8s

POST /creatures/:id/kill poisons a creature (is_alive=false), the same
state natural decay reaches — feeds tamagotchi_creatures_dead_total and
therefore the real TamagotchiCreatureDied -> Alertmanager -> n8n revival
chain. Deliberately app-level, not a pod action.

POST /creatures/:id/delete permanently removes a creature and immediately
reseeds a replacement. Does not touch the dead gauge on purpose: it
demonstrates the app defending its own roster rather than the monitoring
chain, so a public unauthenticated action can't empty it out over time.
This commit is contained in:
khalil
2026-08-13 12:48:55 +00:00
parent 0df4d63fd0
commit bc7f93a22f
+73
View File
@@ -69,6 +69,20 @@ const sleepCounter = new client.Counter({
registers: [register], registers: [register],
}); });
const killCounter = new client.Counter({
name: 'tamagotchi_kill_actions_total',
help: 'Total number of creatures deliberately killed (Chaos Button)',
labelNames: ['creature_type'],
registers: [register],
});
const deleteCounter = new client.Counter({
name: 'tamagotchi_delete_actions_total',
help: 'Total number of creatures permanently deleted and reseeded (Chaos Button)',
labelNames: ['creature_type'],
registers: [register],
});
const httpRequestDuration = new client.Histogram({ const httpRequestDuration = new client.Histogram({
name: 'tamagotchi_http_request_duration_seconds', name: 'tamagotchi_http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds', help: 'Duration of HTTP requests in seconds',
@@ -329,6 +343,65 @@ app.post('/api/creatures/:id/revive', async (req, res) => {
} }
}); });
// POST /creatures/:id/kill — Poison a creature (Chaos Button "meteor" demo).
// Sets is_alive=false directly, the same state a creature reaches through
// natural decay — which is exactly what feeds tamagotchi_creatures_dead_total
// and therefore the real TamagotchiCreatureDied -> Alertmanager -> n8n
// revival chain. This is deliberately NOT a Kubernetes-level action: nothing
// here touches a pod, so the thing that heals it is the business-logic
// automation, not the ReplicaSet controller.
app.post('/api/creatures/:id/kill', async (req, res) => {
try {
const { rows } = await pool.query('SELECT * FROM creatures WHERE id = $1', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Creature not found' });
if (!rows[0].is_alive) return res.status(400).json({ error: 'This creature is already dead 💀' });
await pool.query(
'UPDATE creatures SET is_alive = false, last_interaction = NOW() WHERE id = $1',
[req.params.id]
);
killCounter.inc({ creature_type: rows[0].type });
const result = await pool.query('SELECT * FROM creatures WHERE id = $1', [req.params.id]);
res.json({ message: `${result.rows[0].name} was hit by a meteor and poisoned ☄️💀`, creature: result.rows[0] });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// POST /creatures/:id/delete — Permanently delete a creature (Chaos Button
// demo). Unlike /kill, this never touches tamagotchi_creatures_dead_total —
// the row is gone, not marked dead — so it does NOT trigger the Prometheus
// alert or n8n. That's intentional: it demonstrates a different resilience
// layer, the app defending its own roster instead of relying on external
// monitoring automation. A public, unauthenticated action that only ever
// shrinks the roster would eventually empty it, so a replacement hatches
// immediately.
app.post('/api/creatures/:id/delete', async (req, res) => {
try {
const { rows } = await pool.query('SELECT * FROM creatures WHERE id = $1', [req.params.id]);
if (rows.length === 0) return res.status(404).json({ error: 'Creature not found' });
const deletedName = rows[0].name;
deleteCounter.inc({ creature_type: rows[0].type });
await pool.query('DELETE FROM creatures WHERE id = $1', [req.params.id]);
const seedNames = ['Pixel', 'Nimbus', 'Sprocket', 'Blossom', 'Byte', 'Echo', 'Flux', 'Glitch'];
const allowedTypes = ['dragon', 'cat', 'robot', 'plant', 'alien'];
const newId = uuidv4();
const newName = seedNames[Math.floor(Math.random() * seedNames.length)];
const newType = allowedTypes[Math.floor(Math.random() * allowedTypes.length)];
await pool.query('INSERT INTO creatures (id, name, type) VALUES ($1, $2, $3)', [newId, newName, newType]);
const result = await pool.query('SELECT * FROM creatures WHERE id = $1', [newId]);
res.json({
message: `${deletedName} was deleted permanently. ${newName} the ${newType} hatched to take its place 🥚`,
creature: result.rows[0]
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// GET /stats — Global statistics // GET /stats — Global statistics
app.get('/api/stats', async (req, res) => { app.get('/api/stats', async (req, res) => {
try { try {