diff --git a/app/routers/cache.py b/app/routers/cache.py index 69afa05..4eac9a1 100644 --- a/app/routers/cache.py +++ b/app/routers/cache.py @@ -50,7 +50,9 @@ async def get_cached( auth_service = AuthService(get_redis_client()) ctx = auth_service.get_user_from_cookie(request) - cache_item = await cache_service.get_cache_item(cid) + # Pass actor_id to get friendly name and user-specific metadata + actor_id = ctx.actor_id if ctx else None + cache_item = await cache_service.get_cache_item(cid, actor_id=actor_id) if not cache_item: if wants_html(request): templates = get_templates(request) diff --git a/app/routers/runs.py b/app/routers/runs.py index 0793854..a5fe593 100644 --- a/app/routers/runs.py +++ b/app/routers/runs.py @@ -181,7 +181,13 @@ async def get_run( plan_sexp = None # Native S-expression if available recipe_ipfs_cid = None recipe_id = run.get("recipe") - if recipe_id and len(recipe_id) == 64: # Looks like a hash + # Check for valid recipe ID (64-char hash, IPFS CIDv0 "Qm...", or CIDv1 "bafy...") + is_valid_recipe_id = recipe_id and ( + len(recipe_id) == 64 or + recipe_id.startswith("Qm") or + recipe_id.startswith("bafy") + ) + if is_valid_recipe_id: try: from ..services.recipe_service import RecipeService recipe_service = RecipeService(get_redis_client(), get_cache_manager()) diff --git a/app/services/cache_service.py b/app/services/cache_service.py index d09ec09..516509c 100644 --- a/app/services/cache_service.py +++ b/app/services/cache_service.py @@ -95,7 +95,7 @@ class CacheService: self.cache = cache_manager self.cache_dir = Path(os.environ.get("CACHE_DIR", "/tmp/artdag-cache")) - async def get_cache_item(self, cid: str) -> Optional[Dict[str, Any]]: + async def get_cache_item(self, cid: str, actor_id: str = None) -> Optional[Dict[str, Any]]: """Get cached item with full metadata for display.""" # Check if content exists if not self.cache.has_content(cid): @@ -106,14 +106,14 @@ class CacheService: return None # Get metadata from database - meta = await self.db.load_item_metadata(cid, None) + meta = await self.db.load_item_metadata(cid, actor_id) cache_item = await self.db.get_cache_item(cid) media_type = detect_media_type(path) mime_type = get_mime_type(path) size = path.stat().st_size - return { + result = { "cid": cid, "path": str(path), "media_type": media_type, @@ -123,6 +123,28 @@ class CacheService: "meta": meta, } + # Unpack meta fields to top level for template convenience + if meta: + result["title"] = meta.get("title") + result["description"] = meta.get("description") + result["tags"] = meta.get("tags", []) + result["source_type"] = meta.get("source_type") + result["source_note"] = meta.get("source_note") + result["created_at"] = meta.get("created_at") + result["filename"] = meta.get("filename") + + # Get friendly name if actor_id provided + if actor_id: + from .naming_service import get_naming_service + naming = get_naming_service() + friendly = await naming.get_by_cid(actor_id, cid) + if friendly: + result["friendly_name"] = friendly["friendly_name"] + result["base_name"] = friendly["base_name"] + result["version_id"] = friendly["version_id"] + + return result + async def check_access(self, cid: str, actor_id: str, username: str) -> bool: """Check if user has access to content.""" user_hashes = await self._get_user_cache_hashes(username, actor_id) @@ -508,6 +530,19 @@ class CacheService: limit=limit, offset=offset, ) + + # Add friendly names to items + if actor_id: + from .naming_service import get_naming_service + naming = get_naming_service() + for item in items: + cid = item.get("cid") + if cid: + friendly = await naming.get_by_cid(actor_id, cid) + if friendly: + item["friendly_name"] = friendly["friendly_name"] + item["base_name"] = friendly["base_name"] + return items # Legacy compatibility methods diff --git a/app/services/recipe_service.py b/app/services/recipe_service.py index 05a53f6..565a610 100644 --- a/app/services/recipe_service.py +++ b/app/services/recipe_service.py @@ -75,8 +75,9 @@ class RecipeService: if hasattr(self.cache, 'list_by_type'): items = self.cache.list_by_type('recipe') - logger.info(f"Found {len(items)} recipes in cache") + logger.info(f"Found {len(items)} recipe CIDs in cache: {items[:5]}...") for cid in items: + logger.debug(f"Attempting to get recipe {cid[:16]}...") recipe = await self.get_recipe(cid) if recipe and not recipe.get("error"): owner = recipe.get("owner") @@ -85,9 +86,25 @@ class RecipeService: # means the recipe is shared/public and visible to all users if actor_id is None or owner is None or owner == actor_id: recipes.append(recipe) + elif recipe and recipe.get("error"): + logger.warning(f"Recipe {cid[:16]}... has error: {recipe.get('error')}") + else: + logger.warning(f"Recipe {cid[:16]}... returned None") else: logger.warning("Cache does not have list_by_type method") + # Add friendly names + if actor_id: + from .naming_service import get_naming_service + naming = get_naming_service() + for recipe in recipes: + recipe_id = recipe.get("recipe_id") + if recipe_id: + friendly = await naming.get_by_cid(actor_id, recipe_id) + if friendly: + recipe["friendly_name"] = friendly["friendly_name"] + recipe["base_name"] = friendly["base_name"] + # Sort by name recipes.sort(key=lambda r: r.get("name", "")) diff --git a/app/templates/cache/detail.html b/app/templates/cache/detail.html index 6571800..da68606 100644 --- a/app/templates/cache/detail.html +++ b/app/templates/cache/detail.html @@ -39,6 +39,17 @@ {% endif %} + + {% if cache.friendly_name %} +
+
+ Friendly Name +

{{ cache.friendly_name }}

+
+

Use in recipes: {{ cache.base_name }}

+
+ {% endif %} +
@@ -50,15 +61,19 @@ Edit
- {% if cache.title or cache.description %} + {% if cache.title or cache.description or cache.filename %}
{% if cache.title %}

{{ cache.title }}

+ {% elif cache.filename %} +

{{ cache.filename }}

{% endif %} {% if cache.description %}

{{ cache.description }}

{% endif %}
+ {% else %} +

No title or description set. Click Edit to add metadata.

{% endif %} {% if cache.tags %}
diff --git a/app/templates/cache/media_list.html b/app/templates/cache/media_list.html index 0ea117f..55a11c2 100644 --- a/app/templates/cache/media_list.html +++ b/app/templates/cache/media_list.html @@ -68,7 +68,11 @@ {% endif %}
+ {% if item.friendly_name %} +
{{ item.friendly_name }}
+ {% else %}
{{ item.cid[:16] }}...
+ {% endif %} {% if item.filename %}
{{ item.filename }}
{% endif %} diff --git a/app/templates/recipes/list.html b/app/templates/recipes/list.html index 5d507dd..0cd484f 100644 --- a/app/templates/recipes/list.html +++ b/app/templates/recipes/list.html @@ -6,6 +6,10 @@

Recipes

+

@@ -13,10 +17,10 @@

{% if recipes %} -
+ + + {% if has_more %} +
+ Loading more... +
+ {% endif %} + {% else %}

No recipes available.

-

Recipes are defined in YAML format and submitted via API.

+

+ Recipes are S-expression files (.sexp) that define processing pipelines. +

+
{% endif %}
+ +
+ + {% endblock %} diff --git a/cache_manager.py b/cache_manager.py index 793ed1e..7505827 100644 --- a/cache_manager.py +++ b/cache_manager.py @@ -412,6 +412,14 @@ class L1CacheManager: # Update content index (CID -> node_id mapping) self._set_content_index(cid, node_id) + # Also index by local hash if cid is an IPFS CID + # This ensures both IPFS CID and local hash can be used to find the file + if self._is_ipfs_cid(cid): + local_hash = file_hash(source_path) + if local_hash != cid: + self._set_content_index(local_hash, node_id) + logger.debug(f"Dual-indexed: {local_hash[:16]}... -> {node_id}") + logger.info(f"Cached: {cid[:16]}...") return CachedFile.from_cache_entry(entry), cid diff --git a/legacy_tasks.py b/legacy_tasks.py index bbc34ff..02b6e90 100644 --- a/legacy_tasks.py +++ b/legacy_tasks.py @@ -365,9 +365,13 @@ def execute_dag(self, dag_json: str, run_id: str = None) -> dict: logger.info(f"Cached node {node_id}: {cached.cid[:16]}... -> {ipfs_cid or 'no IPFS'}") # Get output hash from the output node + # Use the same identifier that's in the cache index (IPFS CID if available) if result.output_path and result.output_path.exists(): - output_cid = file_hash(result.output_path) + local_hash = file_hash(result.output_path) output_ipfs_cid = node_ipfs_cids.get(dag.output_id) + # Use IPFS CID as primary identifier if available, otherwise local hash + # This must match what's in the content_index from cache_manager.put() + output_cid = node_hashes.get(dag.output_id, local_hash) # Store output in database (for L2 to query IPFS CID) import asyncio