feat: Sidebar-Links fuer Developer Portal + SDK Dokumentation

Externe Links oeffnen in neuem Tab mit Icon-Indikator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 12:24:39 +01:00
parent 9f0791802b
commit eca0855216
4 changed files with 838 additions and 11 deletions

View File

@@ -0,0 +1,393 @@
/**
* Template-Spec v1 — Context Bridge
*
* Maps a structured TemplateContext (canonical entities: PROVIDER, CUSTOMER, etc.)
* to the flat {{PLACEHOLDER}} map used by legacy templates.
*
* Flow: TemplateContext → contextToPlaceholders() → Record<string, string>
* → string.replace({{KEY}}, value) in renderer
*/
// =============================================================================
// Types
// =============================================================================
export interface ProviderCtx {
LEGAL_NAME: string
LEGAL_FORM?: string
ADDRESS_LINE: string
POSTAL_CODE: string
CITY: string
COUNTRY: string
EMAIL: string
PHONE?: string
WEBSITE_URL?: string
CEO_NAME?: string
REGISTER_COURT?: string
REGISTER_NUMBER?: string
VAT_ID?: string
}
export interface CustomerCtx {
LEGAL_NAME: string
ADDRESS_LINE?: string
POSTAL_CODE?: string
CITY?: string
COUNTRY?: string
CONTACT_NAME?: string
EMAIL: string
IS_CONSUMER: boolean
IS_BUSINESS: boolean
}
export interface ServiceCtx {
NAME: string
DESCRIPTION: string
MODEL: 'SaaS' | 'PaaS' | 'IaaS' | 'OnPrem' | 'Hybrid' | ''
TIER: string
DATA_LOCATION: string
EXPORT_FORMATS?: string[]
EXPORT_WINDOW_DAYS?: number
MIN_TERM_MONTHS?: number
TERMINATION_NOTICE_DAYS?: number
}
export interface LegalCtx {
GOVERNING_LAW: string
JURISDICTION_CITY: string
VERSION_DATE: string // YYYY-MM-DD
EFFECTIVE_DATE: string // YYYY-MM-DD
LANG: 'de' | 'en'
}
export interface PrivacyCtx {
CONTACT_EMAIL: string
DPO_NAME: string
DPO_EMAIL: string
SUPERVISORY_AUTHORITY_NAME?: string
SUPERVISORY_AUTHORITY_ADDRESS?: string
PRIVACY_POLICY_URL: string
COOKIE_POLICY_URL?: string
ANALYTICS_RETENTION_MONTHS?: number
DATA_TRANSFER_THIRD_COUNTRIES?: string
}
export interface SLACtx {
AVAILABILITY_PERCENT: number | ''
MAINTENANCE_NOTICE_HOURS: number | ''
SUPPORT_EMAIL: string
SUPPORT_PHONE?: string
SUPPORT_HOURS: string
RESPONSE_CRITICAL_H: number | ''
RESOLUTION_CRITICAL_H: number | ''
RESPONSE_HIGH_H: number | ''
RESOLUTION_HIGH_H: number | ''
RESPONSE_MEDIUM_H: number | ''
RESOLUTION_MEDIUM_H: number | ''
RESPONSE_LOW_H: number | ''
}
export interface PaymentsCtx {
MONTHLY_FEE_EUR: number | ''
PAYMENT_DUE_DAY: number | ''
PAYMENT_METHOD: string
PAYMENT_DAYS: number | ''
}
export interface SecurityCtx {
INCIDENT_NOTICE_HOURS: number | ''
LOG_RETENTION_DAYS: number | ''
SECURITY_LOG_RETENTION_DAYS: number | ''
}
export interface NDACtx {
PURPOSE: string
DURATION_YEARS: number | ''
PENALTY_AMOUNT_EUR: number | null | ''
}
export interface ConsentCtx {
WEBSITE_NAME: string
ANALYTICS_TOOLS: string | null
MARKETING_PARTNERS: string | null
}
export interface TemplateContext {
PROVIDER: ProviderCtx
CUSTOMER: CustomerCtx
SERVICE: ServiceCtx
LEGAL: LegalCtx
PRIVACY: PrivacyCtx
SLA: SLACtx
PAYMENTS: PaymentsCtx
SECURITY: SecurityCtx
NDA: NDACtx
CONSENT: ConsentCtx
}
export interface ComputedFlags {
IS_B2C: boolean
IS_B2B: boolean
SERVICE_IS_SAAS: boolean
SERVICE_IS_HYBRID: boolean
HAS_PENALTY: boolean
HAS_ANALYTICS: boolean
HAS_MARKETING: boolean
}
// =============================================================================
// Empty default context
// =============================================================================
export const EMPTY_CONTEXT: TemplateContext = {
PROVIDER: {
LEGAL_NAME: '', LEGAL_FORM: '', ADDRESS_LINE: '', POSTAL_CODE: '', CITY: '',
COUNTRY: 'DE', EMAIL: '', PHONE: '', WEBSITE_URL: '',
CEO_NAME: '', REGISTER_COURT: '', REGISTER_NUMBER: '', VAT_ID: '',
},
CUSTOMER: {
LEGAL_NAME: '', ADDRESS_LINE: '', POSTAL_CODE: '', CITY: '', COUNTRY: 'DE',
CONTACT_NAME: '', EMAIL: '', IS_CONSUMER: false, IS_BUSINESS: true,
},
SERVICE: {
NAME: '', DESCRIPTION: '', MODEL: 'SaaS', TIER: '', DATA_LOCATION: 'Deutschland',
EXPORT_FORMATS: [], EXPORT_WINDOW_DAYS: 30, MIN_TERM_MONTHS: 12,
TERMINATION_NOTICE_DAYS: 30,
},
LEGAL: {
GOVERNING_LAW: 'Deutschland', JURISDICTION_CITY: '', VERSION_DATE: '', EFFECTIVE_DATE: '',
LANG: 'de',
},
PRIVACY: {
CONTACT_EMAIL: '', DPO_NAME: '', DPO_EMAIL: '',
SUPERVISORY_AUTHORITY_NAME: '', SUPERVISORY_AUTHORITY_ADDRESS: '',
PRIVACY_POLICY_URL: '', COOKIE_POLICY_URL: '',
ANALYTICS_RETENTION_MONTHS: 13, DATA_TRANSFER_THIRD_COUNTRIES: 'nicht statt',
},
SLA: {
AVAILABILITY_PERCENT: 99.5, MAINTENANCE_NOTICE_HOURS: 72,
SUPPORT_EMAIL: '', SUPPORT_PHONE: '', SUPPORT_HOURS: 'MoFr 09:0018:00 CET',
RESPONSE_CRITICAL_H: 2, RESOLUTION_CRITICAL_H: 8,
RESPONSE_HIGH_H: 4, RESOLUTION_HIGH_H: 24,
RESPONSE_MEDIUM_H: 24, RESOLUTION_MEDIUM_H: 120,
RESPONSE_LOW_H: 72,
},
PAYMENTS: {
MONTHLY_FEE_EUR: '', PAYMENT_DUE_DAY: 1, PAYMENT_METHOD: 'Rechnung', PAYMENT_DAYS: 14,
},
SECURITY: {
INCIDENT_NOTICE_HOURS: 24, LOG_RETENTION_DAYS: 7, SECURITY_LOG_RETENTION_DAYS: 30,
},
NDA: { PURPOSE: '', DURATION_YEARS: 5, PENALTY_AMOUNT_EUR: null },
CONSENT: { WEBSITE_NAME: '', ANALYTICS_TOOLS: null, MARKETING_PARTNERS: null },
}
// =============================================================================
// Bridge: context → flat placeholder map
// =============================================================================
function str(v: unknown): string {
if (v === null || v === undefined || v === '') return ''
if (Array.isArray(v)) return v.join(', ')
return String(v)
}
/** Compute compound address from PROVIDER fields */
function providerAddress(p: ProviderCtx): string {
const parts = [p.ADDRESS_LINE, [p.POSTAL_CODE, p.CITY].filter(Boolean).join(' ')].filter(Boolean)
return parts.join(', ')
}
/**
* Maps a TemplateContext to a flat Record<string, string> of placeholder values.
* Keys are the {{PLACEHOLDER}} strings used in templates.
*/
export function contextToPlaceholders(ctx: TemplateContext): Record<string, string> {
const p = ctx.PROVIDER
const c = ctx.CUSTOMER
const s = ctx.SERVICE
const l = ctx.LEGAL
const prv = ctx.PRIVACY
const sla = ctx.SLA
const pay = ctx.PAYMENTS
const sec = ctx.SECURITY
const nda = ctx.NDA
const con = ctx.CONSENT
const address = providerAddress(p)
return {
// --- PROVIDER ---
'{{COMPANY_NAME}}': str(p.LEGAL_NAME),
'{{PROVIDER_NAME}}': str(p.LEGAL_NAME),
'{{DISCLOSING_PARTY}}': str(p.LEGAL_NAME), // NDA: provider is disclosing party
'{{SERVICE_PROVIDER}}': str(p.LEGAL_NAME), // SLA
'{{COMPANY_ADDRESS}}': address,
'{{PROVIDER_ADDRESS}}': address,
'{{CONTACT_EMAIL}}': str(p.EMAIL),
'{{PROVIDER_EMAIL}}': str(p.EMAIL),
'{{ABUSE_EMAIL}}': str(p.EMAIL), // AUP abuse contact
'{{REPORT_EMAIL}}': prv.CONTACT_EMAIL ? str(prv.CONTACT_EMAIL) : str(p.EMAIL), // community moderation
// --- CUSTOMER ---
'{{CUSTOMER_NAME}}': str(c.LEGAL_NAME),
'{{RECEIVING_PARTY}}': str(c.LEGAL_NAME), // NDA
'{{CUSTOMER}}': str(c.LEGAL_NAME), // SLA
// --- SERVICE ---
'{{SERVICE_NAME}}': str(s.NAME),
'{{PLATFORM_NAME}}': str(s.NAME), // community / copyright
'{{SERVICE_DESCRIPTION}}': str(s.DESCRIPTION),
'{{SERVICE_MODEL}}': str(s.MODEL),
'{{SERVICE_TIER}}': str(s.TIER),
'{{DATA_LOCATION}}': str(s.DATA_LOCATION),
'{{EXPORT_FORMATS}}': Array.isArray(s.EXPORT_FORMATS) ? s.EXPORT_FORMATS.join(', ') : '',
'{{EXPORT_WINDOW_DAYS}}': str(s.EXPORT_WINDOW_DAYS),
'{{MIN_TERM_MONTHS}}': str(s.MIN_TERM_MONTHS),
'{{TERMINATION_NOTICE_DAYS}}': str(s.TERMINATION_NOTICE_DAYS),
// --- LEGAL ---
'{{GOVERNING_LAW}}': str(l.GOVERNING_LAW),
'{{JURISDICTION_CITY}}': str(l.JURISDICTION_CITY),
'{{VERSION_DATE}}': str(l.VERSION_DATE),
'{{EFFECTIVE_DATE}}': str(l.EFFECTIVE_DATE),
'{{START_DATE}}': str(l.EFFECTIVE_DATE), // cloud contract start date
// --- PRIVACY ---
'{{PRIVACY_CONTACT_EMAIL}}': str(prv.CONTACT_EMAIL),
'{{DPO_NAME}}': str(prv.DPO_NAME),
'{{DPO_EMAIL}}': str(prv.DPO_EMAIL),
'{{COPYRIGHT_CONTACT_NAME}}': str(prv.DPO_NAME), // copyright policy
'{{COPYRIGHT_EMAIL}}': str(prv.DPO_EMAIL),
'{{PRIVACY_POLICY_URL}}': str(prv.PRIVACY_POLICY_URL),
'{{COOKIE_POLICY_URL}}': str(prv.COOKIE_POLICY_URL),
'{{ANALYTICS_RETENTION_MONTHS}}': str(prv.ANALYTICS_RETENTION_MONTHS),
'{{DATA_TRANSFER_THIRD_COUNTRIES}}': str(prv.DATA_TRANSFER_THIRD_COUNTRIES),
// --- SLA ---
'{{AVAILABILITY_PERCENT}}': str(sla.AVAILABILITY_PERCENT),
'{{MAINTENANCE_NOTICE_HOURS}}': str(sla.MAINTENANCE_NOTICE_HOURS),
'{{SUPPORT_EMAIL}}': str(sla.SUPPORT_EMAIL),
'{{SUPPORT_PHONE}}': str(sla.SUPPORT_PHONE),
'{{SUPPORT_HOURS}}': str(sla.SUPPORT_HOURS),
'{{RESPONSE_CRITICAL_H}}': str(sla.RESPONSE_CRITICAL_H),
'{{RESOLUTION_CRITICAL_H}}': str(sla.RESOLUTION_CRITICAL_H),
'{{RESPONSE_HIGH_H}}': str(sla.RESPONSE_HIGH_H),
'{{RESOLUTION_HIGH_H}}': str(sla.RESOLUTION_HIGH_H),
'{{RESPONSE_MEDIUM_H}}': str(sla.RESPONSE_MEDIUM_H),
'{{RESOLUTION_MEDIUM_H}}': str(sla.RESOLUTION_MEDIUM_H),
'{{RESPONSE_LOW_H}}': str(sla.RESPONSE_LOW_H),
// --- PAYMENTS ---
'{{MONTHLY_FEE}}': str(pay.MONTHLY_FEE_EUR),
'{{PAYMENT_DUE_DAY}}': str(pay.PAYMENT_DUE_DAY),
'{{PAYMENT_METHOD}}': str(pay.PAYMENT_METHOD),
'{{PAYMENT_DAYS}}': str(pay.PAYMENT_DAYS),
// --- SECURITY ---
'{{INCIDENT_NOTICE_HOURS}}': str(sec.INCIDENT_NOTICE_HOURS),
'{{LOG_RETENTION_DAYS}}': str(sec.LOG_RETENTION_DAYS),
'{{SECURITY_LOG_RETENTION_DAYS}}': str(sec.SECURITY_LOG_RETENTION_DAYS),
// --- NDA ---
'{{PURPOSE}}': str(nda.PURPOSE),
'{{DURATION_YEARS}}': str(nda.DURATION_YEARS),
'{{PENALTY_AMOUNT}}': nda.PENALTY_AMOUNT_EUR !== null ? str(nda.PENALTY_AMOUNT_EUR) : '',
// --- CONSENT ---
'{{WEBSITE_NAME}}': con.WEBSITE_NAME || str(p.WEBSITE_URL),
'{{ANALYTICS_TOOLS}}': str(con.ANALYTICS_TOOLS),
'{{MARKETING_PARTNERS}}': str(con.MARKETING_PARTNERS),
}
}
// =============================================================================
// Computed flags
// =============================================================================
export function computeFlags(ctx: TemplateContext): ComputedFlags {
return {
IS_B2C: ctx.CUSTOMER.IS_CONSUMER,
IS_B2B: ctx.CUSTOMER.IS_BUSINESS,
SERVICE_IS_SAAS: ctx.SERVICE.MODEL === 'SaaS',
SERVICE_IS_HYBRID: ctx.SERVICE.MODEL === 'Hybrid',
HAS_PENALTY: ctx.NDA.PENALTY_AMOUNT_EUR !== null && ctx.NDA.PENALTY_AMOUNT_EUR !== '',
HAS_ANALYTICS: !!ctx.CONSENT.ANALYTICS_TOOLS,
HAS_MARKETING: !!ctx.CONSENT.MARKETING_PARTNERS,
}
}
// =============================================================================
// Section relevance — which sections are useful for a given template
// =============================================================================
/** All placeholder patterns covered by each context section */
const SECTION_COVERS: Record<keyof TemplateContext, string[]> = {
PROVIDER: ['{{COMPANY_NAME}}', '{{PROVIDER_NAME}}', '{{DISCLOSING_PARTY}}', '{{SERVICE_PROVIDER}}', '{{COMPANY_ADDRESS}}', '{{PROVIDER_ADDRESS}}', '{{CONTACT_EMAIL}}', '{{PROVIDER_EMAIL}}', '{{ABUSE_EMAIL}}', '{{REPORT_EMAIL}}'],
CUSTOMER: ['{{CUSTOMER_NAME}}', '{{RECEIVING_PARTY}}', '{{CUSTOMER}}'],
SERVICE: ['{{SERVICE_NAME}}', '{{PLATFORM_NAME}}', '{{SERVICE_DESCRIPTION}}', '{{SERVICE_MODEL}}', '{{SERVICE_TIER}}', '{{DATA_LOCATION}}', '{{EXPORT_FORMATS}}', '{{EXPORT_WINDOW_DAYS}}', '{{MIN_TERM_MONTHS}}', '{{TERMINATION_NOTICE_DAYS}}'],
LEGAL: ['{{GOVERNING_LAW}}', '{{JURISDICTION_CITY}}', '{{VERSION_DATE}}', '{{EFFECTIVE_DATE}}', '{{START_DATE}}'],
PRIVACY: ['{{PRIVACY_CONTACT_EMAIL}}', '{{DPO_NAME}}', '{{DPO_EMAIL}}', '{{COPYRIGHT_CONTACT_NAME}}', '{{COPYRIGHT_EMAIL}}', '{{PRIVACY_POLICY_URL}}', '{{COOKIE_POLICY_URL}}', '{{ANALYTICS_RETENTION_MONTHS}}', '{{DATA_TRANSFER_THIRD_COUNTRIES}}'],
SLA: ['{{AVAILABILITY_PERCENT}}', '{{MAINTENANCE_NOTICE_HOURS}}', '{{SUPPORT_EMAIL}}', '{{SUPPORT_PHONE}}', '{{SUPPORT_HOURS}}', '{{RESPONSE_CRITICAL_H}}', '{{RESOLUTION_CRITICAL_H}}', '{{RESPONSE_HIGH_H}}', '{{RESOLUTION_HIGH_H}}', '{{RESPONSE_MEDIUM_H}}', '{{RESOLUTION_MEDIUM_H}}', '{{RESPONSE_LOW_H}}'],
PAYMENTS: ['{{MONTHLY_FEE}}', '{{PAYMENT_DUE_DAY}}', '{{PAYMENT_METHOD}}', '{{PAYMENT_DAYS}}'],
SECURITY: ['{{INCIDENT_NOTICE_HOURS}}', '{{LOG_RETENTION_DAYS}}', '{{SECURITY_LOG_RETENTION_DAYS}}'],
NDA: ['{{PURPOSE}}', '{{DURATION_YEARS}}', '{{PENALTY_AMOUNT}}'],
CONSENT: ['{{WEBSITE_NAME}}', '{{ANALYTICS_TOOLS}}', '{{MARKETING_PARTNERS}}'],
}
/**
* Returns which context sections are relevant for a given list of template placeholders.
*/
export function getRelevantSections(placeholders: string[]): (keyof TemplateContext)[] {
const pSet = new Set(placeholders)
return (Object.keys(SECTION_COVERS) as (keyof TemplateContext)[]).filter(
(section) => SECTION_COVERS[section].some((ph) => pSet.has(ph))
)
}
/**
* Returns a list of placeholder keys that are NOT covered by the context bridge.
* These need to be filled in manually.
*/
export function getUncoveredPlaceholders(placeholders: string[], ctx: TemplateContext): string[] {
const bridged = contextToPlaceholders(ctx)
return placeholders.filter((ph) => !(ph in bridged))
}
/**
* Strict validation: returns placeholder keys that are required but empty.
* "Required" means: placeholder appears in template AND bridge covers it AND bridge produces empty string.
*/
export function getMissingRequired(placeholders: string[], ctx: TemplateContext): string[] {
const bridged = contextToPlaceholders(ctx)
return placeholders.filter((ph) => ph in bridged && !bridged[ph])
}
// =============================================================================
// Helpers to set nested context values by dot-path
// =============================================================================
/**
* Returns a new TemplateContext with the value at dotPath set.
* e.g. setContextPath(ctx, 'PROVIDER.LEGAL_NAME', 'ACME GmbH')
*/
export function setContextPath(ctx: TemplateContext, dotPath: string, value: unknown): TemplateContext {
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
const key = rest.join('.')
return {
...ctx,
[section]: {
...(ctx[section] as Record<string, unknown>),
[key]: value,
},
}
}
/**
* Get a nested context value by dot-path.
*/
export function getContextPath(ctx: TemplateContext, dotPath: string): unknown {
const [section, ...rest] = dotPath.split('.') as [keyof TemplateContext, ...string[]]
const sectionObj = ctx[section] as Record<string, unknown>
return sectionObj?.[rest.join('.')]
}