stakeout-cli 0.1.0
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/LICENSE +131 -0
- package/README.md +152 -0
- package/dist/commands/chat.d.ts +5 -0
- package/dist/commands/chat.js +162 -0
- package/dist/commands/clear.d.ts +7 -0
- package/dist/commands/clear.js +89 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.js +64 -0
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.js +9 -0
- package/dist/commands/digest.d.ts +6 -0
- package/dist/commands/digest.js +113 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +118 -0
- package/dist/commands/hook.d.ts +6 -0
- package/dist/commands/hook.js +57 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +70 -0
- package/dist/commands/log.d.ts +9 -0
- package/dist/commands/log.js +103 -0
- package/dist/commands/note.d.ts +9 -0
- package/dist/commands/note.js +48 -0
- package/dist/commands/record.d.ts +6 -0
- package/dist/commands/record.js +106 -0
- package/dist/commands/repo.d.ts +7 -0
- package/dist/commands/repo.js +60 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +69 -0
- package/dist/commands/stats.d.ts +1 -0
- package/dist/commands/stats.js +99 -0
- package/dist/commands/tag.d.ts +8 -0
- package/dist/commands/tag.js +61 -0
- package/dist/commands/tui.d.ts +1 -0
- package/dist/commands/tui.js +5 -0
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +101 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +195 -0
- package/dist/lib/config.d.ts +5 -0
- package/dist/lib/config.js +51 -0
- package/dist/lib/database.d.ts +31 -0
- package/dist/lib/database.js +222 -0
- package/dist/lib/diff.d.ts +3 -0
- package/dist/lib/diff.js +118 -0
- package/dist/lib/summarizer.d.ts +2 -0
- package/dist/lib/summarizer.js +90 -0
- package/dist/tui/App.d.ts +1 -0
- package/dist/tui/App.js +125 -0
- package/dist/types/index.d.ts +38 -0
- package/dist/types/index.js +1 -0
- package/dist/web/public/app.js +387 -0
- package/dist/web/public/index.html +131 -0
- package/dist/web/public/styles.css +571 -0
- package/dist/web/server.d.ts +1 -0
- package/dist/web/server.js +402 -0
- package/package.json +69 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface StakeoutEntry {
|
|
2
|
+
id?: number;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
files_changed: string[];
|
|
5
|
+
directories: string[];
|
|
6
|
+
summary: string;
|
|
7
|
+
diff_hash: string;
|
|
8
|
+
commit_hash?: string;
|
|
9
|
+
commit_message?: string;
|
|
10
|
+
tags?: string[];
|
|
11
|
+
favorite?: boolean;
|
|
12
|
+
notes?: string;
|
|
13
|
+
repo_path?: string;
|
|
14
|
+
is_breaking?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface StakeoutConfig {
|
|
17
|
+
llm_provider: 'ollama' | 'openai';
|
|
18
|
+
ollama_model: string;
|
|
19
|
+
ollama_host: string;
|
|
20
|
+
openai_api_key?: string;
|
|
21
|
+
openai_model: string;
|
|
22
|
+
watched_path: string;
|
|
23
|
+
ignore_patterns: string[];
|
|
24
|
+
repos?: string[];
|
|
25
|
+
breaking_patterns?: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface DiffResult {
|
|
28
|
+
files: string[];
|
|
29
|
+
directories: string[];
|
|
30
|
+
diff_text: string;
|
|
31
|
+
diff_hash: string;
|
|
32
|
+
commit_hash?: string;
|
|
33
|
+
commit_message?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface ChatMessage {
|
|
36
|
+
role: 'user' | 'assistant' | 'system';
|
|
37
|
+
content: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
const API = '';
|
|
2
|
+
|
|
3
|
+
// State
|
|
4
|
+
let entries = [];
|
|
5
|
+
let stats = {};
|
|
6
|
+
let searchMode = false;
|
|
7
|
+
|
|
8
|
+
// DOM Elements
|
|
9
|
+
const feed = document.getElementById('feed');
|
|
10
|
+
const feedTitle = document.getElementById('feed-title');
|
|
11
|
+
const btnRecord = document.getElementById('btn-record');
|
|
12
|
+
const btnRecordCommit = document.getElementById('btn-record-commit');
|
|
13
|
+
const btnRefresh = document.getElementById('btn-refresh');
|
|
14
|
+
const btnStats = document.getElementById('btn-stats');
|
|
15
|
+
const btnExport = document.getElementById('btn-export');
|
|
16
|
+
const btnSearch = document.getElementById('btn-search');
|
|
17
|
+
const searchInput = document.getElementById('search-input');
|
|
18
|
+
const filterPath = document.getElementById('filter-path');
|
|
19
|
+
const filterTime = document.getElementById('filter-time');
|
|
20
|
+
const toast = document.getElementById('toast');
|
|
21
|
+
|
|
22
|
+
// Modals
|
|
23
|
+
const statsModal = document.getElementById('stats-modal');
|
|
24
|
+
const exportModal = document.getElementById('export-modal');
|
|
25
|
+
|
|
26
|
+
// Initialize
|
|
27
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
28
|
+
loadStats();
|
|
29
|
+
loadEntries();
|
|
30
|
+
setupEventListeners();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function setupEventListeners() {
|
|
34
|
+
btnRecord.addEventListener('click', () => recordChanges(false));
|
|
35
|
+
btnRecordCommit.addEventListener('click', () => recordChanges(true));
|
|
36
|
+
btnRefresh.addEventListener('click', () => {
|
|
37
|
+
searchMode = false;
|
|
38
|
+
feedTitle.textContent = 'Intel Feed';
|
|
39
|
+
searchInput.value = '';
|
|
40
|
+
loadStats();
|
|
41
|
+
loadEntries();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Search
|
|
45
|
+
btnSearch.addEventListener('click', performSearch);
|
|
46
|
+
searchInput.addEventListener('keypress', (e) => {
|
|
47
|
+
if (e.key === 'Enter') performSearch();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Filters
|
|
51
|
+
filterPath.addEventListener('input', debounce(loadEntries, 300));
|
|
52
|
+
filterTime.addEventListener('change', loadEntries);
|
|
53
|
+
|
|
54
|
+
// Stats modal
|
|
55
|
+
btnStats.addEventListener('click', () => {
|
|
56
|
+
statsModal.classList.add('show');
|
|
57
|
+
loadDetailedStats();
|
|
58
|
+
});
|
|
59
|
+
document.getElementById('stats-close').addEventListener('click', () => {
|
|
60
|
+
statsModal.classList.remove('show');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Export modal
|
|
64
|
+
btnExport.addEventListener('click', () => {
|
|
65
|
+
exportModal.classList.add('show');
|
|
66
|
+
});
|
|
67
|
+
document.getElementById('export-close').addEventListener('click', () => {
|
|
68
|
+
exportModal.classList.remove('show');
|
|
69
|
+
});
|
|
70
|
+
document.getElementById('export-download').addEventListener('click', performExport);
|
|
71
|
+
|
|
72
|
+
// Close modals on backdrop click
|
|
73
|
+
statsModal.addEventListener('click', (e) => {
|
|
74
|
+
if (e.target === statsModal) statsModal.classList.remove('show');
|
|
75
|
+
});
|
|
76
|
+
exportModal.addEventListener('click', (e) => {
|
|
77
|
+
if (e.target === exportModal) exportModal.classList.remove('show');
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function loadStats() {
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${API}/api/stats`);
|
|
84
|
+
stats = await res.json();
|
|
85
|
+
|
|
86
|
+
document.getElementById('stat-total').textContent = stats.total;
|
|
87
|
+
document.getElementById('stat-today').textContent = stats.today;
|
|
88
|
+
document.getElementById('stat-week').textContent = stats.thisWeek;
|
|
89
|
+
|
|
90
|
+
drawActivityChart(stats.activity || []);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('Failed to load stats:', err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadDetailedStats() {
|
|
97
|
+
const content = document.getElementById('stats-content');
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(`${API}/api/stats/detailed`);
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
|
|
103
|
+
content.innerHTML = `
|
|
104
|
+
<div class="stats-section">
|
|
105
|
+
<h4>Overview</h4>
|
|
106
|
+
<div class="stat-row"><span class="label">Total Entries</span><span class="value">${data.total}</span></div>
|
|
107
|
+
<div class="stat-row"><span class="label">Today</span><span class="value">${data.today}</span></div>
|
|
108
|
+
<div class="stat-row"><span class="label">This Week</span><span class="value">${data.thisWeek}</span></div>
|
|
109
|
+
<div class="stat-row"><span class="label">This Month</span><span class="value">${data.thisMonth}</span></div>
|
|
110
|
+
${data.firstEntry ? `<div class="stat-row"><span class="label">First Entry</span><span class="value">${new Date(data.firstEntry).toLocaleDateString()}</span></div>` : ''}
|
|
111
|
+
</div>
|
|
112
|
+
<div class="stats-section">
|
|
113
|
+
<h4>Top Directories</h4>
|
|
114
|
+
<div class="bar-chart">
|
|
115
|
+
${data.topDirs.slice(0, 5).map(([dir, count], i) => `
|
|
116
|
+
<div class="bar-row">
|
|
117
|
+
<span class="bar-label">${dir.slice(0, 10)}</span>
|
|
118
|
+
<div class="bar" style="width: ${(count / data.topDirs[0][1]) * 100}px"></div>
|
|
119
|
+
<span class="bar-value">${count}</span>
|
|
120
|
+
</div>
|
|
121
|
+
`).join('')}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="stats-section">
|
|
125
|
+
<h4>Activity by Day</h4>
|
|
126
|
+
<div class="bar-chart">
|
|
127
|
+
${data.byDayOfWeek.map(({ day, count }) => `
|
|
128
|
+
<div class="bar-row">
|
|
129
|
+
<span class="bar-label">${day}</span>
|
|
130
|
+
<div class="bar" style="width: ${(count / Math.max(...data.byDayOfWeek.map(d => d.count))) * 100}px"></div>
|
|
131
|
+
<span class="bar-value">${count}</span>
|
|
132
|
+
</div>
|
|
133
|
+
`).join('')}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="stats-section">
|
|
137
|
+
<h4>Peak Hours</h4>
|
|
138
|
+
<div class="bar-chart">
|
|
139
|
+
${data.peakHours.slice(0, 5).map(({ hour, count }) => `
|
|
140
|
+
<div class="bar-row">
|
|
141
|
+
<span class="bar-label">${hour.toString().padStart(2, '0')}:00</span>
|
|
142
|
+
<div class="bar" style="width: ${(count / data.peakHours[0].count) * 100}px"></div>
|
|
143
|
+
<span class="bar-value">${count}</span>
|
|
144
|
+
</div>
|
|
145
|
+
`).join('')}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
`;
|
|
149
|
+
} catch (err) {
|
|
150
|
+
content.innerHTML = '<p>Failed to load detailed stats</p>';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function loadEntries() {
|
|
155
|
+
if (searchMode) return;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const params = new URLSearchParams();
|
|
159
|
+
params.set('limit', '50');
|
|
160
|
+
|
|
161
|
+
if (filterPath.value) {
|
|
162
|
+
params.set('path', filterPath.value);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (filterTime.value) {
|
|
166
|
+
const since = new Date();
|
|
167
|
+
since.setDate(since.getDate() - parseInt(filterTime.value));
|
|
168
|
+
params.set('since', since.toISOString());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const res = await fetch(`${API}/api/entries?${params}`);
|
|
172
|
+
const data = await res.json();
|
|
173
|
+
entries = data.entries;
|
|
174
|
+
|
|
175
|
+
renderFeed();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('Failed to load entries:', err);
|
|
178
|
+
feed.innerHTML = '<div class="empty"><div class="empty-icon">⚠️</div>Failed to load intel</div>';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function performSearch() {
|
|
183
|
+
const query = searchInput.value.trim();
|
|
184
|
+
if (!query) {
|
|
185
|
+
searchMode = false;
|
|
186
|
+
feedTitle.textContent = 'Intel Feed';
|
|
187
|
+
loadEntries();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
searchMode = true;
|
|
192
|
+
feedTitle.textContent = `Search: "${query}"`;
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const res = await fetch(`${API}/api/search?q=${encodeURIComponent(query)}`);
|
|
196
|
+
const data = await res.json();
|
|
197
|
+
entries = data.entries;
|
|
198
|
+
renderFeed(query);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error('Search failed:', err);
|
|
201
|
+
showToast('Search failed', 'error');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderFeed(highlightQuery = null) {
|
|
206
|
+
if (entries.length === 0) {
|
|
207
|
+
feed.innerHTML = `
|
|
208
|
+
<div class="empty">
|
|
209
|
+
<div class="empty-icon">🔍</div>
|
|
210
|
+
<p>${searchMode ? 'No results found' : 'No intel recorded yet'}</p>
|
|
211
|
+
${!searchMode ? '<p style="margin-top: 8px; font-size: 12px;">Click "Record Changes" to start tracking</p>' : ''}
|
|
212
|
+
</div>
|
|
213
|
+
`;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
feed.innerHTML = entries.map(entry => {
|
|
218
|
+
let summary = escapeHtml(entry.summary);
|
|
219
|
+
if (highlightQuery) {
|
|
220
|
+
const regex = new RegExp(`(${escapeRegex(highlightQuery)})`, 'gi');
|
|
221
|
+
summary = summary.replace(regex, '<span class="highlight">$1</span>');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `
|
|
225
|
+
<div class="entry">
|
|
226
|
+
<div class="entry-header">
|
|
227
|
+
<div class="entry-meta">
|
|
228
|
+
<span class="entry-id">#${entry.id}</span>
|
|
229
|
+
<span class="entry-time">${formatTime(entry.timestamp)}</span>
|
|
230
|
+
${entry.commit_hash ? `<span class="entry-commit">${entry.commit_hash.slice(0, 7)}</span>` : ''}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
<div class="entry-summary">${summary}</div>
|
|
234
|
+
${entry.directories.length > 0 ? `
|
|
235
|
+
<div class="entry-dirs">
|
|
236
|
+
${entry.directories.map(d => `<span class="entry-dir">📁 ${d}</span>`).join('')}
|
|
237
|
+
</div>
|
|
238
|
+
` : ''}
|
|
239
|
+
<div class="entry-files">
|
|
240
|
+
${entry.files_changed.slice(0, 5).map(f => `<span class="entry-file">${f}</span>`).join('')}
|
|
241
|
+
${entry.files_changed.length > 5 ? `<span class="entry-file">+${entry.files_changed.length - 5} more</span>` : ''}
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
`;
|
|
245
|
+
}).join('');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function recordChanges(lastCommit) {
|
|
249
|
+
const btn = lastCommit ? btnRecordCommit : btnRecord;
|
|
250
|
+
const originalText = btn.innerHTML;
|
|
251
|
+
|
|
252
|
+
btn.disabled = true;
|
|
253
|
+
btn.innerHTML = '<span class="btn-icon">⏳</span> Recording...';
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const res = await fetch(`${API}/api/record`, {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ lastCommit })
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const data = await res.json();
|
|
263
|
+
|
|
264
|
+
if (data.success) {
|
|
265
|
+
showToast('Intel recorded successfully', 'success');
|
|
266
|
+
loadStats();
|
|
267
|
+
loadEntries();
|
|
268
|
+
} else {
|
|
269
|
+
showToast(data.message || 'No changes detected', 'info');
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
showToast('Failed to record: ' + err.message, 'error');
|
|
273
|
+
} finally {
|
|
274
|
+
btn.disabled = false;
|
|
275
|
+
btn.innerHTML = originalText;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function performExport() {
|
|
280
|
+
const format = document.getElementById('export-format').value;
|
|
281
|
+
const range = document.getElementById('export-range').value;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const params = new URLSearchParams();
|
|
285
|
+
params.set('format', format);
|
|
286
|
+
if (range) {
|
|
287
|
+
const since = new Date();
|
|
288
|
+
since.setDate(since.getDate() - parseInt(range));
|
|
289
|
+
params.set('since', since.toISOString());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const res = await fetch(`${API}/api/export?${params}`);
|
|
293
|
+
const blob = await res.blob();
|
|
294
|
+
|
|
295
|
+
const ext = format === 'json' ? 'json' : format === 'csv' ? 'csv' : 'md';
|
|
296
|
+
const url = URL.createObjectURL(blob);
|
|
297
|
+
const a = document.createElement('a');
|
|
298
|
+
a.href = url;
|
|
299
|
+
a.download = `stakeout-export-${Date.now()}.${ext}`;
|
|
300
|
+
a.click();
|
|
301
|
+
URL.revokeObjectURL(url);
|
|
302
|
+
|
|
303
|
+
exportModal.classList.remove('show');
|
|
304
|
+
showToast('Export downloaded', 'success');
|
|
305
|
+
} catch (err) {
|
|
306
|
+
showToast('Export failed: ' + err.message, 'error');
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function drawActivityChart(activity) {
|
|
311
|
+
const canvas = document.getElementById('activity-chart');
|
|
312
|
+
const ctx = canvas.getContext('2d');
|
|
313
|
+
|
|
314
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
315
|
+
|
|
316
|
+
if (activity.length === 0) {
|
|
317
|
+
ctx.fillStyle = '#333';
|
|
318
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const maxCount = Math.max(...activity.map(a => a.count), 1);
|
|
323
|
+
const barWidth = canvas.width / 14;
|
|
324
|
+
const padding = 2;
|
|
325
|
+
|
|
326
|
+
const dateMap = new Map(activity.map(a => [a.date, a.count]));
|
|
327
|
+
|
|
328
|
+
const days = [];
|
|
329
|
+
for (let i = 13; i >= 0; i--) {
|
|
330
|
+
const date = new Date();
|
|
331
|
+
date.setDate(date.getDate() - i);
|
|
332
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
333
|
+
days.push({ date: dateStr, count: dateMap.get(dateStr) || 0 });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
days.forEach((day, i) => {
|
|
337
|
+
const height = (day.count / maxCount) * (canvas.height - 4);
|
|
338
|
+
const x = i * barWidth + padding;
|
|
339
|
+
const y = canvas.height - height - 2;
|
|
340
|
+
|
|
341
|
+
ctx.fillStyle = day.count > 0 ? '#00ff88' : '#2a2a35';
|
|
342
|
+
ctx.fillRect(x, y, barWidth - padding * 2, height || 2);
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function formatTime(timestamp) {
|
|
347
|
+
const date = new Date(timestamp);
|
|
348
|
+
const now = new Date();
|
|
349
|
+
const diffMs = now - date;
|
|
350
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
351
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
352
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
353
|
+
|
|
354
|
+
if (diffMins < 1) return 'just now';
|
|
355
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
356
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
357
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
358
|
+
|
|
359
|
+
return date.toLocaleDateString();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function escapeHtml(text) {
|
|
363
|
+
const div = document.createElement('div');
|
|
364
|
+
div.textContent = text;
|
|
365
|
+
return div.innerHTML;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function escapeRegex(str) {
|
|
369
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function showToast(message, type = 'info') {
|
|
373
|
+
toast.textContent = message;
|
|
374
|
+
toast.className = `toast show ${type}`;
|
|
375
|
+
|
|
376
|
+
setTimeout(() => {
|
|
377
|
+
toast.className = 'toast';
|
|
378
|
+
}, 3000);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function debounce(fn, ms) {
|
|
382
|
+
let timeout;
|
|
383
|
+
return (...args) => {
|
|
384
|
+
clearTimeout(timeout);
|
|
385
|
+
timeout = setTimeout(() => fn(...args), ms);
|
|
386
|
+
};
|
|
387
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>STAKEOUT - Command Center</title>
|
|
7
|
+
<link rel="stylesheet" href="styles.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="container">
|
|
11
|
+
<header>
|
|
12
|
+
<div class="logo">
|
|
13
|
+
<span class="logo-icon">🔍</span>
|
|
14
|
+
<h1>STAKEOUT</h1>
|
|
15
|
+
<span class="tagline">Command Center</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="header-actions">
|
|
18
|
+
<button id="btn-stats" class="btn btn-ghost btn-sm">📊 Stats</button>
|
|
19
|
+
<button id="btn-export" class="btn btn-ghost btn-sm">📤 Export</button>
|
|
20
|
+
<div class="status">
|
|
21
|
+
<span class="status-dot"></span>
|
|
22
|
+
<span id="status-text">ONLINE</span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</header>
|
|
26
|
+
|
|
27
|
+
<div class="stats-bar">
|
|
28
|
+
<div class="stat">
|
|
29
|
+
<span class="stat-value" id="stat-total">-</span>
|
|
30
|
+
<span class="stat-label">Total Intel</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="stat">
|
|
33
|
+
<span class="stat-value" id="stat-today">-</span>
|
|
34
|
+
<span class="stat-label">Today</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="stat">
|
|
37
|
+
<span class="stat-value" id="stat-week">-</span>
|
|
38
|
+
<span class="stat-label">This Week</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="stat chart-stat">
|
|
41
|
+
<canvas id="activity-chart" width="200" height="40"></canvas>
|
|
42
|
+
<span class="stat-label">14-Day Activity</span>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="search-bar">
|
|
47
|
+
<input type="text" id="search-input" placeholder="🔍 Search summaries, files, directories..." />
|
|
48
|
+
<button id="btn-search" class="btn btn-primary btn-sm">Search</button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="actions">
|
|
52
|
+
<button id="btn-record" class="btn btn-primary">
|
|
53
|
+
<span class="btn-icon">⚡</span>
|
|
54
|
+
Record Changes
|
|
55
|
+
</button>
|
|
56
|
+
<button id="btn-record-commit" class="btn btn-secondary">
|
|
57
|
+
<span class="btn-icon">📝</span>
|
|
58
|
+
Record Last Commit
|
|
59
|
+
</button>
|
|
60
|
+
<button id="btn-refresh" class="btn btn-ghost">
|
|
61
|
+
<span class="btn-icon">🔄</span>
|
|
62
|
+
Refresh
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div class="feed-header">
|
|
67
|
+
<h2 id="feed-title">Intel Feed</h2>
|
|
68
|
+
<div class="feed-filters">
|
|
69
|
+
<input type="text" id="filter-path" placeholder="Filter by path..." />
|
|
70
|
+
<select id="filter-time">
|
|
71
|
+
<option value="">All time</option>
|
|
72
|
+
<option value="1">Last 24 hours</option>
|
|
73
|
+
<option value="7">Last 7 days</option>
|
|
74
|
+
<option value="30">Last 30 days</option>
|
|
75
|
+
</select>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div id="feed" class="feed">
|
|
80
|
+
<div class="loading">Loading intel...</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div id="toast" class="toast"></div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Stats Modal -->
|
|
87
|
+
<div id="stats-modal" class="modal">
|
|
88
|
+
<div class="modal-content modal-wide">
|
|
89
|
+
<div class="modal-header">
|
|
90
|
+
<h3>📊 Statistics</h3>
|
|
91
|
+
<button id="stats-close" class="btn btn-ghost btn-sm">✕</button>
|
|
92
|
+
</div>
|
|
93
|
+
<div id="stats-content" class="stats-grid">
|
|
94
|
+
Loading...
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<!-- Export Modal -->
|
|
100
|
+
<div id="export-modal" class="modal">
|
|
101
|
+
<div class="modal-content">
|
|
102
|
+
<h3>📤 Export Data</h3>
|
|
103
|
+
<div class="config-form">
|
|
104
|
+
<label>
|
|
105
|
+
Format
|
|
106
|
+
<select id="export-format">
|
|
107
|
+
<option value="markdown">Markdown</option>
|
|
108
|
+
<option value="json">JSON</option>
|
|
109
|
+
<option value="csv">CSV</option>
|
|
110
|
+
</select>
|
|
111
|
+
</label>
|
|
112
|
+
<label>
|
|
113
|
+
Time Range
|
|
114
|
+
<select id="export-range">
|
|
115
|
+
<option value="">All time</option>
|
|
116
|
+
<option value="7">Last 7 days</option>
|
|
117
|
+
<option value="30">Last 30 days</option>
|
|
118
|
+
<option value="90">Last 90 days</option>
|
|
119
|
+
</select>
|
|
120
|
+
</label>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="modal-actions">
|
|
123
|
+
<button id="export-download" class="btn btn-primary">Download</button>
|
|
124
|
+
<button id="export-close" class="btn btn-ghost">Cancel</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<script src="app.js"></script>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|