Update UI to use Tailwind CSS with dark theme
- Replace custom CSS with Tailwind CSS via CDN - Dark theme matching L1 server styling - Responsive layouts for all pages - Updated: home, login/register, registry, activities, users pages - Modern form styling and table layouts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
489
server.py
489
server.py
@@ -218,200 +218,71 @@ def sign_activity(activity: dict, username: str) -> dict:
|
||||
|
||||
# ============ HTML Templates ============
|
||||
|
||||
# Tailwind CSS config for L2 - dark theme to match L1
|
||||
TAILWIND_CONFIG = '''
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: { 900: '#0a0a0a', 800: '#111', 700: '#1a1a1a', 600: '#222', 500: '#333' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
'''
|
||||
|
||||
|
||||
def base_html(title: str, content: str, username: str = None) -> str:
|
||||
"""Base HTML template."""
|
||||
"""Base HTML template with Tailwind CSS dark theme."""
|
||||
user_section = f'''
|
||||
<div class="user-info">
|
||||
Logged in as <strong>{username}</strong>
|
||||
<button hx-post="/ui/logout" hx-swap="innerHTML" hx-target="body">Logout</button>
|
||||
<div class="flex items-center gap-4 text-sm text-gray-400">
|
||||
Logged in as <strong class="text-white">{username}</strong>
|
||||
<button hx-post="/ui/logout" hx-swap="innerHTML" hx-target="body"
|
||||
class="px-3 py-1 bg-dark-500 hover:bg-dark-600 rounded text-blue-400 hover:text-blue-300 transition-colors">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
''' if username else '''
|
||||
<div class="auth-links">
|
||||
<a href="/ui/login">Login</a> | <a href="/ui/register">Register</a>
|
||||
<div class="flex items-center gap-4 text-sm">
|
||||
<a href="/ui/login" class="text-blue-400 hover:text-blue-300">Login</a>
|
||||
<span class="text-gray-500">|</span>
|
||||
<a href="/ui/register" class="text-blue-400 hover:text-blue-300">Register</a>
|
||||
</div>
|
||||
'''
|
||||
|
||||
return f'''<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title} - Art DAG L2</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}}
|
||||
header {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 2px solid #333;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}}
|
||||
header h1 {{
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}}
|
||||
header h1 a {{
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}}
|
||||
.user-info, .auth-links {{
|
||||
font-size: 0.9rem;
|
||||
}}
|
||||
.auth-links a {{
|
||||
color: #0066cc;
|
||||
}}
|
||||
.user-info button {{
|
||||
margin-left: 10px;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
}}
|
||||
nav {{
|
||||
margin-bottom: 20px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}}
|
||||
nav a {{
|
||||
margin-right: 15px;
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}}
|
||||
nav a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
main {{
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}}
|
||||
.form-group {{
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
.form-group label {{
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
.form-group input {{
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}}
|
||||
button[type="submit"], .btn {{
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}}
|
||||
button[type="submit"]:hover, .btn:hover {{
|
||||
background: #0055aa;
|
||||
}}
|
||||
.error {{
|
||||
color: #cc0000;
|
||||
padding: 10px;
|
||||
background: #ffeeee;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
.success {{
|
||||
color: #006600;
|
||||
padding: 10px;
|
||||
background: #eeffee;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
/* README styling */
|
||||
.readme h1 {{ font-size: 1.8rem; margin-top: 0; }}
|
||||
.readme h2 {{ font-size: 1.4rem; margin-top: 1.5rem; border-bottom: 1px solid #eee; padding-bottom: 5px; }}
|
||||
.readme h3 {{ font-size: 1.1rem; }}
|
||||
.readme pre {{
|
||||
background: #f4f4f4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}}
|
||||
.readme code {{
|
||||
background: #f4f4f4;
|
||||
padding: 2px 5px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}}
|
||||
.readme pre code {{
|
||||
background: none;
|
||||
padding: 0;
|
||||
}}
|
||||
.readme table {{
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}}
|
||||
.readme th, .readme td {{
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}}
|
||||
.readme th {{
|
||||
background: #f4f4f4;
|
||||
}}
|
||||
.stats {{
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.stat-box {{
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}}
|
||||
.stat-box .number {{
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #0066cc;
|
||||
}}
|
||||
.stat-box .label {{
|
||||
color: #666;
|
||||
font-size: 0.9rem;
|
||||
}}
|
||||
@media (max-width: 600px) {{
|
||||
body {{ padding: 10px; }}
|
||||
header {{ flex-direction: column; align-items: flex-start; }}
|
||||
}}
|
||||
</style>
|
||||
{TAILWIND_CONFIG}
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><a href="/ui">Art DAG L2</a></h1>
|
||||
{user_section}
|
||||
</header>
|
||||
<nav>
|
||||
<a href="/ui">Home</a>
|
||||
<a href="/ui/registry">Registry</a>
|
||||
<a href="/ui/activities">Activities</a>
|
||||
<a href="/ui/users">Users</a>
|
||||
</nav>
|
||||
<main>
|
||||
{content}
|
||||
</main>
|
||||
<body class="bg-dark-900 text-gray-100 min-h-screen">
|
||||
<div class="max-w-5xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
|
||||
<header class="flex flex-wrap items-center justify-between gap-4 mb-6 pb-4 border-b border-dark-500">
|
||||
<h1 class="text-2xl font-bold">
|
||||
<a href="/ui" class="text-white hover:text-gray-200">Art DAG L2</a>
|
||||
</h1>
|
||||
{user_section}
|
||||
</header>
|
||||
|
||||
<nav class="flex flex-wrap gap-6 mb-6 pb-4 border-b border-dark-500">
|
||||
<a href="/ui" class="text-gray-400 hover:text-white transition-colors">Home</a>
|
||||
<a href="/ui/registry" class="text-gray-400 hover:text-white transition-colors">Registry</a>
|
||||
<a href="/ui/activities" class="text-gray-400 hover:text-white transition-colors">Activities</a>
|
||||
<a href="/ui/users" class="text-gray-400 hover:text-white transition-colors">Users</a>
|
||||
</nav>
|
||||
|
||||
<main class="bg-dark-700 rounded-lg p-6">
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
|
||||
@@ -437,21 +308,30 @@ async def ui_home(request: Request):
|
||||
readme_html = markdown.markdown(README_CONTENT, extensions=['tables', 'fenced_code'])
|
||||
|
||||
content = f'''
|
||||
<div class="stats">
|
||||
<div class="stat-box">
|
||||
<div class="number">{len(registry.get("assets", {}))}</div>
|
||||
<div class="label">Assets</div>
|
||||
<div class="grid gap-4 sm:grid-cols-3 mb-8">
|
||||
<div class="bg-dark-600 rounded-lg p-6 text-center">
|
||||
<div class="text-3xl font-bold text-blue-400">{len(registry.get("assets", {}))}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">Assets</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="number">{len(activities)}</div>
|
||||
<div class="label">Activities</div>
|
||||
<div class="bg-dark-600 rounded-lg p-6 text-center">
|
||||
<div class="text-3xl font-bold text-blue-400">{len(activities)}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">Activities</div>
|
||||
</div>
|
||||
<div class="stat-box">
|
||||
<div class="number">{len(users)}</div>
|
||||
<div class="label">Users</div>
|
||||
<div class="bg-dark-600 rounded-lg p-6 text-center">
|
||||
<div class="text-3xl font-bold text-blue-400">{len(users)}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">Users</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="readme">
|
||||
<div class="prose prose-invert max-w-none
|
||||
prose-headings:text-white prose-headings:border-b prose-headings:border-dark-500 prose-headings:pb-2
|
||||
prose-h1:text-2xl prose-h1:mt-0
|
||||
prose-h2:text-xl prose-h2:mt-6
|
||||
prose-a:text-blue-400 hover:prose-a:text-blue-300
|
||||
prose-pre:bg-dark-600 prose-pre:border prose-pre:border-dark-500
|
||||
prose-code:bg-dark-600 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-green-300
|
||||
prose-table:border-collapse
|
||||
prose-th:bg-dark-600 prose-th:border prose-th:border-dark-500 prose-th:px-4 prose-th:py-2
|
||||
prose-td:border prose-td:border-dark-500 prose-td:px-4 prose-td:py-2">
|
||||
{readme_html}
|
||||
</div>
|
||||
'''
|
||||
@@ -464,25 +344,31 @@ async def ui_login_page(request: Request):
|
||||
username = get_user_from_cookie(request)
|
||||
if username:
|
||||
return HTMLResponse(base_html("Already Logged In", f'''
|
||||
<div class="success">You are already logged in as <strong>{username}</strong></div>
|
||||
<p><a href="/ui">Go to home page</a></p>
|
||||
<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>{username}</strong>
|
||||
</div>
|
||||
<p><a href="/ui" class="text-blue-400 hover:text-blue-300">Go to home page</a></p>
|
||||
''', username))
|
||||
|
||||
content = '''
|
||||
<h2>Login</h2>
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Login</h2>
|
||||
<div id="login-result"></div>
|
||||
<form hx-post="/ui/login" hx-target="#login-result" hx-swap="innerHTML">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
<form hx-post="/ui/login" hx-target="#login-result" hx-swap="innerHTML" class="space-y-4 max-w-md">
|
||||
<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-dark-600 border border-dark-500 rounded-lg text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
<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-dark-600 border border-dark-500 rounded-lg text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<button type="submit">Login</button>
|
||||
<button type="submit" class="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
<p style="margin-top: 20px;">Don't have an account? <a href="/ui/register">Register</a></p>
|
||||
<p class="mt-6 text-gray-400">Don't have an account? <a href="/ui/register" class="text-blue-400 hover:text-blue-300">Register</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("Login", content))
|
||||
|
||||
@@ -495,16 +381,16 @@ async def ui_login_submit(request: Request):
|
||||
password = form.get("password", "")
|
||||
|
||||
if not username or not password:
|
||||
return HTMLResponse('<div class="error">Username and password are required</div>')
|
||||
return HTMLResponse('<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Username and password are required</div>')
|
||||
|
||||
user = authenticate_user(DATA_DIR, username, password)
|
||||
if not user:
|
||||
return HTMLResponse('<div class="error">Invalid username or password</div>')
|
||||
return HTMLResponse('<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Invalid username or password</div>')
|
||||
|
||||
token = create_access_token(user.username)
|
||||
|
||||
response = HTMLResponse(f'''
|
||||
<div class="success">Login successful! Redirecting...</div>
|
||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">Login successful! Redirecting...</div>
|
||||
<script>window.location.href = "/ui";</script>
|
||||
''')
|
||||
response.set_cookie(
|
||||
@@ -523,33 +409,41 @@ async def ui_register_page(request: Request):
|
||||
username = get_user_from_cookie(request)
|
||||
if username:
|
||||
return HTMLResponse(base_html("Already Logged In", f'''
|
||||
<div class="success">You are already logged in as <strong>{username}</strong></div>
|
||||
<p><a href="/ui">Go to home page</a></p>
|
||||
<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>{username}</strong>
|
||||
</div>
|
||||
<p><a href="/ui" class="text-blue-400 hover:text-blue-300">Go to home page</a></p>
|
||||
''', username))
|
||||
|
||||
content = '''
|
||||
<h2>Register</h2>
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Register</h2>
|
||||
<div id="register-result"></div>
|
||||
<form hx-post="/ui/register" hx-target="#register-result" hx-swap="innerHTML">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required pattern="[a-zA-Z0-9_-]+" title="Letters, numbers, underscore, dash only">
|
||||
<form hx-post="/ui/register" hx-target="#register-result" hx-swap="innerHTML" class="space-y-4 max-w-md">
|
||||
<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_-]+" title="Letters, numbers, underscore, dash only"
|
||||
class="w-full px-4 py-3 bg-dark-600 border border-dark-500 rounded-lg text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email (optional)</label>
|
||||
<input type="email" id="email" name="email">
|
||||
<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-dark-600 border border-dark-500 rounded-lg text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required minlength="6">
|
||||
<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-dark-600 border border-dark-500 rounded-lg text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password2">Confirm Password</label>
|
||||
<input type="password" id="password2" name="password2" required minlength="6">
|
||||
<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-dark-600 border border-dark-500 rounded-lg text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
<button type="submit">Register</button>
|
||||
<button type="submit" class="w-full px-4 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors">
|
||||
Register
|
||||
</button>
|
||||
</form>
|
||||
<p style="margin-top: 20px;">Already have an account? <a href="/ui/login">Login</a></p>
|
||||
<p class="mt-6 text-gray-400">Already have an account? <a href="/ui/login" class="text-blue-400 hover:text-blue-300">Login</a></p>
|
||||
'''
|
||||
return HTMLResponse(base_html("Register", content))
|
||||
|
||||
@@ -564,23 +458,23 @@ async def ui_register_submit(request: Request):
|
||||
password2 = form.get("password2", "")
|
||||
|
||||
if not username or not password:
|
||||
return HTMLResponse('<div class="error">Username and password are required</div>')
|
||||
return HTMLResponse('<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Username and password are required</div>')
|
||||
|
||||
if password != password2:
|
||||
return HTMLResponse('<div class="error">Passwords do not match</div>')
|
||||
return HTMLResponse('<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Passwords do not match</div>')
|
||||
|
||||
if len(password) < 6:
|
||||
return HTMLResponse('<div class="error">Password must be at least 6 characters</div>')
|
||||
return HTMLResponse('<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">Password must be at least 6 characters</div>')
|
||||
|
||||
try:
|
||||
user = create_user(DATA_DIR, username, password, email)
|
||||
except ValueError as e:
|
||||
return HTMLResponse(f'<div class="error">{str(e)}</div>')
|
||||
return HTMLResponse(f'<div class="bg-red-900/50 border border-red-700 text-red-300 px-4 py-3 rounded-lg mb-4">{str(e)}</div>')
|
||||
|
||||
token = create_access_token(user.username)
|
||||
|
||||
response = HTMLResponse(f'''
|
||||
<div class="success">Registration successful! Redirecting...</div>
|
||||
<div class="bg-green-900/50 border border-green-700 text-green-300 px-4 py-3 rounded-lg mb-4">Registration successful! Redirecting...</div>
|
||||
<script>window.location.href = "/ui";</script>
|
||||
''')
|
||||
response.set_cookie(
|
||||
@@ -611,34 +505,39 @@ async def ui_registry_page(request: Request):
|
||||
assets = registry.get("assets", {})
|
||||
|
||||
if not assets:
|
||||
content = '<h2>Registry</h2><p>No assets registered yet.</p>'
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Registry</h2>
|
||||
<p class="text-gray-400">No assets registered yet.</p>
|
||||
'''
|
||||
else:
|
||||
rows = ""
|
||||
for name, asset in sorted(assets.items(), key=lambda x: x[1].get("created_at", ""), reverse=True):
|
||||
hash_short = asset.get("content_hash", "")[:16] + "..."
|
||||
rows += f'''
|
||||
<tr>
|
||||
<td><strong>{name}</strong></td>
|
||||
<td>{asset.get("asset_type", "")}</td>
|
||||
<td><code>{hash_short}</code></td>
|
||||
<td>{", ".join(asset.get("tags", []))}</td>
|
||||
<tr class="border-b border-dark-500">
|
||||
<td class="py-3 px-4"><strong class="text-white">{name}</strong></td>
|
||||
<td class="py-3 px-4 text-gray-400">{asset.get("asset_type", "")}</td>
|
||||
<td class="py-3 px-4"><code class="text-xs bg-dark-600 px-2 py-1 rounded text-green-300">{hash_short}</code></td>
|
||||
<td class="py-3 px-4 text-gray-400">{", ".join(asset.get("tags", []))}</td>
|
||||
</tr>
|
||||
'''
|
||||
content = f'''
|
||||
<h2>Registry ({len(assets)} assets)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Content Hash</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Registry ({len(assets)} assets)</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-dark-600 text-left">
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Name</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Type</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Content Hash</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
return HTMLResponse(base_html("Registry", content, username))
|
||||
|
||||
@@ -650,34 +549,41 @@ async def ui_activities_page(request: Request):
|
||||
activities = load_activities()
|
||||
|
||||
if not activities:
|
||||
content = '<h2>Activities</h2><p>No activities yet.</p>'
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Activities</h2>
|
||||
<p class="text-gray-400">No activities yet.</p>
|
||||
'''
|
||||
else:
|
||||
rows = ""
|
||||
for activity in reversed(activities):
|
||||
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"
|
||||
rows += f'''
|
||||
<tr>
|
||||
<td>{activity.get("activity_type", "")}</td>
|
||||
<td>{obj.get("name", "")}</td>
|
||||
<td><code>{obj.get("contentHash", {}).get("value", "")[:16]}...</code></td>
|
||||
<td>{activity.get("published", "")[:10]}</td>
|
||||
<tr class="border-b border-dark-500">
|
||||
<td class="py-3 px-4"><span class="px-2 py-1 {type_color} text-white text-xs font-medium rounded">{activity_type}</span></td>
|
||||
<td class="py-3 px-4 text-white">{obj.get("name", "")}</td>
|
||||
<td class="py-3 px-4"><code class="text-xs bg-dark-600 px-2 py-1 rounded text-green-300">{obj.get("contentHash", {}).get("value", "")[:16]}...</code></td>
|
||||
<td class="py-3 px-4 text-gray-400">{activity.get("published", "")[:10]}</td>
|
||||
</tr>
|
||||
'''
|
||||
content = f'''
|
||||
<h2>Activities ({len(activities)} total)</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Object</th>
|
||||
<th>Content Hash</th>
|
||||
<th>Published</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 class="text-xl font-semibold text-white mb-6">Activities ({len(activities)} total)</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-dark-600 text-left">
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Type</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Object</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Content Hash</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Published</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
return HTMLResponse(base_html("Activities", content, username))
|
||||
|
||||
@@ -689,34 +595,39 @@ async def ui_users_page(request: Request):
|
||||
users = load_users(DATA_DIR)
|
||||
|
||||
if not users:
|
||||
content = '<h2>Users</h2><p>No users registered yet.</p>'
|
||||
content = '''
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Users</h2>
|
||||
<p class="text-gray-400">No users registered yet.</p>
|
||||
'''
|
||||
else:
|
||||
rows = ""
|
||||
for username, user_data in sorted(users.items()):
|
||||
actor_url = f"https://{DOMAIN}/users/{username}"
|
||||
webfinger = f"@{username}@{DOMAIN}"
|
||||
rows += f'''
|
||||
<tr>
|
||||
<td><a href="{actor_url}" target="_blank"><strong>{username}</strong></a></td>
|
||||
<td><code>{webfinger}</code></td>
|
||||
<td>{user_data.get("created_at", "")[:10]}</td>
|
||||
<tr class="border-b border-dark-500">
|
||||
<td class="py-3 px-4"><a href="{actor_url}" target="_blank" class="text-blue-400 hover:text-blue-300 font-medium">{username}</a></td>
|
||||
<td class="py-3 px-4"><code class="text-xs bg-dark-600 px-2 py-1 rounded text-gray-300">{webfinger}</code></td>
|
||||
<td class="py-3 px-4 text-gray-400">{user_data.get("created_at", "")[:10]}</td>
|
||||
</tr>
|
||||
'''
|
||||
content = f'''
|
||||
<h2>Users ({len(users)} registered)</h2>
|
||||
<p>Each user has their own ActivityPub actor that can be followed from Mastodon and other federated platforms.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>ActivityPub Handle</th>
|
||||
<th>Registered</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
<h2 class="text-xl font-semibold text-white mb-4">Users ({len(users)} registered)</h2>
|
||||
<p class="text-gray-400 mb-6">Each user has their own ActivityPub actor that can be followed from Mastodon and other federated platforms.</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="bg-dark-600 text-left">
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Username</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">ActivityPub Handle</th>
|
||||
<th class="py-3 px-4 font-medium text-gray-300">Registered</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
'''
|
||||
return HTMLResponse(base_html("Users", content, current_user))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user