Funktions-Ticket: Text aus dem Register, Laengendeckel, Notfall-Details und gezielte Wiederholung

markPaid holt Betreff und Details des Einrichtungs-Tickets aus dem Register (ticketTexte), createFeatureTicket
bleibt die einzige Stelle, die das Ticket anlegt. Betreff auf 128, Details auf 7000 Zeichen gekuerzt (surrogatsicher,
Schnitt an der Zeilengrenze, einheitliche Schlusszeile). Wirft die Details-Funktion oder liefert sie keinen Text,
gelten die Notfall-Details (Kopfzeilen und Verweis ins WOMS). Nach einem 400 bei langen Details gibt es genau einen
zweiten Versuch mit den Notfall-Details und derselben woid; 5xx, Netzfehler und kurze Details werden ohne
Wiederholung weitergereicht. Stub termine/ticketDetails.js bis Task D4.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 12:48:02 +00:00
1 parent 7fcb268644
commit 9dd0244537
5 files changed
+678 -20

No files matched your search

+103
View File
@@ -116,3 +116,106 @@ export function oeffentlicheAnsicht(key) {
requiresWartungsAbo: e.requiresWartungsAbo === true,
}
}
// ---------------------------------------------------------------------------------------------------------------
// Einrichtungs-Ticket aus dem Register (Task B8): Texte je Funktion, Laengendeckel und Notfall-Details.
// createFeatureTicket (ticketAdmin.js) ist die einzige Stelle, die das Ticket anlegt; markPaid holt die Texte hier.
// ---------------------------------------------------------------------------------------------------------------
/** Laengendeckel der Ticket-Felder (workorders.topic / workorders.details). */
export const TICKET_TOPIC_MAX = 128
export const TICKET_DETAILS_MAX = 7000
/** Ab dieser Laenge gelten Details als "lang": nur dann wiederholt createFeatureTicket nach einem 400 mit den Notfall-Details. */
export const LANGE_DETAILS = 1000
/** Einheitliche Schlusszeile eines gekuerzten Ticket-Textes. */
export const TICKET_GEKUERZT = '… gekürzt, vollständig im Ticketsystem unter Kunden → Funktionen'
const istHighSurrogat = (code) => code >= 0xd800 && code <= 0xdbff
/**
* Text auf hoechstens max Zeichen (UTF-16-Einheiten, wie string.length) kuerzen, surrogatsicher: nie ein halbes Zeichenpaar.
* Passt der Text, bleibt er unveraendert (ohne endzeile). Sonst wird so viel Text behalten, dass mit der Endzeile genau max
* nicht ueberschritten wird: anZeile schneidet an der letzten Zeilengrenze (ohne Zeilengrenze im Platz: harter Schnitt) und
* setzt die Endzeile in eine eigene Zeile; ohne anZeile haengt die Endzeile direkt an den Schnitt.
*/
export function kuerze(text, max, { anZeile = false, endzeile = '' } = {}) {
const s = typeof text === 'string' ? text : String(text ?? '')
if (s.length <= max) return s
const trenner = anZeile && endzeile ? '\n' : ''
const platz = max - endzeile.length - trenner.length
const hartSchnitt = (n) => {
let teil = s.slice(0, Math.max(0, n))
if (teil && istHighSurrogat(teil.charCodeAt(teil.length - 1))) teil = teil.slice(0, -1)
return teil
}
if (platz <= 0) return hartSchnitt(max)
let teil
if (anZeile) {
const grenze = s.lastIndexOf('\n', platz)
teil = grenze > 0 ? s.slice(0, grenze) : hartSchnitt(platz)
} else {
teil = hartSchnitt(platz)
}
teil = teil.trimEnd()
return teil ? `${teil}${trenner}${endzeile}` : endzeile
}
/** Link ins WOMS-Kundenpanel "Funktionen" mit geoeffneter Einbauanleitung des Projekts (Ticket, T16, Anleitungs-Route). */
export function anleitungsLink(customerId, projectId) {
return `${config.womsBaseUrl}/customers/${encodeURIComponent(customerId ?? '')}?tab=funktionen&anleitung=${encodeURIComponent(projectId ?? '')}`
}
const projektName = (project) => project?.projectName || project?.subdomain || ''
/**
* Notfall-Details des Tickets: Kopfzeilen (Funktion, Projekt, Domain, Kunde, siteKey) und ein Verweis auf die Anleitung im
* WOMS. Ohne Rechenarbeit und ohne Dienste, damit es auch dann klappt, wenn die Anleitung des Registers nicht entsteht.
* ctx = { feature, doc, project, customer } (customer best effort oder null).
*/
export function minimaleTicketDetails(ctx) {
const { feature, doc, project, customer } = ctx || {}
const domain = project?.liveDomain || project?.previewUrl || project?.subdomain || ''
return [
`Funktion: ${feature?.key || doc?.featureKey || ''}`,
`Projekt: ${projektName(project)}`,
`Domain: ${domain}`,
`Kunde: ${customer?.name || doc?.customerId || ''}`,
`siteKey: ${doc?.siteKey || ''}`,
`Verweis: ${anleitungsLink(doc?.customerId, doc?.projectId)}`,
].join('\n')
}
/**
* Texte des Einrichtungs-Tickets einer Funktion aus dem Register: { topic, details, fallbackDetails }.
* topic auf TICKET_TOPIC_MAX, details auf TICKET_DETAILS_MAX gekuerzt (Schnitt an der letzten Zeilengrenze, eine
* einheitliche Schlusszeile). Wirft feature.ticket.details oder liefert es Leeres/Nicht-Text, gilt fallbackDetails
* (minimaleTicketDetails): bezahlt ist bezahlt, das Ticket entsteht immer.
*/
export async function ticketTexte(ctx) {
const { feature, project } = ctx || {}
const kuerzeDetails = (text) => kuerze(text, TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT })
const fallbackDetails = kuerzeDetails(minimaleTicketDetails(ctx))
let topic = ''
try {
topic = feature.ticket.topic(project)
} catch (err) {
console.error(`[funktionen] Ticket-Betreff (${feature?.key}) nicht berechenbar:`, err.message)
}
if (typeof topic !== 'string' || !topic.trim()) topic = `${feature?.label?.name || 'Funktion'} einbauen – ${projektName(project)}`.trim()
let details = ''
try {
details = await feature.ticket.details(ctx)
} catch (err) {
console.error(`[funktionen] Ticket-Details (${feature?.key}) nicht berechenbar, Notfall-Details:`, err.message)
}
if (typeof details !== 'string' || !details.trim()) details = fallbackDetails
return {
topic: kuerze(topic.trim(), TICKET_TOPIC_MAX, { endzeile: '…' }),
details: kuerzeDetails(details),
fallbackDetails,
}
}