Integrate federation app with shared menu/header system
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s

Replace standalone base.html with the shared _types/root layout.
Social pages get a second nav row via _types/social/index.html.
Root / becomes a blank page with shared chrome. Auth pages use
the shared layout without the social nav bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-23 00:10:56 +00:00
parent 9a1be0461c
commit 8dc354ae0b
18 changed files with 103 additions and 94 deletions

17
app.py
View File

@@ -20,10 +20,22 @@ from bp import (
async def federation_context() -> dict:
"""Federation app context processor."""
from shared.infrastructure.context import base_context
from shared.services.navigation import get_navigation_tree
from shared.infrastructure.cart_identity import current_cart_identity
ctx = await base_context()
# If user is logged in, check for ActorProfile
ctx["menu_items"] = await get_navigation_tree(g.s)
# Cart data (consistent with all other apps)
ident = current_cart_identity()
summary = await services.cart.cart_summary(
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
)
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
# Actor profile for logged-in users
if g.get("user"):
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
ctx["actor"] = actor
@@ -60,8 +72,7 @@ def create_app() -> "Quart":
@app.get("/")
async def home():
from quart import render_template
stats = await services.federation.get_stats(g.s)
return await render_template("federation/home.html", stats=stats)
return await render_template("_types/federation/index.html")
return app

View File

@@ -0,0 +1,3 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block content %}{% endblock %}

View File

@@ -0,0 +1,52 @@
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='social-row', oob=oob) %}
<div class="w-full flex flex-row items-center gap-2 flex-wrap">
{% if actor %}
<nav class="flex gap-3 text-sm items-center flex-wrap">
<a href="{{ url_for('social.home_timeline') }}"
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.home_timeline') %}font-bold{% endif %}">
Timeline
</a>
<a href="{{ url_for('social.public_timeline') }}"
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.public_timeline') %}font-bold{% endif %}">
Public
</a>
<a href="{{ url_for('social.compose_form') }}"
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.compose_form') %}font-bold{% endif %}">
Compose
</a>
<a href="{{ url_for('social.following_list') }}"
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.following_list') %}font-bold{% endif %}">
Following
</a>
<a href="{{ url_for('social.followers_list') }}"
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.followers_list') %}font-bold{% endif %}">
Followers
</a>
<a href="{{ url_for('social.search') }}"
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.search') %}font-bold{% endif %}">
Search
</a>
<a href="{{ url_for('social.notifications') }}"
class="px-2 py-1 rounded hover:bg-stone-200 relative {% if request.path == url_for('social.notifications') %}font-bold{% endif %}">
Notifications
<span hx-get="{{ url_for('social.notification_count') }}" hx-trigger="load, every 30s" hx-swap="innerHTML"
class="absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"></span>
</a>
<a href="{{ url_for('actors.profile', username=actor.preferred_username) }}"
class="px-2 py-1 rounded hover:bg-stone-200">
@{{ actor.preferred_username }}
</a>
</nav>
{% else %}
<nav class="flex gap-3 text-sm items-center">
<a href="{{ url_for('identity.choose_username_form') }}"
class="px-2 py-1 rounded hover:bg-stone-200 font-bold">
Choose username
</a>
</nav>
{% endif %}
</div>
{% endcall %}
{% endmacro %}

View File

@@ -0,0 +1,10 @@
{% extends '_types/root/_index.html' %}
{% block meta %}{% endblock %}
{% block root_header_child %}
{% from '_types/root/_n/macros.html' import index_row with context %}
{% call index_row('social-header-child', '_types/social/header/_header.html') %}
{% endcall %}
{% endblock %}
{% block content %}
{% block social_content %}{% endblock %}
{% endblock %}

View File

@@ -1,4 +1,5 @@
{% extends "federation/base.html" %}
{% extends "_types/root/_index.html" %}
{% block meta %}{% endblock %}
{% block title %}Check your email — Rose Ash{% endblock %}
{% block content %}
<div class="py-8 max-w-md mx-auto text-center">

View File

@@ -1,4 +1,5 @@
{% extends "federation/base.html" %}
{% extends "_types/root/_index.html" %}
{% block meta %}{% endblock %}
{% block title %}Login — Rose Ash{% endblock %}
{% block content %}
<div class="py-8 max-w-md mx-auto">

View File

@@ -1,6 +1,6 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Account — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<div class="py-8">
<h1 class="text-2xl font-bold mb-4">Account</h1>

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}{{ remote_actor.display_name or remote_actor.preferred_username }} — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<div class="bg-white rounded-lg shadow-sm border border-stone-200 p-6 mb-6">
<div class="flex items-center gap-4">
{% if remote_actor.icon_url %}

View File

@@ -1,46 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Rose Ash{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</head>
<body class="bg-stone-50 text-stone-900 min-h-screen">
<nav class="bg-stone-800 text-white p-4">
<div class="max-w-4xl mx-auto flex items-center justify-between">
<a href="/" class="font-bold text-lg">Rose Ash</a>
<div class="flex items-center gap-4">
{% if g.user %}
{% if actor %}
<a href="{{ url_for('social.home_timeline') }}" class="hover:underline">Timeline</a>
<a href="{{ url_for('social.public_timeline') }}" class="hover:underline">Public</a>
<a href="{{ url_for('social.following_list') }}" class="hover:underline">Following</a>
<a href="{{ url_for('social.followers_list') }}" class="hover:underline">Followers</a>
<a href="{{ url_for('social.search') }}" class="hover:underline">Search</a>
<a href="{{ url_for('social.notifications') }}" class="hover:underline relative">
Notifications
<span hx-get="{{ url_for('social.notification_count') }}" hx-trigger="load, every 30s" hx-swap="innerHTML" class="absolute -top-2 -right-4"></span>
</a>
<a href="{{ url_for('actors.profile', username=actor.preferred_username) }}" class="hover:underline">
@{{ actor.preferred_username }}
</a>
{% else %}
<a href="{{ url_for('identity.choose_username_form') }}" class="hover:underline">Choose username</a>
{% endif %}
<form method="post" action="{{ url_for('auth.logout') }}" class="inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="hover:underline">Logout</button>
</form>
{% else %}
<a href="{{ url_for('auth.login_form') }}" class="hover:underline">Login</a>
{% endif %}
</div>
</div>
</nav>
<main class="max-w-4xl mx-auto p-4">
{% block content %}{% endblock %}
</main>
</body>
</html>

View File

@@ -1,6 +1,6 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Choose Username — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<div class="py-8 max-w-md mx-auto">
<h1 class="text-2xl font-bold mb-2">Choose your username</h1>
<p class="text-stone-600 mb-6">

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Compose — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<h1 class="text-2xl font-bold mb-6">Compose</h1>
<form method="post" action="{{ url_for('social.compose_submit') }}" class="space-y-4">

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Followers — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<h1 class="text-2xl font-bold mb-6">Followers <span class="text-stone-400 font-normal">({{ total }})</span></h1>
<div id="actor-list">

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Following — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<h1 class="text-2xl font-bold mb-6">Following <span class="text-stone-400 font-normal">({{ total }})</span></h1>
<div id="actor-list">

View File

@@ -1,23 +0,0 @@
{% extends "federation/base.html" %}
{% block title %}Rose Ash — Federation{% endblock %}
{% block content %}
<div class="py-8">
<h1 class="text-3xl font-bold mb-4">Rose Ash</h1>
<p class="text-stone-600 mb-8">Cooperative platform with ActivityPub federation.</p>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white rounded-lg shadow p-6">
<div class="text-3xl font-bold">{{ stats.actors }}</div>
<div class="text-stone-500">Actors</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-3xl font-bold">{{ stats.activities }}</div>
<div class="text-stone-500">Activities</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-3xl font-bold">{{ stats.followers }}</div>
<div class="text-stone-500">Followers</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Notifications — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<h1 class="text-2xl font-bold mb-6">Notifications</h1>
{% if not notifications %}

View File

@@ -1,6 +1,6 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}@{{ actor.preferred_username }} — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<div class="py-8">
<div class="bg-white rounded-lg shadow p-6 mb-6">
<h1 class="text-2xl font-bold">{{ actor.display_name or actor.preferred_username }}</h1>

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}Search — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<h1 class="text-2xl font-bold mb-6">Search</h1>
<form method="get" action="{{ url_for('social.search') }}" class="mb-6">

View File

@@ -1,8 +1,8 @@
{% extends "federation/base.html" %}
{% extends "_types/social/index.html" %}
{% block title %}{{ "Home" if timeline_type == "home" else "Public" }} Timeline — Rose Ash{% endblock %}
{% block content %}
{% block social_content %}
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">{{ "Home" if timeline_type == "home" else "Public" }} Timeline</h1>
{% if actor %}