Desayunos
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js
index ff99beb..6713981 100644
--- a/frontend/src/services/api.js
+++ b/frontend/src/services/api.js
@@ -20,3 +20,7 @@ export async function getPrices() {
export async function getCiStatus() {
return getJson('/health');
}
+
+export async function getBuildHistory() {
+ return getJson('/builds');
+}
diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css
index e6aa29d..6866649 100644
--- a/frontend/src/styles/app.css
+++ b/frontend/src/styles/app.css
@@ -477,3 +477,132 @@ li {
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
+
+.history-card {
+ background: linear-gradient(180deg, #0f172a, #0c1228);
+}
+
+.history-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.6rem;
+}
+
+.history-item {
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.02);
+ overflow: hidden;
+ transition: border-color 160ms ease, box-shadow 160ms ease;
+}
+
+.history-item summary {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.8rem;
+ padding: 0.75rem 0.9rem;
+ list-style: none;
+ cursor: pointer;
+}
+
+.history-item summary::-webkit-details-marker {
+ display: none;
+}
+
+.summary-left {
+ display: flex;
+ align-items: center;
+ gap: 0.55rem;
+ flex-wrap: wrap;
+}
+
+.summary-title {
+ font-weight: 800;
+ color: #f8fafc;
+}
+
+.summary-branch {
+ color: #a5b4fc;
+ font-weight: 700;
+ font-size: 0.95rem;
+}
+
+.summary-meta {
+ display: flex;
+ gap: 0.6rem;
+ flex-wrap: wrap;
+ color: #cbd5f5;
+ font-size: 0.9rem;
+ justify-content: flex-end;
+}
+
+.status-dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+.status-dot.success {
+ background: #22c55e;
+ box-shadow: 0 0 8px rgba(34, 197, 94, 0.6);
+}
+
+.status-dot.failed {
+ background: #f87171;
+ box-shadow: 0 0 8px rgba(248, 113, 113, 0.5);
+}
+
+.history-body {
+ padding: 0.75rem 0.9rem 0.9rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+.history-row {
+ margin: 0;
+ display: flex;
+ justify-content: space-between;
+ font-weight: 700;
+ color: #e2e8f0;
+}
+
+.history-row span:last-child {
+ color: #cbd5f5;
+}
+
+.history-message {
+ margin: 0.2rem 0 0;
+ background: rgba(248, 113, 113, 0.12);
+ border: 1px dashed rgba(248, 113, 113, 0.5);
+ padding: 0.5rem 0.75rem;
+ border-radius: 10px;
+ color: #fecaca;
+}
+
+.history-message.success {
+ background: rgba(34, 197, 94, 0.12);
+ border-color: rgba(34, 197, 94, 0.4);
+ color: #bbf7d0;
+}
+
+.history-item[open] {
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
+}
+
+.history-item.success {
+ border-color: rgba(34, 197, 94, 0.3);
+}
+
+.history-item.failed {
+ border-color: rgba(248, 113, 113, 0.35);
+}
+
+.chip.danger {
+ background: rgba(248, 113, 113, 0.2);
+ color: #fecdd3;
+ border: 1px solid rgba(248, 113, 113, 0.4);
+}
From 80b9f4cb9f2d68142ebf7ae883f63b608b6af5ff Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Mon, 15 Dec 2025 12:51:54 +0100
Subject: [PATCH 02/21] Add mock data
---
backend/app/data/build_history.json | 44 +++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
create mode 100644 backend/app/data/build_history.json
diff --git a/backend/app/data/build_history.json b/backend/app/data/build_history.json
new file mode 100644
index 0000000..cf07192
--- /dev/null
+++ b/backend/app/data/build_history.json
@@ -0,0 +1,44 @@
+{
+ "builds": [
+ {
+ "number": 205,
+ "status": "success",
+ "branch": "main",
+ "commit": "9ac3f91",
+ "author": "Miau",
+ "finished_at": "2024-05-04T10:20:00Z",
+ "duration_seconds": 312
+ },
+ {
+ "number": 204,
+ "status": "failed",
+ "branch": "feature/nosetioestoesunmock",
+ "commit": "75c4ba2",
+ "author": "Miau",
+ "finished_at": "2024-05-04T09:50:00Z",
+ "duration_seconds": 188,
+ "failed_stage": "tests",
+ "fun_message": "woops"
+ },
+ {
+ "number": 203,
+ "status": "failed",
+ "branch": "main",
+ "commit": "512ca7e",
+ "author": "Miau",
+ "finished_at": "2024-05-04T09:10:00Z",
+ "duration_seconds": 140,
+ "failed_stage": "lint",
+ "fun_message": "Nadie pasa en local el linter"
+ },
+ {
+ "number": 202,
+ "status": "success",
+ "branch": "hotfix/tehedichoqueestoesunmock?",
+ "commit": "c73d8ab",
+ "author": "Miau",
+ "finished_at": "2024-05-03T18:30:00Z",
+ "duration_seconds": 276
+ }
+ ]
+}
From 16ff8b13808b9661b1d271558805e0f87f811aff Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Mon, 15 Dec 2025 12:52:10 +0100
Subject: [PATCH 03/21] Add build endpoint
---
backend/app/main.py | 6 ++++++
backend/app/services/builds.py | 22 ++++++++++++++++++++++
2 files changed, 28 insertions(+)
create mode 100644 backend/app/services/builds.py
diff --git a/backend/app/main.py b/backend/app/main.py
index f50c67e..41efe2d 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -4,6 +4,7 @@ from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.services.menu import build_menu
+from app.services.builds import build_history
from app.services.prices import prices_payload, random_price
from app.settings import settings
@@ -55,3 +56,8 @@ def prices():
@app.get("/prices/{item}")
def price_for_item(item: str):
return random_price(item)
+
+
+@app.get("/builds")
+def builds():
+ return build_history()
diff --git a/backend/app/services/builds.py b/backend/app/services/builds.py
new file mode 100644
index 0000000..14edc14
--- /dev/null
+++ b/backend/app/services/builds.py
@@ -0,0 +1,22 @@
+import json
+from pathlib import Path
+from typing import Dict, List
+
+DATA_DIR = Path(__file__).resolve().parent.parent / "data"
+
+
+def _load_json(filename: str) -> Dict:
+ path = DATA_DIR / filename
+ with open(path, encoding="utf-8") as file:
+ return json.load(file)
+
+
+def _sort_builds(builds: List[Dict]) -> List[Dict]:
+ return sorted(builds, key=lambda build: build.get("number", 0), reverse=True)
+
+
+def build_history() -> Dict:
+ """Return Jenkins build history data."""
+ history = _load_json("build_history.json")
+ builds = history.get("builds", [])
+ return {"builds": _sort_builds(builds)}
From 22d75adea27774183e3e9f738325358de902d440 Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Mon, 15 Dec 2025 12:52:31 +0100
Subject: [PATCH 04/21] Add tests
---
backend/tests/test_api.py | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py
index 3a47f10..4cbb309 100644
--- a/backend/tests/test_api.py
+++ b/backend/tests/test_api.py
@@ -34,3 +34,24 @@ def test_prices_random_list():
first = items[0]
assert "item" in first and "price" in first and "currency" in first
assert all(item["price"] <= 3 for item in items)
+
+
+def test_build_history():
+ response = client.get("/builds")
+ assert response.status_code == 200
+ body = response.json()
+ builds = body["builds"]
+ assert isinstance(builds, list)
+ assert len(builds) >= 1
+
+ first = builds[0]
+ assert "number" in first and "status" in first and "branch" in first
+
+ # Ensure descending order by build number
+ numbers = [build["number"] for build in builds]
+ assert numbers == sorted(numbers, reverse=True)
+
+ failed_builds = [build for build in builds if build["status"] == "failed"]
+ if failed_builds:
+ failed = failed_builds[0]
+ assert "failed_stage" in failed and "fun_message" in failed
From cde0b767d261acd42b650f3cecb4d5079b7e6860 Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Mon, 15 Dec 2025 12:52:45 +0100
Subject: [PATCH 05/21] Ruff fix
---
backend/app/main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/app/main.py b/backend/app/main.py
index 41efe2d..84d0e02 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -3,8 +3,8 @@ import time
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
-from app.services.menu import build_menu
from app.services.builds import build_history
+from app.services.menu import build_menu
from app.services.prices import prices_payload, random_price
from app.settings import settings
From 3360a8fc60d3770f92e8b1c02ff69c77e85497eb Mon Sep 17 00:00:00 2001
From: jose-rZM <100773386+jose-rZM@users.noreply.github.com>
Date: Mon, 15 Dec 2025 13:15:26 +0100
Subject: [PATCH 06/21] Add fish easter egg
---
frontend/src/App.svelte | 60 ++++++++++++++++++++++++++++++++-----
frontend/src/styles/app.css | 53 ++++++++++++++++++++++++++++++--
2 files changed, 104 insertions(+), 9 deletions(-)
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index c1e2c66..61d9e54 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -3,6 +3,7 @@
import { API_BASE, ENABLE_POLLING } from './config';
import { getBuildHistory, getCiStatus, getMenu, getPrices } from './services/api';
import { prettify } from './utils/text';
+ import { fly, fade } from 'svelte/transition';
let menu = null;
let prices = [];
@@ -13,6 +14,16 @@
let loadingCiStatus = true;
let buildHistory = [];
let loadingHistory = true;
+ let showFishToast = false;
+
+ function onFishClick() {
+ if (showFishToast) return;
+
+ showFishToast = true;
+ setTimeout(() => {
+ showFishToast = false;
+ }, 2800);
+ }
async function fetchMenu() {
loadingMenu = true;
@@ -129,7 +140,9 @@
{#if menu}
-