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)
|
return JSONResponse({"detail": "Not found"}, status_code=404)
|
||||||
|
|
||||||
# Include routers
|
# 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)
|
# Home and auth routers (root level)
|
||||||
app.include_router(home.router, tags=["home"])
|
app.include_router(home.router, tags=["home"])
|
||||||
app.include_router(auth.router, prefix="/auth", tags=["auth"])
|
app.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||||
app.include_router(inbox.router, tags=["inbox"])
|
app.include_router(inbox.router, tags=["inbox"])
|
||||||
app.include_router(fragments.router, tags=["fragments"])
|
app.include_router(fragments.router, tags=["fragments"])
|
||||||
|
app.include_router(oembed.router, tags=["oembed"])
|
||||||
|
|
||||||
# Feature routers
|
# Feature routers
|
||||||
app.include_router(storage.router, prefix="/storage", tags=["storage"])
|
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.
|
by coop apps via the fragment client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, Response
|
from fastapi import APIRouter, Request, Response
|
||||||
|
|
||||||
router = APIRouter()
|
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)
|
return Response(content="", media_type="text/html", status_code=200)
|
||||||
html = await handler(request)
|
html = await handler(request)
|
||||||
return Response(content=html, media_type="text/html", status_code=200)
|
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