pixelcrew 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -63,13 +63,13 @@ No global installations or complex configuration required. Run directly in your
63
63
 
64
64
  ```bash
65
65
  # 1. Initialize and adapt PixelCrew to your project stack
66
- npx github:hiroqt/PixelCrew init
66
+ npx pixelcrew init
67
67
 
68
68
  # 2. Launch the orchestrator daemon and real-time visual office
69
- npx github:hiroqt/PixelCrew start
69
+ npx pixelcrew start
70
70
 
71
71
  # 3. (Optional) Run an instant multi-agent simulated sprint demo
72
- npx github:hiroqt/PixelCrew demo
72
+ npx pixelcrew demo
73
73
  ```
74
74
 
75
75
  The interactive dashboard opens automatically at:
@@ -240,15 +240,15 @@ Copy the `.agents/skills/` directory into your tool's native skills folder:
240
240
 
241
241
  | Command | Action | Example |
242
242
  | :--- | :--- | :--- |
243
- | `npx github:hiroqt/PixelCrew init` | Scaffolds `.pixel-agents/` and adapts to repository | `npx github:hiroqt/PixelCrew init --yes` |
244
- | `npx github:hiroqt/PixelCrew start` | Launches orchestrator, SSE stream, and web dashboard | `npx github:hiroqt/PixelCrew start --port 4747` |
245
- | `npx github:hiroqt/PixelCrew analyze` | Prints detected frameworks, database, ORM, and test stack | `npx github:hiroqt/PixelCrew analyze` |
246
- | `npx github:hiroqt/PixelCrew demo` | Launches an interactive 6-agent simulated sprint | `npx github:hiroqt/PixelCrew demo` |
247
- | `npx github:hiroqt/PixelCrew task "<text>"` | Dispatches a direct objective to the running agent swarm | `npx github:hiroqt/PixelCrew task "Optimize SQL queries"` |
248
- | `npx github:hiroqt/PixelCrew emit [flags]` | Emits a custom telemetry event into the stream | `npx github:hiroqt/PixelCrew emit --agent db --message "Done"` |
249
- | `npx github:hiroqt/PixelCrew dashboard` | Opens or serves the standalone Floor 42 dashboard UI | `npx github:hiroqt/PixelCrew dashboard` |
250
- | `npx github:hiroqt/PixelCrew status` | Prints ASCII summary of swarm states and active sprints | `npx github:hiroqt/PixelCrew status` |
251
- | `npx github:hiroqt/PixelCrew help` | Displays full CLI manual and flag details | `npx github:hiroqt/PixelCrew help` |
243
+ | `npx pixelcrew init` | Scaffolds `.pixel-agents/` and adapts to repository | `npx pixelcrew init --yes` |
244
+ | `npx pixelcrew start` | Launches orchestrator, SSE stream, and web dashboard | `npx pixelcrew start --port 4747` |
245
+ | `npx pixelcrew analyze` | Prints detected frameworks, database, ORM, and test stack | `npx pixelcrew analyze` |
246
+ | `npx pixelcrew demo` | Launches an interactive 6-agent simulated sprint | `npx pixelcrew demo` |
247
+ | `npx pixelcrew task "<text>"` | Dispatches a direct objective to the running agent swarm | `npx pixelcrew task "Optimize SQL queries"` |
248
+ | `npx pixelcrew emit [flags]` | Emits a custom telemetry event into the stream | `npx pixelcrew emit --agent db --message "Done"` |
249
+ | `npx pixelcrew dashboard` | Opens or serves the standalone Floor 42 dashboard UI | `npx pixelcrew dashboard` |
250
+ | `npx pixelcrew status` | Prints ASCII summary of swarm states and active sprints | `npx pixelcrew status` |
251
+ | `npx pixelcrew help` | Displays full CLI manual and flag details | `npx pixelcrew help` |
252
252
 
253
253
  ### CLI Options & Flags:
254
254
  - `--port <number>`: Dashboard port (default: `4747`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelcrew",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Local multi-agent orchestration framework with an interactive pixel-art tech startup office visual dashboard",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -574,6 +574,7 @@ async function initApp() {
574
574
  appState.state = stRes;
575
575
  renderState();
576
576
  }
577
+ await fetchReports();
577
578
  } catch (e) {
578
579
  // Handled by SSE
579
580
  }
@@ -650,16 +651,59 @@ function setupEventListeners() {
650
651
  });
651
652
  });
652
653
 
653
- // Modal close
654
- document.getElementById('btnCloseModal').addEventListener('click', () => {
655
- document.getElementById('agentModal').classList.remove('open');
656
- });
654
+ // Reports Drawer Buttons & Controls
655
+ const btnReportsToggle = document.getElementById('btnReportsToggle');
656
+ if (btnReportsToggle) {
657
+ btnReportsToggle.addEventListener('click', () => {
658
+ synth.playClick();
659
+ openReportsModal();
660
+ });
661
+ }
657
662
 
658
- document.getElementById('agentModal').addEventListener('click', (e) => {
659
- if (e.target === document.getElementById('agentModal')) {
660
- document.getElementById('agentModal').classList.remove('open');
661
- }
662
- });
663
+ const btnCloseReportsModal = document.getElementById('btnCloseReportsModal');
664
+ if (btnCloseReportsModal) {
665
+ btnCloseReportsModal.addEventListener('click', () => {
666
+ closeReportsModal();
667
+ });
668
+ }
669
+
670
+ const reportsModalEl = document.getElementById('reportsModal');
671
+ if (reportsModalEl) {
672
+ reportsModalEl.addEventListener('click', (e) => {
673
+ if (e.target === reportsModalEl) {
674
+ closeReportsModal();
675
+ }
676
+ });
677
+ }
678
+
679
+ const btnCopyReportMd = document.getElementById('btnCopyReportMd');
680
+ if (btnCopyReportMd) {
681
+ btnCopyReportMd.addEventListener('click', () => {
682
+ copyActiveReportMarkdown();
683
+ });
684
+ }
685
+
686
+ const btnDownloadReport = document.getElementById('btnDownloadReport');
687
+ if (btnDownloadReport) {
688
+ btnDownloadReport.addEventListener('click', () => {
689
+ downloadActiveReport();
690
+ });
691
+ }
692
+
693
+ const btnToggleRawView = document.getElementById('btnToggleRawView');
694
+ if (btnToggleRawView) {
695
+ btnToggleRawView.addEventListener('click', () => {
696
+ toggleRawReportView();
697
+ });
698
+ }
699
+
700
+ const reportsSearchInput = document.getElementById('reportsSearch');
701
+ if (reportsSearchInput) {
702
+ reportsSearchInput.addEventListener('input', (e) => {
703
+ reportsState.searchQuery = e.target.value.toLowerCase().trim();
704
+ renderReportsList();
705
+ });
706
+ }
663
707
 
664
708
  // Global Keyboard Shortcuts
665
709
  window.addEventListener('keydown', (e) => {
@@ -669,6 +713,14 @@ function setupEventListeners() {
669
713
  document.getElementById('btnDemo').click();
670
714
  } else if (e.key === 'Escape') {
671
715
  document.getElementById('agentModal').classList.remove('open');
716
+ closeReportsModal();
717
+ } else if (e.key === 'r' || e.key === 'R') {
718
+ const modal = document.getElementById('reportsModal');
719
+ if (modal && modal.classList.contains('open')) {
720
+ closeReportsModal();
721
+ } else {
722
+ openReportsModal();
723
+ }
672
724
  } else if (['1', '2', '3', '4', '5', '6'].includes(e.key)) {
673
725
  const keys = ['frontend', 'backend', 'database', 'security', 'performance', 'qa'];
674
726
  const agentKey = keys[parseInt(e.key) - 1];
@@ -677,6 +729,357 @@ function setupEventListeners() {
677
729
  });
678
730
  }
679
731
 
732
+ // Reports State & Controller
733
+ const reportsState = {
734
+ reports: [],
735
+ activeReport: null,
736
+ searchQuery: '',
737
+ rawMode: false
738
+ };
739
+
740
+ async function fetchReports() {
741
+ try {
742
+ const res = await fetch('/api/reports');
743
+ if (res.ok) {
744
+ const data = await res.json();
745
+ reportsState.reports = data.reports || [];
746
+ const badge = document.getElementById('reportsCountBadge');
747
+ if (badge) {
748
+ badge.textContent = reportsState.reports.length;
749
+ }
750
+ return reportsState.reports;
751
+ }
752
+ } catch (err) {
753
+ console.error('Error loading reports:', err);
754
+ }
755
+ return [];
756
+ }
757
+
758
+ async function openReportsModal(preferredId = null) {
759
+ const modal = document.getElementById('reportsModal');
760
+ if (!modal) return;
761
+
762
+ await fetchReports();
763
+
764
+ if (preferredId) {
765
+ reportsState.activeReport = reportsState.reports.find(r => r.id === preferredId) || reportsState.reports[0] || null;
766
+ } else if (!reportsState.activeReport && reportsState.reports.length > 0) {
767
+ reportsState.activeReport = reportsState.reports[0];
768
+ }
769
+
770
+ renderReportsList();
771
+ renderActiveReport();
772
+
773
+ modal.classList.add('open');
774
+ synth.playClick();
775
+ }
776
+
777
+ function closeReportsModal() {
778
+ const modal = document.getElementById('reportsModal');
779
+ if (modal) modal.classList.remove('open');
780
+ }
781
+
782
+ function renderReportsList() {
783
+ const container = document.getElementById('reportsHistoryList');
784
+ if (!container) return;
785
+
786
+ container.innerHTML = '';
787
+ const filtered = reportsState.reports.filter(r => {
788
+ if (!reportsState.searchQuery) return true;
789
+ const q = reportsState.searchQuery;
790
+ return (
791
+ (r.objective && r.objective.toLowerCase().includes(q)) ||
792
+ (r.project && r.project.toLowerCase().includes(q)) ||
793
+ (r.dateFormatted && r.dateFormatted.toLowerCase().includes(q))
794
+ );
795
+ });
796
+
797
+ if (filtered.length === 0) {
798
+ container.innerHTML = `
799
+ <div style="padding: 16px; text-align: center; color: var(--text-dim); font-size: 11px;">
800
+ ${reportsState.searchQuery ? 'No matching reports found.' : 'No audit reports recorded yet. Run a sprint demo or task to generate.'}
801
+ </div>
802
+ `;
803
+ return;
804
+ }
805
+
806
+ filtered.forEach(report => {
807
+ const card = document.createElement('div');
808
+ card.className = `report-item-card ${reportsState.activeReport?.id === report.id ? 'active' : ''}`;
809
+
810
+ card.innerHTML = `
811
+ <div class="report-item-top">
812
+ <span class="report-item-date">${report.dateFormatted || new Date(report.timestamp).toLocaleDateString()}</span>
813
+ <span class="report-item-status">${report.status || 'COMPLETED'}</span>
814
+ </div>
815
+ <div class="report-item-title">${report.objective || 'Swarm Audit Sprint'}</div>
816
+ <div class="report-item-meta">
817
+ <span>👥 ${(report.targetAgents || []).length} Squads</span>
818
+ <span>✨ ${report.totalFindings || (report.sections || []).length} Points</span>
819
+ </div>
820
+ `;
821
+
822
+ card.addEventListener('click', () => {
823
+ synth.playClick();
824
+ reportsState.activeReport = report;
825
+ renderReportsList();
826
+ renderActiveReport();
827
+ });
828
+
829
+ container.appendChild(card);
830
+ });
831
+ }
832
+
833
+ function formatMarkdownText(text) {
834
+ if (!text) return '';
835
+ let html = escapeHtml(text);
836
+
837
+ // Bold: **text** or __text__
838
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong class="report-bold">$1</strong>');
839
+ html = html.replace(/__(.*?)__/g, '<strong class="report-bold">$1</strong>');
840
+
841
+ // Inline Code: `code`
842
+ html = html.replace(/`([^`]+)`/g, '<code class="report-inline-code">$1</code>');
843
+
844
+ // Italic: *text* or _text_
845
+ html = html.replace(/\*([^*]+)\*/g, '<em class="report-italic">$1</em>');
846
+ html = html.replace(/_([^_]+)_/g, '<em class="report-italic">$1</em>');
847
+
848
+ return html;
849
+ }
850
+
851
+ function parseFindingItem(finding) {
852
+ let rawCategory = (finding.category || '').trim();
853
+ let rawDesc = (finding.description || '').trim();
854
+
855
+ let combined = rawDesc;
856
+ if (!rawDesc || rawDesc === rawCategory) {
857
+ combined = rawCategory;
858
+ } else if (rawCategory && rawDesc) {
859
+ combined = `${rawCategory}: ${rawDesc}`;
860
+ }
861
+
862
+ let badge = 'Finding';
863
+ let path = null;
864
+ let description = combined;
865
+
866
+ // 1. Extract markdown bold badge if present: **Title (`path`)** or **Title**
867
+ const boldPrefixMatch = combined.match(/^(\*\*|__)?([^*_`:]+?)(?:\s*\(`?([^`)]+?)`?\))?(\*\*|__)?(?::\s*|\s*-\s*|\s*—\s*)(.*)$/s);
868
+ if (boldPrefixMatch) {
869
+ badge = boldPrefixMatch[2].replace(/\*\*/g, '').trim();
870
+ path = boldPrefixMatch[3] ? boldPrefixMatch[3].trim() : null;
871
+ description = boldPrefixMatch[5] ? boldPrefixMatch[5].trim() : '';
872
+ } else {
873
+ // 2. Check if category itself contains title and path
874
+ const catMatch = rawCategory.match(/^(\*\*|__)?([^*_`:]+?)(?:\s*\(`?([^`)]+?)`?\))?(\*\*|__)?$/);
875
+ if (catMatch) {
876
+ badge = catMatch[2].replace(/\*\*/g, '').trim();
877
+ path = catMatch[3] ? catMatch[3].trim() : null;
878
+ description = rawDesc;
879
+ } else {
880
+ const colonIndex = combined.indexOf(':');
881
+ if (colonIndex > 0 && colonIndex < 45) {
882
+ let rawTitle = combined.substring(0, colonIndex).replace(/\*\*/g, '').replace(/__/g, '').trim();
883
+ const pathMatch = rawTitle.match(/^(.*?)(?:\s*\(`?([^`)]+?)`?\))$/);
884
+ if (pathMatch) {
885
+ badge = pathMatch[1].trim();
886
+ path = pathMatch[2].trim();
887
+ } else {
888
+ badge = rawTitle;
889
+ }
890
+ description = combined.substring(colonIndex + 1).trim();
891
+ }
892
+ }
893
+ }
894
+
895
+ badge = badge.replace(/\*\*/g, '').replace(/`/g, '').trim();
896
+ if (!badge) badge = 'Observation';
897
+
898
+ return {
899
+ badge,
900
+ path,
901
+ descriptionHtml: formatMarkdownText(description || combined)
902
+ };
903
+ }
904
+
905
+ function renderActiveReport() {
906
+ const viewer = document.getElementById('reportsViewer');
907
+ if (!viewer) return;
908
+
909
+ const report = reportsState.activeReport;
910
+ if (!report) {
911
+ viewer.innerHTML = `
912
+ <div class="report-empty-state">
913
+ <span style="font-size: 32px;">📋</span>
914
+ <span>SELECT A SPRINT REPORT FROM THE LEFT SIDEBAR</span>
915
+ <span style="color: var(--text-dim); font-size: 11px;">Audit reports are generated automatically after every completed swarm run.</span>
916
+ </div>
917
+ `;
918
+ return;
919
+ }
920
+
921
+ // Raw Markdown Mode
922
+ if (reportsState.rawMode) {
923
+ viewer.innerHTML = `
924
+ <div class="report-hero-card">
925
+ <div class="report-hero-header">
926
+ <div>
927
+ <h3 class="report-hero-title">RAW MARKDOWN SPECIFICATION</h3>
928
+ <div class="report-hero-meta">
929
+ <span>PROJECT: <strong>${escapeHtml(report.project || 'Project')}</strong></span>
930
+ <span>DATE: <strong>${escapeHtml(report.dateFormatted || 'N/A')}</strong></span>
931
+ </div>
932
+ </div>
933
+ </div>
934
+ </div>
935
+ <pre class="report-raw-pre">${escapeHtml(report.markdown || '# No markdown available')}</pre>
936
+ `;
937
+ return;
938
+ }
939
+
940
+ // Visual Interactive Card Mode
941
+ viewer.innerHTML = `
942
+ <!-- HERO SUMMARY -->
943
+ <div class="report-hero-card">
944
+ <div class="report-hero-header">
945
+ <div>
946
+ <h3 class="report-hero-title">★ ${escapeHtml(report.objective || 'Sprint Audit')}</h3>
947
+ <div class="report-hero-meta">
948
+ <span>PRODUCT: <strong>${escapeHtml(report.project || 'Project')}</strong></span>
949
+ <span>DATE: <strong>${escapeHtml(report.dateFormatted || 'Today')}</strong></span>
950
+ <span>SQUADS: <strong>${(report.targetAgents || []).map(a => a.toUpperCase()).join(', ')}</strong></span>
951
+ </div>
952
+ </div>
953
+ <span class="report-item-status">COMPLETED</span>
954
+ </div>
955
+ <div class="report-summary-box">
956
+ <strong>Executive Summary:</strong> The autonomous engineering swarm executed an architectural and quality audit, resolving dependencies and formulating <strong>${report.totalFindings || 0} actionable improvements</strong>.
957
+ </div>
958
+ </div>
959
+
960
+ <!-- METRICS GRID -->
961
+ <div class="report-metrics-grid">
962
+ <div class="report-metric-card">
963
+ <span class="report-metric-label">ENGINEERING SQUADS</span>
964
+ <span class="report-metric-val" style="color: var(--color-cyan);">${(report.targetAgents || []).length}</span>
965
+ </div>
966
+ <div class="report-metric-card">
967
+ <span class="report-metric-label">FINDINGS & ACTIONS</span>
968
+ <span class="report-metric-val" style="color: var(--color-gold);">${report.totalFindings || 0}</span>
969
+ </div>
970
+ <div class="report-metric-card">
971
+ <span class="report-metric-label">QUALITY GATE</span>
972
+ <span class="report-metric-val" style="color: var(--color-green);">PASSED</span>
973
+ </div>
974
+ <div class="report-metric-card">
975
+ <span class="report-metric-label">BLOCKER DEFECTS</span>
976
+ <span class="report-metric-val" style="color: var(--color-green);">0</span>
977
+ </div>
978
+ </div>
979
+
980
+ <!-- SQUAD FINDINGS ACCORDION / CARDS -->
981
+ <div class="report-sections-grid">
982
+ ${(report.sections || []).map(section => {
983
+ const squadColor = section.color || '#00f0ff';
984
+ const findings = section.findings || [];
985
+ return `
986
+ <div class="report-squad-card" style="border-left: 4px solid ${squadColor};">
987
+ <div class="report-squad-header">
988
+ <div class="report-squad-header-left" style="color: ${squadColor};">
989
+ <span>${section.icon || '💼'}</span>
990
+ <span>${escapeHtml(section.name || section.agent.toUpperCase())}</span>
991
+ </div>
992
+ <span class="report-squad-count">${findings.length} ${findings.length === 1 ? 'Point' : 'Points'}</span>
993
+ </div>
994
+ <div class="report-findings-list">
995
+ ${findings.map(f => {
996
+ const parsed = parseFindingItem(f);
997
+ return `
998
+ <div class="report-finding-card">
999
+ <div class="finding-card-header">
1000
+ <span class="finding-badge" style="background: rgba(0, 240, 255, 0.08); border-color: ${squadColor}40; color: ${squadColor};">
1001
+ ${escapeHtml(parsed.badge)}
1002
+ </span>
1003
+ ${parsed.path ? `<span class="finding-path-pill"><code>${escapeHtml(parsed.path)}</code></span>` : ''}
1004
+ </div>
1005
+ <div class="finding-body-text">
1006
+ ${parsed.descriptionHtml}
1007
+ </div>
1008
+ </div>
1009
+ `;
1010
+ }).join('')}
1011
+ </div>
1012
+ </div>
1013
+ `;
1014
+ }).join('')}
1015
+ </div>
1016
+
1017
+ <!-- ACTION ITEMS CHECKLIST -->
1018
+ <div class="report-actions-card">
1019
+ <div class="report-actions-title">📋 RECOMMENDED ACTION ITEMS & NEXT STEPS</div>
1020
+ <div class="report-checklist">
1021
+ ${(report.actionItems || []).map(item => `
1022
+ <label class="report-check-item">
1023
+ <input type="checkbox" />
1024
+ <span>${formatMarkdownText(item)}</span>
1025
+ </label>
1026
+ `).join('')}
1027
+ </div>
1028
+ </div>
1029
+ `;
1030
+ }
1031
+
1032
+ function toggleRawReportView() {
1033
+ synth.playClick();
1034
+ reportsState.rawMode = !reportsState.rawMode;
1035
+ const label = document.getElementById('toggleRawLabel');
1036
+ if (label) {
1037
+ label.textContent = reportsState.rawMode ? 'VISUAL CARDS' : 'RAW MARKDOWN';
1038
+ }
1039
+ renderActiveReport();
1040
+ }
1041
+
1042
+ function copyActiveReportMarkdown() {
1043
+ if (!reportsState.activeReport?.markdown) return;
1044
+ navigator.clipboard.writeText(reportsState.activeReport.markdown).then(() => {
1045
+ synth.playSkill();
1046
+ const btn = document.getElementById('btnCopyReportMd');
1047
+ if (btn) {
1048
+ const originalText = btn.innerHTML;
1049
+ btn.innerHTML = '<span>✓</span> COPIED!';
1050
+ setTimeout(() => { btn.innerHTML = originalText; }, 2000);
1051
+ }
1052
+ }).catch(err => {
1053
+ console.error('Failed to copy markdown:', err);
1054
+ });
1055
+ }
1056
+
1057
+ function downloadActiveReport() {
1058
+ if (!reportsState.activeReport) return;
1059
+ const r = reportsState.activeReport;
1060
+ const content = r.markdown || JSON.stringify(r, null, 2);
1061
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
1062
+ const url = URL.createObjectURL(blob);
1063
+ const a = document.createElement('a');
1064
+ a.href = url;
1065
+ a.download = `${r.id || 'sprint-report'}.md`;
1066
+ document.body.appendChild(a);
1067
+ a.click();
1068
+ document.body.removeChild(a);
1069
+ URL.revokeObjectURL(url);
1070
+ synth.playComplete();
1071
+ }
1072
+
1073
+ function escapeHtml(str) {
1074
+ if (!str) return '';
1075
+ return String(str)
1076
+ .replace(/&/g, '&amp;')
1077
+ .replace(/</g, '&lt;')
1078
+ .replace(/>/g, '&gt;')
1079
+ .replace(/"/g, '&quot;')
1080
+ .replace(/'/g, '&#039;');
1081
+ }
1082
+
680
1083
  // Submit Task to Server
681
1084
  async function submitTask(prompt) {
682
1085
  try {
@@ -759,17 +1162,28 @@ function connectSSE() {
759
1162
  renderState();
760
1163
  }
761
1164
 
762
- // Audio cues
763
- if (event.type === 'spawn') synth.playSpawn();
764
- else if (event.type === 'skill') synth.playSkill();
765
- else if (event.type === 'complete') synth.playComplete();
766
- else if (event.type === 'error') synth.playError();
1165
+ // Audio cues & Report Refresh
1166
+ if (event.type === 'spawn') {
1167
+ synth.playSpawn();
1168
+ } else if (event.type === 'skill') {
1169
+ synth.playSkill();
1170
+ } else if (event.type === 'complete') {
1171
+ synth.playComplete();
1172
+ if (event.agent === 'orchestrator' || event.metadata?.report) {
1173
+ fetchReports();
1174
+ }
1175
+ } else if (event.type === 'error') {
1176
+ synth.playError();
1177
+ }
767
1178
  });
768
1179
 
769
1180
  eventSource.addEventListener('state_change', (e) => {
770
1181
  const data = JSON.parse(e.data);
771
1182
  appState.state = data.state;
772
1183
  renderState();
1184
+ if (data.state?.status === 'COMPLETED') {
1185
+ fetchReports();
1186
+ }
773
1187
  });
774
1188
 
775
1189
  eventSource.onerror = () => {
@@ -50,6 +50,9 @@
50
50
  </div>
51
51
 
52
52
  <div class="header-right">
53
+ <button class="pixel-btn btn-sm btn-info" id="btnReportsToggle" title="Open Sprint Audit Reports History [R]">
54
+ <span>📋</span> REPORTS (<span id="reportsCountBadge">0</span>)
55
+ </button>
53
56
  <button class="pixel-btn btn-sm" id="btnAudioToggle" title="Toggle 8-bit Audio SFX">
54
57
  <span id="audioIcon">🔊</span> SFX
55
58
  </button>
@@ -213,10 +216,52 @@
213
216
  </div>
214
217
  </div>
215
218
 
219
+ <!-- SPRINT AUDIT REPORTS & HISTORY MODAL / DRAWER -->
220
+ <div class="pixel-modal-backdrop" id="reportsModal">
221
+ <div class="pixel-modal-card reports-modal-card">
222
+ <div class="modal-header">
223
+ <div class="modal-title-box">
224
+ <span class="modal-agent-icon">📋</span>
225
+ <h2 class="modal-agent-name" style="color: var(--color-cyan);">SPRINT AUDIT REPORTS & HISTORY</h2>
226
+ </div>
227
+ <div class="reports-header-actions">
228
+ <button class="pixel-btn btn-xs" id="btnToggleRawView" title="Toggle between visual card and raw markdown">
229
+ <span>👁️</span> <span id="toggleRawLabel">RAW MARKDOWN</span>
230
+ </button>
231
+ <button class="pixel-btn btn-xs" id="btnCopyReportMd" title="Copy markdown to clipboard">
232
+ <span>📄</span> COPY MD
233
+ </button>
234
+ <button class="pixel-btn btn-xs" id="btnDownloadReport" title="Download report as .md file">
235
+ <span>💾</span> DOWNLOAD
236
+ </button>
237
+ <button class="pixel-btn btn-sm btn-warn" id="btnCloseReportsModal">✕</button>
238
+ </div>
239
+ </div>
240
+ <div class="reports-modal-body">
241
+ <!-- SPRINT HISTORY SIDEBAR -->
242
+ <aside class="reports-sidebar">
243
+ <div class="reports-search-box">
244
+ <input type="text" class="pixel-input reports-search-input" id="reportsSearch" placeholder="Search past sprint reports..." />
245
+ </div>
246
+ <div class="reports-history-list" id="reportsHistoryList">
247
+ <!-- Populated by app.js -->
248
+ </div>
249
+ </aside>
250
+
251
+ <!-- ACTIVE REPORT DETAIL VIEWER -->
252
+ <main class="reports-viewer" id="reportsViewer">
253
+ <!-- Populated dynamically -->
254
+ </main>
255
+ </div>
256
+ </div>
257
+ </div>
258
+
216
259
  <!-- ARCADE / OFFICE FOOTER -->
217
260
  <footer class="arcade-footer">
218
261
  <div class="footer-left">
219
- <span>PRESS [1..6] TO OPEN EMPLOYEE PROFILE</span>
262
+ <span>PRESS [1..6] EMPLOYEE PROFILE</span>
263
+ <span>•</span>
264
+ <span>[R] SPRINT REPORTS</span>
220
265
  <span>•</span>
221
266
  <span>[SPACE] SPRINT DEMO</span>
222
267
  </div>