diff --git a/routes/canvas.py b/routes/canvas.py index 1106d3c..26207e4 100644 --- a/routes/canvas.py +++ b/routes/canvas.py @@ -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 @@ -892,14 +898,22 @@ def canvas_pages_proxy(path): @canvas_bp.route('/images/') 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}') diff --git a/routes/icons.py b/routes/icons.py index f673697..58a4625 100644 --- a/routes/icons.py +++ b/routes/icons.py @@ -163,7 +163,12 @@ def search_icons(): @icons_bp.route('/api/icons/library/.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' @@ -171,8 +176,11 @@ def serve_icon(name): if not path.exists(): return Response('', 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 # ══════════════════════════════════════════════════════════════ @@ -346,9 +354,18 @@ def list_generated(): @icons_bp.route('/api/icons/generated/') 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 diff --git a/routes/static_files.py b/routes/static_files.py index 5ebd52b..6ce9c47 100644 --- a/routes/static_files.py +++ b/routes/static_files.py @@ -409,13 +409,25 @@ def serve_emulator(filepath): @static_files_bp.route('/uploads/') 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/') @@ -442,12 +454,20 @@ def serve_src(filepath): @static_files_bp.route('/known_faces//') 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 @@ -558,11 +578,19 @@ def serve_sw(): @static_files_bp.route('/static/icons/') 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') diff --git a/routes/workspace.py b/routes/workspace.py index 8fa2c24..e023126 100644 --- a/routes/workspace.py +++ b/routes/workspace.py @@ -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