Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions positronic/server/positronic_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ async def cache_rerun_assets(request: Request, call_next):
return response


def _make_serializable(obj):
"""Ensure obj is JSON serializable (e.g. convert datetime)."""
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, dict):
return {k: _make_serializable(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_make_serializable(v) for v in obj]
return obj


def _iter_file_chunks(path: str, *, chunk_size: int = 128 * 1024):
with open(path, 'rb') as source:
while True:
Expand Down Expand Up @@ -188,16 +199,6 @@ async def episode_viewer(request: Request, episode_id: int):
size_mb = meta.get('size_mb')
size_mb_display = f'{size_mb:.2f}' if isinstance(size_mb, int | float) else None

# Ensure static_data is JSON serializable (e.g. handle datetime)
def _make_serializable(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, dict):
return {k: _make_serializable(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_make_serializable(v) for v in obj]
return obj

return templates.TemplateResponse(
'episode.html',
{
Expand Down Expand Up @@ -428,6 +429,28 @@ async def api_dataset_status():
}


@app.get('/api/episode/{episode_id}')
@require_dataset
async def api_episode(episode_id: int):
ds = app_state.get('dataset')
try:
episode = ds[episode_id]
except IndexError as e:
raise HTTPException(status_code=404, detail='Episode not found') from e

meta = episode.meta
size_mb = meta.get('size_mb')

return {
'episode_id': episode_id,
'num_episodes': len(ds),
'task': episode.static.get('task', None),
'episode_path': meta.get('path'),
'episode_size_mb': f'{size_mb:.2f}' if isinstance(size_mb, int | float) else None,
'static_data': _make_serializable(episode.static),
}


@app.get('/api/episode_rrd/{episode_id}')
@require_dataset
async def api_episode_rrd(episode_id: int):
Expand Down
1 change: 1 addition & 0 deletions positronic/server/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ function loadSidebarState() {

function initializeSidebar(staticData) {
const tbody = document.querySelector('.sidebar-content-wrapper tbody');
tbody.innerHTML = '';

tbody.insertAdjacentHTML('beforeend', renderLevel('', staticData));
document.querySelectorAll('.expand-button').forEach((button) => {
Expand Down
31 changes: 31 additions & 0 deletions positronic/server/static/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Service Worker to cache rerun WASM and JS assets.
// These files are large (~35MB WASM) and don't change between episodes.

const CACHE_NAME = 'rerun-assets-v1';
const RERUN_PATH_PREFIX = '/static/rerun/';

self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(names.filter((n) => n !== CACHE_NAME).map((n) => caches.delete(n)))
).then(() => self.clients.claim())
);
});

self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
if (!url.pathname.startsWith(RERUN_PATH_PREFIX)) return;

event.respondWith(
caches.open(CACHE_NAME).then((cache) =>
cache.match(event.request).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
if (response.ok) cache.put(event.request, response.clone());
return response;
});
})
)
);
});
1 change: 1 addition & 0 deletions positronic/server/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</div>

<script src="/static/app.js"></script>
<script>if ('serviceWorker' in navigator) navigator.serviceWorker.register('/static/sw.js', {scope: '/static/'});</script>
{% block scripts %}{% endblock %}
</body>
</html>
Loading
Loading