@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 +50 -55
- package/frontend/index.html +25 -25
- package/frontend/style.css +102 -13
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/onemancompany/api/routes.py +9 -73
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
|
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
|
|
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) {
|
|
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()) {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
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
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
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
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
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
|
-
|
|
9071
|
-
if (rev.owner)
|
|
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
|
package/frontend/index.html
CHANGED
|
@@ -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
|
-
<
|
|
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">▼</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">↑</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">↑</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
|
-
</
|
|
26
|
+
</aside>
|
|
27
27
|
|
|
28
28
|
<!-- Left bottom: Activity Log -->
|
|
29
|
-
<
|
|
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">▼</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
|
-
</
|
|
37
|
+
</aside>
|
|
38
38
|
|
|
39
39
|
<!-- Center top: Office Canvas -->
|
|
40
|
-
<
|
|
40
|
+
<main id="office-panel">
|
|
41
41
|
<div class="panel-header">
|
|
42
42
|
<h1 class="pixel-title">🏢 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()">🔧 0</span>
|
|
46
46
|
<span class="status-item" id="room-count">🏢 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">📜</button>
|
|
49
|
-
<button id="company-culture-toolbar-btn" class="toolbar-icon-btn" title="Company Culture">🏴</button>
|
|
50
|
-
<button id="company-direction-toolbar-btn" class="toolbar-icon-btn" title="Company Direction">🎯</button>
|
|
51
|
-
<button id="dashboard-toolbar-btn" class="toolbar-icon-btn" title="Dashboard">📊</button>
|
|
52
|
-
<button id="announcements-toolbar-btn" class="toolbar-icon-btn" title="Announcements" style="position:relative;">🔔<span id="announcements-badge" class="announcement-badge hidden"></span></button>
|
|
53
|
-
<button id="settings-toolbar-btn" class="toolbar-icon-btn" title="Settings">⚙</button>
|
|
54
|
-
<span class="status-item" id="connection-status">● OFFLINE</span>
|
|
48
|
+
<button id="ex-employee-toolbar-btn" class="toolbar-icon-btn" title="Ex-Employee Wall" aria-label="Ex-Employee Wall">📜</button>
|
|
49
|
+
<button id="company-culture-toolbar-btn" class="toolbar-icon-btn" title="Company Culture" aria-label="Company Culture">🏴</button>
|
|
50
|
+
<button id="company-direction-toolbar-btn" class="toolbar-icon-btn" title="Company Direction" aria-label="Company Direction">🎯</button>
|
|
51
|
+
<button id="dashboard-toolbar-btn" class="toolbar-icon-btn" title="Dashboard" aria-label="Dashboard">📊</button>
|
|
52
|
+
<button id="announcements-toolbar-btn" class="toolbar-icon-btn" title="Announcements" aria-label="Announcements" style="position:relative;">🔔<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">⚙</button>
|
|
54
|
+
<span class="status-item" id="connection-status" role="status" aria-live="polite">● 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
|
-
</
|
|
60
|
+
</main>
|
|
61
61
|
|
|
62
62
|
<!-- Right top: Team Roster -->
|
|
63
|
-
<
|
|
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">▼</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
|
-
</
|
|
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">👑 YOU</span>
|
|
88
|
-
<button id="dnd-toggle-btn" class="toolbar-icon-btn" title="Do Not Disturb">🌙</button>
|
|
89
|
-
<button id="bg-tasks-toolbar-btn" class="toolbar-icon-btn" title="Background Tasks">▩</button>
|
|
90
|
-
<button id="screenshot-toolbar-btn" class="toolbar-icon-btn" title="Export SVG Screenshot">📷</button>
|
|
91
|
-
<button id="abort-all-toolbar-btn" class="toolbar-icon-btn" title="Stop All Tasks" style="color: #ff4444;">⚠</button>
|
|
92
|
-
<button id="reload-toolbar-btn" class="reload-btn" title="Force reload all data from disk">🔄</button>
|
|
88
|
+
<button id="dnd-toggle-btn" class="toolbar-icon-btn" title="Do Not Disturb" aria-label="Do Not Disturb">🌙</button>
|
|
89
|
+
<button id="bg-tasks-toolbar-btn" class="toolbar-icon-btn" title="Background Tasks" aria-label="Background Tasks">▩</button>
|
|
90
|
+
<button id="screenshot-toolbar-btn" class="toolbar-icon-btn" title="Export SVG Screenshot" aria-label="Export SVG Screenshot">📷</button>
|
|
91
|
+
<button id="abort-all-toolbar-btn" class="toolbar-icon-btn" title="Stop All Tasks" aria-label="Stop All Tasks" style="color: #ff4444;">⚠</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">🔄</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>
|
package/frontend/style.css
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
package/pyproject.toml
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|