Workers P6: Prüfstand für den Reiter Bot-Leads (Desktop und Handy)
Skript-API: botleads({ room, q, page, id }) antwortet wie GET /botleads des Dienstes
(items, total, pageSize 50; Telefon nie bei Heilberufen, gekürzte Stubs nur mit id).
60 sichtbare Einträge in vier Räumen (zwei Seiten), 3 gekürzte Stubs, Heilberufe mit
Altbestand-Telefon, eine http-Quelle, zwei feindliche Quellen, lange Namen und Gründe.
Schalter: faults.botleads, botleadsDelayMs, botleadsEmpty (?botleads=leer), botleadCalls.
Fetch-Weiche und entry.jsx (?bot=<Id>) um die Route und den Deep-Link ergänzt.
Szenarien: botleads (Reiter, Liste, Raumfilter, Blättern mit Fokus, Suche, Leerzustände,
Fehlertexte, Deep-Link, Quellen-Links, Telefon) und handy-botleads (390 x 844: kein
seitliches Scrollen, Knopfhöhe, Kontrast der gekürzten Zeile, Wortumbruch).
Der Pin in workersHarness.test.mjs prüft nun die neue Antwortform statt der leeren Liste.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cd532e0cf0
commit
78c8e3fbc2
5 files changed
+709
-11
No files matched your search
@@ -11,6 +11,8 @@
|
||||
// paste.js (Prüfreihenfolge, 400/404/409). Eigener Satz Daten mit `pult: 'p4'` (p4Fixtures); die P3-Daten bleiben unberührt. Die Schalter
|
||||
// `faults.reply`, `.approve-preview`, `.start-build` und `.paste` lassen eine Aktion mit { status, message } scheitern (409 mit deutschem
|
||||
// Satz, 413 „Anfrage zu groß.“, 400), `pultDelayMs` hält jede Pult-Aktion so lange auf (Knöpfe sind währenddessen gesperrt).
|
||||
// Bot-Leads (P6): botleads({ room, q, page, id }) antwortet mit 60 festen Einträgen in der Form von GET /botleads (workers-dev-p6/src/api/botleads.js:
|
||||
// items, total, pageSize 50, Telefon nie bei Heilberufen, gekürzte Stubs nur mit id); Schalter und Fixtures siehe „Bot-Leads (P6)“ weiter unten.
|
||||
import { MODEL_IDS, ROLES, SEARCH_ROOMS, roleOf } from '../../src/lib/workersRoster.js'
|
||||
import { indexWorkers, reduceEvents } from '../../src/lib/workersEvents.js'
|
||||
|
||||
@@ -468,14 +470,186 @@ function defaultSettings(halted) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Bot-Leads (P6) ----------
|
||||
// GET /botleads in der Form des Dienstes (workers-dev-p6/src/api/botleads.js, listBotLeads): { items, total, pageSize }, neueste zuerst, 50 je Seite.
|
||||
// Der Speicher unten hält die Dokumente (Interessenten der Etage „Ohne Website“, status 'botlead') mit allem, was der Dienst kennt; nach außen geht wie
|
||||
// im Dienst nur botleadEntry(): feste Auswahl der Felder, Telefon nur mit Quelle und nie bei einem Heilberuf (auch nicht aus einem Altbestand, in dem die
|
||||
// Nummer noch steht; leere Zeichenkette statt null, wie im Dienst), Zeit mit „Z“. Gekürzte Stubs ('[gekürzt]', purgedAt) fehlen in der Liste und in
|
||||
// total und kommen nur mit id (Ereignislink). 60 sichtbare Einträge (je 15 in den vier Räumen: zwei Seiten) und 3 gekürzte Stubs mit festen Kennungen.
|
||||
// Besonderheiten: Raum aerzte und eine Physiotherapie im Raum selbst sind Heilberufe mit Altbestand-Telefon im Speicher (darf nie erscheinen); ein
|
||||
// Eintrag hat eine http-Quelle, zwei haben eine feindliche Quelle (javascript:, Zugangsdaten in der Adresse), die der Dienst nie liefern würde und
|
||||
// die Oberfläche trotzdem nicht als Link zeigen darf; ein langer Name und ein langer Grund ohne Umbruchstellen prüfen den Umbruch am Handy.
|
||||
export const BOTLEAD_PAGE_SIZE = 50
|
||||
export const BOTLEAD_ROOM_KEYS = Object.freeze(['aerzte', 'handwerk', 'selbst', 'laden'])
|
||||
export const BOTLEAD_IDS = Object.freeze({
|
||||
health: 'bl-aerzte-01', // Heilberuf mit Altbestand-Telefon im Speicher, Seite 1
|
||||
physio: 'bl-selbst-01', // Heilberuf außerhalb des Raums aerzte
|
||||
http: 'bl-handwerk-02', // Quelle mit http
|
||||
javascript: 'bl-laden-04', // Quelle javascript:
|
||||
credentials: 'bl-selbst-03', // Quelle mit Zugangsdaten in der Adresse
|
||||
long: 'bl-handwerk-01', // langer Name und langer Grund ohne Umbruchstellen
|
||||
cut: 'bl-gekuerzt-01', // gekürzter Stub (nur mit id sichtbar)
|
||||
cuts: Object.freeze(['bl-gekuerzt-01', 'bl-gekuerzt-02', 'bl-gekuerzt-03']),
|
||||
})
|
||||
const BOTLEAD_ROOM_LABEL = Object.freeze({ aerzte: 'Ärzte & Praxen', handwerk: 'Handwerk', selbst: 'Selbstständige', laden: 'Läden & Gastro' })
|
||||
const BOTLEAD_ANCHOR_MS = Date.parse('2026-10-17T20:00:00.000Z')
|
||||
const BOTLEAD_STEP_MS = 14.5 * 60 * 60 * 1000
|
||||
const BOTLEAD_REASONS = Object.freeze([
|
||||
'Keine veröffentlichte E-Mail-Adresse gefunden (nur ein Kontaktformular).',
|
||||
'Gehört zu einer Kette oder einem Filialverbund, wird nicht angeschrieben.',
|
||||
'Website ist in gutem Zustand (modern, mobil nutzbar), kein Bedarf erkennbar.',
|
||||
'Kein Mail-Platz mehr in dieser Nacht (Tageslimit erreicht).',
|
||||
'Mail nur über eine Sammeladresse des Verbunds, nicht eindeutig dem Betrieb zuzuordnen.',
|
||||
'Lesen der Seite fehlgeschlagen (Zeitüberschreitung).',
|
||||
'Name fehlt in Impressum und Karte.',
|
||||
])
|
||||
const BOTLEAD_LONG_REASON = 'Impressum nennt nur eine Postfachadresse und ein Kontaktformular; die einzige Mail-Adresse (verwaltung@sehr-langer-domainname-der-nicht-umbricht-handwerksbetrieb-westpfalz-gruppe.example) gehört einem Dritten.'
|
||||
const BOTLEAD_LONG_NAME = 'Elektroinstallation-und-Haustechnik-Hoffmann-Sonnenschein-Wittgensteiner GmbH'
|
||||
// [Name, PLZ, Ort] je Raum, 15 Einträge; Fund i gehört zum Raum i % 4 und dort zum Eintrag i / 4 (neueste zuerst, Raum für Raum abwechselnd)
|
||||
const BOTLEAD_TABLE = Object.freeze({
|
||||
aerzte: [
|
||||
['Praxis Dr. Lindner', '67655', 'Kaiserslautern'], ['Zahnarztpraxis Brandt & Kollegen', '66849', 'Landstuhl'], ['Kieferorthopädie Dr. Yilmaz', '67433', 'Neustadt an der Weinstraße'],
|
||||
['Augenarztpraxis am Markt', '76829', 'Landau in der Pfalz'], ['Frauenarztpraxis Dr. Schaal', '67098', 'Bad Dürkheim'], ['Kinderarzt Dr. Weigand', '66953', 'Pirmasens'],
|
||||
['HNO-Praxis Dr. Reuter', '67227', 'Frankenthal'], ['Orthopädie Zentrum Westpfalz', '67697', 'Otterberg'], ['Hautarztpraxis Dr. Kessler', '66482', 'Zweibrücken'],
|
||||
['Heilpraktikerin Sabine Roth', '67547', 'Worms'], ['Tierarztpraxis Dr. Keller', '67346', 'Speyer'], ['Psychotherapie Fuchs', '67269', 'Grünstadt'],
|
||||
['Hausarztpraxis Dr. Baumann', '66869', 'Kusel'], ['Zahnärzte am Dom', '67346', 'Speyer'], ['Praxis für Allgemeinmedizin Dr. Heinz', '55411', 'Bingen'],
|
||||
],
|
||||
handwerk: [
|
||||
[BOTLEAD_LONG_NAME, '67655', 'Kaiserslautern'], ['Elektro Hartmann GmbH', '67677', 'Enkenbach-Alsenborn'], ['Sanitär Weber', '66877', 'Ramstein-Miesenbach'],
|
||||
['Malerbetrieb Schneider', '67806', 'Rockenhausen'], ['Schreinerei Ott', '67722', 'Winnweiler'], ['Dachdeckerei Lang & Söhne', '66849', 'Landstuhl'],
|
||||
['Fliesen Becker', '67433', 'Neustadt an der Weinstraße'], ['Zimmerei Jung', '76829', 'Landau in der Pfalz'], ['Gartenbau Grünfinger', '66953', 'Pirmasens'],
|
||||
['Metallbau Schmitt', '66482', 'Zweibrücken'], ['Kfz-Werkstatt Mayer', '67227', 'Frankenthal'], ['Autolackiererei Roth', '67547', 'Worms'],
|
||||
['Bauunternehmen Kraus GmbH & Co. KG', '67547', 'Worms'], ['Heizungsbau Albrecht', '67098', 'Bad Dürkheim'], ['Glaserei Peters', '55543', 'Bad Kreuznach'],
|
||||
],
|
||||
selbst: [
|
||||
['Physiotherapie Moser', '67655', 'Kaiserslautern'], ['Steuerberatung Klein', '67433', 'Neustadt an der Weinstraße'], ['Fotografie Lichtblick', '66849', 'Landstuhl'],
|
||||
['Coaching Anja Berger', '67098', 'Bad Dürkheim'], ['Grafikdesign Pixelwerk', '66953', 'Pirmasens'], ['Architekturbüro Vogel', '67227', 'Frankenthal'],
|
||||
['Hundeschule Pfotenglück', '66869', 'Kusel'], ['Nachhilfe Lernwerk', '76829', 'Landau in der Pfalz'], ['Fahrschule Schreiber', '67547', 'Worms'],
|
||||
['Versicherungsmakler Böhm', '67346', 'Speyer'], ['Immobilien Kaufmann', '67269', 'Grünstadt'], ['Übersetzungsbüro Lingua', '55116', 'Mainz'],
|
||||
['Steuerberater Dr. Franke', '67677', 'Enkenbach-Alsenborn'], ['Hundepension Waldblick', '66482', 'Zweibrücken'], ['Yogastudio Atemraum', '67697', 'Otterberg'],
|
||||
],
|
||||
laden: [
|
||||
['Bäckerei Sommer', '67655', 'Kaiserslautern'], ['Metzgerei Kunz', '66849', 'Landstuhl'], ['Café Eselsburg', '67697', 'Otterberg'],
|
||||
['Restaurant Zum Hirschen', '67433', 'Neustadt an der Weinstraße'], ['Blumenladen Rosengarten', '76829', 'Landau in der Pfalz'], ['Friseursalon Haarscharf', '67098', 'Bad Dürkheim'],
|
||||
['Kosmetikstudio Belle', '66953', 'Pirmasens'], ['Boutique Mode & Mehr', '67227', 'Frankenthal'], ['Buchhandlung Seitenweise', '66482', 'Zweibrücken'],
|
||||
['Eisdiele Dolce Vita', '67547', 'Worms'], ['Geschenkeladen Wunderbar', '67346', 'Speyer'], ['Fahrradladen Kettenreaktion', '67269', 'Grünstadt'],
|
||||
['Weinstube Pfälzer Eck', '67433', 'Neustadt an der Weinstraße'], ['Pizzeria Da Marco', '67655', 'Kaiserslautern'], ['Kiosk am Bahnhof', '67806', 'Rockenhausen'],
|
||||
],
|
||||
})
|
||||
|
||||
function botleadDocs() {
|
||||
const docs = []
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
const room = BOTLEAD_ROOM_KEYS[i % 4]
|
||||
const k = Math.floor(i / 4)
|
||||
const [name, postalCode, city] = BOTLEAD_TABLE[room][k]
|
||||
const id = `bl-${room}-${pad(k + 1)}`
|
||||
const health = room === 'aerzte' || id === BOTLEAD_IDS.physio
|
||||
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 24)
|
||||
const source = room === 'selbst' ? 'impressum' : room === 'laden' ? 'osm' : 'places'
|
||||
let sourceUrl = room === 'selbst' ? `https://www.${slug}.example/impressum` : room === 'laden' ? `https://www.openstreetmap.org/node/${4710000 + i * 37}` : `https://www.11880.com/branchenbuch/${slug}`
|
||||
if (id === BOTLEAD_IDS.http) sourceUrl = 'http://www.elektro-hartmann-alt.example/kontakt'
|
||||
if (id === BOTLEAD_IDS.javascript) sourceUrl = 'javascript:alert(document.domain)'
|
||||
if (id === BOTLEAD_IDS.credentials) sourceUrl = 'https://fotografie-lichtblick.example@evil.example/impressum'
|
||||
let reason = BOTLEAD_REASONS[(i * 3 + k) % BOTLEAD_REASONS.length]
|
||||
if (id === BOTLEAD_IDS.long) reason = BOTLEAD_LONG_REASON
|
||||
// Telefon: die meisten mit Quelle, einer nur mit Nummer (ohne Quelle gilt sie als nicht belegt), einer ganz ohne; Heilberufe tragen im Speicher eine Altbestand-Nummer samt Quelle
|
||||
const phoneStyle = health ? 'alt' : k % 5 === 4 ? 'ohneQuelle' : k % 6 === 5 ? 'keine' : 'mitQuelle'
|
||||
docs.push({
|
||||
$id: id,
|
||||
entrance: 'ohne_website',
|
||||
status: 'botlead',
|
||||
room,
|
||||
name,
|
||||
street: '',
|
||||
postalCode,
|
||||
city,
|
||||
website: reason === BOTLEAD_REASONS[2] ? `https://www.${slug}.example/` : '',
|
||||
score: 35 + ((i * 13) % 60),
|
||||
botLeadReason: reason,
|
||||
source,
|
||||
sourceUrl,
|
||||
foundAt: isoOf(BOTLEAD_ANCHOR_MS - i * BOTLEAD_STEP_MS - (i % 4) * 11 * 60 * 1000),
|
||||
isHealth: health,
|
||||
purgedAt: '',
|
||||
phone: phoneStyle === 'keine' ? '' : `0${631 + (i % 5)} 555 ${String(100 + i * 7).padStart(4, '0')}`,
|
||||
phoneSource: phoneStyle === 'mitQuelle' || phoneStyle === 'alt' ? (source === 'osm' ? 'osm' : 'impressum') : '',
|
||||
})
|
||||
}
|
||||
// Gekürzte Stubs wie nach archive/kuerzen.js: Name '[gekürzt]', Ort, Raum, Punkte und Grund bleiben, alles Persönliche ist leer
|
||||
const cuts = [['handwerk', 'Kaiserslautern', 51], ['laden', 'Landau in der Pfalz', 44], ['aerzte', 'Worms', 62]]
|
||||
cuts.forEach(([room, city, score], n) => {
|
||||
docs.push({
|
||||
$id: BOTLEAD_IDS.cuts[n], entrance: 'ohne_website', status: 'botlead', room, name: '[gekürzt]', street: '', postalCode: '', city, website: '', score,
|
||||
botLeadReason: BOTLEAD_REASONS[n], source: '', sourceUrl: '', foundAt: isoOf(BOTLEAD_ANCHOR_MS - (120 + n * 9) * 24 * 60 * 60 * 1000), isHealth: room === 'aerzte',
|
||||
purgedAt: isoOf(BOTLEAD_ANCHOR_MS - (2 + n) * 24 * 60 * 60 * 1000), phone: '', phoneSource: '',
|
||||
})
|
||||
})
|
||||
return docs
|
||||
}
|
||||
|
||||
// Ein Eintrag der Antwort: feste Auswahl der Felder (wie eintrag() im Dienst; nie E-Mail, Quelle der Adresse, Ansprechpartner, Kennung des Orts, Anfragetext)
|
||||
function botleadEntry(doc) {
|
||||
const health = Boolean(doc.isHealth) || doc.room === 'aerzte'
|
||||
const withNumber = !health && doc.phoneSource !== ''
|
||||
const found = Date.parse(doc.foundAt)
|
||||
return {
|
||||
id: doc.$id,
|
||||
room: doc.room,
|
||||
roomLabel: BOTLEAD_ROOM_LABEL[doc.room] ?? doc.room,
|
||||
name: doc.name,
|
||||
street: doc.street,
|
||||
postalCode: doc.postalCode,
|
||||
city: doc.city,
|
||||
website: doc.website,
|
||||
score: Number.isFinite(doc.score) ? doc.score : 0,
|
||||
reason: doc.botLeadReason,
|
||||
source: doc.source,
|
||||
sourceUrl: doc.sourceUrl,
|
||||
foundAt: Number.isNaN(found) ? doc.foundAt : new Date(found).toISOString(),
|
||||
isHealth: health,
|
||||
gekuerzt: Boolean(doc.purgedAt),
|
||||
phone: withNumber ? doc.phone : '',
|
||||
phoneSource: withNumber ? doc.phoneSource : '',
|
||||
}
|
||||
}
|
||||
|
||||
// Suchtext und Bestand auf dieselbe Form bringen: klein, Umlaute als ae/oe/ue, Satzzeichen weg (wie normalizeName des Dienstes, ohne Rechtsformen)
|
||||
const foldSearch = (value) => String(value ?? '').toLowerCase().replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss').replace(/[^a-z0-9]+/g, ' ').trim()
|
||||
const BOTLEAD_ID_FORM = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/
|
||||
|
||||
/** listBotLeads des Dienstes auf dem Speicher docs: { items, total, pageSize }. Ein unbekannter Raum wirft 400 „Unbekannter Raum.“. */
|
||||
export function listBotleads(docs, { room = '', q = '', page = 1, id = '' } = {}) {
|
||||
const raum = room ?? ''
|
||||
if (raum !== '' && !BOTLEAD_ROOM_KEYS.includes(raum)) throw Object.assign(new Error('Unbekannter Raum.'), { status: 400 })
|
||||
const kennung = typeof id === 'string' ? id : ''
|
||||
if (kennung !== '') {
|
||||
const doc = BOTLEAD_ID_FORM.test(kennung) ? docs.find((d) => d.$id === kennung) : undefined
|
||||
const items = doc ? [botleadEntry(doc)] : []
|
||||
return { items, total: items.length, pageSize: BOTLEAD_PAGE_SIZE }
|
||||
}
|
||||
const words = foldSearch(String(q ?? '').slice(0, 100)).split(' ').filter(Boolean)
|
||||
const hit = docs
|
||||
.filter((d) => !d.purgedAt && (raum === '' || d.room === raum))
|
||||
.filter((d) => { const hay = foldSearch([d.name, d.city, d.botLeadReason, BOTLEAD_ROOM_LABEL[d.room]].join(' ')); return words.every((w) => hay.includes(w)) })
|
||||
.sort((x, y) => Date.parse(y.foundAt) - Date.parse(x.foundAt) || y.score - x.score || (x.$id < y.$id ? -1 : 1))
|
||||
const seite = Number.isInteger(page) && page >= 1 ? page : 1
|
||||
const from = (seite - 1) * BOTLEAD_PAGE_SIZE
|
||||
return { items: hit.slice(from, from + BOTLEAD_PAGE_SIZE).map(botleadEntry), total: hit.length, pageSize: BOTLEAD_PAGE_SIZE }
|
||||
}
|
||||
|
||||
/**
|
||||
* Skript-API für den Prüfstand. now() = Uhr in Millisekunden (nur die Differenz zum Aufruf von createScriptedApi zählt),
|
||||
* base = Zeitpunkt (ms) des Starts für die Zeitstempel der Ereignisse, halted = Bounce-Notbremse ist von Anfang an gesetzt (H4),
|
||||
* pult = das Freigabe-Pult und die Interessenten (P3) mit den festen Daten füllen (sonst ist das Pult leer); 'p4' nimmt statt dessen den
|
||||
* Satz für die Gespräche und die Werkstatt (p4Fixtures).
|
||||
* Satz für die Gespräche und die Werkstatt (p4Fixtures). botleadsEmpty = die Bot-Lead-Liste ist von Anfang an leer (Leerzustand ohne Filter).
|
||||
* Bot-Leads (P6): botleads({ room, q, page, id }) antwortet wie GET /botleads des Dienstes (listBotleads oben). Schalter: `faults.botleads` ({ status, message }),
|
||||
* `botleadsDelayMs` (Zahl oder Funktion (Abfrage) => ms), `botleadsEmpty`; `botleadCalls` hält die Abfragen fest, `botleadsHealthPhones` die Nummern, die im
|
||||
* Speicher bei Heilberufen stehen und nie ausgeliefert werden dürfen.
|
||||
* Rückgabe wie createWorkersApi; saved ist die Liste der Patches, die saveSettings bekam.
|
||||
*/
|
||||
export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pult = false }) {
|
||||
export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pult = false, botleadsEmpty = false }) {
|
||||
const start = now()
|
||||
const elapsed = () => Math.max(0, now() - start)
|
||||
let paused = false
|
||||
@@ -485,11 +659,13 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
const fx = pult === 'p4' ? p4Fixtures(base) : pult ? pultFixtures(base) : { items: [], prospects: {}, threads: {}, messages: {}, previews: {} }
|
||||
const pultItems = fx.items
|
||||
// je { status, message } oder null; die vier Schlüssel mit Bindestrich oder Namen der P4-Aktionen stehen ebenfalls hier
|
||||
const faults = { pult: null, claim: null, sent: null, discard: null, done: null, prospect: null, reply: null, 'approve-preview': null, 'start-build': null, paste: null }
|
||||
const faults = { pult: null, claim: null, sent: null, discard: null, done: null, prospect: null, reply: null, 'approve-preview': null, 'start-build': null, paste: null, botleads: null }
|
||||
const pastedBefore = new Set() // Interessent + Text: dieselbe eingefügte Antwort ist beim zweiten Mal „doppelt“
|
||||
const pultCalls = []
|
||||
const prospectCalls = []
|
||||
const pauseCalls = []
|
||||
const botleadStore = botleadDocs()
|
||||
const botleadCalls = []
|
||||
|
||||
// Alle Ereignisse des Drehbuchs mit festen Kennungen und Zeitstempeln (aufsteigend); due() = bisher fällige
|
||||
const all = SCRIPT.map((s, i) => ({
|
||||
@@ -621,6 +797,10 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
pultCalls,
|
||||
prospectCalls,
|
||||
pauseCalls,
|
||||
botleadCalls,
|
||||
botleadsHealthPhones: botleadStore.filter((d) => d.isHealth && d.phone !== '').map((d) => d.phone),
|
||||
botleadsEmpty,
|
||||
botleadsDelayMs: 0, // > 0 oder Funktion (Abfrage) => ms: die Antwort der Bot-Lead-Liste kommt erst nach so vielen Millisekunden
|
||||
pultEmpty: false, // true: das Pult ist leer (Leerzustand)
|
||||
pultDelayMs: 0, // > 0: jede Pult-Aktion antwortet erst nach so vielen Millisekunden (Knöpfe sind in der Zwischenzeit gesperrt)
|
||||
|
||||
@@ -728,9 +908,14 @@ export function createScriptedApi({ now, base = DEFAULT_BASE, halted = false, pu
|
||||
return clone({ item })
|
||||
},
|
||||
|
||||
// Bot-Leads (P6): leere Liste; der Reiter wird im Prüfstand gebaut und gezeigt, aber nicht mit Daten gefüllt (workersHarness.test.mjs pinnt die leere Antwort)
|
||||
async botleads() {
|
||||
return { items: [], total: 0 }
|
||||
// GET /botleads?room=&q=&page=&id=: Antwort { items, total, pageSize } wie der Dienst (listBotleads); leer bei botleadsEmpty. Abfragen landen in botleadCalls.
|
||||
async botleads(query = {}) {
|
||||
const { room = '', q = '', page = 1, id = '' } = query ?? {}
|
||||
botleadCalls.push(clone({ room, q, page, id }))
|
||||
const wait = typeof api.botleadsDelayMs === 'function' ? api.botleadsDelayMs({ room, q, page, id }) : api.botleadsDelayMs
|
||||
if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait))
|
||||
if (faults.botleads) throw failure(faults.botleads)
|
||||
return clone(listBotleads(api.botleadsEmpty ? [] : botleadStore, { room, q, page, id }))
|
||||
},
|
||||
|
||||
// GET /prospect/:id: { prospect (nur Felder der Whitelist), thread, messages, preview, reply }; Faden und Vorschau fehlen als null
|
||||
|
||||
Reference in new issue
Block a user