Add nav-item + link-card fragments and oEmbed endpoint
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m1s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m1s
- nav-item fragment handler with template - link-card fragment handler with CID-based lookup, friendly names, batch mode - oEmbed router at GET /oembed for media/recipe/effect/run content - Fragment templates in app/templates/fragments/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -164,13 +164,14 @@ def create_app() -> FastAPI:
|
||||
return JSONResponse({"detail": "Not found"}, status_code=404)
|
||||
|
||||
# Include routers
|
||||
from .routers import auth, storage, api, recipes, cache, runs, home, effects, inbox, fragments
|
||||
from .routers import auth, storage, api, recipes, cache, runs, home, effects, inbox, fragments, oembed
|
||||
|
||||
# Home and auth routers (root level)
|
||||
app.include_router(home.router, tags=["home"])
|
||||
app.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
app.include_router(inbox.router, tags=["inbox"])
|
||||
app.include_router(fragments.router, tags=["fragments"])
|
||||
app.include_router(oembed.router, tags=["oembed"])
|
||||
|
||||
# Feature routers
|
||||
app.include_router(storage.router, prefix="/storage", tags=["storage"])
|
||||
|
||||
@@ -5,6 +5,8 @@ Exposes HTML fragments at ``/internal/fragments/{type}`` for consumption
|
||||
by coop apps via the fragment client.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Request, Response
|
||||
|
||||
router = APIRouter()
|
||||
@@ -25,3 +27,117 @@ async def get_fragment(fragment_type: str, request: Request):
|
||||
return Response(content="", media_type="text/html", status_code=200)
|
||||
html = await handler(request)
|
||||
return Response(content=html, media_type="text/html", status_code=200)
|
||||
|
||||
|
||||
# --- nav-item fragment ---
|
||||
|
||||
async def _nav_item_handler(request: Request) -> str:
|
||||
from artdag_common import render_fragment
|
||||
|
||||
templates = request.app.state.templates
|
||||
artdag_url = os.getenv("APP_URL_ARTDAG", "https://celery-artdag.rose-ash.com")
|
||||
return render_fragment(templates, "fragments/nav_item.html", artdag_url=artdag_url)
|
||||
|
||||
|
||||
_handlers["nav-item"] = _nav_item_handler
|
||||
|
||||
|
||||
# --- link-card fragment ---
|
||||
|
||||
async def _link_card_handler(request: Request) -> str:
|
||||
from artdag_common import render_fragment
|
||||
import database
|
||||
|
||||
templates = request.app.state.templates
|
||||
cid = request.query_params.get("cid", "")
|
||||
content_type = request.query_params.get("type", "media")
|
||||
slug = request.query_params.get("slug", "")
|
||||
keys_raw = request.query_params.get("keys", "")
|
||||
|
||||
# Batch mode: return multiple cards separated by markers
|
||||
if keys_raw:
|
||||
keys = [k.strip() for k in keys_raw.split(",") if k.strip()]
|
||||
parts = []
|
||||
for key in keys:
|
||||
parts.append(f"<!-- fragment:{key} -->")
|
||||
card_html = await _render_single_link_card(
|
||||
templates, key, content_type,
|
||||
)
|
||||
parts.append(card_html)
|
||||
return "\n".join(parts)
|
||||
|
||||
# Single mode: use cid or slug
|
||||
lookup_cid = cid or slug
|
||||
if not lookup_cid:
|
||||
return ""
|
||||
return await _render_single_link_card(templates, lookup_cid, content_type)
|
||||
|
||||
|
||||
async def _render_single_link_card(templates, cid: str, content_type: str) -> str:
|
||||
import database
|
||||
from artdag_common import render_fragment
|
||||
|
||||
if not cid:
|
||||
return ""
|
||||
|
||||
artdag_url = os.getenv("APP_URL_ARTDAG", "https://celery-artdag.rose-ash.com")
|
||||
|
||||
# Try item_types first (has metadata)
|
||||
item = await database.get_item_types(cid)
|
||||
# get_item_types returns a list; pick best match for content_type
|
||||
meta = None
|
||||
if item:
|
||||
for it in item:
|
||||
if it.get("type") == content_type:
|
||||
meta = it
|
||||
break
|
||||
if not meta:
|
||||
meta = item[0]
|
||||
|
||||
# Try friendly name for display
|
||||
friendly = None
|
||||
if meta and meta.get("actor_id"):
|
||||
friendly = await database.get_friendly_name_by_cid(meta["actor_id"], cid)
|
||||
|
||||
# Try run cache if type is "run"
|
||||
run = None
|
||||
if content_type == "run":
|
||||
run = await database.get_run_cache(cid)
|
||||
|
||||
title = ""
|
||||
description = ""
|
||||
link = ""
|
||||
|
||||
if friendly:
|
||||
title = friendly.get("display_name") or friendly.get("base_name", cid[:12])
|
||||
elif meta:
|
||||
title = meta.get("filename") or meta.get("description", cid[:12])
|
||||
elif run:
|
||||
title = f"Run {cid[:12]}"
|
||||
else:
|
||||
title = cid[:16]
|
||||
|
||||
if meta:
|
||||
description = meta.get("description", "")
|
||||
|
||||
if content_type == "run":
|
||||
link = f"{artdag_url}/runs/{cid}"
|
||||
elif content_type == "recipe":
|
||||
link = f"{artdag_url}/recipes/{cid}"
|
||||
elif content_type == "effect":
|
||||
link = f"{artdag_url}/effects/{cid}"
|
||||
else:
|
||||
link = f"{artdag_url}/cache/{cid}"
|
||||
|
||||
return render_fragment(
|
||||
templates, "fragments/link_card.html",
|
||||
title=title,
|
||||
description=description,
|
||||
link=link,
|
||||
cid=cid,
|
||||
content_type=content_type,
|
||||
artdag_url=artdag_url,
|
||||
)
|
||||
|
||||
|
||||
_handlers["link-card"] = _link_card_handler
|
||||
|
||||
74
app/routers/oembed.py
Normal file
74
app/routers/oembed.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Art-DAG oEmbed endpoint.
|
||||
|
||||
Returns oEmbed JSON responses for Art-DAG content (media, recipes, effects, runs).
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/oembed")
|
||||
async def oembed(request: Request):
|
||||
url = request.query_params.get("url", "")
|
||||
if not url:
|
||||
return JSONResponse({"error": "url parameter required"}, status_code=400)
|
||||
|
||||
# Parse URL to extract content type and CID
|
||||
# URL patterns: /cache/{cid}, /recipes/{cid}, /effects/{cid}, /runs/{cid}
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(url)
|
||||
parts = [p for p in parsed.path.strip("/").split("/") if p]
|
||||
|
||||
if len(parts) < 2:
|
||||
return JSONResponse({"error": "could not parse content URL"}, status_code=404)
|
||||
|
||||
content_type = parts[0].rstrip("s") # recipes -> recipe, runs -> run
|
||||
cid = parts[1]
|
||||
|
||||
import database
|
||||
|
||||
title = cid[:16]
|
||||
thumbnail_url = None
|
||||
|
||||
# Look up metadata
|
||||
items = await database.get_item_types(cid)
|
||||
if items:
|
||||
meta = items[0]
|
||||
title = meta.get("filename") or meta.get("description") or title
|
||||
|
||||
# Try friendly name
|
||||
actor_id = meta.get("actor_id")
|
||||
if actor_id:
|
||||
friendly = await database.get_friendly_name_by_cid(actor_id, cid)
|
||||
if friendly:
|
||||
title = friendly.get("display_name") or friendly.get("base_name", title)
|
||||
|
||||
# Media items get a thumbnail
|
||||
if meta.get("type") == "media":
|
||||
artdag_url = os.getenv("APP_URL_ARTDAG", "https://celery-artdag.rose-ash.com")
|
||||
thumbnail_url = f"{artdag_url}/cache/{cid}/raw"
|
||||
|
||||
elif content_type == "run":
|
||||
run = await database.get_run_cache(cid)
|
||||
if run:
|
||||
title = f"Run {cid[:12]}"
|
||||
|
||||
artdag_url = os.getenv("APP_URL_ARTDAG", "https://celery-artdag.rose-ash.com")
|
||||
|
||||
resp = {
|
||||
"version": "1.0",
|
||||
"type": "link",
|
||||
"title": title,
|
||||
"provider_name": "art-dag",
|
||||
"provider_url": artdag_url,
|
||||
"url": url,
|
||||
}
|
||||
if thumbnail_url:
|
||||
resp["thumbnail_url"] = thumbnail_url
|
||||
|
||||
return JSONResponse(resp)
|
||||
22
app/templates/fragments/link_card.html
Normal file
22
app/templates/fragments/link_card.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<a href="{{ link }}" class="block rounded border border-stone-200 bg-white hover:bg-stone-50 transition-colors no-underline" data-fragment="link-card" data-app="artdag" data-hx-disable>
|
||||
<div class="flex flex-row items-center gap-3 p-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded bg-stone-100 flex items-center justify-center text-stone-500">
|
||||
{% if content_type == "recipe" %}
|
||||
<i class="fas fa-scroll text-sm"></i>
|
||||
{% elif content_type == "effect" %}
|
||||
<i class="fas fa-magic text-sm"></i>
|
||||
{% elif content_type == "run" %}
|
||||
<i class="fas fa-play-circle text-sm"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-cube text-sm"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium text-stone-900 text-sm truncate">{{ title }}</div>
|
||||
{% if description %}
|
||||
<div class="text-xs text-stone-500 clamp-2">{{ description }}</div>
|
||||
{% endif %}
|
||||
<div class="text-xs text-stone-400 mt-0.5">{{ content_type }} · {{ cid[:12] }}…</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
7
app/templates/fragments/nav_item.html
Normal file
7
app/templates/fragments/nav_item.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="relative nav-group">
|
||||
<a href="{{ artdag_url }}"
|
||||
class="justify-center cursor-pointer flex flex-row items-center gap-2 rounded bg-stone-200 text-black p-3"
|
||||
data-hx-disable>
|
||||
<i class="fas fa-project-diagram text-sm"></i> art-dag
|
||||
</a>
|
||||
</div>
|
||||
Reference in New Issue
Block a user