Compare commits

..

37 Commits

Author SHA1 Message Date
gitea-actions[bot] 501f6864cd chore: deploy version 787a094c59 [skip ci] 2026-08-13 21:17:41 +00:00
khalil 787a094c59 fix(api,frontend): delete is the true inverse of adopt, no reseed
CI/CD Pipeline / test (push) Successful in 10s
CI/CD Pipeline / build-push-deploy (push) Successful in 9s
/creatures/:id/delete now just removes the row, matching how
POST /creatures (adopt) just inserts one — no side effects on anything
else. The auto-reseed was scope creep for a feature that only ever made
sense as a public marketing-site action; now that delete lives in the app
itself as a per-creature control, that safety net isn't needed.

Adds a Delete button next to Feed/Play/Sleep/Revive on every creature
card, same creatureAction() plumbing as the existing actions.
2026-08-13 21:17:15 +00:00
gitea-actions[bot] e3850c93b4 chore: deploy version bc7f93a22f [skip ci] 2026-08-13 12:49:31 +00:00
khalil bc7f93a22f 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.
2026-08-13 12:48:55 +00:00
gitea-actions[bot] 0df4d63fd0 chore: deploy version 1939c36dfb [skip ci] 2026-08-11 10:30:47 +00:00
khalil 1939c36dfb fix(api): move FILTER onto the aggregate in /api/stats
CI/CD Pipeline / test (push) Successful in 1m38s
CI/CD Pipeline / build-push-deploy (push) Successful in 23s
PostgreSQL only accepts FILTER on an aggregate call. It was attached to
ROUND, so the whole query was rejected and /api/stats answered 500. The
showcase reads that endpoint for its live creature stats, which meant the
homepage displayed five dashes instead of numbers.

FILTER now sits on AVG, with the rounding applied to the result.
2026-08-11 10:28:23 +00:00
khalil b5d444d326 fix: restore literal password 2026-07-04 20:36:55 +00:00
gitea-actions[bot] 6d28d80e77 chore: deploy version 83ed8f6876 [skip ci] 2026-07-04 18:42:55 +00:00
khalil 83ed8f6876 chore: fix OCI source label URL to match canonical domain
CI/CD Pipeline / test (push) Successful in 8s
CI/CD Pipeline / build-push-deploy (push) Successful in 6s
2026-07-04 18:42:34 +00:00
gitea-actions[bot] 69f6c18e42 chore: deploy version 4936315a95 [skip ci] 2026-07-04 18:40:32 +00:00
khalil 4936315a95 ci: explicit git push with CR_PAT
CI/CD Pipeline / test (push) Successful in 8s
CI/CD Pipeline / build-push-deploy (push) Successful in 7s
2026-07-04 18:40:11 +00:00
khalil 7dd98100a1 chore: trigger CI with full permissions CR_PAT 2026-07-04 18:38:05 +00:00
khalil 8f00676623 ci: provide CR_PAT to actions/checkout for git push
CI/CD Pipeline / test (push) Successful in 11s
CI/CD Pipeline / build-push-deploy (push) Failing after 7s
2026-07-04 18:37:08 +00:00
khalil 1a0b7e4429 ci: force https for docker login to prevent http redirect auth drop
CI/CD Pipeline / test (push) Successful in 8s
CI/CD Pipeline / build-push-deploy (push) Failing after 6s
2026-07-04 18:35:59 +00:00
khalil 6fb5d31f5e chore: trigger CI with CR_PAT secret 2026-07-04 18:33:46 +00:00
gitea-actions[bot] 7066800918 chore: deploy version df915dcf25 [skip ci] 2026-07-04 18:32:35 +00:00
khalil df915dcf25 ci: use CR_PAT for docker login
CI/CD Pipeline / test (push) Successful in 10s
CI/CD Pipeline / build-push-deploy (push) Successful in 8s
2026-07-04 18:32:07 +00:00
khalil 2e74a7f4f1 ci: use git.khalilaliouich.com for docker push
CI/CD Pipeline / test (push) Successful in 9s
CI/CD Pipeline / build-push-deploy (push) Failing after 2s
2026-07-04 18:30:43 +00:00
khalil 12a97b18bc ci: fix docker login and image push
CI/CD Pipeline / test (push) Successful in 9s
CI/CD Pipeline / build-push-deploy (push) Failing after 3s
2026-07-04 18:28:58 +00:00
gitea-actions[bot] d703e84c4d chore: deploy version ac4f37c8b5 [skip ci] 2026-07-04 18:25:56 +00:00
khalil ac4f37c8b5 chore: liaison des images docker au dépôt Gitea
CI/CD Pipeline / test (push) Successful in 1m54s
CI/CD Pipeline / scan (push) Successful in 19s
CI/CD Pipeline / build-push-deploy (push) Successful in 47s
2026-07-04 18:22:30 +00:00
gitea-actions[bot] cd24ff9887 chore: deploy version 4cb77a9ded [skip ci] 2026-07-04 11:47:09 +00:00
khalil 4cb77a9ded Fix CI: Set contents permissions to write
CI/CD Pipeline / test (push) Successful in 7s
CI/CD Pipeline / scan (push) Successful in 12s
CI/CD Pipeline / build-push-deploy (push) Successful in 6s
2026-07-04 11:46:37 +00:00
khalil 4c850853ff Trigger CI 2
CI/CD Pipeline / test (push) Successful in 9s
CI/CD Pipeline / scan (push) Successful in 12s
CI/CD Pipeline / build-push-deploy (push) Failing after 5s
2026-07-04 11:44:24 +00:00
khalil ec72b0362e Trigger CI 2026-07-04 11:43:55 +00:00
khalil 176fbc2188 Fix CI: Add missing deployment manifests 2026-07-04 11:42:27 +00:00
khalil 73bce74b8c Fix CI: Add --network=host to docker build
CI/CD Pipeline / test (push) Successful in 1m49s
CI/CD Pipeline / scan (push) Successful in 19s
CI/CD Pipeline / build-push-deploy (push) Failing after 26s
2026-07-04 11:35:57 +00:00
Khalil 34fe1eb2b7 Use DOCKER_HOST env without TLS
CI/CD Pipeline / test (push) Successful in 1m53s
CI/CD Pipeline / scan (push) Successful in 19s
CI/CD Pipeline / build-push-deploy (push) Failing after 5m59s
2026-07-04 11:08:20 +00:00
Khalil 8b40a3759e Use CR_PAT for docker login
CI/CD Pipeline / test (push) Successful in 11s
CI/CD Pipeline / scan (push) Successful in 14s
CI/CD Pipeline / build-push-deploy (push) Failing after 6s
2026-07-04 11:02:38 +00:00
Khalil 042064458b Fix docker login token permissions
CI/CD Pipeline / test (push) Successful in 11s
CI/CD Pipeline / scan (push) Successful in 14s
CI/CD Pipeline / build-push-deploy (push) Failing after 5s
2026-07-04 10:58:34 +00:00
Khalil 3b6ce07db8 Remove frontend npm install step
CI/CD Pipeline / test (push) Successful in 11s
CI/CD Pipeline / scan (push) Successful in 22s
CI/CD Pipeline / build-push-deploy (push) Failing after 5s
2026-07-04 10:54:44 +00:00
Khalil 11792019cf Fix Node.js SSL for actions/checkout
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / scan (push) Has been skipped
CI/CD Pipeline / build-push-deploy (push) Has been skipped
2026-07-04 10:51:41 +00:00
Khalil 1aa94c6dfa Trigger CI with file change
CI/CD Pipeline / test (push) Failing after 11s
CI/CD Pipeline / scan (push) Has been skipped
CI/CD Pipeline / build-push-deploy (push) Has been skipped
2026-07-04 10:50:37 +00:00
Khalil e0d55a89f0 Trigger CI after Gitea config fix 3 2026-07-04 10:49:40 +00:00
Khalil da6da31fe7 Trigger CI after Gitea config fix 2 2026-07-04 10:48:22 +00:00
Khalil 16cf0ec7e7 Trigger CI after Gitea config fix 2 2026-07-04 10:48:14 +00:00
Khalil 60c0be6bf7 Trigger CI after Gitea config fix 2026-07-04 10:46:37 +00:00
9 changed files with 157 additions and 27 deletions
+12 -24
View File
@@ -9,6 +9,7 @@ on:
env: env:
GIT_SSL_NO_VERIFY: "true" GIT_SSL_NO_VERIFY: "true"
NODE_TLS_REJECT_UNAUTHORIZED: "0"
jobs: jobs:
test: test:
@@ -23,28 +24,15 @@ jobs:
- name: Install dependencies and test API - name: Install dependencies and test API
working-directory: ./api working-directory: ./api
run: npm install && node -c server.js run: npm install && node -c server.js
- name: Install dependencies and test Frontend
working-directory: ./frontend
run: npm install
scan:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@master
continue-on-error: true
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
build-push-deploy: build-push-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: scan needs: test
permissions:
packages: write
contents: write
env:
DOCKER_HOST: tcp://localhost:2375
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -52,16 +40,16 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Log in to Gitea Container Registry - name: Log in to Gitea Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login git.khalilaliouich.com -u ${{ github.actor }} --password-stdin run: echo "${{ secrets.CR_PAT }}" | docker login https://git.khalilaliouich.com -u ${{ github.actor }} --password-stdin
- name: Build and Push API - name: Build and Push API
run: | run: |
docker build -t git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-api:${{ github.sha }} ./api docker build --network=host -t git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-api:${{ github.sha }} ./api
docker push git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-api:${{ github.sha }} docker push git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-api:${{ github.sha }}
- name: Build and Push Frontend - name: Build and Push Frontend
run: | run: |
docker build -t git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-frontend:${{ github.sha }} ./frontend docker build --network=host -t git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-frontend:${{ github.sha }} ./frontend
docker push git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-frontend:${{ github.sha }} docker push git.khalilaliouich.com/${{ github.repository_owner }}/tamagotchi-frontend:${{ github.sha }}
- name: Update Kubernetes Manifests - name: Update Kubernetes Manifests
@@ -73,8 +61,8 @@ jobs:
run: | run: |
git config --global user.name "gitea-actions[bot]" git config --global user.name "gitea-actions[bot]"
git config --global user.email "gitea-actions@gitea.local" git config --global user.email "gitea-actions@gitea.local"
# Set the origin URL with the token to allow pushing back # Force the push to use the PAT token explicitely
git remote set-url origin http://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@gitea-http.gitea.svc.cluster.local:3000/${{ github.repository }} git remote set-url origin http://${{ github.actor }}:${{ secrets.CR_PAT }}@gitea-http.gitea.svc.cluster.local:3000/${{ github.repository }}.git
git add api-deployment.yaml frontend-deployment.yaml git add api-deployment.yaml frontend-deployment.yaml
git commit -m "chore: deploy version ${{ github.sha }} [skip ci]" || echo "No changes to commit" git commit -m "chore: deploy version ${{ github.sha }} [skip ci]" || echo "No changes to commit"
git push origin main git push origin main
+2
View File
@@ -0,0 +1,2 @@
+38
View File
@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tamagotchi-api
namespace: tamagotchi
labels:
app: tamagotchi-api
tier: backend
spec:
replicas: 2
selector:
matchLabels:
app: tamagotchi-api
template:
metadata:
labels:
app: tamagotchi-api
tier: backend
spec:
containers:
- name: api
image: git.khalilaliouich.com/khalil/tamagotchi-api:787a094c59f2330f5af1e420eab6139b8aadb8be
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: DB_HOST
value: postgres
- name: DB_PORT
value: "5432"
- name: DB_NAME
value: tamagotchi
- name: DB_USER
value: tamagotchi
- name: DB_PASSWORD
value: <YOUR_DB_PASSWORD>
- name: DECAY_INTERVAL_MS
value: "15000"
+1
View File
@@ -1,4 +1,5 @@
FROM node:20-alpine FROM node:20-alpine
LABEL org.opencontainers.image.source="https://git.khalilaliouich.com/khalil/tamagotchi-service"
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json ./
RUN npm install --production RUN npm install --production
+65 -3
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,51 @@ 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. The exact
// inverse of POST /creatures (adopt): adopt inserts a row with no side
// effects on anything else, delete removes one the same way — no reseed,
// no gauge update. It never touches tamagotchi_creatures_dead_total, so it
// does not trigger the Prometheus alert or n8n; deleting is not dying.
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]);
res.json({ message: `${deletedName} has been deleted permanently 🗑️` });
} 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 {
@@ -336,9 +395,12 @@ app.get('/api/stats', async (req, res) => {
SELECT SELECT
COUNT(*) FILTER (WHERE is_alive = true) as alive_count, COUNT(*) FILTER (WHERE is_alive = true) as alive_count,
COUNT(*) FILTER (WHERE is_alive = false) as dead_count, COUNT(*) FILTER (WHERE is_alive = false) as dead_count,
ROUND(AVG(hunger)::numeric, 1) FILTER (WHERE is_alive = true) as avg_hunger, -- FILTER ne s'applique qu'a un agregat : accroche a ROUND, qui n'en
ROUND(AVG(happiness)::numeric, 1) FILTER (WHERE is_alive = true) as avg_happiness, -- est pas un, PostgreSQL rejette toute la requete. Il doit porter sur
ROUND(AVG(energy)::numeric, 1) FILTER (WHERE is_alive = true) as avg_energy, -- AVG, et l'arrondi vient ensuite.
ROUND(AVG(hunger) FILTER (WHERE is_alive = true)::numeric, 1) as avg_hunger,
ROUND(AVG(happiness) FILTER (WHERE is_alive = true)::numeric, 1) as avg_happiness,
ROUND(AVG(energy) FILTER (WHERE is_alive = true)::numeric, 1) as avg_energy,
COUNT(*) FILTER (WHERE hunger > 80 AND is_alive = true) as starving_count, COUNT(*) FILTER (WHERE hunger > 80 AND is_alive = true) as starving_count,
COUNT(*) FILTER (WHERE happiness < 20 AND is_alive = true) as sad_count COUNT(*) FILTER (WHERE happiness < 20 AND is_alive = true) as sad_count
FROM creatures FROM creatures
+25
View File
@@ -0,0 +1,25 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: tamagotchi-frontend
namespace: tamagotchi
labels:
app: tamagotchi-frontend
tier: frontend
spec:
replicas: 1
selector:
matchLabels:
app: tamagotchi-frontend
template:
metadata:
labels:
app: tamagotchi-frontend
tier: frontend
spec:
containers:
- name: frontend
image: git.khalilaliouich.com/khalil/tamagotchi-frontend:787a094c59f2330f5af1e420eab6139b8aadb8be
imagePullPolicy: Always
ports:
- containerPort: 80
+1
View File
@@ -1,4 +1,5 @@
FROM nginx:alpine FROM nginx:alpine
LABEL org.opencontainers.image.source="https://git.khalilaliouich.com/khalil/tamagotchi-service"
COPY nginx.conf /etc/nginx/conf.d/default.conf COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/ COPY index.html /usr/share/nginx/html/
COPY style.css /usr/share/nginx/html/ COPY style.css /usr/share/nginx/html/
+5
View File
@@ -29,6 +29,8 @@ const i18n = {
play: '🎮 Play', play: '🎮 Play',
sleep: '💤 Sleep', sleep: '💤 Sleep',
revive: '✨ Revive', revive: '✨ Revive',
delete: '🗑️',
delete_title: 'Delete permanently — the opposite of Adopt',
alive: 'Alive', alive: 'Alive',
dead: 'Dead', dead: 'Dead',
empty_title: 'No creatures yet!', empty_title: 'No creatures yet!',
@@ -47,6 +49,8 @@ const i18n = {
play: '🎮 Jouer', play: '🎮 Jouer',
sleep: '💤 Dormir', sleep: '💤 Dormir',
revive: '✨ Ressusciter', revive: '✨ Ressusciter',
delete: '🗑️',
delete_title: "Supprimer definitivement — l'oppose d'Adopter",
alive: 'Vivant', alive: 'Vivant',
dead: 'Mort', dead: 'Mort',
empty_title: 'Pas encore de créatures !', empty_title: 'Pas encore de créatures !',
@@ -226,6 +230,7 @@ function renderCreatures() {
` : ` ` : `
<button class="action-btn revive" onclick="creatureAction('${c.id}', 'revive')">${t('revive')}</button> <button class="action-btn revive" onclick="creatureAction('${c.id}', 'revive')">${t('revive')}</button>
`} `}
<button class="action-btn delete" title="${t('delete_title')}" onclick="creatureAction('${c.id}', 'delete')">${t('delete')}</button>
</div> </div>
</div> </div>
`; `;
+8
View File
@@ -295,6 +295,14 @@ body::before {
color: var(--accent-green); color: var(--accent-green);
} }
.action-btn.delete {
background: rgba(255, 68, 102, 0.1);
border-color: rgba(255, 68, 102, 0.2);
color: var(--accent-red);
flex: 0 0 auto;
width: 44px;
}
/* ---- FAB ---- */ /* ---- FAB ---- */
.fab { .fab {
position: fixed; position: fixed;