Skip to content
Merged
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
28 changes: 21 additions & 7 deletions routes/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,15 +873,21 @@ def canvas_pages_proxy(path):
)
return resp
else:
# Non-HTML files: use send_file for proper range request support
# (required for video/audio streaming playback)
# Non-HTML files served from canvas-pages/ — icons, JSON state,
# backing images, generated audio, etc. Agents update these live
# via the API, so caching breaks the "live updates" guarantee.
# See docs/jambot/no-cache-policy.md.
# NOTE: conditional=True is kept so range requests still work for
# audio/video streaming playback; only the cache headers change.
resp = send_file(
resolved,
conditional=True,
max_age=3600,
)
# Tell Cloudflare CDN to cache media files explicitly
resp.headers['CDN-Cache-Control'] = 'public, max-age=86400'
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
resp.headers['CDN-Cache-Control'] = 'no-store'
resp.headers['Cloudflare-CDN-Cache-Control'] = 'no-store'
resp.headers['Accept-Ranges'] = 'bytes'
return resp
return 'Page not found', 404
Expand All @@ -892,14 +898,22 @@ def canvas_pages_proxy(path):

@canvas_bp.route('/images/<path:path>')
def canvas_images_proxy(path):
"""Serve files from Canvas images directory."""
"""Serve files from Canvas images directory.

NO-CACHE: see docs/jambot/no-cache-policy.md. Canvas images are
agent-updatable surfaces.
"""
try:
# P7-T3 security: prevent path traversal
resolved = _safe_canvas_path('/var/www/canvas-display/images', path)
if resolved is None:
return 'Invalid path', 400
if resolved.exists():
return send_file(resolved)
resp = send_file(resolved)
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
return resp
return 'Image not found', 404
except Exception as exc:
logger.error(f'Canvas images proxy error: {exc}')
Expand Down
27 changes: 22 additions & 5 deletions routes/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,24 @@ def search_icons():

@icons_bp.route('/api/icons/library/<name>.svg')
def serve_icon(name):
"""Serve a Lucide SVG icon by name."""
"""Serve a Lucide SVG icon by name.

NO-CACHE: see docs/jambot/no-cache-policy.md. Icons are user-visible
surfaces that agents may swap or redirect. No browser cache anywhere on
icons in this system, even for the "static" Lucide set.
"""
# Sanitize name
safe = re.sub(r'[^a-z0-9\-]', '', name.lower())
path = LUCIDE_DIR / f'{safe}.svg'

if not path.exists():
return Response('<!-- icon not found -->', status=404, mimetype='image/svg+xml')

return send_file(str(path), mimetype='image/svg+xml',
max_age=86400) # cache 1 day
resp = send_file(str(path), mimetype='image/svg+xml')
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
return resp


# ══════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -346,9 +354,18 @@ def list_generated():

@icons_bp.route('/api/icons/generated/<filename>')
def serve_generated(filename):
"""Serve a generated icon."""
"""Serve a generated icon.

NO-CACHE: see docs/jambot/no-cache-policy.md. Agents regenerate icons —
a 1-hour cache here used to hide updates for an hour. Live updates win;
optimize the source images for size instead.
"""
safe = re.sub(r'[^\w.\-]', '', filename)
path = _ensure_generated_dir() / safe
if not path.exists():
return jsonify({'error': 'Not found'}), 404
return send_file(str(path), max_age=3600)
resp = send_file(str(path))
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
return resp
40 changes: 34 additions & 6 deletions routes/static_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,13 +409,25 @@ def serve_emulator(filepath):

@static_files_bp.route('/uploads/<path:filename>')
def serve_upload(filename):
"""Serve uploaded files (path traversal guarded)."""
"""Serve uploaded files (path traversal guarded).

NO-CACHE POLICY (do not change without reading docs/jambot/no-cache-policy.md):
Uploads contain icons, wallpapers, agent-generated assets, and canvas page
media that agents and admins regenerate live. Caching breaks the "live
updates always visible" guarantee of the canvas system. The ONLY assets
that may be cached are known_faces photos (handled by a separate route).
Optimize for size (smaller files), not for browser cache hits.
"""
upload_path = _safe_path(UPLOADS_DIR, filename)
if upload_path is None:
return jsonify({"error": "Invalid path"}), 400
if not upload_path.exists():
return jsonify({"error": "File not found"}), 404
return send_file(upload_path)
resp = send_file(upload_path)
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
return resp


@static_files_bp.route('/src/<path:filepath>')
Expand All @@ -442,12 +454,20 @@ def serve_src(filepath):

@static_files_bp.route('/known_faces/<name>/<filename>')
def serve_face_photo(name, filename):
"""Serve face photos for the My Face section"""
"""Serve face photos for the My Face section.

CACHE ALLOWED — face photos are the documented exception to the
no-cache policy (docs/jambot/no-cache-policy.md). Face content does not
update live and is identity-stable. A long browser cache here is a
deliberate perf win and does NOT violate the canvas/icons rule.
"""
photo_path = _safe_path(KNOWN_FACES_DIR, name, filename)
if photo_path is None:
return jsonify({"error": "Invalid path"}), 400
if photo_path.exists():
return send_file(photo_path)
resp = send_file(photo_path)
resp.headers['Cache-Control'] = 'public, max-age=86400'
return resp
return jsonify({"error": "Photo not found"}), 404


Expand Down Expand Up @@ -558,11 +578,19 @@ def serve_sw():

@static_files_bp.route('/static/icons/<filename>')
def serve_icon(filename):
"""PWA icons"""
"""PWA icons.

NO-CACHE: icons across the system are not cached. See
docs/jambot/no-cache-policy.md.
"""
icon_path = _safe_path(STATIC_DIR / 'icons', filename)
if icon_path is None or not icon_path.exists():
return jsonify({"error": "Icon not found"}), 404
return send_file(icon_path, mimetype='image/png')
resp = send_file(icon_path, mimetype='image/png')
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
return resp


@static_files_bp.route('/install')
Expand Down
8 changes: 7 additions & 1 deletion routes/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,13 @@ def raw_file():
return jsonify({'error': f'File too large ({size // (1024*1024)} MB). Max is 20 MB.'}), 413

try:
return send_file(str(target), conditional=True)
# NO-CACHE: workspace files are agent-edited continuously. See
# docs/jambot/no-cache-policy.md.
resp = send_file(str(target), conditional=True)
resp.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
resp.headers['Pragma'] = 'no-cache'
resp.headers['Expires'] = '0'
return resp
except Exception as e:
return jsonify({'error': str(e)}), 500

Expand Down