Hustler-Stufe: Keller und Bot-Leads erst ab Stufe 6 (Seite, Keller-Etage, Spur-Links, Abruf von /botleads)
WorkersPage: ?tab=botleads (auch mit &bot=) und unbekannte Werte fallen unterhalb von Stufe 6 auf das Hochhaus zurück, BotLeadList wird nie eingehängt; die Weiterleitungen /leads und /workers bleiben. HochhausWelt: die Keller-Etage ist dann ein Knopf wie jede andere (zoomt heran) statt eines Links; Arne bleibt klickbar, die Zahl Bot-Leads in der Kopfzeile bleibt Text. HustlerDialog: Spur-Links in den Keller bleiben Text. workersApi.botleads ruft unterhalb von Stufe 6 nichts ab (kein JWT, keine Anfrage), sondern liefert 404 mit deutschem Satz. Bestehende Keller-Tests laufen ausdrücklich auf Stufe 6; neue Tests prüfen Stufe 1, 3 und 5 (gesperrt) und 6 bis 9 (offen). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5e48fbf264
commit
32f4f1d941
10 files changed
+262
-21
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { HUSTLER_STUFE } from '../../../lib/hustlerStufe'
|
||||
import HochhausKopf from './HochhausKopf'
|
||||
import HochhausWelt from './HochhausWelt'
|
||||
import HustlerDialog from './HustlerDialog'
|
||||
@@ -17,8 +18,9 @@ function Ueberschrift() {
|
||||
// (regieRef, die Seite ruft regieRef.current.push(events) bei neuen Ereignissen), der Bildschirmwechsel (onOpen(tab)) und vonTab (der Bildschirm, von dem man
|
||||
// zurückkommt: 'freigabe' | 'botleads' | null) kommen von der Seite. Beim Zurückkommen bekommt die Etage, über die man gegangen war, den Fokus (M2).
|
||||
// Die Kopfzeile steht im DOM vor der Welt, damit die Tab-Reihenfolge oben beginnt (M7); über der Welt liegt sie per z-index.
|
||||
// stufe (Standard HUSTLER_STUFE, hustlerStufe.js) geht an die Welt (Keller-Etage als Link nur ab Stufe 6) und an die Detailansicht (Spur-Links in den Keller nur ab Stufe 6).
|
||||
// Kein CSS-Import und keine Bilder: die Stile hängt WorkersPage ein (Regeln dieser Datei in styles/hustler/hustler-ui.css).
|
||||
export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen, vonTab = null }) {
|
||||
export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen, vonTab = null, stufe = HUSTLER_STUFE }) {
|
||||
const wurzelRef = useRef(null)
|
||||
const weltRef = useRef(null)
|
||||
const [auswahl, setAuswahl] = useState(null) // workerId der offenen Detailansicht (null = keine)
|
||||
@@ -75,6 +77,7 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen
|
||||
onZeige={setAuswahl}
|
||||
onOpen={onOpen}
|
||||
gesperrt={auswahl !== null}
|
||||
stufe={stufe}
|
||||
>
|
||||
<RegieEbene ref={regieRef} />
|
||||
</HochhausWelt>
|
||||
@@ -84,6 +87,7 @@ export default function Hochhaus({ api, isAdmin, feed, workers, regieRef, onOpen
|
||||
workerId={auswahl}
|
||||
workers={workers}
|
||||
paused={feed.state?.paused === true}
|
||||
stufe={stufe}
|
||||
onClose={() => setAuswahl(null)}
|
||||
onWechsel={setAuswahl}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Himmel, Funkeln, Mond, die 2400 x 1200 große Bühne (per transform verschoben und gezoomt) mit Welt-, Licht-, Haus- und Fensterebenen, Neon, Antenne, Aufzug,
|
||||
// Raumschilder, Bildschirme, Radar, Briefkasten-Zähler, Tresor-/Keller-Text, den Etagen (Knöpfe, Freigabe und Keller sind Links) und den 19 Hustlern; davor der Schnee.
|
||||
// Alles hängt am Dienst-Zustand: Figur und Zusätze nach Status (figurZustand), graue Raum-Ebene samt Papierschild nur für nicht angeschlossene Such-Räume.
|
||||
// Stufen-Sperre (hustlerStufe.js): unterhalb der Keller-Stufe (6) ist die Keller-Etage kein Link auf die Bot-Leads, sondern ein Knopf wie jede andere Etage (heranzoomen).
|
||||
// Die Kamera (Rad, Ziehen, Pinch, Tasten) steckt in useKamera. `children` (die Regie-Ebene) liegt in der Bühne über den Figuren, in Weltkoordinaten.
|
||||
// Bedienung: Tastaturfokus (:focus-visible) auf Etage oder Figur außerhalb des Bildes holt sie per Kamera herein (beiFokus, kein Schwenk bei Mausklick); das Sichtfenster ist
|
||||
// per tabindex -1 fokussierbar (ein Klick in die Welt gibt ihm den Fokus, damit + - 0 Esc wirken); die Tippflächen der Figuren wachsen mit dem Zoom (--hk-tipp-*, tippRand);
|
||||
@@ -9,6 +10,7 @@
|
||||
// Kein CSS-Import (die Stile hängt WorkersPage ein: styles/hustler/hochhaus.css, hustler-ui.css, hustler-welt.css), keine Bilder, kein innerHTML.
|
||||
import { forwardRef, useImperativeHandle } from 'react'
|
||||
import { anzeigeName, figurZustand, schilder, tippRand } from '../../../lib/hustlerSicht.js'
|
||||
import { HUSTLER_STUFE, kellerOffen } from '../../../lib/hustlerStufe.js'
|
||||
import { AUFZUG, ETAGEN, ETAGEN_SPALTE, HAUS_BILD, HUSTLER, RAEUME, SCHILDER } from '../../../lib/hustlerWelt.js'
|
||||
import { FLOOR_TITLES, ROLES, ROOM_TITLES } from '../../../lib/workersRoster.js'
|
||||
import useKamera from './useKamera.js'
|
||||
@@ -19,6 +21,10 @@ const LINK_ETAGEN = Object.freeze({
|
||||
keller: Object.freeze({ tab: 'botleads', text: 'Keller · Bot-Leads ▸', label: 'Keller · Bot-Leads: Bot-Leads öffnen' }),
|
||||
})
|
||||
|
||||
// Welche Etagen Links sind: der Keller nur, wenn es die Bot-Leads schon gibt; sonst zoomt er heran wie jede andere Etage
|
||||
const OHNE_KELLER = Object.freeze({ freigabe: LINK_ETAGEN.freigabe })
|
||||
const linkEtagen = (stufe) => (kellerOffen(stufe) ? LINK_ETAGEN : OHNE_KELLER)
|
||||
|
||||
const HAUS_RECHTECK = Object.freeze({ left: HAUS_BILD.x, top: HAUS_BILD.y, width: HAUS_BILD.w, height: HAUS_BILD.h })
|
||||
|
||||
const PX = (n) => `${n}px`
|
||||
@@ -43,8 +49,9 @@ function zettelLage(raum) {
|
||||
return { left: r.x0 + 16, top: r.y0 + 1 }
|
||||
}
|
||||
|
||||
const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, onOpen, gesperrt = false, children }, ref) {
|
||||
const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige, onOpen, gesperrt = false, stufe = HUSTLER_STUFE, children }, ref) {
|
||||
const kam = useKamera({ gesperrt })
|
||||
const links = linkEtagen(stufe)
|
||||
useImperativeHandle(ref, () => ({ ganz: kam.ganz }), [kam.ganz])
|
||||
|
||||
const pausiert = state?.paused === true
|
||||
@@ -213,10 +220,10 @@ const HochhausWelt = forwardRef(function HochhausWelt({ state, workers, onZeige,
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{/* Etagen: Knöpfe zoomen heran, Freigabe und Keller sind Links auf die anderen Bildschirme */}
|
||||
{/* Etagen: Knöpfe zoomen heran, Freigabe (und ab der Keller-Stufe der Keller) sind Links auf die anderen Bildschirme */}
|
||||
{ETAGEN.map((e) => {
|
||||
const stil = { left: ETAGEN_SPALTE.x, top: e.oben, width: ETAGEN_SPALTE.w, height: e.unten - e.oben }
|
||||
const link = LINK_ETAGEN[e.id]
|
||||
const link = links[e.id]
|
||||
if (link) {
|
||||
return (
|
||||
<a key={e.id} className="hk-etage" data-hk-etage={e.id} href={`/hustler?tab=${link.tab}`} draggable={false} onClick={linkKlick(link.tab)} aria-label={link.label} style={stil}>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useWorkerTrace } from '../../../hooks/useWorkersFeed'
|
||||
import { blaettern, detailModell } from '../../../lib/hustlerSicht'
|
||||
import { HUSTLER_STUFE, linkErlaubt } from '../../../lib/hustlerStufe'
|
||||
import { DETAIL } from '../../../lib/hustlerWelt'
|
||||
import { safeLink } from '../../../lib/workersView'
|
||||
|
||||
@@ -25,10 +26,11 @@ const EINFACHE_ID = /^[A-Za-z0-9_-]+$/
|
||||
const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
|
||||
|
||||
// Einziger Ort des Dialogs, an dem ein Ziel für einen Link entsteht (wie TraceText in WorkerCard): nur was safeLink durchlässt, wird ein Link
|
||||
// (extern in neuem Tab mit noopener, intern über den Router), alles andere bleibt Text.
|
||||
function SpurText({ link, children }) {
|
||||
// (extern in neuem Tab mit noopener, intern über den Router), alles andere bleibt Text. Stufen-Sperre: ein interner Link in den Keller (?tab=botleads) bleibt
|
||||
// unterhalb der Keller-Stufe (hustlerStufe.js) Text.
|
||||
function SpurText({ link, stufe, children }) {
|
||||
const safe = isRecord(link) ? safeLink(link.href) : null
|
||||
if (!safe) return <>{children}</>
|
||||
if (!safe || !linkErlaubt(safe.href, stufe)) return <>{children}</>
|
||||
if (safe.external) return <a href={safe.href} target="_blank" rel="noopener noreferrer">{children}</a>
|
||||
return <Link to={safe.href}>{children}</Link>
|
||||
}
|
||||
@@ -44,8 +46,8 @@ function figurStelle(szene) {
|
||||
const ansageVon = (m) => `${m.name}, ${m.statusText}, ${m.nummer.replace(' / ', ' von ')}`
|
||||
|
||||
// Props: api (Dienst-API für die Spur), workerId (null = nichts anzeigen), workers (Hustler-Tabelle der Seite, Zustandseinträge nach workerId),
|
||||
// paused (Dienst angehalten: Animationen stehen), onClose(), onWechsel(nextId) (Blättern; die Seite setzt die neue Id)
|
||||
export default function HustlerDialog({ api, workerId, workers, paused = false, onClose, onWechsel }) {
|
||||
// paused (Dienst angehalten: Animationen stehen), onClose(), onWechsel(nextId) (Blättern; die Seite setzt die neue Id), stufe (Standard HUSTLER_STUFE: Spur-Links in den Keller)
|
||||
export default function HustlerDialog({ api, workerId, workers, paused = false, stufe = HUSTLER_STUFE, onClose, onWechsel }) {
|
||||
const { trace, loading, error } = useWorkerTrace(api, workerId)
|
||||
const ersterRef = useRef(null) // Knopf „Schließen“ (Fokus beim Öffnen, Anfang der Fokusfalle)
|
||||
const letzterRef = useRef(null) // Knopf „Nächster Hustler“ (Ende der Fokusfalle)
|
||||
@@ -183,7 +185,7 @@ export default function HustlerDialog({ api, workerId, workers, paused = false,
|
||||
<li key={`${row.id}-${i}`} className="hk-dialog-spurzeile">
|
||||
<span className="hk-dialog-zeit">{row.time}</span>
|
||||
<span className="hk-dialog-spurtext">
|
||||
<SpurText link={row.link}>{row.text}</SpurText>
|
||||
<SpurText link={row.link} stufe={stufe}>{row.text}</SpurText>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
|
||||
+10
-1
@@ -2,11 +2,15 @@
|
||||
// Rein: kein Appwrite-Import, getJwt und fetch werden injiziert (workersClient.js verdrahtet sie), damit die
|
||||
// deutschen Fehlertexte unter Node testbar sind. Fehler sind Error mit deutscher `message` und `status`.
|
||||
|
||||
import { HUSTLER_STUFE, kellerOffen } from './hustlerStufe'
|
||||
|
||||
export const DOWN_TEXT = 'Der Hustler-Dienst ist gerade nicht erreichbar.'
|
||||
export const BAD_REPLY_TEXT = 'Der Hustler-Dienst antwortet nicht richtig.'
|
||||
export const SESSION_TEXT = 'Anmeldung abgelaufen. Bitte neu anmelden.'
|
||||
export const FORBIDDEN_TEXT = 'Dafür fehlt dir die Berechtigung.'
|
||||
export const INVALID_ID_TEXT = 'Ungültige Kennung.'
|
||||
// Der Keller (GET /botleads) gehört zu Stufe 6 (hustlerStufe.js); darunter gibt es den Endpunkt im Dienst noch nicht
|
||||
export const KELLER_GESPERRT_TEXT = 'Der Keller ist noch nicht freigeschaltet.'
|
||||
|
||||
// Zeitgrenze je Abruf (JWT holen, senden, Antwort lesen): ohne sie bliebe ein Abruf auf toter Verbindung (Rechner im Ruhezustand,
|
||||
// Netzwechsel) minutenlang offen und der Feed (ein Abruf je Schleife) stünde still, ohne Fehlermeldung.
|
||||
@@ -81,6 +85,7 @@ export function createWorkersApi({
|
||||
actionTimeoutMs = ACTION_TIMEOUT_MS,
|
||||
authCooldownMs = AUTH_COOLDOWN_MS,
|
||||
now = Date.now,
|
||||
stufe = HUSTLER_STUFE,
|
||||
}) {
|
||||
let freshBlockedUntil = 0 // bis dahin wird nach einer 401-Folge kein frisches JWT erzeugt und nach einem gescheiterten createJWT gar keins (AUTH_COOLDOWN_MS)
|
||||
let mintFailed = false // Appwrite hat createJWT abgelehnt (HTTP-Code ab 400, kein Übertragungsfehler): bis zum Ende der Sperre wird getJwt nicht mehr gerufen, auch nicht bei leerem Cache
|
||||
@@ -193,7 +198,11 @@ export function createWorkersApi({
|
||||
if (!PULT_ACTIONS.includes(action)) throw statusError(`Unbekannte Aktion: ${action}`, 400)
|
||||
return request(`/pult/${pathId(id)}/${action}`, { method: 'POST', body, limitMs: SLOW_PULT_ACTIONS.includes(action) ? actionTimeoutMs : undefined })
|
||||
},
|
||||
botleads: ({ room, q, page, id } = {}) => request(`/botleads${query([['room', room], ['q', q], ['page', page], ['id', id]])}`),
|
||||
// Unterhalb der Keller-Stufe (6) geht gar keine Anfrage hinaus: der Endpunkt fehlt im Dienst noch (die Oberfläche zeigt den Keller dort auch nicht)
|
||||
botleads: async ({ room, q, page, id } = {}) => {
|
||||
if (!kellerOffen(stufe)) throw statusError(KELLER_GESPERRT_TEXT, 404)
|
||||
return request(`/botleads${query([['room', room], ['q', q], ['page', page], ['id', id]])}`)
|
||||
},
|
||||
prospect: async (id) => request(`/prospect/${pathId(id)}`),
|
||||
// Beim Abmelden: Sperre aufheben, damit die nächste Anmeldung sofort wieder ein JWT holen darf (den JWT-Cache leert workersClient.js)
|
||||
resetAuth: () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import Hochhaus from '../components/workers/hochhaus/Hochhaus'
|
||||
import PultList from '../components/workers/PultList'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useWorkersFeed } from '../hooks/useWorkersFeed'
|
||||
import { HUSTLER_STUFE, tabErlaubt } from '../lib/hustlerStufe'
|
||||
import { indexWorkers, reduceEvents } from '../lib/workersEvents'
|
||||
import { workersApi } from '../lib/workersClient'
|
||||
import { pickTab } from '../lib/workersView'
|
||||
@@ -23,6 +24,8 @@ import '../styles/hustler/hustler-regie.css'
|
||||
|
||||
// Bildschirme der Seite: das Hochhaus, das Pult (Freigabe von P3) und der Keller (Bot-Leads von P6). Eine Reiterleiste gibt es nicht mehr;
|
||||
// gewechselt wird im Haus (Etage Freigabe, Etage Keller) und über „◂ Hochhaus“ in Pult und Keller.
|
||||
// Stufen-Sperre (lib/hustlerStufe.js, HUSTLER_STUFE): der Keller ist erst ab Stufe 6 ein Bildschirm. Darunter fallen ?tab=botleads (auch mit &bot=) und jeder
|
||||
// andere unbekannte Wert auf das Hochhaus zurück (pickTab nimmt den ersten Reiter), BotLeadList wird nie eingehängt und /api/workers/botleads nie abgerufen.
|
||||
const WORKERS_TABS = [
|
||||
{ id: 'hochhaus', label: 'Hochhaus' },
|
||||
{ id: 'freigabe', label: 'Freigabe' },
|
||||
@@ -39,10 +42,11 @@ const api = workersApi
|
||||
// Die Tabelle liegt nie einen Render hinter dem Schnappschuss zurück (sonst stünden alle Figuren kurz auf „Leerlauf“): sie merkt sich, aus welchem Schnappschuss sie
|
||||
// entstand, und wird im Render neu aus `state` gebaut, sobald der Schnappschuss ein anderer ist. Pause (state.paused) geht an alle drei Bildschirme.
|
||||
// Das Hochhaus bekommt vonTab (der Bildschirm, von dem man zurückkommt) und fokussiert dann die Etage, über die man gegangen war.
|
||||
export default function WorkersPage() {
|
||||
// stufe (Standard HUSTLER_STUFE) ist nur für Tests ein Parameter; die App rendert die Seite ohne und liest die Stufe aus dem Bau.
|
||||
export default function WorkersPage({ stufe = HUSTLER_STUFE }) {
|
||||
const { isAdmin, user } = useAuth()
|
||||
const [params, setParams] = useSearchParams()
|
||||
const active = pickTab(WORKERS_TABS, params.get('tab'))
|
||||
const active = pickTab(WORKERS_TABS.filter((t) => tabErlaubt(t.id, stufe)), params.get('tab'))
|
||||
const bot = params.get('bot') ?? ''
|
||||
const [tabelle, setTabelle] = useState({ von: null, workers: {} }) // Hustler-Tabelle samt dem Schnappschuss, aus dem sie entstand
|
||||
const [verlauf, setVerlauf] = useState({ aktuell: active.id, vorher: null }) // aktueller und vorheriger Bildschirm
|
||||
@@ -82,7 +86,8 @@ export default function WorkersPage() {
|
||||
workers={workers}
|
||||
regieRef={regieRef}
|
||||
vonTab={vonTab}
|
||||
onOpen={(tab) => setParams({ tab })}
|
||||
stufe={stufe}
|
||||
onOpen={(tab) => { if (tabErlaubt(tab, stufe)) setParams({ tab }) }}
|
||||
/>
|
||||
)}
|
||||
{active.id === 'freigabe' && (
|
||||
@@ -92,6 +97,7 @@ export default function WorkersPage() {
|
||||
kpis={state?.kpis}
|
||||
workers={workers}
|
||||
paused={paused}
|
||||
stufe={stufe}
|
||||
onBack={() => setParams({ tab: 'hochhaus' })}
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user