Klick auf den PO oeffnet den Projekte-Dialog zuverlaessig
Der Konfig-Dialog liess sich in der Praxis kaum oeffnen (Fehlerbericht in Issue #27). Drei Ursachen, alle reproduziert: - Ein Klick auf den PO selbst tat gar nichts: der Klick-Handler hing nur am Schreibtisch, die Figur liegt aber in der Avatar-Schicht darueber. Genau sie meint das Ticket ("bei Klick auf den PO"). Der Avatar ist jetzt klickbar, auch waehrend er am Board steht. - Klicks auf den Platz trafen nur, wenn sie zufaellig auf einem gefuellten Moebelstueck landeten - eine <g> faengt in ihren Luecken keine Klicks. Der Platz hat jetzt eine eigene Trefferflaeche, wie die Board-Spalten. - Dass hinter dem PO ueberhaupt etwas steckt, war nicht zu sehen. Ein kleines Zahnrad auf seinem Tisch (mit Tooltip) zeigt es an. Ausserdem oeffnet der Dialog jetzt auch bei nicht erreichbarer API - er meldet dann selbst, dass er die Projekte nicht laden konnte, statt tot zu wirken. Und genau dieser Fall trat leicht ein: der eingebaute PHP-Server beantwortet Anfragen nacheinander, eine offene SSE-Verbindung blockiert damit alles andere (gemessen 16s fuer ein GET /api/v1/tickets, ab dem zweiten offenen Monitor dauerhaft). Das Frontend faellt dann auf Demo-Daten zurueck. Mit PHP_CLI_SERVER_WORKERS=4 sind es 0,006s; PRAGMA busy_timeout deckt die Schreibzugriffe ab, die sich jetzt begegnen koennen. ApplicationTest::testFullFlowCreateUpdateAndDeleteRepoConfigWithAdminKey war seit dem Seed in 003_repo_configs.sql rot (legte fds2610/monitor ein zweites Mal an) - der Test nutzt jetzt ein nicht geseedetes Repo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
1ad4f25777
commit
d5f7ac9f92
6 changed files with 128 additions and 13 deletions
1
api/.phpunit.result.cache
Normal file
1
api/.phpunit.result.cache
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -20,6 +20,11 @@ final class Database
|
|||
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$this->pdo->exec('PRAGMA foreign_keys = ON');
|
||||
// Seit der API-Server mit mehreren Workern laeuft (PHP_CLI_SERVER_WORKERS, siehe
|
||||
// docker-compose.yml) koennen sich zwei Anfragen beim Schreiben begegnen. Ohne
|
||||
// Wartezeit meldet SQLite dann sofort "database is locked"; fuenf Sekunden sind fuer
|
||||
// die kurzen Schreibvorgaenge dieser Anwendung reichlich.
|
||||
$this->pdo->exec('PRAGMA busy_timeout = 5000');
|
||||
}
|
||||
|
||||
public static function forFile(string $path): self
|
||||
|
|
|
|||
|
|
@ -143,10 +143,13 @@ final class ApplicationTest extends TestCase
|
|||
|
||||
public function testFullFlowCreateUpdateAndDeleteRepoConfigWithAdminKey(): void
|
||||
{
|
||||
// Bewusst ein Repo, das die Migration nicht schon geseedet hat (monitor/Steuerkram/
|
||||
// nc-gantt, siehe 003_repo_configs.sql) - sonst scheitert schon das Anlegen an der
|
||||
// Dublettenpruefung, und der eigentliche Ablauf wird gar nicht geprueft.
|
||||
$createResponse = $this->app->handle(Request::create(
|
||||
'POST',
|
||||
'/api/v1/repo-configs',
|
||||
body: ['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#fbca04'],
|
||||
body: ['owner' => 'fds2610', 'repo' => 'weiteres-projekt', 'color' => '#fbca04'],
|
||||
headers: ['X-Admin-Key' => 'admin-secret']
|
||||
));
|
||||
self::assertSame(201, $createResponse->status);
|
||||
|
|
@ -155,7 +158,7 @@ final class ApplicationTest extends TestCase
|
|||
$updateResponse = $this->app->handle(Request::create(
|
||||
'PATCH',
|
||||
'/api/v1/repo-configs/' . $id,
|
||||
body: ['owner' => 'fds2610', 'repo' => 'monitor', 'color' => '#000000'],
|
||||
body: ['owner' => 'fds2610', 'repo' => 'weiteres-projekt', 'color' => '#000000'],
|
||||
headers: ['X-Admin-Key' => 'admin-secret']
|
||||
));
|
||||
self::assertSame(200, $updateResponse->status);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,12 @@ services:
|
|||
ports:
|
||||
- "8091:8080"
|
||||
environment:
|
||||
# Der eingebaute PHP-Server arbeitet Anfragen sonst streng nacheinander ab: eine einzige
|
||||
# offene SSE-Verbindung (GET /api/v1/stream, bis zu 60s) blockiert damit jede weitere
|
||||
# Anfrage - gemessen 16s fuer ein GET /api/v1/tickets. Ab dem zweiten offenen Monitor
|
||||
# laeuft die API praktisch dauerhaft dicht, das Frontend faellt auf Demo-Daten zurueck
|
||||
# und der PO reagiert dann auf nichts mehr (Fehlerbericht zu Issue #27).
|
||||
PHP_CLI_SERVER_WORKERS: 4
|
||||
MONITOR_DB_PATH: /app/data/monitor.sqlite
|
||||
MONITOR_ADMIN_API_KEY: ${MONITOR_ADMIN_API_KEY:-dev-admin-key}
|
||||
MONITOR_STATUS_FILE: /status.txt
|
||||
|
|
|
|||
|
|
@ -344,6 +344,41 @@ header.office-header h1 {
|
|||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Trefferflaeche des PO-Platzes (Issue #27): macht den ganzen Platz klickbar, nicht nur die
|
||||
gefuellten Moebelstuecke - ein Klick daneben landete sonst auf dem Boden. */
|
||||
.po-desk-hit {
|
||||
fill: transparent;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* Zahnrad am PO-Platz (Issue #27): zeigt ueberhaupt erst, dass sich hier die
|
||||
Projektkonfiguration oeffnen laesst. Beim Hover ueber den Platz tritt es hervor. */
|
||||
.po-config-hint-bg {
|
||||
fill: #ffffff;
|
||||
stroke: #94a3b8;
|
||||
stroke-width: 1.5;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.po-config-hint-tooth,
|
||||
.po-config-hint-hub {
|
||||
stroke: #475569;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.po-desk:hover .po-config-hint-bg {
|
||||
fill: #e0f2fe;
|
||||
stroke: #0284c7;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.po-desk:hover .po-config-hint-tooth,
|
||||
.po-desk:hover .po-config-hint-hub {
|
||||
stroke: #0284c7;
|
||||
}
|
||||
|
||||
.just-updated .board-frame,
|
||||
.just-updated .monitor-frame {
|
||||
animation: office-flash 0.9s ease-out;
|
||||
|
|
|
|||
|
|
@ -533,24 +533,67 @@ function renderDesk({ deskIndex, agent }) {
|
|||
* @param {Array<object>} desks - reguläre 8er-Raster-Plaetze (echte Agenten, F1).
|
||||
* @param {object|null} poAgent - {name: "PO", role: "product_owner", pose} bei erreichbarer
|
||||
* API, sonst null (dann zeigt der feste PO-Platz "frei", Issue #19).
|
||||
* @param {(() => void)|null} onPoClick - oeffnet das Projekte-Modal (Issue #27); nur gesetzt
|
||||
* bei erreichbarer API, da das Modal echte Daten braucht und im Demo-Fallback ohne
|
||||
* funktionierende API nichts zu konfigurieren waere.
|
||||
* @param {() => void} onPoClick - oeffnet das Projekte-Modal (Issue #27).
|
||||
*/
|
||||
function renderDesks(desks, poAgent, onPoClick = null) {
|
||||
function renderDesks(desks, poAgent, onPoClick) {
|
||||
const g = el("g", { class: "desks" });
|
||||
desks.forEach((desk) => g.appendChild(renderDesk(desk)));
|
||||
const poDeskGroup = renderDeskAt(PO_DESK, poAgent, PO_DESK.w);
|
||||
poDeskGroup.classList.add("po-desk");
|
||||
if (onPoClick) {
|
||||
poDeskGroup.classList.add("clickable");
|
||||
poDeskGroup.setAttribute("aria-label", "Projekte des Codeberg-Sync verwalten");
|
||||
poDeskGroup.addEventListener("click", onPoClick);
|
||||
}
|
||||
poDeskGroup.classList.add("po-desk", "clickable");
|
||||
poDeskGroup.setAttribute("aria-label", "Projekte des Codeberg-Sync verwalten");
|
||||
poDeskGroup.appendChild(renderPoConfigHint());
|
||||
// Eine <g> selbst faengt keine Klicks - nur ihre gefuellten Kinder. Zwischen Tischplatte,
|
||||
// Stuhl und Namensschild liegen Luecken, in denen ein Klick "auf den PO" beim Boden landete
|
||||
// (Fehlerbericht zu Issue #27). Diese unsichtbare Flaeche deckt den ganzen Platz ab, analog
|
||||
// zu .board-column-hit bei den Board-Spalten.
|
||||
poDeskGroup.appendChild(
|
||||
el("rect", {
|
||||
x: -10,
|
||||
y: 0,
|
||||
width: PO_DESK.w + 20,
|
||||
height: 40 + DESK.h + 90,
|
||||
class: "po-desk-hit",
|
||||
})
|
||||
);
|
||||
poDeskGroup.addEventListener("click", onPoClick);
|
||||
g.appendChild(poDeskGroup);
|
||||
return g;
|
||||
}
|
||||
|
||||
// Dass hinter dem PO die Projektkonfiguration steckt, war bisher nirgends zu sehen - der
|
||||
// Fehlerbericht zu Issue #27 ("damit ich den Konfig Dialog irgendwie oeffnen kann") war
|
||||
// deshalb auch eine Frage der Auffindbarkeit. Das Zahnrad sitzt auf der Tischplatte des
|
||||
// PO-Platzes; klickbar ist weiterhin der ganze Platz (und der PO selbst). <title> ist der
|
||||
// SVG-eigene Tooltip.
|
||||
function renderPoConfigHint() {
|
||||
const cx = PO_DESK.w - 34;
|
||||
const cy = 40 + DESK.h / 2;
|
||||
const g = el("g", { class: "po-config-hint", transform: `translate(${cx}, ${cy})` });
|
||||
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = "Projekte konfigurieren";
|
||||
g.appendChild(title);
|
||||
|
||||
g.appendChild(el("circle", { cx: 0, cy: 0, r: 15, class: "po-config-hint-bg" }));
|
||||
// Zahnrad aus acht Zaehnen plus Nabe - in dieser Groesse von einem exakten Zahnradpfad
|
||||
// nicht zu unterscheiden und ohne fremde Icon-Bibliothek zu haben.
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = (i * Math.PI) / 4;
|
||||
g.appendChild(
|
||||
el("line", {
|
||||
x1: Math.cos(angle) * 5.5,
|
||||
y1: Math.sin(angle) * 5.5,
|
||||
x2: Math.cos(angle) * 9.5,
|
||||
y2: Math.sin(angle) * 9.5,
|
||||
class: "po-config-hint-tooth",
|
||||
})
|
||||
);
|
||||
}
|
||||
g.appendChild(el("circle", { cx: 0, cy: 0, r: 5, class: "po-config-hint-hub" }));
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Phase 5 - Agenten-Avatare (bewegliche Schicht, siehe Architektur-Hinweis oben)
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
|
@ -634,6 +677,9 @@ function updateAgentsLayer(layer, placements) {
|
|||
let node = agentAvatarElements.get(id);
|
||||
if (!node) {
|
||||
node = createAgentAvatarElement();
|
||||
if (id === PO_AGENT_ID) {
|
||||
makePoAvatarClickable(node);
|
||||
}
|
||||
agentAvatarElements.set(id, node);
|
||||
layer.appendChild(node);
|
||||
}
|
||||
|
|
@ -641,6 +687,11 @@ function updateAgentsLayer(layer, placements) {
|
|||
const feet = feetForLocation(agent.location, homeFeet);
|
||||
const showName = agent.location !== AGENT_LOCATIONS.DESK;
|
||||
updateAgentAvatarElement(node, agent, feet, showName);
|
||||
if (id === PO_AGENT_ID) {
|
||||
// updateAgentAvatarElement() setzt class komplett neu (Pose) - ohne diese Zeile
|
||||
// verloere der PO die Klickbarkeit beim ersten Zustandswechsel.
|
||||
node.classList.add("clickable");
|
||||
}
|
||||
});
|
||||
|
||||
for (const [id, node] of agentAvatarElements) {
|
||||
|
|
@ -652,6 +703,17 @@ function updateAgentsLayer(layer, placements) {
|
|||
}
|
||||
}
|
||||
|
||||
// Das Ticket sagt woertlich "bei Klick auf den PO" - gemeint ist die Figur, nicht ihr
|
||||
// Schreibtisch. Der Avatar liegt in einer eigenen Schicht ueber der Szene und hatte bisher
|
||||
// keinen Klick-Handler; ein Klick auf den PO selbst lief damit ins Leere. Beides ist jetzt
|
||||
// klickbar, auch wenn der PO gerade am Board steht statt an seinem Platz.
|
||||
function makePoAvatarClickable(node) {
|
||||
const title = document.createElementNS(SVG_NS, "title");
|
||||
title.textContent = "Projekte konfigurieren";
|
||||
node.appendChild(title);
|
||||
node.addEventListener("click", openRepoConfigModal);
|
||||
}
|
||||
|
||||
function clearIdleCosmeticTimer(agentId) {
|
||||
const handle = idleCosmeticTimers.get(agentId);
|
||||
if (handle) {
|
||||
|
|
@ -1347,7 +1409,10 @@ export function renderOfficeScene(container, data = {}) {
|
|||
|
||||
const deskLayout = buildDeskLayout(normalizedDeskAgents(agents));
|
||||
const poDeskAgent = isLive ? { name: PO_DISPLAY_NAME, role: "product_owner", pose: "idle" } : null;
|
||||
const desksGroup = renderDesks(deskLayout, poDeskAgent, isLive ? openRepoConfigModal : null);
|
||||
// Auch ohne erreichbare API klickbar: das Modal sagt dann selbst, dass es die Projekte
|
||||
// nicht laden konnte. Ein toter PO waere in genau der Lage, in der man nachsehen will,
|
||||
// die schlechtere Antwort (Fehlerbericht zu Issue #27).
|
||||
const desksGroup = renderDesks(deskLayout, poDeskAgent, openRepoConfigModal);
|
||||
const existingDesks = svg.querySelector(".desks");
|
||||
if (existingDesks) {
|
||||
existingDesks.replaceWith(desksGroup);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue