' =================================================================== ' File: logger.puml ' Purpose: Single source of truth for module-level activity + per-method sequences. ' Module: logger.js — Structured logger with level gating, buffer, anti-spam utilities. ' Edit rules: Follow the legend at bottom; preserve VIEW/METHOD anchors for automation. ' =================================================================== ' Neutral defaults — typography/layout only (keeps partition colors intact) skinparam Shadowing false skinparam SequenceMessageAlign center skinparam SequenceLifeLineBorderColor #666666 skinparam SequenceLifeLineBorderThickness 1 ' ==== VIEW: Branch Flow (logger.js) ======================================== @startuml title logger.js — Branch Flow (full module) start :logger; fork ' -------- Logger.constructor() -------- partition "Logger.constructor()" #E7FAE3 { :constructor; :config = window.AI_REPO_CONFIG;\nbuffer=[]; loopCounts=Map(); startedAt=Date.now(); :setInterval(clean loopCounts if > 2× watchMs); kill } fork again ' -------- error/warn/info/verbose/trace -------- partition "level helpers (error/warn/info/verbose/trace)" #FFF6D1 { :error(msg,data) -> _log(1,'ERROR',...); :warn(msg,data) -> _log(2,'WARN',...); :info(msg,data) -> _log(3,'INFO',...); :verbose(msg,data)-> _log(4,'VERBOSE',...); :trace(msg,data) -> _log(5,'TRACE',...); kill } fork again ' -------- command(action,status,extra) -------- partition "command(action, status, extra)" #FFE1DB { :command; :icon by status; info(`${icon} ${action} [${status}]`, extra); kill } fork again ' -------- logLoop(kind, msg) -------- partition "logLoop(kind, msg)" #DCF9EE { :logLoop; :k=kind+msg; cur=loopCounts.get(k)||0; :withinWatch = now-startedAt <= watchMs; :if !withinWatch && kind!='WARN' -> return; :if cur>=10 -> return; :loopCounts.set(k,cur+1); suffix=(cur+1>1?` (${cur+1}x)`:''); :route to error/warn/info(kind, msg+suffix); kill } fork again ' -------- _log(levelNum, levelName, msg, data) -------- partition "_log(levelNum, levelName, msg, data)" #FFE6F0 { :_log; :enabled = !!config.debug.enabled;\nlevel = config.debug.level ?? 0;\nif !enabled or levelNum>level -> return; :entry={ts, levelName, message, data:_sanitize(data)}; :buffer.push(entry); trim to debug.maxLines (default 400); :console.log(`[AI RC ${levelName}]`, msg, optional sanitized data); kill } fork again ' -------- _sanitize(data) -------- partition "_sanitize(data)" #E6F3FF { :_sanitize; :null -> null;\nHTMLElement -> "HTMLElement"; :long string (>200) -> truncated + ellipsis; :plain object -> shallow sanitize values recursively (HTMLElement/long string); :else return data; kill } fork again ' -------- getRecentLogs(n=50) -------- partition "getRecentLogs(n=50)" #F0E6FA { :getRecentLogs; :tail buffer n; map to lines: ISO LEVEL message {jsonData?}; join with \\n; kill } fork again ' -------- setLevel(n) -------- partition "setLevel(n)" #E7FAF7 { :setLevel; :lv = clamp(n,0..5); config.set('debug.level', lv);\ninfo(`Log level => ${lv}`); kill } fork again ' -------- bootstrap (global) -------- partition "bootstrap (global)" #FFF7E7 { :window.AI_REPO_LOGGER = new Logger(); kill } end fork @enduml ' ==== METHOD: Logger.constructor() ========================================= @startuml title logger:constructor(): \n Prepare config, buffers, anti-spam counters, and cleanup timer participant "Caller" as CL participant "constructor()" as CTOR participant "Config" as CFG participant "Timer" as TMR activate CL CL -> CTOR : new Logger() activate CTOR CTOR -> CFG : window.AI_REPO_CONFIG CFG --> CTOR : config CTOR -> CTOR : buffer=[]; loopCounts=new Map(); startedAt=Date.now() ' periodic cleanup ~ every watchMs (default 120s) CTOR -> CFG : get('debug.watchMs') || 120000 CFG --> CTOR : watchMs CTOR -> TMR : setInterval(fn, watchMs) TMR --> CTOR : id CTOR -> CTOR : fn: if (Date.now()-startedAt > watchMs*2)\n loopCounts.clear(); startedAt=Date.now() CTOR --> CL : instance deactivate CTOR deactivate CL @enduml ' ==== METHOD: level helpers (error/warn/info/verbose/trace) ================ @startuml title logger:level helpers: \n Route to _log() with appropriate numeric level participant "Caller" as CL participant "error()" as ERR participant "warn()" as WRN participant "info()" as INF participant "verbose()" as VRB participant "trace()" as TRC participant "_log(...)" as LOG activate CL CL -> ERR : msg, data ERR -> LOG : _log(1,'ERROR',msg,data) LOG --> ERR : (void) CL -> WRN : msg, data WRN -> LOG : _log(2,'WARN',msg,data) LOG --> WRN : (void) CL -> INF : msg, data INF -> LOG : _log(3,'INFO',msg,data) LOG --> INF : (void) CL -> VRB : msg, data VRB -> LOG : _log(4,'VERBOSE',msg,data) LOG --> VRB : (void) CL -> TRC : msg, data TRC -> LOG : _log(5,'TRACE',msg,data) LOG --> TRC : (void) TRC --> CL : done deactivate TRC deactivate CL @enduml ' ==== METHOD: command(action, status, extra) ================================ @startuml title logger:command(action, status, extra): \n Friendly lifecycle log with icon participant "Caller" as CL participant "command(action,status,extra)" as CMD participant "info()" as INF activate CL CL -> CMD : action, status, extra activate CMD CMD -> CMD : icon = map(status)\n('👁️','📝','✓','⏳','⚙️','✅','❌','•') CMD -> INF : info(`${icon} ${action} [${status}]`, extra) INF --> CMD : (void) CMD --> CL : (void) deactivate CMD deactivate CL @enduml ' ==== METHOD: logLoop(kind, msg) =========================================== @startuml title logger:logLoop(kind, msg): \n Anti-spam logging for hot paths (max 10 per watch window) participant "Caller" as CL participant "logLoop(kind,msg)" as LLP participant "Config" as CFG participant "error()" as ERR participant "warn()" as WRN participant "info()" as INF activate CL CL -> LLP : kind, msg activate LLP LLP -> LLP : k=`${kind}:${msg}`; cur=loopCounts.get(k)||0 LLP -> CFG : get('debug.watchMs') || 120000 CFG --> LLP : watchMs LLP -> LLP : withinWatch = (now-startedAt) <= watchMs alt !withinWatch && kind != 'WARN' LLP --> CL : (void) deactivate LLP deactivate CL return end alt cur >= 10 LLP --> CL : (void) deactivate LLP deactivate CL return end LLP -> LLP : loopCounts.set(k, cur+1);\nsuffix = (cur+1>1) ? ` (${cur+1}x)` : '' alt kind == 'ERROR' LLP -> ERR : error(msg+suffix) ERR --> LLP : ok else kind == 'WARN' LLP -> WRN : warn(msg+suffix) WRN --> LLP : ok else LLP -> INF : info(msg+suffix) INF --> LLP : ok end LLP --> CL : (void) deactivate LLP deactivate CL @enduml ' ==== METHOD: _log(levelNum, levelName, msg, data) ========================= @startuml title logger:_log(levelNum, levelName, msg, data): \n Gate by config, buffer tail, print to console participant "Caller" as CL participant "_log(...)" as LOG participant "Config" as CFG participant "_sanitize(data)" as SAN participant "Console" as CON activate CL CL -> LOG : levelNum, levelName, msg, data activate LOG LOG -> CFG : get('debug.enabled') CFG --> LOG : enabled (bool) LOG -> CFG : get('debug.level') ?? 0 CFG --> LOG : level (0..5) alt !enabled or levelNum > level LOG --> CL : (void) deactivate LOG deactivate CL return end LOG -> SAN : _sanitize(data) SAN --> LOG : safeData LOG -> LOG : entry = {timestamp, level:levelName, message:String(msg), data:safeData} LOG -> CFG : get('debug.maxLines') || 400 CFG --> LOG : maxLines LOG -> LOG : buffer.push(entry); if buffer.length>maxLines -> splice head ' console output alt with data LOG -> CON : console.log(`[AI RC ${levelName}]`, msg, entry.data) else no data LOG -> CON : console.log(`[AI RC ${levelName}]`, msg) end CON --> LOG : printed LOG --> CL : (void) deactivate LOG deactivate CL @enduml ' ==== METHOD: _sanitize(data) ============================================== @startuml title logger:_sanitize(data): \n Redact DOM elements, truncate long strings, shallow-sanitize objects participant "Caller" as CL participant "_sanitize(data)" as SAN participant "HTMLElement" as HTM activate CL CL -> SAN : data activate SAN alt !data SAN --> CL : null deactivate SAN deactivate CL return end alt data instanceof HTMLElement SAN -> HTM : tagName HTM --> SAN : TAG SAN --> CL : "HTMLElement" deactivate SAN deactivate CL return end alt typeof data === 'string' && length > 200 SAN --> CL : data.slice(0,200) + '…' deactivate SAN deactivate CL return end alt typeof data === 'object' SAN -> SAN : out = {} loop for each [k,v] of Object.entries(data) alt v instanceof HTMLElement SAN -> SAN : out[k] = `HTMLElement<${v.tagName}>` else typeof v === 'string' && v.length>200 SAN -> SAN : out[k] = v.slice(0,200) + '…' else SAN -> SAN : out[k] = v end end SAN --> CL : out else other primitive SAN --> CL : data end deactivate SAN deactivate CL @enduml ' ==== METHOD: getRecentLogs(n=50) ========================================== @startuml title logger:getRecentLogs(n): \n Tail buffer to plain-text lines for copy/export participant "Caller" as CL participant "getRecentLogs(n)" as GRL activate CL CL -> GRL : n (default 50) activate GRL GRL -> GRL : tail = buffer.slice(-n) GRL -> GRL : lines = tail.map(e => `${e.timestamp} ${e.level.padEnd(7)} ${e.message}${e.data ? ' ' + JSON.stringify(e.data) : ''}`) GRL --> CL : lines.join('\\n') deactivate GRL deactivate CL @enduml ' ==== METHOD: setLevel(n) ================================================== @startuml title logger:setLevel(n): \n Clamp and persist numeric level, then self-log the change participant "Caller" as CL participant "setLevel(n)" as SLV participant "Config" as CFG participant "info()" as INF activate CL CL -> SLV : n activate SLV SLV -> SLV : lv = Math.max(0, Math.min(5, n)) SLV -> CFG : set('debug.level', lv) CFG --> SLV : ok SLV -> INF : info(`Log level => ${lv}`) INF --> SLV : logged SLV --> CL : (void) deactivate SLV deactivate CL @enduml ' ==== METHOD: bootstrap (global) =========================================== @startuml title logger:bootstrap: \n Expose singleton logger on window participant "Caller" as CL participant "bootstrap" as BOOT participant "Logger()" as CTOR participant "window" as WIN activate CL CL -> BOOT : module load activate BOOT BOOT -> CTOR : new Logger() CTOR --> BOOT : instance BOOT -> WIN : window.AI_REPO_LOGGER = instance WIN --> BOOT : ok BOOT --> CL : (void) deactivate BOOT deactivate CL @enduml ' ==== LEGEND =============================================================== @startuml legend bottom == logger UML Style Guide (for future edits) == • Scope: One .puml per module. Keep two views: (1) Activity "Branch Flow" for the whole module (partitions + soft colors), (2) Per-function/Per-method Sequence diagrams for each exported or significant internal method. • Sequence conventions: 1) First participant is the external caller (use "Caller" or a concrete origin). 2) Do NOT add a module/class lifeline; the name appears in the title only. 3) Include every directly-called helper/system as a participant (e.g., "info()", "_log()", "_sanitize()", "Config", "Console", "Timer", "HTMLElement"). 4) Prefer simple messages; Use --> for returns; -> for calls. 5) Use activate/deactivate as you see fit for clarity. 6) Use alt blocks only when branches meaningfully change the message flow. • Activity view conventions: A) Start with module node then fork partitions for each function/method. B) One partition per function; soft background color; terminate branches with 'kill'. C) Keep wording aligned with code (e.g., watchMs default 120000, maxLines default 400, 10x cap in logLoop). • Color palette (soft pastels) • Use --> for returns; -> for calls. • Participants use quoted method names for internals; plain nouns for systems ("Config", "Console", "Timer"). • Keep this legend at the end of the file to standardize edits. endlegend @enduml