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.
Files changed (34) hide show
  1. htmlgraph/__init__.py +1 -1
  2. htmlgraph/api/broadcast.py +316 -0
  3. htmlgraph/api/broadcast_routes.py +357 -0
  4. htmlgraph/api/broadcast_websocket.py +115 -0
  5. htmlgraph/api/cost_alerts_websocket.py +7 -16
  6. htmlgraph/api/main.py +135 -1
  7. htmlgraph/api/offline.py +776 -0
  8. htmlgraph/api/presence.py +446 -0
  9. htmlgraph/api/reactive.py +455 -0
  10. htmlgraph/api/reactive_routes.py +195 -0
  11. htmlgraph/api/static/broadcast-demo.html +393 -0
  12. htmlgraph/api/static/presence-widget-demo.html +785 -0
  13. htmlgraph/api/sync_routes.py +184 -0
  14. htmlgraph/api/templates/partials/agents.html +308 -80
  15. htmlgraph/api/websocket.py +112 -37
  16. htmlgraph/broadcast_integration.py +227 -0
  17. htmlgraph/cli_commands/sync.py +207 -0
  18. htmlgraph/db/schema.py +226 -0
  19. htmlgraph/hooks/event_tracker.py +53 -2
  20. htmlgraph/models.py +1 -0
  21. htmlgraph/reactive_integration.py +148 -0
  22. htmlgraph/session_manager.py +7 -0
  23. htmlgraph/sync/__init__.py +21 -0
  24. htmlgraph/sync/git_sync.py +458 -0
  25. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/METADATA +1 -1
  26. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/RECORD +32 -19
  27. htmlgraph/dashboard.html +0 -6592
  28. htmlgraph-0.27.7.data/data/htmlgraph/dashboard.html +0 -6592
  29. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/styles.css +0 -0
  30. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  31. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  32. {htmlgraph-0.27.7.data → htmlgraph-0.28.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  33. {htmlgraph-0.27.7.dist-info → htmlgraph-0.28.1.dist-info}/WHEEL +0 -0
  34. {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>