Compare commits

..
Author SHA1 Message Date
bruno 99a7ebd504 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
CI / test (pull_request) Failing after 0s
2026-10-28 22:35:55 -04:00
bruno e8f48d3009 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:35:53 -04:00
bruno dd6a4dd82c Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:35:51 -04:00
bruno a2f3564cc8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Successful in 4m17s
2026-10-28 22:35:49 -04:00
bruno ea39916db5 Merge pull request 'Page Projets : copie d'URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)' (#46) from menu-copie-url-44-45 into main
CI / test (push) Successful in 4m2s
2026-10-28 22:27:21 -04:00
bruno 724d7a7799 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
CI / test (pull_request) Failing after 1s
2026-10-28 22:12:12 -04:00
bruno 5ff51d8e2d Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 22:12:09 -04:00
bruno 436084aebd Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-28 22:12:07 -04:00
bruno 8ab30d25ef Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 22:12:04 -04:00
bruno 6cac4e71f5 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 22:12:01 -04:00
bruno 210f4f8983 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Successful in 4m9s
2026-10-28 22:11:58 -04:00
bruno f150d4e136 Changelog 0.37.6 : CORS recherche console Spark (PR #43)
CI / test (push) Successful in 4m20s
2026-10-28 16:21:02 -04:00
bruno 5787bcf168 Bump 0.37.6
CI / test (push) Failing after 0s
2026-10-28 16:20:59 -04:00
bruno 80006bdf35 Merge pull request 'CORS recherche : autoriser la console Spark (agent prod+dev)' (#43) from federation-recherche-console into main
CI / test (push) Failing after 0s
2026-10-28 16:17:13 -04:00
bruno 0219fafa34 Tests CORS recherche: origines agent autorisees, tiers refuse
CI / test (push) Failing after 3s
CI / test (pull_request) Failing after 0s
2026-10-28 16:11:30 -04:00
bruno c97dfb0ef4 CORS recherche: autoriser les origines agent (console Spark) prod+dev
CI / test (push) Failing after 2s
2026-10-28 16:11:26 -04:00
9 changed files with 175 additions and 39 deletions
+14
View File
@@ -1,3 +1,17 @@
## 0.38.1 — 2026-10-28 — Page Projets : le correctif 0.38.0 atteint enfin le navigateur (#47)
- **Cache-busting (#47)** : `dashboard.html` charge désormais `style.css?v={{ version }}` et `dashboard.js?v={{ version }}`, comme les autres pages (admin, board, project) et la barre Fusion. Sans ça, le navigateur gardait l'ancien CSS (menu transparent, bouton de copie non stylé) et l'ancien JS (aucun gestionnaire de copie) alors que le HTML était déjà neuf — c'est pourquoi la 0.38.0 semblait ne rien changer, bouton présent mais inerte. Le code 0.38.0 lui-même (jetons `--menu-bg` opaques, gestionnaire `data-copy-url`) était correct dans le dépôt.
## 0.38.0 — 2026-10-28 — Page Projets : copie d'URL et refonte du menu ⋯
- **Copie d'URL (#44)** : chaque tuile projet (grille, y compris Épinglés) propose désormais « 🔗 Copier l'URL du projet » dans son menu ⋯ — l'URL Gitea du dépôt est copiée dans le presse-papiers, avec confirmation « URL du projet copiée » et repli (textarea + execCommand) quand l'API Clipboard n'est pas disponible.
- **Menu ⋯ (#45)** : refonte sur les jetons de la barre d'apps commune (draco-barre.css, prototype Console v7.2 / console Spark) — fond opaque, bordure, rayon 14 px, ombre portée, survol/focus visibles, séparateur avant l'action de copie. Corrige la cause du menu transparent : il utilisait `var(--surface-1)`, variable jamais définie.
- **Thèmes sombre et clair** : jetons clairs par défaut, variante sombre via `[data-theme="dark"]` sur `<html>` et repli `prefers-color-scheme`, comme la barre Fusion.
## 0.37.6 — 2026-10-28 — Corrections console/recherche : CORS console Spark
- **CORS recherche (PR #43)** : la recherche Kanbante autorise désormais la console Spark (agent prod+dev) en inter-origines — les pages console peuvent appeler la recherche de l'hôte sans erreur CORS/réseau.
## 0.37.5 — 2026-10-28 — Alignement recherche console : onglet Console Spark sur /api/v1/recherche
- **Recherche (#68)** : l'onglet Console Spark de `/recherche` appelle désormais `GET /api/v1/recherche` (au lieu de `/api/search`) et reconstruit les liens profonds console (`?section=…&objet=…`, `disc=`, `activite=`) — les résultats console réels s'affichent enfin dans la page hôte.
+1 -1
View File
@@ -177,7 +177,7 @@ from urllib.parse import urlparse
BASE_DIR = Path(__file__).resolve().parent.parent
APP_VERSION = "0.37.5"
APP_VERSION = "0.38.1"
def _env_bool(name: str, default: bool) -> bool:
+7 -8
View File
@@ -10,9 +10,10 @@ testables sans base ni serveur.
tous projets du workspace) et la regroupe par dépôt.
- La CORS bornée n'autorise que ``GET /api/search``, et seulement
pour les origines sœurs officielles **prod ET dev, listées
explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus** les
origines du service d'agents (``AGENT_ORIGINS`` : console Spark
prod ET dev — fédération console, défaut 2), **plus** les
explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus les
origines de la console Spark** (``AGENT_ORIGINS`` : agent prod ET
dev — fédération de la recherche depuis la console, 2026-10-28),
**plus** les
origines résolues par ``app.config`` — jamais ``*``. Correctif
2026-10-28 (fusion #68/#69, matrice constatée par Bruno) :
l'ancienne allowlist ne contenait que l'environnement résolu
@@ -45,15 +46,13 @@ DEV_ORIGINS = frozenset({
"https://tiktak-dev.dracodev.net",
"https://kanbante-dev.dracodev.net",
})
# Origines du service d'agents (console Spark) : la page de
# recherche de la console appelle /api/search depuis le navigateur
# avec le cookie SSO — sans ces deux origines, le navigateur refuse
# la lecture (fédération console, défaut 2). Jamais "*".
AGENT_ORIGINS = frozenset({
"https://agent.dracodev.net",
"https://agent-dev.dracodev.net",
})
# Clés du dictionnaire rendu par render() (app/main.py).
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
@@ -106,7 +105,7 @@ def sister_origins(
"kanbante": urls.public_url,
}
except Exception:
return PROD_ORIGINS | DEV_ORIGINS | AGENT_ORIGINS
return PROD_ORIGINS | DEV_ORIGINS
origins = set()
for key in _SOEUR_KEYS:
origin = origin_of((resolved or {}).get(key))
+4 -2
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanbante — Dashboard</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>">
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/style.css?v={{ version }}">
<script defer src="/static/vendor/alpine.min.js"></script>
</head>
<body>
@@ -109,6 +109,8 @@
<a href="/sprint/{{ full }}">📖 Récit de sprint</a>
{% if repo.html_url %}
<a href="{{ repo.html_url }}" target="_blank" rel="noopener">🔗 Gitea ↗</a>
<div class="card-menu-sep" role="separator"></div>
<button type="button" class="card-menu-item" data-copy-url="{{ repo.html_url }}" title="Copier l'URL Gitea de {{ repo.name }}" aria-label="Copier l'URL du projet {{ repo.name }}">🔗 Copier l'URL du projet</button>
{% endif %}
</div>
</div>
@@ -259,7 +261,7 @@
<script src="/static/js/live.js" defer></script>
<script>window.KANBANTE_CSRF = "{{ csrf_token }}";</script>
<script src="/static/js/stars.js" defer></script>
<script src="/static/js/dashboard.js" defer></script>
<script src="/static/js/dashboard.js?v={{ version }}" defer></script>
<script src="/static/js/tablesort.js" defer></script>
<script>
document.addEventListener('keydown', function (e) {
+48 -8
View File
@@ -1486,20 +1486,60 @@ body.sidebar-collapsed .side-foot { justify-content: center; }
padding: 2px 7px; border-radius: 8px; line-height: 1;
}
.card-menu-btn:hover { background: var(--surface-2); color: var(--text); }
.card-menu-btn[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }
/* Menu ⋯ des tuiles projets — refonte issues #44/#45.
Jetons repris de la barre d'apps commune (draco-barre.css,
prototype validé Console v7.2, aussi utilisé par la console
Spark) : clair par défaut, sombre via [data-theme="dark"]
ou prefers-color-scheme. Correctif du bug d'origine : le
menu utilisait var(--surface-1), variable jamais définie,
donc aucun fond — le menu était transparent et illisible. */
.card-menu {
--menu-bg: #ffffff; --menu-bg-2: #e9eae6; --menu-ink: #1b1d1a;
--menu-ink-2: #5c615a; --menu-line: #dedfd9; --menu-agent: #c2437e;
position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
min-width: 210px; display: flex; flex-direction: column; gap: 2px;
background: var(--surface-1); border: 1px solid var(--border);
border-radius: 12px; padding: 6px;
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.45);
min-width: 248px; display: flex; flex-direction: column; gap: 2px;
background: var(--menu-bg); border: 1px solid var(--menu-line);
border-radius: 14px; padding: 6px; color: var(--menu-ink);
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
}
@media (prefers-color-scheme: dark) {
.card-menu {
--menu-bg: #1a1c1a; --menu-bg-2: #242624; --menu-ink: #eceee9;
--menu-ink-2: #a6aba2; --menu-line: #2e302c;
}
}
[data-theme="dark"] .card-menu {
--menu-bg: #1a1c1a; --menu-bg-2: #242624; --menu-ink: #eceee9;
--menu-ink-2: #a6aba2; --menu-line: #2e302c;
}
.card-menu[hidden] { display: none !important; }
.card-menu a {
.card-menu a, .card-menu-item {
display: flex; align-items: center; min-height: 32px;
padding: 8px 10px; border-radius: 8px;
color: var(--text-2); font-size: 0.92rem;
padding: 8px 10px; border-radius: 10px;
color: var(--menu-ink-2); font-size: 0.92rem;
}
.card-menu-item {
width: 100%; box-sizing: border-box; border: 0;
background: transparent; font: inherit; text-align: left; cursor: pointer;
}
.card-menu a:hover, .card-menu a:focus-visible,
.card-menu-item:hover, .card-menu-item:focus-visible {
background: var(--menu-bg-2); color: var(--menu-ink); text-decoration: none;
}
.card-menu a:focus-visible, .card-menu-item:focus-visible {
outline: 2px solid var(--menu-agent); outline-offset: 1px;
}
.card-menu-sep { height: 1px; margin: 6px 10px; background: var(--menu-line); }
/* Confirmation de copie — pastille fixe, lisible sur fond
clair comme sombre, même patron que la barre (draco). */
.kanbante-toast {
position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
z-index: 3000; background: #1f1f1f; color: #eee;
padding: 10px 16px; border-radius: 10px;
font: 500 13.5px system-ui, "Segoe UI", sans-serif;
box-shadow: 0 6px 24px rgb(0 0 0 / .35);
}
.card-menu a:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.proj-table {
width: calc(100% - 48px); margin: 0 24px 18px;
border-collapse: collapse; font-size: 0.93rem;
+41
View File
@@ -176,6 +176,47 @@
if (e.key === 'Escape') closeMenus();
});
/* ── Copier l'URL du projet (#44) ─────────────────────── */
function showCopyToast(message) {
var toast = document.createElement('div');
toast.className = 'kanbante-toast';
toast.setAttribute('role', 'status');
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(function () { toast.remove(); }, 2600);
}
function copyTextFallback(text) {
var area = document.createElement('textarea');
area.value = text;
area.style.cssText = 'position:fixed;opacity:0;pointer-events:none;';
document.body.appendChild(area);
area.select();
var ok = false;
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
area.remove();
return ok;
}
function copyText(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text).then(
function () { return true; },
function () { return copyTextFallback(text); }
);
}
return Promise.resolve(copyTextFallback(text));
}
document.querySelectorAll('[data-copy-url]').forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.stopPropagation();
var url = btn.getAttribute('data-copy-url') || '';
if (!url) return;
copyText(url).then(function (ok) {
closeMenus();
showCopyToast(ok ? 'URL du projet copiée' : 'La copie a échoué');
});
});
});
/* Le tri des tableaux denses (#22) vit dans tablesort.js,
partagé avec /forecast et /health (0.19.0). */
})();
+34
View File
@@ -0,0 +1,34 @@
"""Menu ⋯ des tuiles projets : copie d'URL (#44) et thème
opaque barre Fusion / Spark, sombre et clair (#45).
Le menu était transparent parce que son fond utilisait
var(--surface-1), variable jamais définie dans :root.
"""
def test_card_menu_offers_copy_url(client, fake_gitea):
page = client.get("/")
assert "Copier l'URL du projet" in page.text
assert 'data-copy-url="http://gitea.test/Projets/demo"' in page.text
def test_card_menu_theme_is_opaque_and_dual(client, fake_gitea):
css = client.get("/static/css/style.css").text
# Le menu ne repose plus sur la variable indéfinie.
menu = css.split(".card-menu {", 1)[1].split("}", 1)[0]
assert "var(--surface-1)" not in menu
assert "var(--menu-bg)" in menu
# Jetons barre Fusion / Console v7.2, clair par défaut…
assert "--menu-bg: #ffffff" in css
# …et variante sombre explicite, comme draco-barre.css.
assert '[data-theme="dark"] .card-menu' in css
assert "--menu-bg: #1a1c1a" in css
assert "prefers-color-scheme: dark" in css
def test_dashboard_js_copy_contract(client, fake_gitea):
js = client.get("/static/js/dashboard.js").text
assert "data-copy-url" in js
assert "navigator.clipboard" in js
assert "execCommand('copy')" in js
assert "URL du projet copiée" in js
+12
View File
@@ -0,0 +1,12 @@
"""Page Projets (#47) : les fichiers qui portent le menu ⋯ et la
copie d'URL doivent être versionnés, sinon le navigateur garde
l'ancien CSS/JS après une release alors que le HTML est neuf —
c'est exactement ce qui a fait paraître la 0.38.0 inefficace
(menu toujours transparent, bouton de copie présent mais inerte).
"""
def test_dashboard_static_assets_are_versioned(client, fake_gitea):
page = client.get("/")
assert "/static/css/style.css?v=" in page.text
assert "/static/js/dashboard.js?v=" in page.text
+14 -20
View File
@@ -34,7 +34,7 @@ def test_origin_of():
def test_sister_origins_dev():
# Correctif 2026-10-28 : 8 origines officielles, prod ET dev,
# Correctif 2026-10-28 : 10 origines officielles (8 sœurs + 2 agent), prod ET dev,
# quelle que soit la résolution.
assert sister_origins(RESOLVED_DEV) == {
"https://og.dracodev.net",
@@ -50,6 +50,19 @@ def test_sister_origins_dev():
}
def test_cors_headers_agent_console_autorise():
# Fédération depuis la console (2026-10-28) : les origines agent
# prod ET dev passent, avec credentials ; un tiers reste refusé.
for origine in ("https://agent.dracodev.net",
"https://agent-dev.dracodev.net"):
headers = cors_headers_for(origine, RESOLVED_DEV)
assert headers is not None
assert headers["Access-Control-Allow-Origin"] == origine
assert headers["Access-Control-Allow-Credentials"] == "true"
assert cors_headers_for("https://agent.dracodev.net.evil.example",
RESOLVED_DEV) is None
def test_cors_headers():
headers = cors_headers_for("https://tiktak-dev.dracodev.net", RESOLVED_DEV)
assert headers["Access-Control-Allow-Origin"] == "https://tiktak-dev.dracodev.net"
@@ -109,22 +122,3 @@ def test_sister_origins_hote_requete():
assert "https://og-dev.dracodev.net" in sister_origins(
{}, request_host="kanbante.dracodev.net"
)
def test_origines_agent_console():
# Fédération console (défaut 2) : la page de recherche de la
# console appelle /api/search depuis agent(. -dev) avec le
# cookie SSO — ces deux origines passent, prod ET dev, quelle
# que soit la résolution ; un tiers reste refusé.
assert cors_headers_for(
"https://agent.dracodev.net", RESOLVED_DEV) is not None
assert cors_headers_for(
"https://agent-dev.dracodev.net", {}) is not None
assert cors_headers_for(
"https://agent.dracodev.net",
RESOLVED_DEV)["Access-Control-Allow-Credentials"] == "true"
assert preflight_headers(
"https://agent-dev.dracodev.net", RESOLVED_DEV) is not None
assert "https://agent.dracodev.net" in sister_origins({})
assert "https://agent-dev.dracodev.net" in sister_origins({})
assert "https://agent-dev.dracodev.net" in sister_origins()