Wenn ein Projekteintrag auf eine neue Seite überläuft (Tasks zu lang),
wird jetzt trotzdem eine pendingLine vom Seitenanfang der neuen Seite
bis zum Gap vor dem nächsten Eintrag vorgemerkt. Dadurch entsteht eine
durchgehende Timeline-Linie auf der Fortsetzungsseite, die den
überlaufenden Inhalt visuell mit dem nächsten Eintrag verbindet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Vorherige Ansätze berechneten die Ziel-Header-Höhe am Ende des Eintrags
neu (fehleranfällig durch doppelte Font-State-Operationen). Neuer Ansatz:
Linie für Entry i wird am ANFANG von Entry i+1 gezeichnet, BEVOR der
Seitenumbruch-Check läuft — mit demselben headerH der bereits berechnet
wurde. Eine einzige Bedingung entscheidet konsistent ob Linie gezeichnet
wird UND ob ein Seitenumbruch folgt, ohne Redundanz oder State-Probleme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Berechnet die tatsächliche Mindesthöhe des nächsten Projekteintrags
(Datum + Rolle + Firma) identisch zur Seitenumbruch-Logik. Verhindert
hängende Linien wenn der nächste Header > 40px hoch ist und eine neue
Seite benötigt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wenn ein Projekteintrag nahe am Seitenende endet und der nächste
Eintrag auf einer neuen Seite beginnt, wird keine Verbindungslinie
mehr gezeichnet. Vorher entstand ein hängender Strich ohne Ziel-Punkt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- darkenColor() Funktion: extrahierte Logo-Farbe um 30% abdunkeln
(gilt für PDF und DOCX Export) → kräftigerer, druckfreundlicher Ton
- ExpertProfileTab: min-width: 130px für btnPrimary und btnSecondary
→ alle Aktions-Buttons haben einheitliche Mindestbreite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PDF-Export: doc.flushPages() vor doc.end() verhindert leere
Schlussseite (PDFKit bufferPages-Bug nach Footer-Loop)
- ExpertProfileTab: height: 32px für btnPrimary/Secondary/Danger
sowie chipInput- und headerForm-Inputs → einheitliche Höhe
- Primärfarbe #1a56db → #1040bb (dunkler, besser zum Logo passend)
- LoginPage CSS-Fallback-Gradient ebenfalls auf neue Primärfarbe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root causes identified via DB hex-dump and server logs:
1. Tab character in budget lines: DB stores e.g. `**Budget Verantwortung:**\t750.000 EUR`
(byte 0x09 between `:**` and the amount). PDFKit can't render \t in WinAnsiEncoding,
producing garbage output like `"sSãUU`. Fix: `.replace(/\t/g, ' ')` in cleaned text.
2. Unconditional bullet: `\u2022 ${sanitize(hasBullet ? cleaned : cleaned)}` always
prepended `•` — the ternary was a no-op. Fix: only add `•` when hasBullet is true;
`**...**` header lines now render as Helvetica-Bold without a bullet.
3. ITIL4 Foundation Cert.pdf is owner-password-encrypted. pdf-lib threw
"Input document is encrypted" → cert was silently skipped.
Fix: `PdfLib.load(attBuffer, { ignoreEncryption: true })`.
Applies to both PDF and DOCX export paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
LoginPage calls /settings/branding to load branding config (logo, colors).
Without @Public(), the JWT guard returns 401, which triggered the axios
response interceptor to attempt a silent refresh, fail, and call
window.location.href = '/login' — creating an infinite reload loop on
the login page itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add @SkipThrottle() to POST /auth/refresh so repeated silent-refresh calls
from page reloads no longer exhaust the rate limit (HTTP 429)
- Configure Vite HMR explicitly with host/clientPort/protocol=wss so the
WebSocket connects correctly through Traefik instead of reconnecting every ~1s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pdf-lib installiert und importiert
- PDF-Anhänge werden nicht mehr als Platzhalter-Seite angezeigt, sondern
alle Seiten des Anhang-PDFs direkt in das Export-PDF eingebettet (pdf-lib merge)
- Bild-Anhänge bleiben weiterhin per PDFKit eingebettet
- sanitizePdfText() erweitert: vollständige Windows-1252 U+0080-U+009F Mapping-Tabelle
für mis-enkodierte Zeichen (€, Anführungszeichen, Gedankenstriche, TM usw.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pageBottom von 800 auf 740 reduziert: verhindert PDFKit-Auto-Seitenumbrüche
mitten in Projekt-Einträgen (bisher: Datum/Rolle/Firma je auf eigener Seite)
- Vor jedem Projekt-Eintrag Header-Höhe (Datum+Rolle+Firma) vorberechnen und
Seitenumbruch proaktiv auslösen, bevor der Header gezeichnet wird
- sanitizePdfText() Hilfsmethode: ersetzt €→EUR sowie Zeichen außerhalb Latin-1
die Helvetica (WinAnsiEncoding) nicht rendern kann (bisher: Zeichensalat)
- sanitizePdfText auf Projekt-Texte und Zertifizierungs-Texte angewendet
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PDF-Export: alle Anhänge als zusätzliche Seiten (Bilder als Vorschau, andere mit Hinweis)
- DOCX-Export: Bild-Anhänge als zusätzliche Sections (je eine Seite pro Bild)
- ExpertProfileTab: Skills/Sprachen/Erfahrungen nebeneinander (3 Spalten)
- ExpertProfileTab: Zertifizierungen und Profilanlagen nebeneinander (2 Spalten)
- threeColumnRow CSS-Klasse hinzugefügt (responsive auf 1 Spalte bei <900px)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Neue Prisma-Modelle ProfileAccessGroup + ProfileAccessGroupMember mit canView/canExport/canEdit
- Manuelle Migration 20260314_profile_access_groups
- ProfileAccessModule: CRUD-Endpoints für Gruppen und Mitglieder (nur PLATFORM_ADMIN)
- Neue Admin-Endpoints in ExpertProfileService/-Controller für alle Profil-Mutationen
- verifyOwnership mit skipCheck-Parameter für Admin-Bypass
- ExpertProfileTab + alle Section-Komponenten erhalten apiBase-Prop für Wiederverwendung
- AdminProfileAccessPage: Gruppen-Tab (CRUD) + Profile-Tab (alle User mit Aktionen)
- AdminProfileDetailPage: Profil eines beliebigen Users im Admin-Kontext bearbeiten
- Route /admin/profile-access + /admin/profiles/:userId + Nav-Tab Profilzugriff
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Jobtitel aus Profil statt experiences[0].area unter dem Namen
- Platform-Logo aus Redis über Avatar einfügen
- Dominante Akzentfarbe dynamisch aus Logo extrahieren
- Firmen-Fußzeile aus Redis-Settings als DOCX-Footer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- getExportData(): jobTitle zu den selektierten User-Feldern hinzugefuegt
- ExportData-Interface: jobTitle: string | null ergaenzt
- PDF: Unter dem Namen wird jetzt data.jobTitle angezeigt statt
profile.experiences[0].area
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extractDominantColor(): 20x20 Resize via sharp, Alpha gegen Weiss flatten,
alle gesaettigten Pixel (nicht weiss/schwarz/grau, range > 35) mitteln
- Ergebnis wird als accentColor fuer Timeline-Linien, Ueberschriften,
Skill-Chips usw. verwendet
- Fallback auf #009688 wenn kein Logo hinterlegt oder keine Farbe extrahierbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Footer: doc.page.margins.bottom temporaer auf 0 gesetzt beim Footer-Zeichnen
verhindert PDFKit Auto-Pagination (footerTextY > maxY wuerde sonst
eine leere zweite Seite erzeugen)
- Logo: Platform-Branding-Logo (aus Redis platform_branding_logo) wird oben
in der linken Spalte ueber dem Profilfoto gerendert (fit 180x50px)
- Firmendaten + Branding parallel via Promise.all aus Redis geladen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ZERTIFIZIERUNGEN von rechter Spalte in linke Spalte verschoben
(nach ERFAHRUNG, vor FÄHIGKEITEN)
- Textbreite auf leftColWidth (certContentWidth = leftColWidth - 14) angepasst
- Timeline-Linie dynamisch (certEntryStartY gespeichert, Linie nach Content)
- Alte rechte-Spalte-Sektion entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pdfSectionTitle: Linie zieht sich jetzt ueber die volle Spaltenbreite (width-Parameter)
statt nur bis zum Textende
- Titel 'BERUFSERFAHRUNG' umbenannt in 'BERUFSERFAHRUNG / PROJEKTE'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BERUFSERFAHRUNG: Timeline-Linie wird jetzt NACH dem Content gezeichnet
(entryStartY gespeichert, Linie von entryStartY+8 bis yRight-4)
Damit stimmt die Laenge exakt mit der tatsaechlichen Eintraghoehe ueberein
- Seitenumbruch-Flag (pageBreakOccurred): Linie wird nicht gezeichnet wenn
der Content ueber eine Seite hinausgeht
- FAEHIGKEITEN: aus dem full-width Bereich am Seitenende entfernt und in die
linke Spalte nach ERFAHRUNG verschoben (kleinere Chips: 7pt, 16px, 5px Pad)
- Alte full-width FAEHIGKEITEN-Sektion entfernt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Tasks: Bullet-Praefix nur fuer Zeilen mit echtem Aufzaehlungszeichen (kein spurious Bullet bei Plaintext)
- Zertifizierungen: Schriftgroesse reduziert (Titel 10->9pt, Aussteller 9->8pt) und Timeline-Linie gekuerzt
- Anhaenge: Bild-Anhaenge werden als zusaetzliche Seiten ans PDF angehaengt
- ExportData-Interface + getExportData() um attachments[] erweitert
- Gleiche Bullet-Fix-Logik im DOCX-Export
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 Korrekturen am Experten-Profil PDF-Export:
1. Dateiname: Vorname_Nachname_CV.pdf/.docx (RFC 5987 Umlaut-sicher)
2. Fettschrift: Zeitraum, Firmenname und Branche in Berufserfahrung
3. Zeichen-Darstellung: Markdown-Marker (**bold**/*italic*/__u__)
werden aus Tasks-Text entfernt, doppelte Bullet-Praefix-Normalisierung
4. Abstaende: Sprachen 14->11px, Erfahrung 14->11px, Gap 8->4px
5. Zertifizierungen in rechte Spalte verschoben (nach Berufserfahrung)
statt als Vollbreite-Sektion am Ende
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Im Bereich Anpassungen (AdminCustomizePage) kann der Platform-Admin
nun den Login-Screen individuell gestalten:
- Hintergrundtyp: Farbverlauf, Einfarbig oder Hintergrundbild
- Farbverlauf: zwei Farbpicker (Von/Bis) mit Hex-Eingabe
- Hintergrundbild: Datei-Upload max. 2MB, Live-Vorschau
- Logo auf Login-Screen: wird automatisch aus dem Sidebar-Logo uebernommen
Backend: settings.controller.ts GET/POST /settings/branding um
loginBgType, loginBgColor1, loginBgColor2, loginBgImage erweitert.
LoginPage laedt Branding per oeffentlichem Endpoint (kein Auth), leitet
containerStyle per useMemo ab und zeigt Logo-Bild statt Hardcode-Text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend @IsIn erlaubte nur CEFR-Codes (C1/C2/B2…), Frontend schickte
aber deutsche Bezeichnungen (Verhandlungssicher/Fließend/Gut).
Alle 4 Frontend-Level + CEFR-Codes für Rückwärtskompatibilität aufgenommen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Neue Felder im Benutzerprofil (analog Microsoft 365 /me):
- Stellenbezeichnung (jobTitle), Abteilung (department)
- Firma (companyName), Standort (officeLocation)
Changes:
- Core: Prisma-Migration + neue Felder in User-Model, UpdateUserDto,
findById/update/updateProfile
- CRM: M365UserProfile-Interface + getM365Profile um neue Felder erweitert;
neue Methode getM365Photo() lädt 96x96 JPEG als Base64 Data-URL;
neuer Endpoint GET /crm/office365/photo
- Frontend: AuthContext User-Interface, M365UserProfile-Typ, office365Api.getM365Photo()
ProfilePage: Neues Formular-Fieldset "Organisation" mit 4 Feldern;
manueller Sync-Button übernimmt auch Profilbild (immer überschreiben);
useO365ProfileSync: Auto-Sync lädt Foto nur wenn noch kein INSIGHT-Avatar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Traefik leitet x-forwarded-proto nicht korrekt weiter, sodass der
Controller http:// statt https:// generierte — Azure lehnt nicht-HTTPS
Redirect-URIs für nicht-localhost ab (AADSTS50011).
Protokoll wird jetzt aus der konfigurierten SSO-Redirect-URI abgeleitet
(immer HTTPS), der Host bleibt dynamisch (IP oder DNS).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MSAL-node v5 erzeugt bei getAuthCodeUrl mit reinen Graph-API-Scopes
(ohne openid) einen fehlerhaften Authorize-URL → AADSTS900561.
getIntegrationAuthUrl und handleIntegrationCallback verwenden jetzt
direkte fetch-Aufrufe (analog zu refreshIntegrationToken) ohne MSAL,
was den Fehler umgeht und denselben Standard-OAuth2-Flow garantiert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Problem: Redirect-URI wurde falsch aus SSO-URI abgeleitet, und unterstützte
nur eine feste URL statt sowohl IP als auch DNS-Name.
Lösung:
- initM365Integration: Host aus x-forwarded-host/host Header lesen,
korrekte Redirect-URI bauen (proto://host/api/v1/auth/integrations/...)
- Redis-State speichert jetzt {userId, redirectUri} als JSON
- handleIntegrationCallback: gespeicherte redirectUri aus State verwenden
- getIntegrationAuthUrl/handleIntegrationCallback: optionaler redirectUri-Parameter
- Fallback-Derivation: base URL aus SSO-URI + fester Integrations-Pfad
Beide URIs müssen in Azure registriert sein:
- http://172.20.10.59/api/v1/auth/integrations/microsoft-365/callback
- http://insight.xinion.lan/api/v1/auth/integrations/microsoft-365/callback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
IntegrationsService benötigt EntraIdService (Token-Refresh), der in
AuthModule als Provider registriert aber nicht exportiert war.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: 4 new endpoints in SettingsController (GET/POST/DELETE /settings/ssl, POST /settings/ssl/check-dns)
- Certificate validation via Node.js crypto.X509Certificate (PEM format, expiry, SAN match)
- DNS resolution check via dns.promises.resolve4
- Auto-generates Traefik dynamic config (ssl-domain.yml) with custom domain routing + HTTP->HTTPS redirect
- Frontend: AdminSslPage with DNS name input, cert/key upload, status display
- Docker: Core-service gets access to traefik-certs volume and dynamic config directory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Light/Dark/System theme toggle with ThemeContext and CSS variables
- Sidebar fully collapsible (icons-only mode, persisted in localStorage)
- Anwendungen section collapsible with chevron toggle
- Admin "Anpassungen" page: logo upload, sidebar color picker with presets
- Backend branding endpoints (GET/POST /settings/branding) stored in Redis
- Optional custom icon upload for external links (click icon field)
- Backend favicon proxy with HTML parsing for reliable icon loading
- Dark mode CSS variables for all components
- Login page SSO button and error styles use CSS variables
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add GET /settings/favicon?url= endpoint that parses HTML for <link rel="icon"> tags
- Falls back to /favicon.ico if no icon link found in HTML
- Caches favicon URLs in Redis (24h TTL)
- Frontend uses backend proxy for reliable favicon loading (fixes Atlassian etc.)
- Anwendungen section in sidebar is now collapsible with chevron toggle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
External links now automatically show the favicon of the target website
using Google's favicon service. No manual icon upload needed — just
enter label and URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
crypto.randomUUID() is only available in secure contexts (HTTPS).
Since the app runs over HTTP in development, this caused a blank page
crash on the external links admin page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Admin section moved to dedicated area with horizontal tab navigation
- Sidebar now shows gear icon link to Administration (PLATFORM_ADMIN only)
- External links management page for configuring sidebar shortcuts
- External links displayed in sidebar for all authenticated users
- Backend: Redis-based CRUD endpoints for external link configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSO config (Tenant ID, Client ID, Client Secret, Redirect URI) can now
be managed entirely from the Admin SSO page. Config is stored in Redis
(persistent) and the MSAL client is reinitialized on save — no server
restart or console access required.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend-driven Authorization Code Flow with @azure/msal-node:
- EntraIdService: MSAL ConfidentialClientApplication, auth URL generation, token exchange
- SsoController: /auth/sso/microsoft (initiate) + /auth/sso/microsoft/callback (callback)
- AuthService.loginViaSso(): User provisioning (find by OID, auto-link by email, or create new)
- CSRF protection via state parameter stored in Redis
- SSO status endpoint for frontend feature detection
Frontend:
- "Mit Microsoft anmelden" button on login page (shown only when SSO is configured)
- SsoCallbackPage: handles redirect from backend, sets token, loads user profile
- AuthContext.loginWithToken(): new method for SSO token handling
Configuration:
- AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_REDIRECT_URI env vars
- docker-compose.yml updated to pass Azure vars to core service
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uses sharp to resize and apply a circular SVG mask to the avatar
before embedding it in the Word document, matching the PDF export.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>