@1mancompany/onemancompany 0.7.39 → 0.7.44

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/frontend/app.js CHANGED
@@ -95,6 +95,7 @@ class AppController {
95
95
  const { employees, tasks, rooms, tools, activity_log, version, office_layout } = data;
96
96
  console.debug(`[bootstrap] /api/bootstrap took ${(performance.now() - t0).toFixed(0)}ms`);
97
97
 
98
+ this._cachedEmployees = employees || [];
98
99
  this.updateRoster(employees);
99
100
  this.updateOneononeDropdown(employees);
100
101
  this.updateProjectsPanel();
@@ -201,6 +202,7 @@ class AppController {
201
202
 
202
203
  async _fetchAndRenderRoster() {
203
204
  const employees = await fetch('/api/employees').then(r => r.json());
205
+ this._cachedEmployees = employees || [];
204
206
  this.updateRoster(employees);
205
207
  this.updateOneononeDropdown(employees);
206
208
  if (window.officeRenderer) {
@@ -1090,11 +1092,11 @@ class AppController {
1090
1092
  if (data.status === 'ok') {
1091
1093
  console.log('Abort all result:', data);
1092
1094
  } else {
1093
- alert(data.detail || data.message || 'Failed to abort all tasks');
1095
+ this._showToast(data.detail || data.message || 'Failed to abort all tasks', 'error');
1094
1096
  }
1095
1097
  } catch (e) {
1096
1098
  console.error('Abort all failed:', e);
1097
- alert('Failed to abort all tasks');
1099
+ this._showToast('Failed to abort all tasks', 'error');
1098
1100
  }
1099
1101
  });
1100
1102
 
@@ -1259,7 +1261,7 @@ class AppController {
1259
1261
  startBtn.disabled = false;
1260
1262
 
1261
1263
  if (res.error) {
1262
- alert(res.error);
1264
+ this._showToast(res.error, 'error');
1263
1265
  return;
1264
1266
  }
1265
1267
 
@@ -1752,7 +1754,7 @@ class AppController {
1752
1754
  })
1753
1755
  .then(data => {
1754
1756
  if (data.error) {
1755
- alert(`Cannot dismiss: ${data.error}`);
1757
+ this._showToast(`Cannot dismiss: ${data.error}`, 'error');
1756
1758
  } else {
1757
1759
  this.closeEmployeeDetail();
1758
1760
  this.addLog(`Dismissed ${data.name} (${data.nickname}) — ${data.reason}`);
@@ -1761,7 +1763,7 @@ class AppController {
1761
1763
  })
1762
1764
  .catch(err => {
1763
1765
  console.error('Fire employee error:', err);
1764
- alert('Failed to dismiss employee. See console for details.');
1766
+ this._showToast('Failed to dismiss employee', 'error');
1765
1767
  });
1766
1768
  }
1767
1769
 
@@ -2027,11 +2029,11 @@ class AppController {
2027
2029
  if (data.status === 'ok') {
2028
2030
  this._fetchCronList(empId);
2029
2031
  } else {
2030
- alert(data.detail || data.message || 'Failed to stop cron');
2032
+ this._showToast(data.detail || data.message || 'Failed to stop cron', 'error');
2031
2033
  }
2032
2034
  } catch (err) {
2033
2035
  console.error('Failed to cancel cron:', err);
2034
- alert('Failed to stop cron job');
2036
+ this._showToast('Failed to stop cron job', 'error');
2035
2037
  }
2036
2038
  }
2037
2039
 
@@ -2045,11 +2047,11 @@ class AppController {
2045
2047
  if (data.status === 'ok') {
2046
2048
  this._fetchCronList(empId);
2047
2049
  } else {
2048
- alert(data.detail || data.message || 'Failed to stop all crons');
2050
+ this._showToast(data.detail || data.message || 'Failed to stop all crons', 'error');
2049
2051
  }
2050
2052
  } catch (err) {
2051
2053
  console.error('Failed to stop all crons:', err);
2052
- alert('Failed to stop all cron jobs');
2054
+ this._showToast('Failed to stop all cron jobs', 'error');
2053
2055
  }
2054
2056
  }
2055
2057
 
@@ -2737,7 +2739,7 @@ class AppController {
2737
2739
  fetch('/api/ceo/dnd').then(r => r.json()).then(data => {
2738
2740
  dndBtn.classList.toggle('active', data.dnd);
2739
2741
  if (data.dnd) dndBtn.title = 'Do Not Disturb (ON)';
2740
- }).catch(() => {});
2742
+ }).catch(err => console.warn('[dnd] state load failed:', err));
2741
2743
  }
2742
2744
 
2743
2745
  // ===== @Mention Autocomplete ===== //
@@ -5898,7 +5900,7 @@ class AppController {
5898
5900
  });
5899
5901
  const result = await resp.json();
5900
5902
  if (result.status === 'error') {
5901
- alert(result.message);
5903
+ this._showToast(result.message, 'error');
5902
5904
  } else {
5903
5905
  this._renderSystemCrons();
5904
5906
  }
@@ -6679,7 +6681,7 @@ class AppController {
6679
6681
  window.open(data.auth_url, '_blank', 'width=600,height=700');
6680
6682
  setTimeout(() => this.openToolDetail(toolId), 5000);
6681
6683
  } else {
6682
- alert(data.message || 'OAuth login failed');
6684
+ this._showToast(data.message || 'OAuth login failed', 'error');
6683
6685
  }
6684
6686
  break;
6685
6687
  }
@@ -6692,7 +6694,7 @@ class AppController {
6692
6694
  case 'credentials': {
6693
6695
  const clientId = document.getElementById('tool-oauth-client-id')?.value || '';
6694
6696
  const clientSecret = document.getElementById('tool-oauth-client-secret')?.value || '';
6695
- if (!clientId || !clientSecret) { alert('Both Client ID and Client Secret required'); return; }
6697
+ if (!clientId || !clientSecret) { this._showToast('Both Client ID and Client Secret required', 'error'); return; }
6696
6698
  const res = await fetch(`/api/tools/${esc}/oauth/credentials`, {
6697
6699
  method: 'POST',
6698
6700
  headers: { 'Content-Type': 'application/json' },
@@ -6700,7 +6702,7 @@ class AppController {
6700
6702
  });
6701
6703
  const data = await res.json();
6702
6704
  if (data.status === 'ok') this.openToolDetail(toolId);
6703
- else alert(data.message || 'Failed');
6705
+ else this._showToast(data.message || 'Failed', 'error');
6704
6706
  break;
6705
6707
  }
6706
6708
  case 'save_env': {
@@ -6714,7 +6716,7 @@ class AppController {
6714
6716
  });
6715
6717
  const data = await res.json();
6716
6718
  if (data.status === 'ok') this.openToolDetail(toolId);
6717
- else alert(data.message || 'Failed');
6719
+ else this._showToast(data.message || 'Failed', 'error');
6718
6720
  break;
6719
6721
  }
6720
6722
  }
@@ -6725,7 +6727,7 @@ class AppController {
6725
6727
  async _templateOpen(toolId, filename) {
6726
6728
  const esc = encodeURIComponent;
6727
6729
  const res = await fetch(`/api/tools/${esc(toolId)}/templates/${esc(filename)}`);
6728
- if (!res.ok) { alert('Failed to load template'); return; }
6730
+ if (!res.ok) { this._showToast('Failed to load template', 'error'); return; }
6729
6731
  const data = await res.json();
6730
6732
  const body = document.getElementById('tool-list-body');
6731
6733
  const escH = (t) => this._escapeHtml(t);
@@ -6743,7 +6745,7 @@ class AppController {
6743
6745
 
6744
6746
  async _templateSave(toolId, filename) {
6745
6747
  const content = document.getElementById('template-editor')?.value || '';
6746
- if (!content.trim()) { alert('Template cannot be empty'); return; }
6748
+ if (!content.trim()) { this._showToast('Template cannot be empty', 'error'); return; }
6747
6749
  const esc = encodeURIComponent;
6748
6750
  const res = await fetch(`/api/tools/${esc(toolId)}/templates/${esc(filename)}`, {
6749
6751
  method: 'PUT',
@@ -6752,7 +6754,7 @@ class AppController {
6752
6754
  });
6753
6755
  const data = await res.json();
6754
6756
  if (data.status === 'ok') this.openToolDetail(toolId);
6755
- else alert(data.message || 'Save failed');
6757
+ else this._showToast(data.message || 'Save failed', 'error');
6756
6758
  }
6757
6759
 
6758
6760
  async _templateDelete(toolId, filename) {
@@ -6761,7 +6763,7 @@ class AppController {
6761
6763
  const res = await fetch(`/api/tools/${esc(toolId)}/templates/${esc(filename)}`, { method: 'DELETE' });
6762
6764
  const data = await res.json();
6763
6765
  if (data.status === 'ok') this.openToolDetail(toolId);
6764
- else alert(data.message || 'Delete failed');
6766
+ else this._showToast(data.message || 'Delete failed', 'error');
6765
6767
  }
6766
6768
 
6767
6769
  _templateNew(toolId, templatesDir) {
@@ -6961,7 +6963,7 @@ class AppController {
6961
6963
  }
6962
6964
  } catch (err) {
6963
6965
  this._chatPanel.showTyping(false);
6964
- alert(`Failed to send message: ${err.message}`);
6966
+ this._showToast(`Failed to send message: ${err.message}`, 'error');
6965
6967
  }
6966
6968
  // Reply arrives via WebSocket conversation_message event
6967
6969
  }
@@ -6987,7 +6989,7 @@ class AppController {
6987
6989
  const empName = this._resolveEmployeeName(data.employee_id || '');
6988
6990
  this.logEntry('SYSTEM', `🧹 Cleared 1-on-1 history for ${empName}.`, 'system');
6989
6991
  } catch (err) {
6990
- alert(`Failed to clear history: ${err.message}`);
6992
+ this._showToast(`Failed to clear history: ${err.message}`, 'error');
6991
6993
  }
6992
6994
  }
6993
6995
 
@@ -7099,6 +7101,7 @@ class AppController {
7099
7101
  }
7100
7102
 
7101
7103
  _escapeHtml(text) {
7104
+ if (text == null) return '';
7102
7105
  const div = document.createElement('div');
7103
7106
  div.textContent = text;
7104
7107
  return div.innerHTML;
@@ -7396,20 +7399,15 @@ class AppController {
7396
7399
  list.appendChild(row);
7397
7400
  }
7398
7401
 
7399
- async _populateProductOwnerDropdown() {
7400
- try {
7401
- const data = await fetch('/api/bootstrap').then(r => r.json());
7402
- const sel = document.getElementById('create-product-owner');
7403
- if (!sel) return;
7404
- sel.innerHTML = '<option value="">Select owner...</option>';
7405
- for (const emp of (data.employees || [])) {
7406
- const opt = document.createElement('option');
7407
- opt.value = emp.id;
7408
- opt.textContent = `${emp.name || emp.id} (${emp.role || ''})`;
7409
- sel.appendChild(opt);
7410
- }
7411
- } catch (e) {
7412
- console.debug('Failed to populate owner dropdown:', e);
7402
+ _populateProductOwnerDropdown() {
7403
+ const sel = document.getElementById('create-product-owner');
7404
+ if (!sel) return;
7405
+ sel.innerHTML = '<option value="">Select owner...</option>';
7406
+ for (const emp of (this._cachedEmployees || [])) {
7407
+ const opt = document.createElement('option');
7408
+ opt.value = emp.id;
7409
+ opt.textContent = `${emp.name || emp.id} (${emp.role || ''})`;
7410
+ sel.appendChild(opt);
7413
7411
  }
7414
7412
  }
7415
7413
 
@@ -7725,15 +7723,13 @@ class AppController {
7725
7723
  ownerEl.className = 'form-input';
7726
7724
  ownerEl.style.width = 'auto';
7727
7725
  ownerEl.innerHTML = '<option value="">Unassigned</option>';
7728
- fetch('/api/bootstrap').then(r => r.json()).then(d => {
7729
- for (const emp of (d.employees || [])) {
7730
- const opt = document.createElement('option');
7731
- opt.value = emp.id;
7732
- opt.textContent = `${emp.name || emp.id}`;
7733
- ownerEl.appendChild(opt);
7734
- }
7735
- ownerEl.value = product.owner_id || '';
7736
- });
7726
+ for (const emp of (this._cachedEmployees || [])) {
7727
+ const opt = document.createElement('option');
7728
+ opt.value = emp.id;
7729
+ opt.textContent = `${emp.name || emp.id}`;
7730
+ ownerEl.appendChild(opt);
7731
+ }
7732
+ ownerEl.value = product.owner_id || '';
7737
7733
  ownerEl.addEventListener('change', () => {
7738
7734
  fetch(`/api/product/${encodeURIComponent(slug)}`, {
7739
7735
  method: 'PUT',
@@ -8220,15 +8216,13 @@ class AppController {
8220
8216
  assignSel.style.width = 'auto';
8221
8217
  assignSel.style.display = 'inline';
8222
8218
  assignSel.innerHTML = '<option value="">Unassigned</option>';
8223
- fetch('/api/bootstrap').then(r => r.json()).then(d => {
8224
- for (const emp of (d.employees || [])) {
8225
- const opt = document.createElement('option');
8226
- opt.value = emp.id;
8227
- opt.textContent = emp.name || emp.id;
8228
- assignSel.appendChild(opt);
8229
- }
8230
- assignSel.value = issue.assignee_id || '';
8231
- });
8219
+ for (const emp of (this._cachedEmployees || [])) {
8220
+ const opt = document.createElement('option');
8221
+ opt.value = emp.id;
8222
+ opt.textContent = emp.name || emp.id;
8223
+ assignSel.appendChild(opt);
8224
+ }
8225
+ assignSel.value = issue.assignee_id || '';
8232
8226
  assignSel.addEventListener('click', (e) => e.stopPropagation());
8233
8227
  assignSel.addEventListener('change', () => {
8234
8228
  fetch(`/api/product/${encodeURIComponent(slug)}/issue/${encodeURIComponent(issue.id)}`, {
@@ -9067,8 +9061,9 @@ class AppController {
9067
9061
  header.className = 'review-card-header';
9068
9062
  const trigger = rev.trigger || 'manual';
9069
9063
  const dateStr = rev.created_at ? new Date(rev.created_at).toLocaleDateString() : '';
9070
- header.innerHTML = `<span class="review-trigger">${this._escHtml(trigger)}</span> <span class="review-date">${dateStr}</span>`;
9071
- if (rev.owner) header.innerHTML += ` <span class="review-owner">Owner: ${this._escHtml(rev.owner)}</span>`;
9064
+ let headerHtml = `<span class="review-trigger">${this._escHtml(trigger)}</span> <span class="review-date">${dateStr}</span>`;
9065
+ if (rev.owner) headerHtml += ` <span class="review-owner">Owner: ${this._escHtml(rev.owner)}</span>`;
9066
+ header.innerHTML = headerHtml;
9072
9067
  card.appendChild(header);
9073
9068
 
9074
9069
  // Checklist items
@@ -11,22 +11,22 @@
11
11
  <script defer src="https://cloud.umami.is/script.js" data-website-id="e72ced7e-c551-40a9-b60e-18bb7db0592f"></script>
12
12
  </head>
13
13
  <body>
14
- <div id="app">
14
+ <div id="app" role="application" aria-label="One Man Company CEO Console">
15
15
  <!-- Left top: Products & Projects -->
16
- <div id="left-top-panel">
16
+ <aside id="left-top-panel" aria-label="Products">
17
17
  <div class="collapsible-header" data-target="projects-panel-body">
18
18
  <span class="collapse-arrow">&#9660;</span>
19
19
  <h3 class="pixel-title">PRODUCTS</h3>
20
- <button id="create-product-btn" class="panel-add-btn" title="New Product">+</button>
21
- <button id="import-product-btn" class="panel-add-btn" title="Import Product">&#8593;</button>
20
+ <button id="create-product-btn" class="panel-add-btn" title="New Product" aria-label="New Product">+</button>
21
+ <button id="import-product-btn" class="panel-add-btn" title="Import Product" aria-label="Import Product">&#8593;</button>
22
22
  </div>
23
23
  <div id="projects-panel-body" class="collapsible-body">
24
24
  <div id="projects-panel-list"></div>
25
25
  </div>
26
- </div>
26
+ </aside>
27
27
 
28
28
  <!-- Left bottom: Activity Log -->
29
- <div id="left-bottom-panel">
29
+ <aside id="left-bottom-panel" aria-label="Activity Log">
30
30
  <div class="collapsible-header" data-target="activity-body">
31
31
  <span class="collapse-arrow">&#9660;</span>
32
32
  <h3 class="pixel-title">ACTIVITY LOG</h3>
@@ -34,10 +34,10 @@
34
34
  <div id="activity-body" class="collapsible-body collapsible-flex">
35
35
  <div id="activity-log" style="background:#0a0a0a;flex:1"></div>
36
36
  </div>
37
- </div>
37
+ </aside>
38
38
 
39
39
  <!-- Center top: Office Canvas -->
40
- <div id="office-panel">
40
+ <main id="office-panel">
41
41
  <div class="panel-header">
42
42
  <h1 class="pixel-title">&#127970; ONE MAN COMPANY <span id="app-version" class="version-badge"></span></h1>
43
43
  <div id="status-bar">
@@ -45,22 +45,22 @@
45
45
  <span class="status-item status-clickable" id="tool-count" onclick="window.app.openToolList()">&#128295; 0</span>
46
46
  <span class="status-item" id="room-count">&#127970; 0/0</span>
47
47
  <!-- Meeting moved to /meeting slash command in CEO console -->
48
- <button id="ex-employee-toolbar-btn" class="toolbar-icon-btn" title="Ex-Employee Wall">&#128220;</button>
49
- <button id="company-culture-toolbar-btn" class="toolbar-icon-btn" title="Company Culture">&#127988;</button>
50
- <button id="company-direction-toolbar-btn" class="toolbar-icon-btn" title="Company Direction">&#127919;</button>
51
- <button id="dashboard-toolbar-btn" class="toolbar-icon-btn" title="Dashboard">&#128202;</button>
52
- <button id="announcements-toolbar-btn" class="toolbar-icon-btn" title="Announcements" style="position:relative;">&#128276;<span id="announcements-badge" class="announcement-badge hidden"></span></button>
53
- <button id="settings-toolbar-btn" class="toolbar-icon-btn" title="Settings">&#9881;</button>
54
- <span class="status-item" id="connection-status">&#9679; OFFLINE</span>
48
+ <button id="ex-employee-toolbar-btn" class="toolbar-icon-btn" title="Ex-Employee Wall" aria-label="Ex-Employee Wall">&#128220;</button>
49
+ <button id="company-culture-toolbar-btn" class="toolbar-icon-btn" title="Company Culture" aria-label="Company Culture">&#127988;</button>
50
+ <button id="company-direction-toolbar-btn" class="toolbar-icon-btn" title="Company Direction" aria-label="Company Direction">&#127919;</button>
51
+ <button id="dashboard-toolbar-btn" class="toolbar-icon-btn" title="Dashboard" aria-label="Dashboard">&#128202;</button>
52
+ <button id="announcements-toolbar-btn" class="toolbar-icon-btn" title="Announcements" aria-label="Announcements" style="position:relative;">&#128276;<span id="announcements-badge" class="announcement-badge hidden"></span></button>
53
+ <button id="settings-toolbar-btn" class="toolbar-icon-btn" title="Settings" aria-label="Settings">&#9881;</button>
54
+ <span class="status-item" id="connection-status" role="status" aria-live="polite">&#9679; OFFLINE</span>
55
55
  <span class="status-item" id="last-sync-time"></span>
56
56
  </div>
57
57
  </div>
58
- <canvas id="office-canvas" width="640" height="480"></canvas>
58
+ <canvas id="office-canvas" width="640" height="480" aria-label="Office floor plan" role="img"></canvas>
59
59
  <div id="tooltip" class="tooltip hidden"></div>
60
- </div>
60
+ </main>
61
61
 
62
62
  <!-- Right top: Team Roster -->
63
- <div id="roster-panel">
63
+ <aside id="roster-panel" aria-label="Team Roster">
64
64
  <div class="collapsible-header" data-target="roster-body">
65
65
  <span class="collapse-arrow">&#9660;</span>
66
66
  <h3 class="pixel-title">TEAM ROSTER</h3>
@@ -73,7 +73,7 @@
73
73
  </div>
74
74
  <div id="roster-list"></div>
75
75
  </div>
76
- </div>
76
+ </aside>
77
77
 
78
78
  <!-- Bottom spanning center+right: CEO Console -->
79
79
  <div id="console-panel">
@@ -85,11 +85,11 @@
85
85
  <div id="ceo-avatar-area">
86
86
  <img id="ceo-avatar" width="32" height="32" src="/api/employees/00001/avatar" onerror="this.style.display='none'" />
87
87
  <span class="ceo-label">&#128081; YOU</span>
88
- <button id="dnd-toggle-btn" class="toolbar-icon-btn" title="Do Not Disturb">&#127769;</button>
89
- <button id="bg-tasks-toolbar-btn" class="toolbar-icon-btn" title="Background Tasks">&#9641;</button>
90
- <button id="screenshot-toolbar-btn" class="toolbar-icon-btn" title="Export SVG Screenshot">&#128247;</button>
91
- <button id="abort-all-toolbar-btn" class="toolbar-icon-btn" title="Stop All Tasks" style="color: #ff4444;">&#9888;</button>
92
- <button id="reload-toolbar-btn" class="reload-btn" title="Force reload all data from disk">&#128260;</button>
88
+ <button id="dnd-toggle-btn" class="toolbar-icon-btn" title="Do Not Disturb" aria-label="Do Not Disturb">&#127769;</button>
89
+ <button id="bg-tasks-toolbar-btn" class="toolbar-icon-btn" title="Background Tasks" aria-label="Background Tasks">&#9641;</button>
90
+ <button id="screenshot-toolbar-btn" class="toolbar-icon-btn" title="Export SVG Screenshot" aria-label="Export SVG Screenshot">&#128247;</button>
91
+ <button id="abort-all-toolbar-btn" class="toolbar-icon-btn" title="Stop All Tasks" aria-label="Stop All Tasks" style="color: #ff4444;">&#9888;</button>
92
+ <button id="reload-toolbar-btn" class="reload-btn" title="Force reload all data from disk" aria-label="Force reload all data from disk">&#128260;</button>
93
93
  </div>
94
94
  <div id="ceo-split">
95
95
  <div id="ceo-project-list">
@@ -118,7 +118,7 @@
118
118
  </select>
119
119
  </div>
120
120
  <div id="ceo-conv-input-row">
121
- <textarea id="ceo-conv-input" placeholder="$ Type message, / for commands (Enter to send)" rows="1"></textarea>
121
+ <textarea id="ceo-conv-input" placeholder="$ Type message, / for commands (Enter to send)" rows="1" aria-label="CEO message input"></textarea>
122
122
  <input type="file" id="ceo-file-input" multiple hidden />
123
123
  </div>
124
124
  <div id="ceo-slash-menu" class="hidden"></div>
@@ -15,7 +15,22 @@
15
15
  --pixel-white: #e0e0f0;
16
16
  --pixel-gray: #888899;
17
17
  --text-dim: #6666aa;
18
+ --text-primary: #e0e0f0;
19
+ --font-pixel: 'Press Start 2P', monospace;
20
+ --font-mono: 'Courier New', Courier, monospace;
21
+ --bg-card: #16162a;
18
22
  --font-boost: 2px; /* Text size boost: 0px (small), 2px (medium, default), 4px (large) */
23
+
24
+ /* z-index scale */
25
+ --z-base: 2;
26
+ --z-dropdown: 10;
27
+ --z-sticky: 100;
28
+ --z-overlay: 200;
29
+ --z-floating: 300;
30
+ --z-modal: 900;
31
+ --z-toast-backdrop: 9998;
32
+ --z-toast: 9999;
33
+ --z-critical: 99999;
19
34
  }
20
35
 
21
36
  * {
@@ -24,6 +39,18 @@
24
39
  image-rendering: crisp-edges;
25
40
  }
26
41
 
42
+ :focus-visible {
43
+ outline: 2px solid var(--pixel-cyan);
44
+ outline-offset: 2px;
45
+ }
46
+
47
+ input:focus-visible,
48
+ textarea:focus-visible,
49
+ select:focus-visible {
50
+ outline: 2px solid var(--pixel-cyan);
51
+ outline-offset: 0;
52
+ }
53
+
27
54
  body {
28
55
  background: var(--bg-dark);
29
56
  color: var(--pixel-white);
@@ -224,7 +251,7 @@ body.resize-dragging {
224
251
  font-size: calc(7px + var(--font-boost));
225
252
  line-height: 1.8;
226
253
  pointer-events: none;
227
- z-index: 10;
254
+ z-index: var(--z-dropdown);
228
255
  max-width: 200px;
229
256
  white-space: pre-wrap;
230
257
  }
@@ -258,7 +285,7 @@ body.resize-dragging {
258
285
  /* ===== Floating Panel (Settings dropdown etc.) ===== */
259
286
  .floating-panel {
260
287
  position: fixed;
261
- z-index: 300;
288
+ z-index: var(--z-floating);
262
289
  background: var(--bg-panel, #1a1a2e);
263
290
  border: 2px solid var(--pixel-cyan, #0ff);
264
291
  box-shadow: 0 4px 20px rgba(0, 255, 255, 0.2);
@@ -361,7 +388,7 @@ body.resize-dragging {
361
388
  border-radius: 2px;
362
389
  white-space: nowrap;
363
390
  pointer-events: none;
364
- z-index: 100;
391
+ z-index: var(--z-sticky);
365
392
  }
366
393
 
367
394
  .reload-btn:disabled {
@@ -908,7 +935,7 @@ body.resize-dragging {
908
935
  width: 100vw;
909
936
  height: 100vh;
910
937
  background: rgba(0, 0, 0, 0.75);
911
- z-index: 100;
938
+ z-index: var(--z-sticky);
912
939
  display: flex;
913
940
  align-items: center;
914
941
  justify-content: center;
@@ -1794,7 +1821,7 @@ body.resize-dragging {
1794
1821
  border: 1px solid var(--pixel-green);
1795
1822
  background: var(--pixel-green);
1796
1823
  opacity: 0;
1797
- z-index: 2;
1824
+ z-index: var(--z-base);
1798
1825
  display: flex;
1799
1826
  align-items: center;
1800
1827
  justify-content: center;
@@ -2017,7 +2044,7 @@ body.resize-dragging {
2017
2044
  background: var(--bg-panel);
2018
2045
  border: 1px solid var(--pixel-cyan);
2019
2046
  box-shadow: 0 0 20px rgba(0, 221, 255, 0.2);
2020
- z-index: 900;
2047
+ z-index: var(--z-modal);
2021
2048
  font-family: var(--font-pixel);
2022
2049
  overflow: hidden;
2023
2050
  transition: max-height 0.3s ease;
@@ -3567,7 +3594,7 @@ body.resize-dragging {
3567
3594
  top: 0;
3568
3595
  left: 0;
3569
3596
  right: 0;
3570
- z-index: 9998;
3597
+ z-index: var(--z-toast-backdrop);
3571
3598
  display: flex;
3572
3599
  align-items: center;
3573
3600
  justify-content: center;
@@ -3608,7 +3635,7 @@ body.resize-dragging {
3608
3635
  top: 0;
3609
3636
  left: 0;
3610
3637
  right: 0;
3611
- z-index: 9999;
3638
+ z-index: var(--z-toast);
3612
3639
  display: flex;
3613
3640
  justify-content: center;
3614
3641
  padding: 8px;
@@ -4600,7 +4627,7 @@ body.resize-dragging {
4600
4627
  padding: 16px;
4601
4628
  background: #111;
4602
4629
  border-left: 1px solid var(--pixel-green-dim, #1a3a2a);
4603
- z-index: 10;
4630
+ z-index: var(--z-dropdown);
4604
4631
  }
4605
4632
 
4606
4633
  .project-tree-drawer.hidden {
@@ -4677,7 +4704,7 @@ body.resize-dragging {
4677
4704
  top: 0; left: 0;
4678
4705
  width: 100vw; height: 100vh;
4679
4706
  background: rgba(0, 0, 0, 0.8);
4680
- z-index: 200;
4707
+ z-index: var(--z-overlay);
4681
4708
  display: flex;
4682
4709
  align-items: center;
4683
4710
  justify-content: center;
@@ -4949,7 +4976,7 @@ body.resize-dragging {
4949
4976
  position: fixed;
4950
4977
  top: 0; left: 0; right: 0; bottom: 0;
4951
4978
  background: rgba(0, 0, 0, 0.7);
4952
- z-index: 200;
4979
+ z-index: var(--z-overlay);
4953
4980
  display: flex;
4954
4981
  align-items: center;
4955
4982
  justify-content: center;
@@ -5602,7 +5629,7 @@ body.resize-dragging {
5602
5629
  border-radius: 4px;
5603
5630
  max-height: 180px;
5604
5631
  overflow-y: auto;
5605
- z-index: 100;
5632
+ z-index: var(--z-sticky);
5606
5633
  width: 220px;
5607
5634
  }
5608
5635
  .mention-dropdown.hidden { display: none; }
@@ -6548,7 +6575,7 @@ body.resize-dragging {
6548
6575
  position: fixed;
6549
6576
  top: 12px;
6550
6577
  right: 12px;
6551
- z-index: 99999;
6578
+ z-index: var(--z-critical);
6552
6579
  display: flex;
6553
6580
  flex-direction: column;
6554
6581
  gap: 6px;
@@ -6836,3 +6863,65 @@ body.resize-dragging {
6836
6863
  .release-form {
6837
6864
  margin-top: 8px;
6838
6865
  }
6866
+
6867
+ /* ===== Responsive Breakpoints ===== */
6868
+
6869
+ /* Tablet: stack left panels above, roster collapses */
6870
+ @media (max-width: 1024px) {
6871
+ #app {
6872
+ grid-template-columns: 200px 1fr;
6873
+ grid-template-rows: auto 1fr auto;
6874
+ grid-template-areas:
6875
+ "left-top canvas"
6876
+ "left-bottom canvas"
6877
+ "console console";
6878
+ }
6879
+ #roster-panel {
6880
+ display: none;
6881
+ }
6882
+ .floating-panel {
6883
+ width: 280px !important;
6884
+ }
6885
+ }
6886
+
6887
+ /* Mobile: single column stack */
6888
+ @media (max-width: 768px) {
6889
+ #app {
6890
+ grid-template-columns: 1fr;
6891
+ grid-template-rows: auto auto 200px auto;
6892
+ grid-template-areas:
6893
+ "canvas"
6894
+ "console"
6895
+ "left-top"
6896
+ "left-bottom";
6897
+ height: auto;
6898
+ min-height: 100vh;
6899
+ }
6900
+ #roster-panel {
6901
+ display: none;
6902
+ }
6903
+ #office-panel .panel-header {
6904
+ flex-wrap: wrap;
6905
+ }
6906
+ #status-bar {
6907
+ flex-wrap: wrap;
6908
+ gap: 4px;
6909
+ }
6910
+ .floating-panel {
6911
+ position: fixed !important;
6912
+ left: 0 !important;
6913
+ right: 0 !important;
6914
+ top: auto !important;
6915
+ bottom: 0 !important;
6916
+ width: 100% !important;
6917
+ max-height: 60vh;
6918
+ border-radius: 8px 8px 0 0;
6919
+ }
6920
+ #ceo-split {
6921
+ flex-direction: column;
6922
+ }
6923
+ #ceo-project-list {
6924
+ max-height: 120px;
6925
+ overflow-y: auto;
6926
+ }
6927
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1mancompany/onemancompany",
3
- "version": "0.7.39",
3
+ "version": "0.7.44",
4
4
  "description": "The AI Operating System for One-Person Companies",
5
5
  "bin": {
6
6
  "onemancompany": "bin/cli.js"
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "onemancompany"
3
- version = "0.7.39"
3
+ version = "0.7.44"
4
4
  description = "A one-man company simulation with pixel art visualization and LangChain AI agents"
5
5
  requires-python = ">=3.12"
6
6
  dependencies = [
@@ -2659,9 +2659,10 @@ async def delete_employee_session(employee_id: str, project_id: str) -> dict:
2659
2659
  # ===== Company Culture =====
2660
2660
 
2661
2661
  @router.get("/api/company-culture")
2662
- async def get_company_culture() -> dict:
2662
+ async def get_company_culture(limit: int = 100, offset: int = 0) -> dict:
2663
2663
  """Get all company culture items."""
2664
- return {"items": _store.load_culture()}
2664
+ items = _store.load_culture()
2665
+ return {"items": items[offset:offset + limit], "total": len(items)}
2665
2666
 
2666
2667
 
2667
2668
  @router.post("/api/company-culture")
@@ -2846,10 +2847,11 @@ async def get_dashboard_costs() -> dict:
2846
2847
 
2847
2848
 
2848
2849
  @router.get("/api/projects")
2849
- async def get_projects() -> dict:
2850
+ async def get_projects(limit: int = 100, offset: int = 0) -> dict:
2850
2851
  """List all projects (v1 + v2 summary view for the project wall)."""
2851
2852
  from onemancompany.core.project_archive import list_projects
2852
- return {"projects": list_projects()}
2853
+ all_projects = list_projects()
2854
+ return {"projects": all_projects[offset:offset + limit], "total": len(all_projects)}
2853
2855
 
2854
2856
 
2855
2857
  @router.post("/api/projects")
@@ -3792,44 +3794,6 @@ async def get_project_file(project_id: str, file_path: str):
3792
3794
  return Response(content=content, media_type=media)
3793
3795
 
3794
3796
 
3795
- @router.get("/api/projects/{project_id}/download")
3796
- async def download_project_files(project_id: str):
3797
- """Download all project workspace files as a zip archive."""
3798
- import io
3799
- import zipfile
3800
- from pathlib import Path
3801
-
3802
- from fastapi.responses import StreamingResponse
3803
-
3804
- from onemancompany.core.project_archive import get_project_dir, list_project_files
3805
-
3806
- workspace = Path(get_project_dir(project_id))
3807
- if not workspace.exists():
3808
- raise HTTPException(status_code=404, detail="Project workspace not found")
3809
-
3810
- files = list_project_files(project_id)
3811
- if not files:
3812
- raise HTTPException(status_code=404, detail="No files to download")
3813
-
3814
- buf = io.BytesIO()
3815
- with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
3816
- for rel_path in files:
3817
- abs_path = workspace / rel_path
3818
- if abs_path.is_file():
3819
- zf.write(abs_path, rel_path)
3820
- buf.seek(0)
3821
-
3822
- slug = project_id.split("/")[0]
3823
- from urllib.parse import quote as _quote_url
3824
- _safe = slug.encode("ascii", "ignore").decode() or "project"
3825
- _enc = _quote_url(f"{slug}-files.zip", safe="")
3826
- return StreamingResponse(
3827
- buf,
3828
- media_type="application/zip",
3829
- headers={"Content-Disposition": f'attachment; filename="{_safe}-files.zip"; filename*=UTF-8\'\'{_enc}'},
3830
- )
3831
-
3832
-
3833
3797
  # ===== Employee Workspace =====
3834
3798
 
3835
3799
  @router.get("/api/employee/{employee_id}/workspace")
@@ -3970,10 +3934,11 @@ async def download_project_workspace(project_id: str):
3970
3934
  # ===== Ex-Employees =====
3971
3935
 
3972
3936
  @router.get("/api/ex-employees")
3973
- async def get_ex_employees() -> dict:
3937
+ async def get_ex_employees(limit: int = 100, offset: int = 0) -> dict:
3974
3938
  """List all ex-employees."""
3975
3939
  ex_emps = _store.load_ex_employees()
3976
- return {"ex_employees": list(ex_emps.values())}
3940
+ all_ex = list(ex_emps.values())
3941
+ return {"ex_employees": all_ex[offset:offset + limit], "total": len(all_ex)}
3977
3942
 
3978
3943
 
3979
3944
  @router.post("/api/ex-employees/{employee_id}/rehire")
@@ -6062,35 +6027,6 @@ async def get_room_chat(room_id: str):
6062
6027
  return load_room_chat(room_id)
6063
6028
 
6064
6029
 
6065
- @router.get("/api/rooms/{room_id}/minutes")
6066
- async def get_room_minutes(room_id: str):
6067
- """List archived meeting minutes for a room."""
6068
- from onemancompany.core.store import load_meeting_minutes
6069
- minutes = load_meeting_minutes(room_id)
6070
- # Return lightweight list (exclude full messages)
6071
- return [
6072
- {
6073
- "minute_id": m.get("minute_id", ""),
6074
- "topic": m.get("topic", ""),
6075
- "room_name": m.get("room_name", ""),
6076
- "participants": m.get("participants", []),
6077
- "summary": (m.get("summary", "") or "")[:200],
6078
- "message_count": len(m.get("messages", [])),
6079
- }
6080
- for m in minutes
6081
- ]
6082
-
6083
-
6084
- @router.get("/api/meeting-minutes/{minute_id}")
6085
- async def get_meeting_minute(minute_id: str):
6086
- """Get full content of a single meeting minute."""
6087
- from onemancompany.core.store import load_meeting_minute
6088
- data = load_meeting_minute(minute_id)
6089
- if not data:
6090
- raise HTTPException(status_code=404, detail="Meeting minute not found")
6091
- return data
6092
-
6093
-
6094
6030
  @router.post("/api/rooms/{room_id}/chat")
6095
6031
  async def post_room_chat(room_id: str, body: dict):
6096
6032
  """CEO sends a message to a meeting room chat.