Add modular app structure for L2 server

- Create app factory with routers and templates
- Auth, assets, activities, anchors, storage, users, renderers routers
- Federation router for WebFinger and nodeinfo
- Jinja2 templates for L2 pages
- Config and dependency injection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
giles
2026-01-11 07:46:26 +00:00
parent a5619208cf
commit d1e9287829
19 changed files with 1870 additions and 0 deletions

11
app/templates/404.html Normal file
View File

@@ -0,0 +1,11 @@
{% extends "base.html" %}
{% block title %}Not Found - Art-DAG{% endblock %}
{% block content %}
<div class="text-center py-16">
<h2 class="text-6xl font-bold text-gray-600 mb-4">404</h2>
<p class="text-xl text-gray-400 mb-8">Page not found</p>
<a href="/" class="text-blue-400 hover:text-blue-300">Go to home page</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}Assets - Art-DAG{% endblock %}
{% block content %}
<div class="max-w-6xl mx-auto">
<h1 class="text-3xl font-bold mb-6">Your Assets</h1>
{% if assets %}
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" id="assets-grid">
{% for asset in assets %}
<a href="/assets/{{ asset.id }}"
class="bg-gray-800 rounded-lg overflow-hidden hover:ring-2 hover:ring-blue-500 transition-all">
{% if asset.asset_type == 'image' %}
<img src="{{ asset.thumbnail_url or '/assets/' + asset.id + '/thumb' }}"
alt="{{ asset.name }}"
class="w-full h-40 object-cover">
{% elif asset.asset_type == 'video' %}
<div class="w-full h-40 bg-gray-900 flex items-center justify-center">
<svg class="w-12 h-12 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z"/>
</svg>
</div>
{% else %}
<div class="w-full h-40 bg-gray-900 flex items-center justify-center">
<span class="text-gray-600">{{ asset.asset_type }}</span>
</div>
{% endif %}
<div class="p-3">
<div class="font-medium text-white truncate">{{ asset.name }}</div>
<div class="text-xs text-gray-500">{{ asset.asset_type }}</div>
{% if asset.ipfs_cid %}
<div class="text-xs text-green-400 mt-1">Published</div>
{% endif %}
</div>
</a>
{% endfor %}
</div>
{% if has_more %}
<div hx-get="/assets?offset={{ offset + limit }}"
hx-trigger="revealed"
hx-swap="beforeend"
hx-target="#assets-grid"
class="h-20 flex items-center justify-center text-gray-500 mt-4">
Loading more...
</div>
{% endif %}
{% else %}
<div class="bg-gray-800 border border-gray-700 rounded-lg p-12 text-center">
<p class="text-gray-500 mb-4">No assets yet</p>
<p class="text-gray-600 text-sm">Create content on an L1 renderer and publish it here.</p>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends "base.html" %}
{% block title %}Already Logged In - Art-DAG{% endblock %}
{% block content %}
<div class="max-w-md mx-auto text-center">
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">
You are already logged in as <strong>{{ user.username }}</strong>
</div>
<p><a href="/" class="text-blue-400 hover:text-blue-300">Go to home page</a></p>
</div>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Login - Art-DAG{% endblock %}
{% block content %}
<div class="max-w-md mx-auto">
<h2 class="text-xl font-semibold mb-6">Login</h2>
<div id="login-result"></div>
<form hx-post="/auth/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-4">
{% if return_to %}
<input type="hidden" name="return_to" value="{{ return_to }}">
{% endif %}
<div>
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label>
<input type="text" id="username" name="username" required
class="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input type="password" id="password" name="password" required
class="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none">
</div>
<button type="submit" class="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg">
Login
</button>
</form>
<p class="mt-6 text-gray-400">
Don't have an account? <a href="/auth/register" class="text-blue-400 hover:text-blue-300">Register</a>
</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,45 @@
{% extends "base.html" %}
{% block title %}Register - Art-DAG{% endblock %}
{% block content %}
<div class="max-w-md mx-auto">
<h2 class="text-xl font-semibold mb-6">Register</h2>
<div id="register-result"></div>
<form hx-post="/auth/register" hx-target="#register-result" hx-swap="innerHTML" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium text-gray-300 mb-2">Username</label>
<input type="text" id="username" name="username" required pattern="[a-zA-Z0-9_-]+"
class="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none">
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-300 mb-2">Email (optional)</label>
<input type="email" id="email" name="email"
class="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none">
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input type="password" id="password" name="password" required minlength="6"
class="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none">
</div>
<div>
<label for="password2" class="block text-sm font-medium text-gray-300 mb-2">Confirm Password</label>
<input type="password" id="password2" name="password2" required minlength="6"
class="w-full px-4 py-3 bg-gray-800 border border-gray-600 rounded-lg text-white focus:border-blue-500 focus:outline-none">
</div>
<button type="submit" class="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg">
Register
</button>
</form>
<p class="mt-6 text-gray-400">
Already have an account? <a href="/auth/login" class="text-blue-400 hover:text-blue-300">Login</a>
</p>
</div>
{% endblock %}

27
app/templates/base.html Normal file
View File

@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block brand %}Art-DAG{% endblock %}
{% block nav_items %}
<nav class="flex items-center space-x-6">
<a href="/assets" class="text-gray-300 hover:text-white {% if active_tab == 'assets' %}text-white font-medium{% endif %}">Assets</a>
<a href="/activities" class="text-gray-300 hover:text-white {% if active_tab == 'activities' %}text-white font-medium{% endif %}">Activities</a>
<a href="/anchors" class="text-gray-300 hover:text-white {% if active_tab == 'anchors' %}text-white font-medium{% endif %}">Anchors</a>
<a href="/storage" class="text-gray-300 hover:text-white {% if active_tab == 'storage' %}text-white font-medium{% endif %}">Storage</a>
<a href="/renderers" class="text-gray-300 hover:text-white {% if active_tab == 'renderers' %}text-white font-medium{% endif %}">Renderers</a>
</nav>
{% endblock %}
{% block nav_right %}
{% if user %}
<div class="flex items-center space-x-4">
<a href="/users/{{ user.username }}" class="text-gray-400 hover:text-white">{{ user.username }}</a>
<a href="/auth/logout" class="text-gray-300 hover:text-white">Logout</a>
</div>
{% else %}
<div class="flex items-center space-x-4">
<a href="/auth/login" class="text-gray-300 hover:text-white">Login</a>
<a href="/auth/register" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded font-medium">Register</a>
</div>
{% endif %}
{% endblock %}

42
app/templates/home.html Normal file
View File

@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% block title %}Art-DAG{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto">
{% if readme_html %}
<div class="prose prose-invert max-w-none mb-12">
{{ readme_html | safe }}
</div>
{% else %}
<div class="text-center py-12">
<h1 class="text-4xl font-bold mb-4">Art-DAG</h1>
<p class="text-xl text-gray-400 mb-8">Content-Addressable Media with ActivityPub Federation</p>
{% if not user %}
<div class="flex justify-center space-x-4">
<a href="/auth/login" class="bg-gray-700 hover:bg-gray-600 px-6 py-3 rounded-lg font-medium">Login</a>
<a href="/auth/register" class="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded-lg font-medium">Register</a>
</div>
{% endif %}
</div>
{% endif %}
{% if activities %}
<h2 class="text-2xl font-bold mb-4">Recent Activity</h2>
<div class="space-y-4">
{% for activity in activities %}
<div class="bg-gray-800 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<span class="text-blue-400">{{ activity.actor }}</span>
<span class="text-gray-500 text-sm">{{ activity.created_at }}</span>
</div>
<div class="text-gray-300">
{{ activity.type }}: {{ activity.summary or activity.object_type }}
</div>
</div>
{% endfor %}
</div>
{% endif %}
</div>
{% endblock %}