2026-05-10 09:08:12 +02:00
{% extends "base.html" %}
{% block extra_head %}
2026-06-14 02:16:22 +02:00
< link rel = "stylesheet" href = "{{ static_url('/static/css/feed.css') }}" >
< link rel = "stylesheet" href = "{{ static_url('/static/css/profile.css') }}" >
< link rel = "stylesheet" href = "{{ static_url('/static/css/projects.css') }}" >
< link rel = "stylesheet" href = "{{ static_url('/static/css/gists.css') }}" >
2026-05-10 09:08:12 +02:00
{% endblock %}
{% block content %}
< div class = "profile-layout" >
< aside class = "profile-sidebar" >
< div class = "profile-card" >
2026-05-11 03:14:43 +02:00
< div class = "profile-avatar-wrap" >
2026-05-11 05:30:51 +02:00
< img src = "{{ avatar_url('multiavatar', profile_user['username'], 80) }}" class = "avatar-img avatar-lg" alt = "{{ profile_user['username'] }}" id = "profile-avatar-preview" loading = "lazy" >
2026-05-10 09:08:12 +02:00
< / div >
2026-05-11 05:30:51 +02:00
< h1 class = "profile-name" > {{ profile_user['username'] }}< / h1 >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
{% if is_admin(user) and profile_user.get('role') %}
2026-05-10 09:08:12 +02:00
< div class = "profile-role" > {{ profile_user['role'] }}< / div >
{% endif %}
< div class = "profile-stats" >
< div class = "profile-stat" >
2026-05-11 00:41:41 +02:00
< span class = "profile-stat-value" > {{ posts_count }}< / span >
2026-05-10 09:08:12 +02:00
< span class = "profile-stat-label" > Posts< / span >
< / div >
< div class = "profile-stat" >
< span class = "profile-stat-value" > {{ profile_user.get('level', 1) }}< / span >
< span class = "profile-stat-label" > Level< / span >
< / div >
< div class = "profile-stat" >
< span class = "profile-stat-value" > {{ profile_user.get('stars', 0) }}< / span >
< span class = "profile-stat-label" > Stars< / span >
< / div >
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
< div class = "profile-stat" >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
< a href = "/leaderboard" class = "profile-stat-value" > {% if rank %}#{{ rank }}{% else %}-{% endif %}< / a >
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
< span class = "profile-stat-label" > Rank< / span >
< / div >
2026-05-10 09:08:12 +02:00
< / div >
< div class = "profile-level-bar" >
< div class = "level-label" >
< span > Progress to next level< / span >
< span > {{ profile_user.get('xp', 0) % 100 }}%< / span >
< / div >
< div class = "bar" >
2026-05-12 15:07:34 +02:00
< div class = "bar-fill" style = "width: {{ profile_user.get('xp', 0) % 100 }}%;" > < / div >
2026-05-10 09:08:12 +02:00
< / div >
< / div >
< div class = "profile-badges" >
{% for badge in badges %}
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
{% set meta = badge_info(badge['badge_name']) %}
< span class = "profile-badge" title = "{{ meta['description'] }}" > < span class = "profile-badge-icon" > {{ meta['icon'] }}< / span > {{ badge['badge_name'] }}< / span >
2026-05-10 09:08:12 +02:00
{% else %}
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
{% set meta = badge_info('Member') %}
< span class = "profile-badge" title = "{{ meta['description'] }}" > < span class = "profile-badge-icon" > {{ meta['icon'] }}< / span > Member< / span >
2026-05-10 09:08:12 +02:00
{% endfor %}
< / div >
< / div >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
{% if not is_self(user, profile_user['uid']) %}
2026-05-12 15:07:34 +02:00
< div class = "profile-actions" >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
< a href = "{% if user %}/messages?with_uid={{ profile_user['uid'] }}{% else %}/auth/login{% endif %}" class = "btn btn-secondary btn-sm full-width justify-center{% if not user %} login-required{% endif %}" { % if not user % } title = "Log in to participate" { % endif % } > < span class = "icon" > ✉ ️ < / span > Send Message< / a >
2026-05-11 00:41:41 +02:00
{% if is_following %}
< form method = "POST" action = "/follow/unfollow/{{ profile_user['username'] }}" >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
< button type = "submit" class = "btn btn-secondary btn-sm full-width" { { guest_disabled ( user ) } } > < span class = "icon" > ➖ < / span > Unfollow< / button >
2026-05-11 00:41:41 +02:00
< / form >
{% else %}
< form method = "POST" action = "/follow/{{ profile_user['username'] }}" >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
< button type = "submit" class = "btn btn-primary btn-sm full-width" { { guest_disabled ( user ) } } > < span class = "icon" > ➕ < / span > Follow< / button >
2026-05-11 00:41:41 +02:00
< / form >
{% endif %}
< / div >
{% endif %}
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
{% if is_self(user, profile_user['uid']) %}
2026-05-10 09:08:12 +02:00
< div class = "profile-info" >
< form method = "POST" action = "/profile/update" >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Bio< / span >
2026-05-12 15:07:34 +02:00
< div class = "flex-center-gap" >
2026-05-10 09:08:12 +02:00
< span class = "profile-info-value bio-display" id = "display-bio" > {{ profile_user.get('bio', '') or 'Not set' }}< / span >
< button type = "button" class = "profile-info-edit" data-edit-field = "bio" > ✎ < / button >
< / div >
< / div >
2026-05-12 15:07:34 +02:00
< div class = "auth-field hidden input-edit-field" id = "input-bio" >
< textarea name = "bio" maxlength = "500" rows = "2" class = "input-sm" > {{ profile_user.get('bio', '') }}< / textarea >
2026-05-10 09:08:12 +02:00
< / div >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Location< / span >
2026-05-12 15:07:34 +02:00
< div class = "flex-center-gap" >
2026-05-10 09:08:12 +02:00
< span class = "profile-info-value" id = "display-location" > {{ profile_user.get('location', '') or 'Not set' }}< / span >
< button type = "button" class = "profile-info-edit" data-edit-field = "location" > ✎ < / button >
< / div >
< / div >
2026-05-12 15:07:34 +02:00
< div class = "auth-field hidden input-edit-field" id = "input-location" >
< input type = "text" name = "location" maxlength = "200" value = "{{ profile_user.get('location', '') }}" class = "input-sm" >
2026-05-10 09:08:12 +02:00
< / div >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Git Link< / span >
2026-05-12 15:07:34 +02:00
< div class = "flex-center-gap" >
2026-05-10 09:08:12 +02:00
< span class = "profile-info-value" id = "display-git_link" > {{ profile_user.get('git_link', '') or 'Not set' }}< / span >
< button type = "button" class = "profile-info-edit" data-edit-field = "git_link" > ✎ < / button >
< / div >
< / div >
2026-05-12 15:07:34 +02:00
< div class = "auth-field hidden input-edit-field" id = "input-git_link" >
< input type = "url" name = "git_link" maxlength = "500" value = "{{ profile_user.get('git_link', '') }}" class = "input-sm" >
2026-05-10 09:08:12 +02:00
< / div >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Website< / span >
2026-05-12 15:07:34 +02:00
< div class = "flex-center-gap" >
2026-05-10 09:08:12 +02:00
< span class = "profile-info-value" id = "display-website" > {{ profile_user.get('website', '') or 'Not set' }}< / span >
< button type = "button" class = "profile-info-edit" data-edit-field = "website" > ✎ < / button >
< / div >
< / div >
2026-05-12 15:07:34 +02:00
< div class = "auth-field hidden input-edit-field" id = "input-website" >
< input type = "url" name = "website" maxlength = "500" value = "{{ profile_user.get('website', '') }}" class = "input-sm" >
2026-05-10 09:08:12 +02:00
< / div >
2026-05-12 15:07:34 +02:00
< button type = "submit" class = "btn btn-primary btn-sm full-width save-btn-top" > Save Changes< / button >
2026-05-10 09:08:12 +02:00
< / form >
< / div >
{% else %}
< div class = "profile-info" >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Bio< / span >
< span class = "profile-info-value" > {{ profile_user.get('bio', '') or 'Not set' }}< / span >
< / div >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Location< / span >
< span class = "profile-info-value" > {{ profile_user.get('location', '') or 'Not set' }}< / span >
< / div >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Git Link< / span >
< span class = "profile-info-value" > {{ profile_user.get('git_link', '') or 'Not set' }}< / span >
< / div >
< div class = "profile-info-row" >
< span class = "profile-info-label" > Website< / span >
< span class = "profile-info-value" > {{ profile_user.get('website', '') or 'Not set' }}< / span >
< / div >
< / div >
{% endif %}
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
{% if can_view_api_key %}
< div class = "api-key-card" data-api-key-card >
< div class = "api-key-head" >
< span class = "api-key-title" > API Key< / span >
< a href = "/docs/authentication.html" class = "api-key-docs" > Docs< / a >
< / div >
< code class = "api-key-value" data-api-key data-key = "{{ api_key }}" data-masked = "true" > {{ '•' * 12 }}< / code >
< div class = "api-key-actions" >
< button type = "button" class = "btn btn-sm" data-api-key-reveal > Reveal< / button >
< button type = "button" class = "btn btn-sm" data-api-key-copy > Copy< / button >
{% if is_owner %}
< button type = "button" class = "btn btn-sm btn-primary" data-regen-api-key > Regenerate< / button >
{% endif %}
< / div >
< p class = "api-key-hint" > Use this key with < code > X-API-KEY< / code > , < code > Authorization: Bearer< / code > , or HTTP Basic auth on any request. {% if is_owner %}Regenerating immediately invalidates the old key.{% endif %}< / p >
< / div >
{% endif %}
2026-06-09 22:52:51 +02:00
{% if can_manage_customization %}
< div class = "customization-card" data-customization data-username = "{{ profile_user['username'] }}" >
< div class = "customization-head" >
< span class = "customization-title" > {% if is_owner %}My customizations{% else %}{{ profile_user['username'] }}'s customizations{% endif %}< / span >
< / div >
< div class = "customization-row" >
< span class = "customization-label" > Site-wide custom CSS and JS< / span >
< form method = "POST" action = "/profile/{{ profile_user['username'] }}/customization/global" data-customization-form >
< input type = "hidden" name = "value" value = "{{ 1 if cust_disable_global else 0 }}" >
< button type = "submit" class = "btn btn-sm {% if cust_disable_global %}btn-primary{% endif %}" data-customization-toggle = "global" data-confirm = "{% if cust_disable_global %}Re-enable your site-wide custom CSS and JS?{% else %}Disable your site-wide custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}" > {% if cust_disable_global %}Enable{% else %}Disable{% endif %}< / button >
< / form >
< / div >
< div class = "customization-row" >
< span class = "customization-label" > Per-page custom CSS and JS< / span >
< form method = "POST" action = "/profile/{{ profile_user['username'] }}/customization/pagetype" data-customization-form >
< input type = "hidden" name = "value" value = "{{ 1 if cust_disable_pagetype else 0 }}" >
< button type = "submit" class = "btn btn-sm {% if cust_disable_pagetype %}btn-primary{% endif %}" data-customization-toggle = "pagetype" data-confirm = "{% if cust_disable_pagetype %}Re-enable your per-page custom CSS and JS?{% else %}Disable your per-page custom CSS and JS? Your saved code is kept and can be re-enabled.{% endif %}" > {% if cust_disable_pagetype %}Enable{% else %}Disable{% endif %}< / button >
< / form >
< / div >
< p class = "customization-hint" > Disabling hides your custom code without deleting it.< / p >
< / div >
{% endif %}
2026-06-16 05:32:19 +02:00
{% if is_owner %}
< div class = "customization-card" data-ai-correction data-username = "{{ profile_user['username'] }}" >
< div class = "customization-head" >
< span class = "customization-title" > AI content correction< / span >
< / div >
< div class = "customization-row" >
< span class = "customization-label" > Enable correction< / span >
< label class = "ai-correction-switch" >
< input type = "checkbox" data-ai-correction-toggle { % if ai_correction_enabled % } checked { % endif % } >
< / label >
< / div >
< div class = "customization-row" >
< span class = "customization-label" > Apply mode< / span >
< select data-ai-correction-sync class = "input-sm" >
< option value = "async" { % if not ai_correction_sync % } selected { % endif % } > In background (faster, applies after saving)< / option >
< option value = "sync" { % if ai_correction_sync % } selected { % endif % } > Synchronously (wait while saving)< / option >
< / select >
< / div >
< textarea data-ai-correction-prompt class = "input-sm ai-correction-prompt" maxlength = "2000" rows = "3" > {{ ai_correction_prompt }}< / textarea >
< div class = "customization-row" >
< button type = "button" class = "btn btn-sm btn-primary" data-ai-correction-save > Save< / button >
< span class = "ai-correction-status" data-ai-correction-status > < / span >
< / div >
< p class = "customization-hint" > Corrections apply to new and edited content using your API key, on posts, comments, projects, gists, direct messages and your bio. Code and source files are never changed. In background mode the correction is applied just after saving; in synchronous mode the save waits for the correction to finish.< / p >
< / div >
< div class = "customization-card" data-ai-modifier data-username = "{{ profile_user['username'] }}" >
< div class = "customization-head" >
< span class = "customization-title" > AI modifier< / span >
< / div >
< div class = "customization-row" >
< span class = "customization-label" > Enable modifier< / span >
< label class = "ai-correction-switch" >
< input type = "checkbox" data-ai-modifier-toggle { % if ai_modifier_enabled % } checked { % endif % } >
< / label >
< / div >
< div class = "customization-row" >
< span class = "customization-label" > Apply mode< / span >
< select data-ai-modifier-sync class = "input-sm" >
< option value = "async" { % if not ai_modifier_sync % } selected { % endif % } > In background (applies after saving)< / option >
< option value = "sync" { % if ai_modifier_sync % } selected { % endif % } > Synchronously (wait while saving)< / option >
< / select >
< / div >
< textarea data-ai-modifier-prompt class = "input-sm ai-correction-prompt" maxlength = "2000" rows = "3" > {{ ai_modifier_prompt }}< / textarea >
< div class = "customization-row" >
< button type = "button" class = "btn btn-sm btn-primary" data-ai-modifier-save > Save< / button >
< span class = "ai-correction-status" data-ai-modifier-status > < / span >
< / div >
< p class = "customization-hint" > Runs only when your text contains an inline `@ai instruction` directive: it executes that instruction and removes the `@ai` marker, using your API key. Applies to posts, comments, projects, gists, direct messages and your bio. Code and source files are never touched.< / p >
< / div >
{% endif %}
2026-05-10 09:08:12 +02:00
< / aside >
< div class = "profile-content" >
2026-05-11 05:30:51 +02:00
< a href = "/feed" class = "back-link" > ← Back< / a >
2026-05-10 09:08:12 +02:00
2026-06-06 16:31:42 +02:00
< div class = "profile-heatmap-card" >
< div class = "heatmap-header" >
< h3 class = "heatmap-title" > Contributions< / h3 >
< span class = "streak-info" > < span class = "icon" > 🔥 < / span > {{ streak.current }} day streak · Longest {{ streak.longest }}< / span >
< / div >
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
< div class = "heatmap-scroll" >
< div class = "heatmap-inner" >
< div class = "heatmap-months" >
{% for month in heatmap_months %}< span > {{ month }}< / span > {% endfor %}
< / div >
< div class = "heatmap-grid" >
{% for week in heatmap %}
< div class = "heatmap-week" >
{% for day in week %}
< span class = "heatmap-cell level-{{ day.level }}" title = "{{ day.count }} on {{ day.date }}" > < / span >
{% endfor %}
< / div >
{% endfor %}
< / div >
2026-06-06 16:31:42 +02:00
< / div >
< / div >
< / div >
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
{% if is_admin(user) %}
< div class = "ai-usage-card" data-ai-usage data-uid = "{{ profile_user['uid'] }}" >
< div class = "ai-usage-head" >
< h3 class = "ai-usage-title" > AI usage (last 24h)< / h3 >
< span class = "ai-usage-updated" data-ai-usage-updated > < / span >
< / div >
{% if ai_quota %}
< div class = "ai-quota" data-ai-quota >
< div class = "ai-quota-bar" >
2026-06-16 00:44:12 +02:00
< span class = "ai-quota-fill {% if not ai_quota.unlimited and ai_quota.used_pct >= 90 %}ai-quota-danger{% endif %}" style = "width: {{ 0 if ai_quota.unlimited else ai_quota.used_pct }}%" > < / span >
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
< / div >
< div class = "ai-quota-meta" >
2026-06-16 00:44:12 +02:00
< span class = "ai-quota-pct" > {% if ai_quota.unlimited %}No daily spend cap{% else %}{{ ai_quota.used_pct }}% of daily quota used{% endif %}< / span >
< span class = "ai-quota-detail" > ${{ '%.4f'|format(ai_quota.spent_usd) }} / {% if ai_quota.unlimited %}no cap{% else %}${{ '%.2f'|format(ai_quota.limit_usd) }}{% endif %} · {{ ai_quota.turns }} Devii turns< / span >
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
< / div >
< / div >
{% endif %}
< div class = "ai-usage-body" data-ai-usage-body >
< p class = "ai-usage-empty" > Loading...< / p >
< / div >
< / div >
{% elif is_self(user, profile_user['uid']) %}
< div class = "ai-usage-card ai-quota-only" >
< div class = "ai-usage-head" >
< h3 class = "ai-usage-title" > AI quota< / h3 >
< / div >
{% if ai_quota %}
< div class = "ai-quota" data-ai-quota >
< div class = "ai-quota-bar" >
2026-06-16 00:44:12 +02:00
< span class = "ai-quota-fill {% if not ai_quota.unlimited and ai_quota.used_pct >= 90 %}ai-quota-danger{% endif %}" style = "width: {{ 0 if ai_quota.unlimited else ai_quota.used_pct }}%" > < / span >
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
< / div >
< div class = "ai-quota-meta" >
2026-06-16 00:44:12 +02:00
< span class = "ai-quota-pct" > {% if ai_quota.unlimited %}No daily AI spend cap{% else %}{{ ai_quota.used_pct }}% of your daily AI quota used{% endif %}< / span >
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
< / div >
< / div >
{% else %}
< p class = "ai-usage-empty" > Quota unavailable.< / p >
{% endif %}
< / div >
{% endif %}
2026-06-16 05:32:19 +02:00
{% if correction_usage and correction_usage.calls %}
< div class = "ai-usage-card correction-usage-card" >
< div class = "ai-usage-head" >
< h3 class = "ai-usage-title" > AI correction usage< / h3 >
{% if correction_usage.last_used %}< span class = "ai-usage-updated" > {{ format_date(correction_usage.last_used) }}< / span > {% endif %}
< / div >
< div class = "correction-usage-stats" >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(correction_usage.calls) }}< / span >
< span class = "correction-usage-label" > Corrections< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(correction_usage.total_tokens) }}< / span >
< span class = "correction-usage-label" > Total tokens< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(correction_usage.prompt_tokens) }}< / span >
< span class = "correction-usage-label" > Prompt tokens< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(correction_usage.completion_tokens) }}< / span >
< span class = "correction-usage-label" > Completion tokens< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.0f"|format(correction_usage.avg_tokens) }}< / span >
< span class = "correction-usage-label" > Avg tokens/call< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.0f"|format(correction_usage.avg_latency_ms) }} ms< / span >
< span class = "correction-usage-label" > Avg latency< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.1f"|format(correction_usage.avg_tokens_per_second) }}/s< / span >
< span class = "correction-usage-label" > Avg speed< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.1f"|format(correction_usage.total_time_s) }} s< / span >
< span class = "correction-usage-label" > Total time< / span >
< / div >
{% if correction_usage.cost_usd is defined %}
< div class = "correction-usage-stat correction-usage-cost" >
< span class = "correction-usage-value" > ${{ "%.4f"|format(correction_usage.cost_usd) }}< / span >
< span class = "correction-usage-label" > Total cost< / span >
< / div >
< div class = "correction-usage-stat correction-usage-cost" >
< span class = "correction-usage-value" > ${{ "%.5f"|format(correction_usage.avg_cost_usd) }}< / span >
< span class = "correction-usage-label" > Avg cost/call< / span >
< / div >
{% endif %}
< / div >
< / div >
{% endif %}
{% if modifier_usage and modifier_usage.calls %}
< div class = "ai-usage-card correction-usage-card" >
< div class = "ai-usage-head" >
< h3 class = "ai-usage-title" > AI modifier usage< / h3 >
{% if modifier_usage.last_used %}< span class = "ai-usage-updated" > {{ format_date(modifier_usage.last_used) }}< / span > {% endif %}
< / div >
< div class = "correction-usage-stats" >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(modifier_usage.calls) }}< / span >
< span class = "correction-usage-label" > Modifications< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(modifier_usage.total_tokens) }}< / span >
< span class = "correction-usage-label" > Total tokens< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(modifier_usage.prompt_tokens) }}< / span >
< span class = "correction-usage-label" > Prompt tokens< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "{:,}".format(modifier_usage.completion_tokens) }}< / span >
< span class = "correction-usage-label" > Completion tokens< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.0f"|format(modifier_usage.avg_tokens) }}< / span >
< span class = "correction-usage-label" > Avg tokens/call< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.0f"|format(modifier_usage.avg_latency_ms) }} ms< / span >
< span class = "correction-usage-label" > Avg latency< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.1f"|format(modifier_usage.avg_tokens_per_second) }}/s< / span >
< span class = "correction-usage-label" > Avg speed< / span >
< / div >
< div class = "correction-usage-stat" >
< span class = "correction-usage-value" > {{ "%.1f"|format(modifier_usage.total_time_s) }} s< / span >
< span class = "correction-usage-label" > Total time< / span >
< / div >
{% if modifier_usage.cost_usd is defined %}
< div class = "correction-usage-stat correction-usage-cost" >
< span class = "correction-usage-value" > ${{ "%.4f"|format(modifier_usage.cost_usd) }}< / span >
< span class = "correction-usage-label" > Total cost< / span >
< / div >
< div class = "correction-usage-stat correction-usage-cost" >
< span class = "correction-usage-value" > ${{ "%.5f"|format(modifier_usage.avg_cost_usd) }}< / span >
< span class = "correction-usage-label" > Avg cost/call< / span >
< / div >
{% endif %}
< / div >
< / div >
{% endif %}
< details class = "profile-achievements" >
< summary class = "profile-achievements-summary" >
< span class = "profile-achievements-title" > < span class = "icon" > 🏆 < / span > Achievements< / span >
< span class = "profile-achievements-count" > {{ badge_earned }} / {{ badge_total }}< / span >
< / summary >
{% for group in achievements %}
< div class = "achievement-group" >
< h3 class = "achievement-group-title" > {{ group['group'] }} < span class = "achievement-group-count" > {{ group['earned'] }}/{{ group['total'] }}< / span > < / h3 >
< div class = "achievement-grid" >
{% for item in group['items'] %}
< div class = "achievement {% if item['earned'] %}earned{% else %}locked{% endif %}" title = "{{ item['description'] }}" >
< span class = "achievement-icon" > {{ item['icon'] }}< / span >
< span class = "achievement-body" >
< span class = "achievement-name" > {{ item['name'] }}< / span >
< span class = "achievement-desc" > {{ item['description'] }}< / span >
< / span >
< / div >
{% endfor %}
< / div >
< / div >
{% endfor %}
< / details >
2026-06-13 21:47:50 +02:00
< div class = "profile-tabs" data-profile-tabs >
< a href = "/profile/{{ profile_user['username'] }}?tab=posts" class = "profile-tab {% if current_tab == 'posts' %}active{% endif %}" data-menu-icon = "📝" data-menu-label = "Posts" > < span class = "icon" > 📝 < / span > Posts< / a >
< a href = "/profile/{{ profile_user['username'] }}?tab=projects" class = "profile-tab {% if current_tab == 'projects' %}active{% endif %}" data-menu-icon = "🚀" data-menu-label = "Projects" > < span class = "icon" > 🚀 < / span > Projects< / a >
< a href = "/profile/{{ profile_user['username'] }}?tab=gists" class = "profile-tab {% if current_tab == 'gists' %}active{% endif %}" data-menu-icon = "📝" data-menu-label = "Gists" > < span class = "icon" > 📝 < / span > Gists< / a >
< a href = "/profile/{{ profile_user['username'] }}?tab=media" class = "profile-tab {% if current_tab == 'media' %}active{% endif %}" data-menu-icon = "🖼️" data-menu-label = "Media" > < span class = "icon" > 🖼 ️ < / span > Media< / a >
< a href = "/profile/{{ profile_user['username'] }}?tab=activity" class = "profile-tab {% if current_tab == 'activity' %}active{% endif %}" data-menu-icon = "📊" data-menu-label = "Activity" > < span class = "icon" > 📊 < / span > Activity< / a >
< a href = "/profile/{{ profile_user['username'] }}?tab=followers" class = "profile-tab {% if current_tab == 'followers' %}active{% endif %}" data-menu-icon = "👥" data-menu-label = "Followers ({{ followers_count }})" > < span class = "icon" > 👥 < / span > Followers ({{ followers_count }})< / a >
< a href = "/profile/{{ profile_user['username'] }}?tab=following" class = "profile-tab {% if current_tab == 'following' %}active{% endif %}" data-menu-icon = "👤" data-menu-label = "Following ({{ following_count }})" > < span class = "icon" > 👤 < / span > Following ({{ following_count }})< / a >
2026-06-13 12:09:48 +02:00
{% if is_owner or viewer_is_admin %}
2026-06-13 21:47:50 +02:00
< a href = "/profile/{{ profile_user['username'] }}?tab=notifications" class = "profile-tab {% if current_tab == 'notifications' %}active{% endif %}" data-menu-icon = "🔔" data-menu-label = "Notifications" > < span class = "icon" > 🔔 < / span > Notifications< / a >
2026-06-13 12:09:48 +02:00
{% endif %}
2026-06-13 21:47:50 +02:00
< button type = "button" class = "profile-tab profile-tabs-more" aria-haspopup = "menu" aria-label = "More tabs" hidden > < span class = "icon" > ⋯ < / span > More< / button >
2026-05-10 09:08:12 +02:00
< / div >
< div class = "profile-posts" >
{% if current_tab == 'posts' %}
{% for item in posts %}
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
{% set _author = profile_user %}{% set _time = item.time_ago %}{% set _show_share = false %}{% set _show_comment_form = false %}{% include "_post_card.html" %}
2026-05-10 09:08:12 +02:00
{% else %}
2026-05-10 21:33:53 +02:00
< div class = "empty-state" > No posts yet.< / div >
2026-05-10 09:08:12 +02:00
{% endfor %}
{% elif current_tab == 'projects' %}
{% for p in projects %}
2026-06-06 16:31:42 +02:00
< a href = "/projects/{{ p['slug'] or p['uid'] }}" class = "project-card" >
2026-05-10 21:33:53 +02:00
< div class = "project-card-header" >
< h3 class = "project-card-title" > {{ p['title'] }}< / h3 >
< / div >
< div class = "project-card-meta" >
< span class = "project-status {% if p.get('status') == 'Released' %}released{% else %}dev{% endif %}" >
● {{ p.get('status', 'In Development') }}
< / span >
< span > {{ p.get('project_type', 'software')|replace('_', ' ')|capitalize }}< / span >
< / div >
2026-05-11 05:30:51 +02:00
< div class = "project-card-desc rendered-content" data-render > {{ p.get('description', '')[:200] }}< / div >
2026-05-10 21:33:53 +02:00
{% if p.get('platforms') %}
< div class = "project-card-platforms" >
{% for plat in p['platforms'].split(',') %}
< span class = "platform-tag" > {{ plat.strip() }}< / span >
{% endfor %}
2026-05-10 09:08:12 +02:00
< / div >
2026-05-10 21:33:53 +02:00
{% endif %}
2026-05-11 07:02:06 +02:00
< / a >
2026-05-10 09:08:12 +02:00
{% else %}
2026-05-10 21:33:53 +02:00
< div class = "empty-state" > No projects yet.< / div >
2026-05-10 09:08:12 +02:00
{% endfor %}
2026-05-12 15:07:34 +02:00
{% elif current_tab == 'gists' %}
2026-05-13 21:17:57 +02:00
{% for entry in gists %}
{% set g = entry.gist %}
2026-06-06 16:31:42 +02:00
< a href = "/gists/{{ g['slug'] or g['uid'] }}" class = "gist-card" >
2026-05-12 15:07:34 +02:00
< div class = "gist-card-header" >
< h3 class = "gist-card-title" > {{ g['title'] }}< / h3 >
< span class = "gist-card-star" > ☆ {{ g.get('stars', 0) }}< / span >
< / div >
< div class = "gist-card-meta" >
2026-05-13 21:17:57 +02:00
< span class = "gist-language-badge" > 📝 {{ language_name(g['language']) }}< / span >
2026-05-12 15:07:34 +02:00
< / div >
{% if g.get('description') %}
< div class = "gist-card-desc" > {{ g['description'][:200] }}{% if g['description']|length > 200 %}...{% endif %}< / div >
{% endif %}
2026-05-13 21:17:57 +02:00
< div class = "gist-card-footer" >
2026-06-16 05:32:19 +02:00
< span > {{ dt_ago(entry.gist.created_at) if entry.gist.created_at else entry.time_ago }}< / span >
2026-05-13 21:17:57 +02:00
< / div >
2026-05-12 15:07:34 +02:00
< / a >
{% else %}
< div class = "empty-state" > No gists yet.< / div >
{% endfor %}
2026-06-11 20:52:56 +02:00
{% elif current_tab == 'media' %}
{% include "_media_gallery.html" %}
{% set pagination = media_pagination %}
2026-06-11 22:36:04 +02:00
{% set pagination_query = "tab=media& " %}
2026-06-11 20:52:56 +02:00
{% include "_pagination.html" %}
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
{% elif current_tab == 'followers' or current_tab == 'following' %}
{% for person in people %}
< div class = "follow-row" >
< a href = "/profile/{{ person['username'] }}" class = "follow-user" >
< img src = "{{ avatar_url('multiavatar', person['username'], 40) }}" class = "avatar-img avatar-md" alt = "{{ person['username'] }}" loading = "lazy" >
< div class = "follow-user-info" >
< span class = "follow-user-name" > {{ person['username'] }}< / span >
{% if person['bio'] %}< span class = "follow-user-bio" > {{ person['bio'] }}< / span > {% endif %}
< / div >
< / a >
{% if user and not person['is_self'] %}
{% if person['is_following'] %}
< form method = "post" action = "/follow/unfollow/{{ person['username'] }}" >
< button type = "submit" class = "btn btn-ghost btn-sm" { { guest_disabled ( user ) } } > Following< / button >
< / form >
{% else %}
< form method = "post" action = "/follow/{{ person['username'] }}" >
< button type = "submit" class = "btn btn-primary btn-sm" { { guest_disabled ( user ) } } > Follow< / button >
< / form >
{% endif %}
{% endif %}
< / div >
{% else %}
< div class = "empty-state" > {% if current_tab == 'followers' %}No followers yet.{% else %}Not following anyone yet.{% endif %}< / div >
{% endfor %}
{% if follow_pagination and follow_pagination.total_pages > 1 %}
< div class = "follow-pagination" >
{% if follow_pagination.has_prev %}< a class = "btn btn-ghost btn-sm" href = "/profile/{{ profile_user['username'] }}?tab={{ current_tab }}&page={{ follow_pagination.prev_page }}" > Previous< / a > {% endif %}
< span class = "follow-page-label" > Page {{ follow_pagination.page }} of {{ follow_pagination.total_pages }}< / span >
{% if follow_pagination.has_next %}< a class = "btn btn-ghost btn-sm" href = "/profile/{{ profile_user['username'] }}?tab={{ current_tab }}&page={{ follow_pagination.next_page }}" > Next< / a > {% endif %}
< / div >
{% endif %}
2026-06-13 12:09:48 +02:00
{% elif current_tab == 'notifications' %}
{% if is_owner or viewer_is_admin %}
< div class = "notif-prefs" data-notif-prefs data-username = "{{ profile_user['username'] }}" >
< div class = "notif-prefs-intro" >
{% if not is_owner %}< p class = "notif-prefs-admin" > Editing notification settings for < strong > {{ profile_user['username'] }}< / strong > .< / p > {% endif %}
< p > Choose how you want to be notified for each event. < strong > In-app< / strong > shows the notification on DevPlace; < strong > Push< / strong > sends it to your subscribed devices.< / p >
< / div >
< table class = "notif-prefs-table" >
< thead >
< tr >
< th > Notification< / th >
< th class = "notif-prefs-col" > In-app< / th >
< th class = "notif-prefs-col" > Push< / th >
< / tr >
< / thead >
< tbody >
{% for pref in notification_prefs %}
< tr >
< td >
< span class = "notif-prefs-label" > {{ pref.label }}< / span >
< span class = "notif-prefs-desc" > {{ pref.description }}< / span >
< / td >
< td class = "notif-prefs-col" >
< label class = "notif-switch" >
< input type = "checkbox" data-notif-toggle data-type = "{{ pref.key }}" data-channel = "in_app" { % if pref . in_app % } checked { % endif % } >
< span class = "notif-switch-track" > < / span >
< / label >
< / td >
< td class = "notif-prefs-col" >
< label class = "notif-switch" >
< input type = "checkbox" data-notif-toggle data-type = "{{ pref.key }}" data-channel = "push" { % if pref . push % } checked { % endif % } >
< span class = "notif-switch-track" > < / span >
< / label >
< / td >
< / tr >
{% endfor %}
< / tbody >
< / table >
< div class = "notif-prefs-actions" >
< button type = "button" class = "btn btn-ghost btn-sm" data-notif-reset > Reset to defaults< / button >
< / div >
< / div >
{% else %}
< div class = "empty-state" > Not available.< / div >
{% endif %}
2026-05-10 09:08:12 +02:00
{% else %}
2026-05-11 00:41:41 +02:00
{% for act in activities %}
2026-06-06 16:31:42 +02:00
< div class = "post-card activity-card" >
2026-05-12 15:07:34 +02:00
< div class = "activity-row" >
< span class = "activity-icon" >
2026-05-11 00:41:41 +02:00
{% if act['type'] == 'post' %}📝 {% else %}💬 {% endif %}
< / span >
2026-05-12 15:07:34 +02:00
< span class = "activity-type" >
2026-05-11 00:41:41 +02:00
{% if act['type'] == 'post' %}Posted{% else %}Commented{% endif %}
< / span >
2026-06-16 05:32:19 +02:00
< span class = "activity-time" > {{ dt_ago(act['created_at']) if act['created_at'] else act['time_ago'] }}< / span >
2026-05-11 00:41:41 +02:00
< / div >
2026-05-12 15:07:34 +02:00
< p class = "activity-content" > {{ act['content'] }}< / p >
2026-05-11 00:41:41 +02:00
< / div >
{% else %}
< div class = "empty-state" > No activity yet.< / div >
{% endfor %}
2026-05-10 09:08:12 +02:00
{% endif %}
< / div >
< / div >
< / div >
{% endblock %}
2026-06-13 21:47:50 +02:00
{% block extra_js %}
< script type = "module" >
2026-06-14 02:16:22 +02:00
import { ProfileTabs } from "{{ static_url('/static/js/ProfileTabs.js') }}";
2026-06-13 21:47:50 +02:00
const tabs = document.querySelector("[data-profile-tabs]");
if (tabs) {
new ProfileTabs(tabs);
}
< / script >
{% endblock %}