diff --git a/public/docs/changelog.json b/public/docs/changelog.json index 57fa8b8..ea17b9e 100644 --- a/public/docs/changelog.json +++ b/public/docs/changelog.json @@ -62,6 +62,24 @@ "breaking": false, "title": "Der Parameter search ist jetzt beschrieben", "text": "action=list kennt search seit jeher, in der OpenAPI-Beschreibung stand er nicht. Nachgetragen, mitsamt dem Verhalten bei Zahlen." + }, + { + "area": "webui", + "breaking": false, + "title": "Statusfilter als Mehrfachauswahl, mit Vorgabe auf das Laufende", + "text": "Vorher liess sich genau eine Lage waehlen; wer \"offen und in Nachpruefung\" sehen wollte, brauchte eine vorgefertigte Kombination - stand sie nicht in der Liste, ging es nicht. Jetzt Ankreuzfelder, gruppiert nach Laufend / Verlangt Aufmerksamkeit / Abgeschlossen. Angehakt ist ab Werk, was laeuft oder Aufmerksamkeit verlangt (open, planned, in_progress, triage_external, awaiting_verification, in_verification, fix_insufficient); Erledigtes, Geschlossenes und Abgelehntes bleibt draussen - es ist der groesste Teil des Bestandes und verdeckt sonst das, was noch zu tun ist. \"deferred\" fehlt mit Absicht: Der Sinn dieses Status ist \"nicht jetzt\". Kein Haken bedeutet keine Einschraenkung; das versteckte Feld im Formular macht \"alle Haken entfernt\" von \"erster Aufruf\" unterscheidbar, denn HTML sendet fuer leere Ankreuzfelder nichts." + }, + { + "area": "webui", + "breaking": false, + "title": "Die Suche sagt, wenn ein Filter Treffer verschluckt", + "text": "Eine Suche, die schweigt, obwohl sie etwas gefunden hat, ist schlimmer als gar keine Suche: Wer \"288\" eingibt und nichts bekommt, schliesst daraus, es gebe den Eintrag nicht - dabei ist er nur erledigt und deshalb ausgeblendet. Bei aktiver Suche wird jetzt zusaetzlich gezaehlt, wie viele Treffer die aktuellen Einschraenkungen wegnehmen, und das Ergebnis steht als Hinweis ueber der Liste, mit einem Verweis \"Ueberall suchen\". Die zweite Abfrage laeuft nur bei aktiver Suche und holt eine einzige Zeile." + }, + { + "area": "webui", + "breaking": false, + "title": "Roadmap-Eintraege stehen nicht mehr in der Bugliste", + "text": "Seit 2.7.0 IST ein Roadmap-Eintrag ein bugtracker_item - richtig so, es gibt keine zweite Ablage. Nur landet damit die gesamte Planung aller Projekte in derselben Liste wie die Meldungen: Nach der Uebernahme der Projekt-Roadmaps waren 217 von 229 Feature Requests Roadmap-Eintraege, und die zwoelf Vorschlaege, um die es in dieser Liste geht, lagen darunter begraben. Die Bugliste zeigt jetzt ab Werk nur, was NICHT auf der Roadmap steht (Kennzeichen ist unveraendert der Themenbereich). Sichtbar bleibt beides, nur an getrennten Orten; der neue Filter holt die Planung jederzeit wieder herein, und die Suche weist auf verdeckte Treffer hin." } ] }, diff --git a/public/index.php b/public/index.php index d819ecf..a33de81 100644 --- a/public/index.php +++ b/public/index.php @@ -1897,18 +1897,101 @@ $coreAllTokens = $coreTokenManager->getAllTokens(); $btPerPage = 50; $btPage = max(1, (int)($_GET['bt_page'] ?? 1)); +/** + * Status, die beim ersten Aufruf angehakt sind. + * + * Alles, was gerade laeuft oder Aufmerksamkeit verlangt. Erledigtes, + * Geschlossenes und Abgelehntes bleibt draussen - es ist der weitaus groesste + * Teil des Bestandes und verdeckt sonst das, was noch zu tun ist. + * + * "deferred" fehlt mit Absicht: Der ganze Sinn dieses Status ist "nicht jetzt". + * Ein zurueckgestelltes Vorhaben in der Standardliste waere ein Widerspruch in + * sich - es steht unter "Zurueckgestellt" bereit, wenn jemand danach sucht. + */ +const BT_STATUS_VORGABE = [ + 'open', 'planned', 'in_progress', 'triage_external', + 'awaiting_verification', 'in_verification', 'fix_insufficient', +]; + +/** + * Liest die angehakten Status aus der Anfrage. + * + * Drei Faelle, und der mittlere ist der Grund fuer das versteckte Feld im + * Formular: + * + * Parameter fehlt ganz -> erster Aufruf, es gilt die Vorgabe + * Parameter ist leer -> alle Haken entfernt, also KEINE Einschraenkung + * Parameter hat Werte -> genau diese Status + * + * Ohne den zweiten Fall waere "alle Haken weg" von "noch nie etwas gewaehlt" + * nicht zu unterscheiden - HTML sendet fuer leere Ankreuzfelder nichts. Die + * Vorgabe kaeme dann bei jedem Versuch zurueck, sie loszuwerden. + * + * Zeichenketten werden weiterhin angenommen: Aeltere Lesezeichen und die + * Verweise aus den Kennzahlen kommen als bt_status=open,in_progress herein. + * + * @return list Leer bedeutet "keine Einschraenkung". + */ +function btStatusAuswahl(): array +{ + if (!array_key_exists('bt_status', $_GET)) { + return BT_STATUS_VORGABE; + } + + $roh = $_GET['bt_status']; + $werte = is_array($roh) ? $roh : explode(',', (string)$roh); + + $werte = array_map(static fn($v): string => trim((string)$v), $werte); + $werte = array_values(array_filter( + $werte, + static fn(string $v): bool => in_array($v, BugRepo::STATUSES, true) + )); + + // "all" als einzelner Wert ist die alte Schreibweise fuer "ohne Filter". + if (!is_array($roh) && trim((string)$roh) === 'all') { + return []; + } + + return array_values(array_unique($werte)); +} + +$btStatusAuswahl = btStatusAuswahl(); + +/** + * Roadmap-Eintraege gehoeren in die Roadmap, nicht in die Bugliste. + * + * Seit 2.7.0 IST ein Roadmap-Eintrag ein Bugtracker-Item - es gibt keine + * zweite Ablage, und ein Item steht auf der Roadmap, sobald es einen + * Themenbereich hat. Richtig so; nur landet damit die gesamte Planung aller + * Projekte in derselben Liste wie die Meldungen. Nach der Uebernahme der + * Projekt-Roadmaps waren das 217 von 229 Feature Requests - die zwoelf + * Vorschlaege, um die es in dieser Liste geht, lagen darunter begraben. + * + * Die Bugliste zeigt deshalb ab Werk nur, was NICHT auf der Roadmap steht. + * Sichtbar bleibt beides, nur an getrennten Orten - und der Filter holt die + * Planung jederzeit wieder herein. + */ +$btRoadmap = (string)($_GET['bt_roadmap'] ?? 'ohne'); +if (!in_array($btRoadmap, ['ohne', 'nur', 'alle'], true)) { + $btRoadmap = 'ohne'; +} + $btFilters = [ 'project_slug' => (string)($_GET['bt_project'] ?? 'all'), 'environment' => (string)($_GET['bt_env'] ?? 'all'), 'type' => (string)($_GET['bt_type'] ?? 'all'), 'severity' => (string)($_GET['bt_severity'] ?? 'all'), - 'status' => (string)($_GET['bt_status'] ?? 'all'), + 'status' => $btStatusAuswahl === [] ? 'all' : $btStatusAuswahl, 'search' => (string)($_GET['bt_q'] ?? ''), 'order' => (string)($_GET['bt_order'] ?? 'newest'), 'limit' => $btPerPage, 'offset' => ($btPage - 1) * $btPerPage, ]; +if ($btRoadmap !== 'alle') { + $btFilters['on_roadmap'] = ($btRoadmap === 'nur'); +} + $bugRepo = new BugRepo($pdo); $bugtrackerResult = $bugRepo->getItems($btFilters); $bugtrackerItems = $bugtrackerResult['items']; @@ -1918,6 +2001,29 @@ $bugtrackerStats = $bugRepo->getStats( $btFilters['project_slug'] !== 'all' ? $btFilters['project_slug'] : null ); +/** + * Wie viele Treffer der Statusfilter gerade verschluckt. + * + * Der Fall, um den es geht: Jemand sucht nach "288", bekommt nichts und + * schliesst daraus, dass es den Eintrag nicht gibt - dabei ist er nur + * erledigt und deshalb ausgeblendet. Eine Suche, die schweigt, obwohl sie + * etwas gefunden hat, ist schlimmer als gar keine Suche. + * + * Die zusaetzliche Abfrage laeuft nur bei aktiver Suche UND aktivem + * Statusfilter - also genau dann, wenn die Verwechslung moeglich ist. Sie + * holt eine einzige Zeile; gebraucht wird nur die Gesamtzahl. + */ +$btVerdeckt = 0; +if ($btFilters['search'] !== '' && ($btStatusAuswahl !== [] || $btRoadmap !== 'alle')) { + $ohneEinschraenkung = $btFilters; + unset($ohneEinschraenkung['on_roadmap']); + $ohneEinschraenkung['status'] = 'all'; + $ohneEinschraenkung['limit'] = 1; + $ohneEinschraenkung['offset'] = 0; + + $btVerdeckt = max(0, $bugRepo->getItems($ohneEinschraenkung)['total'] - $bugtrackerTotal); +} + /** Baut eine URL mit den aktuellen Bugtracker-Filtern und einzelnen Ueberschreibungen. */ function btUrl(array $overrides = []): string { @@ -1927,6 +2033,7 @@ function btUrl(array $overrides = []): string 'bt_type' => $_GET['bt_type'] ?? null, 'bt_severity' => $_GET['bt_severity'] ?? null, 'bt_status' => $_GET['bt_status'] ?? null, + 'bt_roadmap' => $_GET['bt_roadmap'] ?? null, 'bt_q' => $_GET['bt_q'] ?? null, 'bt_order' => $_GET['bt_order'] ?? null, 'bt_page' => $_GET['bt_page'] ?? null, @@ -2428,6 +2535,41 @@ $csrfField = Csrf::field(); .zelle-aktionen > div { display: flex; gap: 0.3rem; flex-wrap: nowrap; } .tab-content, .subtab-content { display: none; } + + /* Mehrfachauswahl im Filterbalken. +
statt eigenem Klappmechanismus: Auf- und Zuklappen kann der + Browser, und ohne JavaScript bleibt es bedienbar. */ + .filter-mehrfach { position: relative; display: inline-block; } + .filter-mehrfach > summary { + list-style: none; cursor: pointer; white-space: nowrap; + padding: 0.3rem 0.6rem; font-size: 0.8rem; + border: 1px solid var(--border-glass, rgba(255,255,255,0.12)); + border-radius: 6px; background: rgba(255,255,255,0.04); + } + .filter-mehrfach > summary::-webkit-details-marker { display: none; } + .filter-mehrfach[open] > summary { border-color: #7aa2ff; } + .filter-mehrfach-inhalt { + position: absolute; z-index: 50; top: calc(100% + 4px); left: 0; + min-width: 240px; padding: 0.6rem 0.75rem; + background: #161c28; + border: 1px solid var(--border-glass, rgba(255,255,255,0.12)); + border-radius: 8px; box-shadow: 0 12px 32px rgba(0,0,0,0.5); + } + .filter-mehrfach-inhalt label { + display: flex; align-items: center; gap: 0.45rem; + padding: 0.15rem 0; font-size: 0.82rem; cursor: pointer; font-weight: 400; + } + .filter-gruppe { + font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; + opacity: 0.55; margin: 0.55rem 0 0.2rem; + } + .filter-gruppe:first-child { margin-top: 0; } + .filter-mehrfach-fuss { + display: flex; gap: 0.35rem; margin-top: 0.7rem; + padding-top: 0.6rem; + border-top: 1px solid var(--border-glass, rgba(255,255,255,0.12)); + } + .filter-hinweis { font-size: 0.7rem; opacity: 0.5; margin-top: 0.4rem; } .tab-content.active, .subtab-content.active { display: block; } @@ -4706,34 +4848,100 @@ chmod +x update-agent - + '🗺️ Ohne Roadmap', + 'nur' => '🗺️ Nur Roadmap', + 'alle' => '🗺️ Mit Roadmap', + ] as $wert => $beschriftung): ?> + +
+ + [ + 'open' => 'Offen', + 'planned' => 'Geplant', + 'in_progress' => 'In Bearbeitung', + ], + 'Verlangt Aufmerksamkeit' => [ + 'triage_external' => '📥 Fremdmeldung', + 'awaiting_verification' => '🔎 Wartet auf Nachpruefung', + 'in_verification' => '🔎 In Nachpruefung', + 'fix_insufficient' => '❗ Fix traegt nicht', + ], + 'Abgeschlossen' => [ + 'resolved' => 'Gelöst', + 'closed' => 'Geschlossen', + 'rejected' => 'Abgelehnt', + 'ignored' => 'Ignoriert', + 'deferred' => 'Zurückgestellt', + ], + ]; + + $btStatusLabels = []; + foreach ($statusGruppen as $eintraege) { + $btStatusLabels += $eintraege; + } + + $anzahl = count($btStatusAuswahl); + if ($anzahl === 0) { + $btStatusText = '📌 Alle Status'; + } elseif ($anzahl === 1) { + $btStatusText = '📌 ' . ($btStatusLabels[$btStatusAuswahl[0]] ?? $btStatusAuswahl[0]); + } else { + $btStatusText = sprintf('📌 %d Status', $anzahl); + } + ?> + ▾ + + +
+ + + $eintraege): ?> +
+ $beschriftung): ?> + + + + +
+ + + +
+
Kein Haken = keine Einschränkung.
+
+
+