1// Module: taskbar-os2045 â OS-2045 Taskbar (start button, running apps, tray, clock) 2// 3// Public API: window.OS2045Taskbar = { 4// mount(container?), update(), refreshClock(), getElement(), pin(appId,opts), unpin(appId) 5// } 6// 7// Behaviour: 8// - mount() inserts a .os-taskbar element into the desktop container if not present. 9// - Subscribes to OS2045WM.onChange() to keep running/active button state in sync. 10// - Click on app button: smart toggle (focus / minimize / open). 11// - Right-click: contextmenu via OS2045Ctx. 12// - Clock ticks every 30s. 13 14(function () { 15 'use strict'; 16 17 const PIN_STORAGE = 'os2045.taskbar.pinned'; 18 const DAYS_SHORT = ['SO','MO','DI','MI','DO','FR','SA']; 19 const MONTHS = ['JAN','FEB','MÃR','APR','MAI','JUN','JUL','AUG','SEP','OKT','NOV','DEZ']; 20 21 let taskbarEl = null; 22 let runningEl = null; 23 let trayClockEl = null; 24 let trayDateEl = null; 25 let clockTimer = null; 26 let pinned = loadPinned(); 27 let unsubWM = null; 28 29 function isOs2045() { 30 return document.body && document.body.dataset.themeMode === 'os2045'; 31 } 32 33 function loadPinned() { 34 try { 35 const raw = localStorage.getItem(PIN_STORAGE); 36 const list = raw ? JSON.parse(raw) : null; 37 if (!Array.isArray(list)) return list; 38 const revKey = PIN_STORAGE + '.glyphrev'; 39 let rev = 0; 40 try { rev = parseInt(localStorage.getItem(revKey) || '0', 10) || 0; } catch (e) {} 41 if (rev < 2) { 42 list.forEach(p => { 43 if (p && p.id === 'backlog') { 44 p.icon = backlogIcon(); 45 p.title = p.title === 'Backlog' ? 'Aufgaben' : (p.title || 'Aufgaben'); 46 } 47 }); 48 try { localStorage.setItem(PIN_STORAGE, JSON.stringify(list)); } catch (e) {} 49 try { localStorage.setItem(revKey, '2'); } catch (e) {} 50 } 51 return list; 52 } catch (e) { return null; } 53 } 54 55 function savePinned() { 56 try { localStorage.setItem(PIN_STORAGE, JSON.stringify(pinned)); } catch (e) {} 57 } 58 59 function defaultPinned() { 60 // Hartcodierte Default-Pins 61 return [ 62 { id: 'dashboard', title: 'Dashboard', icon: dashIcon() }, 63 { id: 'backlog', title: 'Aufgaben', icon: backlogIcon() }, 64 { id: 'firmen', title: 'Firmen', icon: firmenIcon() }, 65 { id: 'pm2', title: 'PM2', icon: pm2Icon() }, 66 { id: 'files', title: 'Dateien', icon: filesIcon() }, 67 { id: 'shell', title: 'Shell', icon: shellIcon() } 68 ]; 69 } 70 71 // âââ Inline SVG icons (small) âââââââââââââââââââââââââââââââââââââââââ 72 function svg(d, opts) { 73 opts = opts || {}; 74 return `<svg viewBox="0 0 24 24" fill="${opts.fill||'none'}" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${d}</svg>`; 75 } 76 // Pure svg symbols â no outer frame. Each icon uses its native 0..24 77 // geometry so it stays large and legible. Shell + SSH still carry their 78 // own 16:18-style frame because the rectangle is part of their glyph 79 // (a screen/terminal silhouette), not a generic decoration. 80 function fsvg(symbol) { return svg(symbol); } 81 // App tiles â pure symbol 82 function dashIcon() { return fsvg('<rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/>'); } 83 function backlogIcon() { return fsvg('<rect x="5" y="3" width="14" height="18" rx="2"/><path d="M9 3h6v2.8H9z"/><path d="M8.5 11.5l2 2 4.2-4.2"/><path d="M8.5 17h7"/>'); } 84 function firmenIcon() { return fsvg('<rect x="3" y="7" width="18" height="14" rx="1.5"/><path d="M8 7V4h8v3"/>'); } 85 function pm2Icon() { return fsvg('<rect x="3" y="5" width="18" height="6" rx="1"/><rect x="3" y="13" width="18" height="6" rx="1"/><circle cx="6" cy="8" r="0.7" fill="currentColor"/><circle cx="6" cy="16" r="0.7" fill="currentColor"/>'); }
86 // shell keeps its native rect â it's the screen/terminal silhouette, 87 // not a generic frame. 88 function shellIcon() { return svg('<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9l3 3-3 3M12 15h6"/>'); } 89 function filesIcon() { return fsvg('<path d="M3 6a2 2 0 012-2h4l2 2h8a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V6z"/>'); } 90 function clockIcon() { return fsvg('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'); } 91 function shieldIcon() { return fsvg('<path d="M12 3l8 3v6c0 5-3.5 8.5-8 10-4.5-1.5-8-5-8-10V6l8-3z"/>'); } 92 function bellIcon() { return fsvg('<path d="M6 8a6 6 0 1112 0v5l1.5 3h-15L6 13z"/><path d="M10 19a2 2 0 004 0"/>'); } 93 function powerIcon() { return fsvg('<path d="M12 3v8"/><path d="M5.6 8a8 8 0 1012.8 0"/>'); } 94 function qsIcon() { return fsvg('<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>'); } 95 // Start button glyph â 2Ã2 tile grid (Win11-style). Filled rounded 96 // squares in native 0..24 viewBox geometry, no outer frame. 97 // CorSch-Mark â gleiches Glyph wie auf dem Lockscreen (offener Kreis = "C"). 98 function startIcon() { return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6.5A8 8 0 106 17.5"/></svg>`; } 99 function searchIcon() { return fsvg('<circle cx="11" cy="11" r="7"/><path d="M21 21l-4-4"/>'); } 100 101 function safeSvgInsert(container, svgString) { 102 if (!svgString) return; 103 const trimmed = String(svgString).trim(); 104 // Plain text / emoji icons (e.g. registry icons like ð¥): render 105 // as text content. No parser involved â no XSS risk. 106 if (!trimmed.startsWith('<svg')) { 107 container.textContent = trimmed; 108 return; 109 } 110 // SVG strings: strict image/svg+xml parser needs xmlns. Inject if 111 // missing so the SVG actually parses instead of returning a 112 // <parsererror>. 113 const withNs = / xmlns=/.test(trimmed) 114 ? trimmed 115 : trimmed.replace(/^<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"'); 116 const parser = new DOMParser(); 117 const doc = parser.parseFromString(withNs, 'image/svg+xml'); 118 const svgEl = doc.querySelector('svg'); 119 if (!svgEl || doc.querySelector('parsererror')) { 120 container.textContent = trimmed; 121 return; 122 } 123 svgEl.querySelectorAll('script').forEach(s => s.remove()); 124 svgEl.querySelectorAll('*').forEach(el => { 125 Array.from(el.attributes).forEach(attr => { 126 if (/^on/i.test(attr.name)) el.removeAttribute(attr.name); 127 }); 128 }); 129 container.appendChild(document.importNode(svgEl, true)); 130 } 131 132 // âââ Build markup ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 133 function build() { 134 if (taskbarEl) return taskbarEl; 135 taskbarEl = document.createElement('div'); 136 taskbarEl.className = 'os-taskbar'; 137 taskbarEl.id = 'os2045Taskbar'; 138 taskbarEl.innerHTML = ` 139 <button class="tb-start" id="os2045StartBtn" title="CorSch Me
139nü" aria-label="Start-Menü öffnen">${startIcon()}</button> 140 <div class="tb-search" id="os2045TbSearch" role="button" tabindex="0" title="Suche öffnen"> 141 ${searchIcon()} 142 <span>Suche â Apps, Befehle</span> 143 <span class="kbd">âK</span> 144 </div> 145 <div class="tb-sep"></div> 146 <div class="tb-running" id="os2045TbRunning" role="toolbar" aria-label="Geöffnete Anwendungen"></div> 147 <div class="tb-sep"></div> 148 <div class="tray" id="os2045Tray"> 149 <div class="tb-nowplaying" id="os2045TbNowPlaying" hidden role="button" tabindex="0" title="Wiedergabe â Klick: Play/Pause · Doppelklick: Cor-Amp"> 150 <span class="np-icon" id="os2045TbNpIcon">â¶</span> 151 <span class="np-title" id="os2045TbNpTitle">â</span> 152 </div> 153 <button class="ti" id="os2045TrayQs" title="Schnell-Einstellungen (Win+A)" aria-label="Schnell-Einstellungen">${qsIcon()}</button> 154 <button class="ti" id="os2045TrayShield" title="Guardian aktiv" aria-label="Guardian Status">${shieldIcon()}</button> 155 <button class="ti" id="os2045TrayBell" title="Benachrichtigungen (Win+N)" aria-label="Benachrichtigungen">${bellIcon()}</button> 156 <button class="ti" id="os2045TrayPower" title="Energie-Optionen (Win+X)" aria-label="Energie-Optionen">${powerIcon()}</button> 157 <div class="tsep"></div> 158 <div class="clock" id="os2045TrayClock"> 159 <span id="os2045ClockTime">--:--</span> 160 <small id="os2045ClockDate">â · â</small> 161 </div> 162 </div> 163 <button class="tray-ai" id="os2045TrayAi" title="CorSch Copilot" aria-label="Copilot öffnen"></button> 164 `; 165 runningEl = taskbarEl.querySelector('#os2045TbRunning'); 166 trayClockEl = taskbarEl.querySelector('#os2045ClockTime'); 167 trayDateEl = taskbarEl.querySelector('#os2045ClockDate'); 168 169 // Wire start button 170 taskbarEl.querySelector('#os2045StartBtn').addEventListener('click', (e) => { 171 e.stopPropagation(); 172 if (window.OS2045StartMenu) window.OS2045StartMenu.toggle(); 173 }); 174 // Search opens start menu (placeholder for command palette in future) 175 taskbarEl.querySelector('#os2045TbSearch').addEventListener('click', () => { 176 if (window.OS2045StartMenu) window.OS2045StartMenu.open(true); 177 }); 178 // Tray bell â notification center 179 taskbarEl.querySelector('#os2045TrayBell').addEventListener('click', (e) => { 180 e.stopPropagation(); 181 if (window.OS2045Notify) window.OS2045Notify.toggle(); 182 else if (typeof showToast === 'function') showToast('Keine neuen Benachrichtigungen', 'info'); 183 }); 184 // Tray quick-settings 185 taskbarEl.querySelector('#os2045TrayQs').addEventListener('click', (e) => { 186 e.stopPropagation(); 187 if (window.OS2045QuickSettings) window.OS2045QuickSettings.toggle(); 188 }); 189 // Tray power 190 taskbarEl.querySelector('#os2045TrayPower').addEventListener('click', (e) => { 191 e.stopPropagation(); 192 if (window.OS2045Power) window.OS2045Power.toggle(); 193 }); 194 // Tray shield â opens Guardian app placeholder 195 taskbarEl.querySelector('#os2045TrayShield').addEventListener('click', () => { 196 if (window.OS2045WM) window.OS2045WM.openApp('guardian', { title: 'Guardian 2FA' }); 197 }); 198 taskbarEl.querySelector('#os2045TrayAi').addEventListener('click', () => { 199 // Route through the registered app opener so loadAdminModules() 200 // fires and OS2045Copilot.open() actually mounts the chat UI. 201 // Direct OS2045WM.openApp('copilot', { title }) â without a mount 202 // function â falls back to the WM "Phase 3" placeholder. 203 const apps = window.OS2045Apps; 204 if (apps && apps.copilot && typeof apps.copilot.open === 'function') { 205 apps.copilot.open(); 206 } else if (window.OS2045Copilot && typeof window.OS2045Copilot.open === 'function') { 207 window.OS2045Copilot.open(); 208 } else if (window.OS2045WM) { 209 // Last-resort placeholder so the click still produces a window. 210 window.OS2045WM.openApp('copilot', { 211 title: 'CorSch Copilot', 212 html: '<div class="os-window-empty"><div class="os-window-empty-sub">Copilot-Modul wird geladenâ¦</div></div>' 213 }); 214 } 215 }); 216
217 // Cor-Amp now-playing tray-widget. Hidden until coramp.js dispatches 218 // 'os2045:coramp-state' events (see coramp.js play/pause/load hooks). 219 const npEl = taskbarEl.querySelector('#os2045TbNowPlaying'); 220 if (npEl) { 221 // Click â Play/Pause via the public CorAmp API (or a custom event 222 // when the module isn't loaded yet, which coramp.js intercepts). 223 npEl.addEventListener('click', () => { 224 window.dispatchEvent(new CustomEvent('os2045:coramp-toggle-play')); 225 }); 226 npEl.addEventListener('dblclick', () => { 227 if (window.OS2045Apps && window.OS2045Apps.corplayer 228 && typeof window.OS2045Apps.corplayer.open === 'function') { 229 window.OS2045Apps.corplayer.open(); 230 } 231 }); 232 window.addEventListener('os2045:coramp-state', (e) => { 233 const d = e.detail || {}; 234 if (!d.title) { 235 npEl.hidden = true; 236 return; 237 } 238 npEl.hidden = false; 239 const titleEl = npEl.querySelector('#os2045TbNpTitle'); 240 const iconEl = npEl.querySelector('#os2045TbNpIcon'); 241 if (titleEl) titleEl.textContent = d.title; 242 if (iconEl) iconEl.textContent = d.playing ? 'â¸' : 'â¶'; 243 npEl.classList.toggle('is-playing', !!d.playing); 244 }); 245 } 246 // Tray icon visibility is permission-aware: 247 // - Shield: admin only (Guardian config is admin-only feature) 248 // - Copilot: admin OR a non-admin user whose allowedIcons includes 249 // 'copilot' (admin grants the icon via the Benutzer-Window). 250 const aiBtn = taskbarEl.querySelector('#os2045TrayAi'); 251 const shieldBtn = taskbarEl.querySelector('#os2045TrayShield'); 252 function syncTrayPermissions() { 253 const loggedIn = !!(window.adminToken && document.body.classList.contains('logged-in')); 254 const role = window.currentUserRole || 'admin'; 255 const isAdmin = loggedIn && (!window.currentUserRole || role === 'admin'); 256 // Copilot visible if admin, or non-admin user has 'copilot' in allowedIcons 257 let canCopilot = false; 258 if (loggedIn) { 259 if (isAdmin) canCopilot = true; 260 else if (Array.isArray(window.currentUserAllowedIcons) 261 && window.currentUserAllowedIcons.includes('copilot')) { 262 canCopilot = true; 263 } 264 } 265 if (aiBtn) aiBtn.style.display = canCopilot ? '' : 'none'; 266 if (shieldBtn) shieldBtn.style.display = isAdmin ? '' : 'none'; 267 } 268 syncTrayPermissions(); 269 if (window.MutationObserver) { 270 new MutationObserver(syncTrayPermissions).observe(document.body, { 271 attributes: true, attributeFilter: ['class'] 272 }); 273 } 274 // Re-evaluate when allowedIcons change at runtime (admin granted the 275 // icon while user was logged in). 276 window.addEventListener('os2045:allowed-icons-changed', syncTrayPermissions); 277 return taskbarEl; 278 } 279 280 // Mirror of start-menu / shell-bootstrap whitelist for guest sessions. 281 const TASKBAR_GUEST_APPS = new Set([ 282 'calculator', 'notes', 'color-picker', 283 'games', 'settings', 'impressum', 'datenschutz' 284 ]); 285 const TASKBAR_NONADMIN_ALWAYS_ON = new Set([ 286 'settings', 'account', 'logout', 'impressum', 'datenschutz' 287 ]); 288 function isAdminLoggedIn() { 289 return !!(window.adminToken && document.body && document.body.classList.contains('logged-in') 290 && (!window.currentUserRole || window.currentUserRole === 'admin')); 291 } 292 function isAppVisibleForRole(appId) { 293 if (isAdminLoggedIn()) return true; 294 if (window.currentUserRole === 'viewer') return TASKBAR_GUEST_APPS.has(appId); 295 if (window.adminToken && window.currentUserRole && window.currentUserRole !== 'admin') { 296 const allowed = window.currentUserAllowedIcons; 297 if (Array.isArray(allowed)) { 298 return allowed.includes(appId) || TASKBAR_NONADMIN_ALWAYS_ON.has(appId); 299 } 300 } 301 return TASKBAR_GUEST_APPS.has(appId); 302 } 303 304 function render() { 305 if (!runningEl) return; 306 const wmState = window.OS2045WM ? window.OS2045WM.getState() : []; 307 const runningIds = new Set(wmState.map(w => w.id)); 308 // Defensive copy so we don't mutate the persisted/default array 309 let list = (pinned || defaultPinned()).map(p => Object.assign({}, p));
310 // Guest filter: hide admin app pins. The persisted localStorage list 311 // can carry admin apps from a previous admin session â strip them 312 // when we're running anonymously. 313 list = list.filter(p => p && isAppVisibleForRole(p.id)); 314 // Add running apps that aren't pinned (transient buttons). 315 // Resolve icon: window-supplied â OS2045Apps registry â start-menu 316 // catalog â generic files-fallback. Without the catalog lookup the 317 // taskbar showed a blank/default icon for windows whose creator 318 // didn't pass opts.icon (e.g. SSH from the legacy ssh module). 319 wmState.forEach(w => { 320 if (!list.find(p => p.id === w.id)) { 321 // Prefer start-menu catalog over OS2045Apps registry + 322 // window-supplied icon, so taskbar / window-header / desktop 323 // all show the same glyph for one app id. 324 let iconFor = null; 325 if (window.OS2045StartMenu && typeof window.OS2045StartMenu._catalog === 'function') { 326 try { 327 const cat = window.OS2045StartMenu._catalog(); 328 const app = Array.isArray(cat) ? cat.find(a => a && a.id === w.id) : null; 329 if (app && app.icon) iconFor = app.icon; 330 } catch (_) {} 331 } 332 if (!iconFor && window.OS2045Apps && window.OS2045Apps[w.id] && window.OS2045Apps[w.id].icon) { 333 iconFor = window.OS2045Apps[w.id].icon; 334 } 335 if (!iconFor) iconFor = w.icon; 336 list.push({ id: w.id, title: w.title, icon: iconFor || filesIcon(), transient: true }); 337 } 338 }); 339 340 runningEl.innerHTML = ''; 341 list.forEach(p => { 342 const btn = document.createElement('button'); 343 btn.className = 'tb-btn'; 344 btn.dataset.appId = p.id; 345 btn.title = p.title; 346 btn.setAttribute('aria-label', p.title); 347 const tiDiv = document.createElement('div'); 348 tiDiv.className = 'ti'; 349 safeSvgInsert(tiDiv, p.icon || filesIcon()); 350 btn.appendChild(tiDiv); 351 const isRunning = runningIds.has(p.id); 352 const winState = wmState.find(w => w.id === p.id); 353 if (isRunning) btn.classList.add('running'); 354 if (isRunning && !winState.minimized && winState.focused) btn.classList.add('active'); 355 btn.addEventListener('click', () => smartToggle(p)); 356 runningEl.appendChild(btn); 357 }); 358 } 359 360 function smartToggle(p) { 361 const wm = window.OS2045WM; 362 if (!wm) return; 363 if (!wm.isOpen(p.id)) { 364 // Open the app via OS2045Apps if registered, else open empty window 365 const reg = (window.OS2045Apps && window.OS2045Apps[p.id]) || null; 366 if (reg && typeof reg.open === 'function') reg.open(); 367 else wm.openApp(p.id, { title: p.title, icon: p.icon }); 368 return; 369 } 370 const st = wm.getState().find(w => w.id === p.id); 371 if (!st) return; 372 if (st.minimized) wm.restore(p.id); 373 else if (st.focused) wm.minimize(p.id); 374 else wm.focus(p.id); 375 } 376 377 function pad2(n) { return String(n).padStart(2, '0'); } 378 379 function refreshClock() { 380 const d = new Date(); 381 if (trayClockEl) trayClockEl.textContent = pad2(d.getHours()) + ':' + pad2(d.getMinutes()); 382 if (trayDateEl) trayDateEl.textContent = DAYS_SHORT[d.getDay()] + ' ' + pad2(d.getDate()) + ' ' + MONTHS[d.getMonth()]; 383 } 384 385 const api = { 386 mount(container) { 387 if (!isOs2045()) return null; 388 const el = build(); 389 const desktop = container || document.getElementById('os2045Desktop'); 390 if (desktop && !desktop.contains(el)) desktop.appendChild(el); 391 else if (!desktop && !document.body.contains(el)) document.body.appendChild(el); 392 // Subscribe to WM 393 if (window.OS2045WM && typeof window.OS2045WM.onChange === 'function') { 394 if (typeof unsubWM === 'function') unsubWM(); 395 unsubWM = window.OS2045WM.onChange(render); 396 } 397 render(); 398 refreshClock(); 399 if (clockTimer) clearInterval(clockTimer); 400 clockTimer = setInterval(refreshClock, 30000); 401 return el; 402 }, 403 update: render, 404 refreshClock, 405 getElement() { return taskbarEl; }, 406 pin(appId, opts) { 407 opts = opts || {}; 408 const list = pinned || defaultPinned(); 409 if (!list.find(p => p.id === appId)) { 410 list.push({ id: appId, title: opts.title || appId, icon: opts.icon || filesIcon() }); 411 pinned = list; 412 savePinned(); 413 render(); 414 } 415 }, 416 unpin(appId) { 417 const list = pinned || defaultPinned(); 418 pinned = list.filter(p => p.id !== appId); 419 savePinned(); 420 render(); 421 } 422 }; 423 424 window.OS2045Taskbar = api; 425})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.