Compare commits
37 Commits
311285866e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 501f6864cd | |||
| 787a094c59 | |||
| e3850c93b4 | |||
| bc7f93a22f | |||
| 0df4d63fd0 | |||
| 1939c36dfb | |||
| b5d444d326 | |||
| 6d28d80e77 | |||
| 83ed8f6876 | |||
| 69f6c18e42 | |||
| 4936315a95 | |||
| 7dd98100a1 | |||
| 8f00676623 | |||
| 1a0b7e4429 | |||
| 6fb5d31f5e | |||
| 7066800918 | |||
| df915dcf25 | |||
| 2e74a7f4f1 | |||
| 12a97b18bc | |||
| d703e84c4d | |||
| ac4f37c8b5 | |||
| cd24ff9887 | |||
| 4cb77a9ded | |||
| 4c850853ff | |||
| ec72b0362e | |||
| 176fbc2188 | |||
| 73bce74b8c | |||
| 34fe1eb2b7 | |||
| 8b40a3759e | |||
| 042064458b | |||
| 3b6ce07db8 | |||
| 11792019cf | |||
| 1aa94c6dfa | |||
| e0d55a89f0 | |||
| da6da31fe7 | |||
| 16cf0ec7e7 | |||
| 60c0be6bf7 |
@@ -9,6 +9,7 @@ on:
|
||||
|
||||
env:
|
||||
GIT_SSL_NO_VERIFY: "true"
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: "0"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -23,28 +24,15 @@ jobs:
|
||||
- name: Install dependencies and test API
|
||||
working-directory: ./api
|
||||
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:
|
||||
runs-on: ubuntu-latest
|
||||
needs: scan
|
||||
needs: test
|
||||
permissions:
|
||||
packages: write
|
||||
contents: write
|
||||
env:
|
||||
DOCKER_HOST: tcp://localhost:2375
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
@@ -52,16 +40,16 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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
|
||||
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 }}
|
||||
|
||||
- name: Build and Push Frontend
|
||||
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 }}
|
||||
|
||||
- name: Update Kubernetes Manifests
|
||||
@@ -73,8 +61,8 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name "gitea-actions[bot]"
|
||||
git config --global user.email "gitea-actions@gitea.local"
|
||||
# Set the origin URL with the token to allow pushing back
|
||||
git remote set-url origin http://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@gitea-http.gitea.svc.cluster.local:3000/${{ github.repository }}
|
||||
# Force the push to use the PAT token explicitely
|
||||
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 commit -m "chore: deploy version ${{ github.sha }} [skip ci]" || echo "No changes to commit"
|
||||
git push origin main
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
FROM node:20-alpine
|
||||
LABEL org.opencontainers.image.source="https://git.khalilaliouich.com/khalil/tamagotchi-service"
|
||||
WORKDIR /app
|
||||
COPY package.json ./
|
||||
RUN npm install --production
|
||||
|
||||
+65
-3
@@ -69,6 +69,20 @@ const sleepCounter = new client.Counter({
|
||||
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({
|
||||
name: 'tamagotchi_http_request_duration_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
|
||||
app.get('/api/stats', async (req, res) => {
|
||||
try {
|
||||
@@ -336,9 +395,12 @@ app.get('/api/stats', async (req, res) => {
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE is_alive = true) as alive_count,
|
||||
COUNT(*) FILTER (WHERE is_alive = false) as dead_count,
|
||||
ROUND(AVG(hunger)::numeric, 1) FILTER (WHERE is_alive = true) as avg_hunger,
|
||||
ROUND(AVG(happiness)::numeric, 1) FILTER (WHERE is_alive = true) as avg_happiness,
|
||||
ROUND(AVG(energy)::numeric, 1) FILTER (WHERE is_alive = true) as avg_energy,
|
||||
-- FILTER ne s'applique qu'a un agregat : accroche a ROUND, qui n'en
|
||||
-- est pas un, PostgreSQL rejette toute la requete. Il doit porter sur
|
||||
-- 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 happiness < 20 AND is_alive = true) as sad_count
|
||||
FROM creatures
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
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 index.html /usr/share/nginx/html/
|
||||
COPY style.css /usr/share/nginx/html/
|
||||
|
||||
@@ -29,6 +29,8 @@ const i18n = {
|
||||
play: '🎮 Play',
|
||||
sleep: '💤 Sleep',
|
||||
revive: '✨ Revive',
|
||||
delete: '🗑️',
|
||||
delete_title: 'Delete permanently — the opposite of Adopt',
|
||||
alive: 'Alive',
|
||||
dead: 'Dead',
|
||||
empty_title: 'No creatures yet!',
|
||||
@@ -47,6 +49,8 @@ const i18n = {
|
||||
play: '🎮 Jouer',
|
||||
sleep: '💤 Dormir',
|
||||
revive: '✨ Ressusciter',
|
||||
delete: '🗑️',
|
||||
delete_title: "Supprimer definitivement — l'oppose d'Adopter",
|
||||
alive: 'Vivant',
|
||||
dead: 'Mort',
|
||||
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 delete" title="${t('delete_title')}" onclick="creatureAction('${c.id}', 'delete')">${t('delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -295,6 +295,14 @@ body::before {
|
||||
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 {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user