htmlgraph 0.27.7__py3-none-any.whl → 0.28.1__py3-none-any.whl
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.
- htmlgraph/__init__.py +1 -1
- htmlgraph/api/broadcast.py +316 -0
- htmlgraph/api/broadcast_routes.py +357 -0
- htmlgraph/api/broadcast_websocket.py +115 -0
- htmlgraph/api/cost_alerts_websocket.py +7 -16
- htmlgraph/api/main.py +135 -1
- htmlgraph/api/offline.py +776 -0
- htmlgraph/api/presence.py +446 -0
- htmlgraph/api/reactive.py +455 -0
- htmlgraph/api/reactive_routes.py +195 -0
- htmlgraph/api/static/broadcast-demo.html +393 -0
- htmlgraph/api/static/presence-widget-demo.html +785 -0
- htmlgraph/api/sync_routes.py +184 -0
- htmlgraph/api/templates/partials/agents.html +308 -80
- htmlgraph/api/websocket.py +112 -37
- htmlgraph/broadcast_integration.py +227 -0
- htmlgraph/cli_commands/sync.py +207 -0
- htmlgraph/db/schema.py +226 -0
- htmlgraph/hooks/event_tracker.py +53 -2
- htmlgraph/models.py +1 -0
- htmlgraph/reactive_integration.py +148 -0
- htmlgraph/session_manager.py +7 -0
- htmlgraph/sync/__init__.py +21 -0
- htmlgraph/sync/git_sync.py +458 -0
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/METADATA +1 -1
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/RECORD +32 -19
- htmlgraph/dashboard.html +0 -6592
- htmlgraph-0.27.7.data/data/htmlgraph/dashboard.html +0 -6592
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/WHEEL +0 -0
- {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,393 @@
|
|
|
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>Cross-Session Broadcast Demo</title>
|
|
7
|
+
<style>
|
|
8
|
+
body {
|
|
9
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
10
|
+
max-width: 1200px;
|
|
11
|
+
margin: 0 auto;
|
|
12
|
+
padding: 20px;
|
|
13
|
+
background: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.container {
|
|
17
|
+
display: grid;
|
|
18
|
+
grid-template-columns: 1fr 1fr;
|
|
19
|
+
gap: 20px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.panel {
|
|
23
|
+
background: white;
|
|
24
|
+
border-radius: 8px;
|
|
25
|
+
padding: 20px;
|
|
26
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
h2 {
|
|
30
|
+
margin-top: 0;
|
|
31
|
+
color: #333;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.status {
|
|
35
|
+
display: inline-block;
|
|
36
|
+
padding: 4px 12px;
|
|
37
|
+
border-radius: 12px;
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
margin-left: 10px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.status.connected {
|
|
44
|
+
background: #10b981;
|
|
45
|
+
color: white;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.status.disconnected {
|
|
49
|
+
background: #ef4444;
|
|
50
|
+
color: white;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.event {
|
|
54
|
+
padding: 12px;
|
|
55
|
+
margin: 8px 0;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
border-left: 4px solid #3b82f6;
|
|
58
|
+
background: #eff6ff;
|
|
59
|
+
animation: slideIn 0.3s ease;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@keyframes slideIn {
|
|
63
|
+
from {
|
|
64
|
+
transform: translateX(-20px);
|
|
65
|
+
opacity: 0;
|
|
66
|
+
}
|
|
67
|
+
to {
|
|
68
|
+
transform: translateX(0);
|
|
69
|
+
opacity: 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.event-type {
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
color: #1e40af;
|
|
76
|
+
margin-bottom: 4px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.event-details {
|
|
80
|
+
font-size: 14px;
|
|
81
|
+
color: #64748b;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.timestamp {
|
|
85
|
+
font-size: 12px;
|
|
86
|
+
color: #94a3b8;
|
|
87
|
+
margin-top: 4px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.feature-card {
|
|
91
|
+
padding: 16px;
|
|
92
|
+
margin: 12px 0;
|
|
93
|
+
border-radius: 8px;
|
|
94
|
+
border: 1px solid #e2e8f0;
|
|
95
|
+
background: white;
|
|
96
|
+
transition: all 0.3s ease;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.feature-card.updated {
|
|
100
|
+
border-color: #10b981;
|
|
101
|
+
background: #ecfdf5;
|
|
102
|
+
animation: pulse 0.5s ease;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@keyframes pulse {
|
|
106
|
+
0%, 100% { transform: scale(1); }
|
|
107
|
+
50% { transform: scale(1.02); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.feature-title {
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
margin-bottom: 8px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.feature-status {
|
|
116
|
+
display: inline-block;
|
|
117
|
+
padding: 4px 8px;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
font-weight: 500;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.feature-status.todo { background: #e0e7ff; color: #4338ca; }
|
|
124
|
+
.feature-status.in_progress { background: #fef3c7; color: #92400e; }
|
|
125
|
+
.feature-status.done { background: #d1fae5; color: #065f46; }
|
|
126
|
+
.feature-status.blocked { background: #fee2e2; color: #991b1b; }
|
|
127
|
+
|
|
128
|
+
button {
|
|
129
|
+
padding: 8px 16px;
|
|
130
|
+
background: #3b82f6;
|
|
131
|
+
color: white;
|
|
132
|
+
border: none;
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
font-size: 14px;
|
|
136
|
+
margin: 4px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
button:hover {
|
|
140
|
+
background: #2563eb;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.controls {
|
|
144
|
+
margin: 20px 0;
|
|
145
|
+
padding: 15px;
|
|
146
|
+
background: #f8fafc;
|
|
147
|
+
border-radius: 4px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.notification {
|
|
151
|
+
position: fixed;
|
|
152
|
+
top: 20px;
|
|
153
|
+
right: 20px;
|
|
154
|
+
background: #10b981;
|
|
155
|
+
color: white;
|
|
156
|
+
padding: 16px 24px;
|
|
157
|
+
border-radius: 8px;
|
|
158
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
159
|
+
animation: slideInRight 0.3s ease;
|
|
160
|
+
z-index: 1000;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@keyframes slideInRight {
|
|
164
|
+
from {
|
|
165
|
+
transform: translateX(400px);
|
|
166
|
+
opacity: 0;
|
|
167
|
+
}
|
|
168
|
+
to {
|
|
169
|
+
transform: translateX(0);
|
|
170
|
+
opacity: 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.event-feed {
|
|
175
|
+
max-height: 500px;
|
|
176
|
+
overflow-y: auto;
|
|
177
|
+
}
|
|
178
|
+
</style>
|
|
179
|
+
</head>
|
|
180
|
+
<body>
|
|
181
|
+
<h1>Cross-Session Broadcast Demo</h1>
|
|
182
|
+
<p>Demonstrates real-time feature updates across multiple sessions</p>
|
|
183
|
+
|
|
184
|
+
<div class="controls">
|
|
185
|
+
<strong>Simulate Updates:</strong>
|
|
186
|
+
<button onclick="simulateFeatureUpdate()">Update Feature</button>
|
|
187
|
+
<button onclick="simulateStatusChange()">Change Status</button>
|
|
188
|
+
<button onclick="simulateLinkAdd()">Add Link</button>
|
|
189
|
+
<button onclick="simulateCreate()">Create Feature</button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<div class="container">
|
|
193
|
+
<div class="panel">
|
|
194
|
+
<h2>
|
|
195
|
+
Live Features
|
|
196
|
+
<span class="status" id="ws-status">Disconnected</span>
|
|
197
|
+
</h2>
|
|
198
|
+
<div id="features"></div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div class="panel">
|
|
202
|
+
<h2>Broadcast Events</h2>
|
|
203
|
+
<div class="event-feed" id="event-feed"></div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<script>
|
|
208
|
+
let ws = null;
|
|
209
|
+
let features = new Map();
|
|
210
|
+
const maxEvents = 20;
|
|
211
|
+
|
|
212
|
+
// Initialize features
|
|
213
|
+
features.set('feat-123', { title: 'User Authentication', status: 'in_progress' });
|
|
214
|
+
features.set('feat-456', { title: 'Dashboard API', status: 'todo' });
|
|
215
|
+
features.set('feat-789', { title: 'WebSocket Support', status: 'done' });
|
|
216
|
+
|
|
217
|
+
function connectWebSocket() {
|
|
218
|
+
// Connect to broadcast WebSocket endpoint
|
|
219
|
+
ws = new WebSocket('ws://localhost:8000/ws/broadcasts');
|
|
220
|
+
|
|
221
|
+
ws.onopen = () => {
|
|
222
|
+
console.log('WebSocket connected');
|
|
223
|
+
document.getElementById('ws-status').className = 'status connected';
|
|
224
|
+
document.getElementById('ws-status').textContent = 'Connected';
|
|
225
|
+
|
|
226
|
+
// Send subscribe message
|
|
227
|
+
ws.send('subscribe');
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
ws.onmessage = (event) => {
|
|
231
|
+
const msg = JSON.parse(event.data);
|
|
232
|
+
console.log('Received message:', msg);
|
|
233
|
+
|
|
234
|
+
if (msg.type === 'broadcast_event') {
|
|
235
|
+
handleBroadcastEvent(msg);
|
|
236
|
+
} else if (msg.type === 'subscribed') {
|
|
237
|
+
addEventToFeed('info', 'Connected to broadcast channel', msg.message);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
ws.onclose = () => {
|
|
242
|
+
console.log('WebSocket disconnected');
|
|
243
|
+
document.getElementById('ws-status').className = 'status disconnected';
|
|
244
|
+
document.getElementById('ws-status').textContent = 'Disconnected';
|
|
245
|
+
|
|
246
|
+
// Reconnect after 3 seconds
|
|
247
|
+
setTimeout(connectWebSocket, 3000);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
ws.onerror = (error) => {
|
|
251
|
+
console.error('WebSocket error:', error);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleBroadcastEvent(msg) {
|
|
256
|
+
const eventType = msg.event_type;
|
|
257
|
+
const resourceId = msg.resource_id;
|
|
258
|
+
const agentId = msg.agent_id;
|
|
259
|
+
const payload = msg.payload;
|
|
260
|
+
|
|
261
|
+
console.log(`Broadcast: ${eventType} for ${resourceId} by ${agentId}`);
|
|
262
|
+
|
|
263
|
+
// Update UI based on event type
|
|
264
|
+
if (eventType === 'feature_updated') {
|
|
265
|
+
updateFeature(resourceId, payload);
|
|
266
|
+
showNotification(`Feature ${resourceId} updated by ${agentId}`);
|
|
267
|
+
addEventToFeed('update', `Feature Updated: ${resourceId}`,
|
|
268
|
+
`Agent: ${agentId}, Changes: ${JSON.stringify(payload)}`);
|
|
269
|
+
} else if (eventType === 'feature_created') {
|
|
270
|
+
createFeature(resourceId, payload);
|
|
271
|
+
showNotification(`New feature ${resourceId} created by ${agentId}`);
|
|
272
|
+
addEventToFeed('create', `Feature Created: ${resourceId}`,
|
|
273
|
+
`Agent: ${agentId}, Title: ${payload.title}`);
|
|
274
|
+
} else if (eventType === 'status_changed') {
|
|
275
|
+
updateFeatureStatus(resourceId, payload.new_status);
|
|
276
|
+
showNotification(`Status changed: ${payload.old_status} → ${payload.new_status}`);
|
|
277
|
+
addEventToFeed('status', `Status Changed: ${resourceId}`,
|
|
278
|
+
`${payload.old_status} → ${payload.new_status} by ${agentId}`);
|
|
279
|
+
} else if (eventType === 'link_added') {
|
|
280
|
+
showNotification(`Link added: ${resourceId} → ${payload.linked_feature_id}`);
|
|
281
|
+
addEventToFeed('link', `Link Added`,
|
|
282
|
+
`${resourceId} ${payload.link_type} ${payload.linked_feature_id}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
renderFeatures();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function updateFeature(featureId, payload) {
|
|
289
|
+
if (!features.has(featureId)) {
|
|
290
|
+
features.set(featureId, {});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const feature = features.get(featureId);
|
|
294
|
+
Object.assign(feature, payload);
|
|
295
|
+
|
|
296
|
+
// Highlight updated card
|
|
297
|
+
setTimeout(() => {
|
|
298
|
+
const card = document.querySelector(`[data-feature-id="${featureId}"]`);
|
|
299
|
+
if (card) {
|
|
300
|
+
card.classList.add('updated');
|
|
301
|
+
setTimeout(() => card.classList.remove('updated'), 1000);
|
|
302
|
+
}
|
|
303
|
+
}, 100);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function createFeature(featureId, payload) {
|
|
307
|
+
features.set(featureId, payload);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function updateFeatureStatus(featureId, newStatus) {
|
|
311
|
+
if (features.has(featureId)) {
|
|
312
|
+
features.get(featureId).status = newStatus;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function renderFeatures() {
|
|
317
|
+
const container = document.getElementById('features');
|
|
318
|
+
container.innerHTML = '';
|
|
319
|
+
|
|
320
|
+
features.forEach((feature, id) => {
|
|
321
|
+
const card = document.createElement('div');
|
|
322
|
+
card.className = 'feature-card';
|
|
323
|
+
card.setAttribute('data-feature-id', id);
|
|
324
|
+
|
|
325
|
+
card.innerHTML = `
|
|
326
|
+
<div class="feature-title">${feature.title || id}</div>
|
|
327
|
+
<span class="feature-status ${feature.status}">${feature.status || 'todo'}</span>
|
|
328
|
+
`;
|
|
329
|
+
|
|
330
|
+
container.appendChild(card);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function addEventToFeed(type, title, details) {
|
|
335
|
+
const feed = document.getElementById('event-feed');
|
|
336
|
+
|
|
337
|
+
const event = document.createElement('div');
|
|
338
|
+
event.className = 'event';
|
|
339
|
+
event.innerHTML = `
|
|
340
|
+
<div class="event-type">${title}</div>
|
|
341
|
+
<div class="event-details">${details}</div>
|
|
342
|
+
<div class="timestamp">${new Date().toLocaleTimeString()}</div>
|
|
343
|
+
`;
|
|
344
|
+
|
|
345
|
+
feed.insertBefore(event, feed.firstChild);
|
|
346
|
+
|
|
347
|
+
// Keep only last N events
|
|
348
|
+
while (feed.children.length > maxEvents) {
|
|
349
|
+
feed.removeChild(feed.lastChild);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function showNotification(message) {
|
|
354
|
+
const notification = document.createElement('div');
|
|
355
|
+
notification.className = 'notification';
|
|
356
|
+
notification.textContent = message;
|
|
357
|
+
document.body.appendChild(notification);
|
|
358
|
+
|
|
359
|
+
setTimeout(() => notification.remove(), 3000);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Simulation functions for demo
|
|
363
|
+
function simulateFeatureUpdate() {
|
|
364
|
+
const featureId = 'feat-123';
|
|
365
|
+
updateFeature(featureId, { title: 'User Authentication (Updated)', status: 'in_progress' });
|
|
366
|
+
addEventToFeed('update', 'Simulated Feature Update', `Updated ${featureId}`);
|
|
367
|
+
renderFeatures();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function simulateStatusChange() {
|
|
371
|
+
const featureId = 'feat-456';
|
|
372
|
+
updateFeatureStatus(featureId, 'in_progress');
|
|
373
|
+
addEventToFeed('status', 'Simulated Status Change', `${featureId}: todo → in_progress`);
|
|
374
|
+
renderFeatures();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function simulateLinkAdd() {
|
|
378
|
+
addEventToFeed('link', 'Simulated Link Add', 'feat-123 depends_on feat-456');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function simulateCreate() {
|
|
382
|
+
const newId = `feat-${Math.random().toString(36).substr(2, 9)}`;
|
|
383
|
+
createFeature(newId, { title: 'New Feature', status: 'todo' });
|
|
384
|
+
addEventToFeed('create', 'Simulated Feature Create', `Created ${newId}`);
|
|
385
|
+
renderFeatures();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Initialize
|
|
389
|
+
renderFeatures();
|
|
390
|
+
connectWebSocket();
|
|
391
|
+
</script>
|
|
392
|
+
</body>
|
|
393
|
+
</html>
|