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:
|
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
|
||||||
|
|||||||
@@ -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
|
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
@@ -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
|
||||||
|
|||||||
@@ -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
|
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/
|
||||||
|
|||||||
@@ -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>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user