personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Remove dev tools app and update documentation references

Delete apps/dev/ and its test fixture. Replace all documentation
references in docs/APPS.md with real production app examples
(support, entities, stats). Remove dev smoke test entry.

+5 -806
-4
apps/dev/app.json
··· 1 - { 2 - "icon": "🛠️", 3 - "label": "Dev Tools" 4 - }
-48
apps/dev/background.html
··· 1 - // Dev App Background Service 2 - // Scratch playground for testing submenu features 3 - 4 - AppServices.register('dev', { 5 - initialize() { 6 - // Test submenu with various item types 7 - AppServices.submenus.set('dev', [ 8 - { 9 - id: 'workspace', 10 - label: 'Workspace', 11 - icon: '🏠', 12 - href: '/app/dev' 13 - }, 14 - { 15 - id: 'notifications', 16 - label: 'Test Notifications', 17 - icon: '🔔', 18 - href: '/app/dev#notifications', 19 - badge: 3 20 - }, 21 - { 22 - id: 'websocket', 23 - label: 'WebSocket Events', 24 - icon: '📡', 25 - href: '/app/dev#websocket' 26 - }, 27 - { 28 - id: 'facet-test', 29 - label: 'With Facet', 30 - icon: '🎯', 31 - href: '/app/dev#facet', 32 - facet: 'work' 33 - } 34 - ]); 35 - 36 - // Demo: Update badge dynamically every 5 seconds 37 - let badgeCount = 3; 38 - setInterval(() => { 39 - badgeCount = (badgeCount % 10) + 1; 40 - AppServices.submenus.upsert('dev', { 41 - id: 'notifications', 42 - badge: badgeCount 43 - }); 44 - }, 5000); 45 - 46 - console.log('[Dev] Background service initialized with test submenu'); 47 - } 48 - });
-24
apps/dev/events.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Dev app event handlers - debug logging for all Callosum events. 5 - 6 - This module demonstrates server-side event handling for apps. 7 - Enable verbose logging to see events in the Convey log. 8 - """ 9 - 10 - import logging 11 - 12 - from apps.events import EventContext, on_event 13 - 14 - logger = logging.getLogger(__name__) 15 - 16 - 17 - @on_event("*", "*") 18 - def log_all_events(ctx: EventContext) -> None: 19 - """Log all Callosum events for debugging. 20 - 21 - This handler matches all events via wildcards and logs them at DEBUG level. 22 - Useful for understanding event flow during development. 23 - """ 24 - logger.debug(f"[dev] Event: {ctx.tract}/{ctx.event} - keys: {list(ctx.msg.keys())}")
-36
apps/dev/maint/000_example.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - """Example maintenance task demonstrating the maint interface. 5 - 6 - This no-op task shows the recommended patterns for maintenance scripts: 7 - - Proper docstring (first line used as description in `sol maint --list`) 8 - - Using setup_cli for consistent argument parsing and logging 9 - - Progress output to stdout (captured in state file) 10 - - Clean exit with appropriate exit code 11 - """ 12 - 13 - import argparse 14 - import logging 15 - 16 - from think.utils import setup_cli 17 - 18 - logger = logging.getLogger(__name__) 19 - 20 - 21 - def main(): 22 - parser = argparse.ArgumentParser(description=__doc__.split("\n")[0]) 23 - setup_cli(parser) 24 - 25 - logger.info("Example maintenance task starting") 26 - print("This is an example maintenance task.") 27 - print("Real tasks would process journal data here.") 28 - print("Use numeric prefixes (000_, 001_) to control execution order.") 29 - logger.info("Example maintenance task complete") 30 - 31 - # Exit code 0 = success, non-zero = failure 32 - # No explicit sys.exit(0) needed - implicit on clean return 33 - 34 - 35 - if __name__ == "__main__": 36 - main()
-673
apps/dev/workspace.html
··· 1 - <style> 2 - .dev-container { 3 - max-width: 1200px; 4 - margin: 0 auto; 5 - padding: 2rem; 6 - } 7 - 8 - .dev-section { 9 - background: white; 10 - border: 1px solid #e5e7eb; 11 - border-radius: 8px; 12 - padding: 1.5rem; 13 - margin-bottom: 1.5rem; 14 - } 15 - 16 - .dev-section h2 { 17 - margin: 0 0 1rem 0; 18 - font-size: 1.25rem; 19 - font-weight: 600; 20 - color: #1f2937; 21 - } 22 - 23 - .dev-section p { 24 - margin: 0 0 1rem 0; 25 - color: #6b7280; 26 - font-size: 0.9rem; 27 - } 28 - 29 - .dev-controls { 30 - display: flex; 31 - flex-wrap: wrap; 32 - gap: 0.75rem; 33 - } 34 - 35 - .dev-button { 36 - padding: 0.5rem 1rem; 37 - background: #667eea; 38 - color: white; 39 - border: none; 40 - border-radius: 6px; 41 - cursor: pointer; 42 - font-size: 0.9rem; 43 - font-weight: 500; 44 - transition: background 0.2s, transform 0.1s; 45 - } 46 - 47 - .dev-button:hover { 48 - background: #5568d3; 49 - transform: translateY(-1px); 50 - } 51 - 52 - .dev-button:active { 53 - transform: translateY(0); 54 - } 55 - 56 - .dev-button.secondary { 57 - background: #6b7280; 58 - } 59 - 60 - .dev-button.secondary:hover { 61 - background: #4b5563; 62 - } 63 - 64 - .dev-button.danger { 65 - background: #ef4444; 66 - } 67 - 68 - .dev-button.danger:hover { 69 - background: #dc2626; 70 - } 71 - 72 - .dev-form { 73 - display: flex; 74 - flex-direction: column; 75 - gap: 1rem; 76 - max-width: 500px; 77 - } 78 - 79 - .dev-form-row { 80 - display: flex; 81 - flex-direction: column; 82 - gap: 0.25rem; 83 - } 84 - 85 - .dev-form-row label { 86 - font-size: 0.85rem; 87 - font-weight: 500; 88 - color: #374151; 89 - } 90 - 91 - .dev-form-row input, 92 - .dev-form-row textarea, 93 - .dev-form-row select { 94 - padding: 0.5rem; 95 - border: 1px solid #d1d5db; 96 - border-radius: 4px; 97 - font-size: 0.9rem; 98 - } 99 - 100 - .dev-form-row textarea { 101 - resize: vertical; 102 - min-height: 60px; 103 - } 104 - 105 - .dev-output { 106 - background: #f9fafb; 107 - border: 1px solid #e5e7eb; 108 - border-radius: 4px; 109 - padding: 1rem; 110 - font-family: monospace; 111 - font-size: 0.85rem; 112 - color: #374151; 113 - max-height: 200px; 114 - overflow-y: auto; 115 - } 116 - 117 - .dev-stats { 118 - display: grid; 119 - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 120 - gap: 1rem; 121 - margin-top: 1rem; 122 - } 123 - 124 - .dev-stat { 125 - background: #f9fafb; 126 - border: 1px solid #e5e7eb; 127 - border-radius: 4px; 128 - padding: 1rem; 129 - text-align: center; 130 - } 131 - 132 - .dev-stat-value { 133 - font-size: 2rem; 134 - font-weight: 600; 135 - color: #667eea; 136 - } 137 - 138 - .dev-stat-label { 139 - font-size: 0.75rem; 140 - color: #6b7280; 141 - margin-top: 0.25rem; 142 - } 143 - 144 - /* Events Card */ 145 - .events-card { 146 - background: white; 147 - border-radius: 12px; 148 - padding: 1.5em; 149 - box-shadow: 0 2px 8px rgba(0,0,0,0.1); 150 - } 151 - 152 - .events-card.paused { 153 - border-left: 4px solid #f59e0b; 154 - } 155 - 156 - .events-header { 157 - display: flex; 158 - justify-content: space-between; 159 - align-items: center; 160 - margin-bottom: 1em; 161 - } 162 - 163 - .events-title { 164 - font-size: 1.1em; 165 - font-weight: 600; 166 - } 167 - 168 - .events-controls { 169 - display: flex; 170 - align-items: center; 171 - gap: 1em; 172 - } 173 - 174 - .pause-btn { 175 - background: #3b82f6; 176 - color: white; 177 - border: none; 178 - padding: 0.5em 1em; 179 - border-radius: 6px; 180 - cursor: pointer; 181 - font-size: 0.85em; 182 - font-weight: 500; 183 - display: flex; 184 - align-items: center; 185 - gap: 0.4em; 186 - transition: background 0.2s; 187 - } 188 - 189 - .pause-btn:hover { 190 - background: #2563eb; 191 - } 192 - 193 - .pause-btn.paused { 194 - background: #f59e0b; 195 - } 196 - 197 - .pause-btn.paused:hover { 198 - background: #d97706; 199 - } 200 - 201 - .missed-count { 202 - font-size: 0.85em; 203 - color: #f59e0b; 204 - font-weight: 500; 205 - } 206 - 207 - .event-log { 208 - height: 400px; 209 - overflow-y: auto; 210 - font-family: 'Monaco', 'Menlo', 'Consolas', monospace; 211 - font-size: 12px; 212 - border: 1px solid #e5e7eb; 213 - border-radius: 6px; 214 - padding: 0.5em; 215 - background: #f9fafb; 216 - } 217 - 218 - .event-line { 219 - white-space: nowrap; 220 - overflow: hidden; 221 - text-overflow: ellipsis; 222 - padding: 3px 0; 223 - border-bottom: 1px solid #f3f4f6; 224 - color: #374151; 225 - } 226 - 227 - .event-line:last-child { 228 - border-bottom: none; 229 - } 230 - 231 - .event-line.error { 232 - color: #dc2626; 233 - background: #fef2f2; 234 - } 235 - 236 - .event-log:empty::before { 237 - content: 'Waiting for events...'; 238 - color: #9ca3af; 239 - font-style: italic; 240 - } 241 - </style> 242 - 243 - <div class="dev-container"> 244 - <h1>🛠️ Dev Tools</h1> 245 - 246 - <!-- Quick Tests --> 247 - <div class="dev-section"> 248 - <h2>Quick Tests</h2> 249 - <p>Click buttons to trigger different notification types</p> 250 - <div class="dev-controls"> 251 - <button class="dev-button" onclick="testBasic()">Basic Notification</button> 252 - <button class="dev-button" onclick="testWithBadge()">With Badge (5)</button> 253 - <button class="dev-button" onclick="testAutoDismiss()">Auto-Dismiss (5s)</button> 254 - <button class="dev-button" onclick="testNonDismissible()">Non-Dismissible</button> 255 - <button class="dev-button" onclick="testLongMessage()">Long Message</button> 256 - <button class="dev-button" onclick="testNoAction()">No Click Action</button> 257 - <button class="dev-button" onclick="testWithFacet()">With Facet (work)</button> 258 - </div> 259 - </div> 260 - 261 - <!-- Custom Notification --> 262 - <div class="dev-section"> 263 - <h2>Custom Notification</h2> 264 - <p>Build your own notification with custom parameters</p> 265 - <div class="dev-form"> 266 - <div class="dev-form-row"> 267 - <label>App Name</label> 268 - <input type="text" id="custom-app" value="dev" /> 269 - </div> 270 - <div class="dev-form-row"> 271 - <label>Icon (emoji)</label> 272 - <input type="text" id="custom-icon" value="🛠️" maxlength="2" /> 273 - </div> 274 - <div class="dev-form-row"> 275 - <label>Title</label> 276 - <input type="text" id="custom-title" value="Custom Notification" /> 277 - </div> 278 - <div class="dev-form-row"> 279 - <label>Message</label> 280 - <textarea id="custom-message">This is a custom notification message</textarea> 281 - </div> 282 - <div class="dev-form-row"> 283 - <label>Action URL</label> 284 - <input type="text" id="custom-action" value="/app/dev" /> 285 - </div> 286 - <div class="dev-form-row"> 287 - <label>Facet (optional)</label> 288 - <select id="custom-facet"> 289 - <option value="">None</option> 290 - </select> 291 - </div> 292 - <div class="dev-form-row"> 293 - <label>Badge Count (0 = none)</label> 294 - <input type="number" id="custom-badge" value="0" min="0" /> 295 - </div> 296 - <div class="dev-form-row"> 297 - <label>Auto-Dismiss (ms, 0 = never)</label> 298 - <input type="number" id="custom-autodismiss" value="0" min="0" step="1000" /> 299 - </div> 300 - <div class="dev-form-row"> 301 - <label> 302 - <input type="checkbox" id="custom-dismissible" checked /> Dismissible (show X button) 303 - </label> 304 - </div> 305 - <button class="dev-button" onclick="testCustom()">Show Custom Notification</button> 306 - </div> 307 - </div> 308 - 309 - <!-- Notification Management --> 310 - <div class="dev-section"> 311 - <h2>Notification Management</h2> 312 - <p>Test dismissing and managing notifications</p> 313 - <div class="dev-controls"> 314 - <button class="dev-button secondary" onclick="updateLast()">Update Last Notification</button> 315 - <button class="dev-button secondary" onclick="dismissLast()">Dismiss Last</button> 316 - <button class="dev-button danger" onclick="dismissAll()">Dismiss All</button> 317 - <button class="dev-button secondary" onclick="showStats()">Show Stats</button> 318 - </div> 319 - <div class="dev-stats" id="stats"></div> 320 - </div> 321 - 322 - <!-- Stress Test --> 323 - <div class="dev-section"> 324 - <h2>Stress Test</h2> 325 - <p>Generate multiple notifications to test stacking and performance</p> 326 - <div class="dev-controls"> 327 - <button class="dev-button" onclick="spawnThree()">Spawn 3 Notifications</button> 328 - <button class="dev-button" onclick="spawnTen()">Spawn 10 Notifications</button> 329 - <button class="dev-button" onclick="spawnSequential()">Spawn Sequential (5s intervals)</button> 330 - </div> 331 - </div> 332 - 333 - <!-- Console Log --> 334 - <div class="dev-section"> 335 - <h2>Console Log</h2> 336 - <p>Notification IDs and events</p> 337 - <div class="dev-output" id="console"></div> 338 - </div> 339 - 340 - <!-- Callosum Event Viewer --> 341 - <div class="dev-section"> 342 - <h2>Callosum Event Viewer</h2> 343 - <p>Raw callosum event stream with pause/resume</p> 344 - <div class="events-card" id="eventsCard"> 345 - <div class="events-header"> 346 - <div class="events-title">EVENTS</div> 347 - <div class="events-controls"> 348 - <span class="missed-count" id="missedCount" style="display: none;"></span> 349 - <button class="pause-btn" id="pauseBtn"> 350 - <span>||</span> 351 - <span>Pause</span> 352 - </button> 353 - </div> 354 - </div> 355 - <div id="eventLog" class="event-log"></div> 356 - </div> 357 - </div> 358 - </div> 359 - 360 - <script> 361 - let lastNotificationId = null; 362 - let sequentialTimer = null; 363 - 364 - // Helper to log to console 365 - function log(message) { 366 - const consoleEl = document.getElementById('console'); 367 - const timestamp = new Date().toLocaleTimeString(); 368 - consoleEl.innerHTML = `[${timestamp}] ${message}<br>` + consoleEl.innerHTML; 369 - } 370 - 371 - // Quick Tests 372 - function testBasic() { 373 - const id = window.AppServices.notifications.show({ 374 - app: 'dev', 375 - icon: '🛠️', 376 - title: 'Basic Notification', 377 - message: 'This is a basic notification with all default settings', 378 - action: '/app/dev' 379 - }); 380 - lastNotificationId = id; 381 - log(`Created basic notification (ID: ${id})`); 382 - } 383 - 384 - function testWithBadge() { 385 - const id = window.AppServices.notifications.show({ 386 - app: 'dev', 387 - icon: '🔔', 388 - title: 'Notification with Badge', 389 - message: 'This notification has a badge count', 390 - badge: 5, 391 - action: '/app/dev' 392 - }); 393 - lastNotificationId = id; 394 - log(`Created notification with badge (ID: ${id})`); 395 - } 396 - 397 - function testAutoDismiss() { 398 - const id = window.AppServices.notifications.show({ 399 - app: 'dev', 400 - icon: '⏱️', 401 - title: 'Auto-Dismiss Test', 402 - message: 'This notification will disappear after 5 seconds', 403 - action: '/app/dev', 404 - autoDismiss: 5000 405 - }); 406 - lastNotificationId = id; 407 - log(`Created auto-dismiss notification (ID: ${id})`); 408 - } 409 - 410 - function testNonDismissible() { 411 - const id = window.AppServices.notifications.show({ 412 - app: 'dev', 413 - icon: '🔒', 414 - title: 'Non-Dismissible', 415 - message: 'This notification has no X button', 416 - dismissible: false, 417 - action: '/app/dev' 418 - }); 419 - lastNotificationId = id; 420 - log(`Created non-dismissible notification (ID: ${id})`); 421 - } 422 - 423 - function testLongMessage() { 424 - const id = window.AppServices.notifications.show({ 425 - app: 'dev', 426 - icon: '📝', 427 - title: 'Long Message Test', 428 - message: 'This is a very long notification message that should be truncated with ellipsis after two lines to prevent the card from becoming too tall and taking up too much space in the notification center.', 429 - action: '/app/dev' 430 - }); 431 - lastNotificationId = id; 432 - log(`Created long message notification (ID: ${id})`); 433 - } 434 - 435 - function testNoAction() { 436 - const id = window.AppServices.notifications.show({ 437 - app: 'dev', 438 - icon: '🚫', 439 - title: 'No Click Action', 440 - message: 'Clicking this card does nothing (no action URL)', 441 - action: null 442 - }); 443 - lastNotificationId = id; 444 - log(`Created no-action notification (ID: ${id})`); 445 - } 446 - 447 - function testWithFacet() { 448 - const id = window.AppServices.notifications.show({ 449 - app: 'dev', 450 - icon: '🎯', 451 - title: 'Facet Navigation Test', 452 - message: 'Clicking this will navigate to /app/home and select "work" facet', 453 - action: '/app/home', 454 - facet: 'work' 455 - }); 456 - lastNotificationId = id; 457 - log(`Created facet notification (ID: ${id})`); 458 - } 459 - 460 - // Custom Notification 461 - function testCustom() { 462 - const app = document.getElementById('custom-app').value; 463 - const icon = document.getElementById('custom-icon').value; 464 - const title = document.getElementById('custom-title').value; 465 - const message = document.getElementById('custom-message').value; 466 - const action = document.getElementById('custom-action').value || null; 467 - const facet = document.getElementById('custom-facet').value || null; 468 - const badge = parseInt(document.getElementById('custom-badge').value) || null; 469 - const autoDismiss = parseInt(document.getElementById('custom-autodismiss').value) || null; 470 - const dismissible = document.getElementById('custom-dismissible').checked; 471 - 472 - const id = window.AppServices.notifications.show({ 473 - app, 474 - icon, 475 - title, 476 - message, 477 - action, 478 - facet, 479 - badge: badge > 0 ? badge : null, 480 - autoDismiss: autoDismiss > 0 ? autoDismiss : null, 481 - dismissible 482 - }); 483 - lastNotificationId = id; 484 - log(`Created custom notification (ID: ${id})`); 485 - } 486 - 487 - // Management 488 - function updateLast() { 489 - if (!lastNotificationId) { 490 - log('No notification to update'); 491 - return; 492 - } 493 - 494 - window.AppServices.notifications.update(lastNotificationId, { 495 - title: 'Updated Notification', 496 - message: 'This notification was updated!', 497 - badge: 99 498 - }); 499 - log(`Updated notification (ID: ${lastNotificationId})`); 500 - } 501 - 502 - function dismissLast() { 503 - if (!lastNotificationId) { 504 - log('No notification to dismiss'); 505 - return; 506 - } 507 - 508 - window.AppServices.notifications.dismiss(lastNotificationId); 509 - log(`Dismissed notification (ID: ${lastNotificationId})`); 510 - lastNotificationId = null; 511 - } 512 - 513 - function dismissAll() { 514 - window.AppServices.notifications.dismissAll(); 515 - log('Dismissed all notifications'); 516 - lastNotificationId = null; 517 - } 518 - 519 - function showStats() { 520 - const count = window.AppServices.notifications.count(); 521 - const statsEl = document.getElementById('stats'); 522 - statsEl.innerHTML = ` 523 - <div class="dev-stat"> 524 - <div class="dev-stat-value">${count}</div> 525 - <div class="dev-stat-label">active notifications</div> 526 - </div> 527 - <div class="dev-stat"> 528 - <div class="dev-stat-value">${lastNotificationId || 'N/A'}</div> 529 - <div class="dev-stat-label">last id</div> 530 - </div> 531 - `; 532 - log(`Stats: ${count} active notifications`); 533 - } 534 - 535 - // Stress Tests 536 - function spawnThree() { 537 - for (let i = 1; i <= 3; i++) { 538 - setTimeout(() => { 539 - const id = window.AppServices.notifications.show({ 540 - app: 'dev', 541 - icon: '🚀', 542 - title: `Notification ${i}/3`, 543 - message: `Testing notification stacking (${i} of 3)`, 544 - action: '/app/dev', 545 - badge: i 546 - }); 547 - log(`Spawned notification ${i}/3 (ID: ${id})`); 548 - }, (i - 1) * 300); 549 - } 550 - } 551 - 552 - function spawnTen() { 553 - for (let i = 1; i <= 10; i++) { 554 - setTimeout(() => { 555 - const id = window.AppServices.notifications.show({ 556 - app: 'dev', 557 - icon: '💥', 558 - title: `Batch ${i}/10`, 559 - message: `Testing max visible limit (only last 5 should show)`, 560 - action: '/app/dev', 561 - badge: i 562 - }); 563 - log(`Spawned notification ${i}/10 (ID: ${id})`); 564 - }, (i - 1) * 200); 565 - } 566 - } 567 - 568 - function spawnSequential() { 569 - if (sequentialTimer) { 570 - clearInterval(sequentialTimer); 571 - sequentialTimer = null; 572 - log('Stopped sequential spawn'); 573 - return; 574 - } 575 - 576 - let count = 1; 577 - sequentialTimer = setInterval(() => { 578 - const id = window.AppServices.notifications.show({ 579 - app: 'dev', 580 - icon: '⏰', 581 - title: `Sequential #${count}`, 582 - message: 'New notification every 5 seconds', 583 - action: '/app/dev', 584 - autoDismiss: 8000 585 - }); 586 - log(`Sequential spawn #${count} (ID: ${id})`); 587 - count++; 588 - 589 - if (count > 10) { 590 - clearInterval(sequentialTimer); 591 - sequentialTimer = null; 592 - log('Sequential spawn complete'); 593 - } 594 - }, 5000); 595 - 596 - log('Started sequential spawn (10 notifications, 5s intervals)'); 597 - } 598 - 599 - // Populate facet dropdown from window.facetsData 600 - function populateFacetDropdown() { 601 - const facetSelect = document.getElementById('custom-facet'); 602 - if (!facetSelect || !window.facetsData) return; 603 - 604 - // Clear existing options except "None" 605 - facetSelect.innerHTML = '<option value="">None</option>'; 606 - 607 - // Add facet options 608 - window.facetsData.forEach(facet => { 609 - const option = document.createElement('option'); 610 - option.value = facet.name; 611 - option.textContent = `${facet.emoji} ${facet.title}`; 612 - facetSelect.appendChild(option); 613 - }); 614 - } 615 - 616 - // Callosum Event Viewer 617 - (function() { 618 - const eventLog = document.getElementById('eventLog'); 619 - const eventsCard = document.getElementById('eventsCard'); 620 - const pauseBtn = document.getElementById('pauseBtn'); 621 - const missedCountEl = document.getElementById('missedCount'); 622 - let paused = false; 623 - let missed = 0; 624 - 625 - function togglePause() { 626 - paused = !paused; 627 - if (paused) { 628 - pauseBtn.classList.add('paused'); 629 - pauseBtn.innerHTML = '<span>|></span><span>Resume</span>'; 630 - eventsCard.classList.add('paused'); 631 - missed = 0; 632 - missedCountEl.style.display = 'none'; 633 - } else { 634 - pauseBtn.classList.remove('paused'); 635 - pauseBtn.innerHTML = '<span>||</span><span>Pause</span>'; 636 - eventsCard.classList.remove('paused'); 637 - missed = 0; 638 - missedCountEl.style.display = 'none'; 639 - } 640 - } 641 - 642 - function appendEvent(msg) { 643 - if (paused) { 644 - missed++; 645 - missedCountEl.textContent = missed + ' missed'; 646 - missedCountEl.style.display = 'inline'; 647 - return; 648 - } 649 - 650 - const line = document.createElement('div'); 651 - line.className = 'event-line'; 652 - if (msg.event === 'error' || (msg.event === 'exit' && msg.exit_code !== 0)) { 653 - line.className = 'event-line error'; 654 - } 655 - line.textContent = JSON.stringify(msg); 656 - eventLog.appendChild(line); 657 - eventLog.scrollTop = eventLog.scrollHeight; 658 - 659 - while (eventLog.children.length > 100) { 660 - eventLog.removeChild(eventLog.firstChild); 661 - } 662 - } 663 - 664 - pauseBtn.addEventListener('click', togglePause); 665 - if (window.appEvents) { 666 - window.appEvents.listen('*', appendEvent); 667 - } 668 - })(); 669 - 670 - // Initial log and setup 671 - populateFacetDropdown(); 672 - log('Dev Tools loaded - ready to test notifications'); 673 - </script>
+5 -7
docs/APPS.md
··· 105 105 106 106 **Reference implementations:** 107 107 - Minimal: `apps/home/workspace.html` (simple content) 108 - - Styled: `apps/dev/workspace.html` (custom CSS, forms, interactive JS) 108 + - Styled: `apps/support/workspace.html` (custom CSS, forms, interactive JS) 109 109 - Data-driven: `apps/todos/workspace.html` (facet sections, dynamic rendering) 110 110 111 111 --- ··· 230 230 231 231 **Reference implementations:** 232 232 - `apps/todos/background.html` - App icon badge with API fetch 233 - - `apps/dev/background.html` - Submenu quick-links with dynamic badges 234 - 235 233 **Implementation source:** `convey/static/app.js` - AppServices framework, `convey/static/websocket.js` - WebSocket API 236 234 237 235 --- ··· 478 476 **CLI:** `sol maint` (run pending), `sol maint --list` (show status), `sol maint --force` (re-run all) 479 477 480 478 **Reference implementations:** 481 - - Example task: `apps/dev/maint/000_example.py` - recommended patterns 479 + - Example task: `apps/entities/maint/001_migrate_to_journal_entities.py` - real migration task demonstrating maint patterns 482 480 - Discovery logic: `convey/maint.py` - `discover_tasks()`, `run_task()` 483 481 484 482 --- ··· 530 528 531 529 **Reference implementations:** 532 530 - Framework: `apps/events.py` - `EventContext` dataclass, decorator, discovery 533 - - Example: `apps/dev/events.py` - Debug handler showing usage pattern 531 + - Example: `apps/entities/events.py` - Entity activity tracking via event handlers 534 532 535 533 --- 536 534 ··· 726 724 727 725 **Best practice:** Scope styles with unique class prefix to avoid conflicts. 728 726 729 - **Example:** `apps/dev/workspace.html` shows scoped `.dev-*` classes for all custom styles in its `<style>` block. 727 + **Example:** `apps/stats/workspace.html` shows scoped `.stats-*` classes for all custom styles in its `<style>` block. 730 728 731 729 ### Global Styles 732 730 ··· 810 808 Browse `apps/*/` directories for reference implementations. Apps range in complexity: 811 809 812 810 - **Minimal** - Just `workspace.html` (e.g., `apps/home/`, `apps/health/`) 813 - - **Styled** - Custom CSS, background services (e.g., `apps/dev/`) 811 + - **Styled** - Custom CSS, background services (e.g., `apps/support/`) 814 812 - **Full-featured** - Routes, forms, AJAX, badges, tools (e.g., `apps/todos/`, `apps/entities/`) 815 813 816 814 ---
-5
tests/fixtures/journal/maint/dev/000_example.jsonl
··· 1 - {"event": "exec", "ts": 1770778598171, "app": "dev", "task": "000_example", "cmd": ["/home/jer/projects/sunstone/.venv/bin/python3", "-m", "apps.dev.maint.000_example"]} 2 - {"event": "line", "ts": 1770778598293, "line": "This is an example maintenance task."} 3 - {"event": "line", "ts": 1770778598293, "line": "Real tasks would process journal data here."} 4 - {"event": "line", "ts": 1770778598293, "line": "Use numeric prefixes (000_, 001_) to control execution order."} 5 - {"event": "exit", "ts": 1770778598315, "exit_code": 0, "duration_ms": 143}
-9
tests/verify_browser.py
··· 87 87 ], 88 88 }, 89 89 { 90 - "app": "dev", 91 - "name": "smoke", 92 - "steps": [ 93 - {"do": "navigate", "path": "/app/dev"}, 94 - {"do": "wait", "ms": 1000}, 95 - {"do": "screenshot"}, 96 - ], 97 - }, 98 - { 99 90 "app": "entities", 100 91 "name": "smoke", 101 92 "steps": [