diff --git a/server.py b/server.py index b76a5e8..1075c3f 100644 --- a/server.py +++ b/server.py @@ -565,15 +565,23 @@ async def ui_activities_page(request: Request): ''' else: rows = "" - for activity in reversed(activities): + for i, activity in enumerate(reversed(activities)): + # Index from end since we reversed + activity_index = len(activities) - 1 - i obj = activity.get("object_data", {}) activity_type = activity.get("activity_type", "") type_color = "bg-green-600" if activity_type == "Create" else "bg-yellow-600" if activity_type == "Update" else "bg-gray-600" + actor_id = activity.get("actor_id", "") + actor_name = actor_id.split("/")[-1] if actor_id else "unknown" rows += f''' - + {activity_type} - {obj.get("name", "")} - {obj.get("contentHash", {}).get("value", "")[:16]}... + + {obj.get("name", "Untitled")} + + + {actor_name} + {activity.get("published", "")[:10]} ''' @@ -585,7 +593,7 @@ async def ui_activities_page(request: Request): Type Object - Content Hash + Actor Published @@ -598,6 +606,239 @@ async def ui_activities_page(request: Request): return HTMLResponse(base_html("Activities", content, username)) +@app.get("/ui/activity/{activity_index}", response_class=HTMLResponse) +async def ui_activity_detail(activity_index: int, request: Request): + """Activity detail page with full content display.""" + username = get_user_from_cookie(request) + activities = load_activities() + + if activity_index < 0 or activity_index >= len(activities): + content = ''' +

Activity Not Found

+

This activity does not exist.

+

← Back to Activities

+ ''' + return HTMLResponse(base_html("Activity Not Found", content, username)) + + activity = activities[activity_index] + activity_type = activity.get("activity_type", "") + activity_id = activity.get("activity_id", "") + actor_id = activity.get("actor_id", "") + actor_name = actor_id.split("/")[-1] if actor_id else "unknown" + published = activity.get("published", "")[:10] + obj = activity.get("object_data", {}) + + # Object details + obj_name = obj.get("name", "Untitled") + obj_type = obj.get("type", "") + content_hash_obj = obj.get("contentHash", {}) + content_hash = content_hash_obj.get("value", "") if isinstance(content_hash_obj, dict) else "" + media_type = obj.get("mediaType", "") + description = obj.get("summary", "") or obj.get("content", "") + + # Provenance from object + provenance = obj.get("provenance", {}) + origin = obj.get("origin", {}) + + # Type colors + type_color = "bg-green-600" if activity_type == "Create" else "bg-yellow-600" if activity_type == "Update" else "bg-gray-600" + obj_type_color = "bg-blue-600" if "Image" in obj_type else "bg-purple-600" if "Video" in obj_type else "bg-gray-600" + + # Determine L1 server and asset type + l1_server = provenance.get("l1_server", L1_PUBLIC_URL).rstrip("/") if provenance else L1_PUBLIC_URL.rstrip("/") + is_video = "Video" in obj_type or "video" in media_type + + # Content display + if is_video: + content_html = f''' +
+ +
+ + Download Original + +
+
+ ''' + elif "Image" in obj_type or "image" in media_type: + content_html = f''' +
+ {obj_name} +
+ + Download + +
+
+ ''' + else: + content_html = f''' +
+

Content type: {media_type or obj_type}

+ + Download + +
+ ''' + + # Origin display + origin_html = 'Not specified' + if origin: + origin_type = origin.get("type", "") + if origin_type == "self": + origin_html = 'Original content by author' + elif origin_type == "external": + origin_url = origin.get("url", "") + origin_note = origin.get("note", "") + origin_html = f'{origin_url}' + if origin_note: + origin_html += f'

{origin_note}

' + + # Provenance section + provenance_html = "" + if provenance and provenance.get("recipe"): + recipe = provenance.get("recipe", "") + inputs = provenance.get("inputs", []) + l1_run_id = provenance.get("l1_run_id", "") + rendered_at = provenance.get("rendered_at", "")[:10] if provenance.get("rendered_at") else "" + effects_commit = provenance.get("effects_commit", "") + effect_url = provenance.get("effect_url") + infrastructure = provenance.get("infrastructure", {}) + + if not effect_url: + if effects_commit and effects_commit != "unknown": + effect_url = f"{EFFECTS_REPO_URL}/src/commit/{effects_commit}/{recipe}" + else: + effect_url = f"{EFFECTS_REPO_URL}/src/branch/main/{recipe}" + + # Build inputs display + inputs_html = "" + for inp in inputs: + inp_hash = inp.get("content_hash", "") if isinstance(inp, dict) else inp + if inp_hash: + inputs_html += f''' + {inp_hash[:20]}... + ''' + + # Infrastructure display + infra_html = "" + if infrastructure: + software = infrastructure.get("software", {}) + hardware = infrastructure.get("hardware", {}) + if software or hardware: + infra_parts = [] + if software: + infra_parts.append(f"Software: {software.get('name', 'unknown')}") + if hardware: + infra_parts.append(f"Hardware: {hardware.get('name', 'unknown')}") + infra_html = f'

{" | ".join(infra_parts)}

' + + provenance_html = f''' +
+

Provenance

+

This content was created by applying an effect to input content.

+
+
+

Effect

+ + + + + {recipe} + + {f'
Commit: {effects_commit[:12]}...
' if effects_commit else ''} +
+
+

Input(s)

+ {inputs_html if inputs_html else 'No inputs recorded'} +
+
+

L1 Run

+ {l1_run_id[:20]}... +
+
+

Rendered

+ {rendered_at if rendered_at else 'Unknown'} + {infra_html} +
+
+
+ ''' + + content = f''' +

← Back to Activities

+ +
+ {activity_type} +

{obj_name}

+ {obj_type} +
+ + {content_html} + +
+
+
+

Actor

+ {actor_name} +
+ +
+

Description

+

{description if description else 'No description'}

+
+ +
+

Origin

+ {origin_html} +
+
+ +
+
+

Content Hash

+ {content_hash} +
+ +
+

Published

+ {published} +
+ +
+

Activity ID

+ {activity_id} +
+
+
+ + {provenance_html} + +
+

ActivityPub

+
+

+ Object URL: + https://{DOMAIN}/objects/{content_hash} +

+

+ Actor: + {actor_id} +

+
+
+ ''' + return HTMLResponse(base_html(f"Activity: {obj_name}", content, username)) + + @app.get("/ui/users", response_class=HTMLResponse) async def ui_users_page(request: Request): """Users page showing all registered users."""