2026-05-11 07:02:06 +02:00
{% extends "base.html" %}
2026-06-11 00:17:25 +02:00
{% from "_macros.html" import modal %}
2026-05-11 07:02:06 +02:00
{% block extra_head %}
2026-06-14 02:16:22 +02:00
< link rel = "stylesheet" href = "{{ static_url('/static/css/projects.css') }}" >
< link rel = "stylesheet" href = "{{ static_url('/static/css/post.css') }}" >
2026-05-11 07:02:06 +02:00
{% endblock %}
{% block content %}
< div class = "project-detail-page" >
< a href = "/projects" class = "back-link" > ← Back to Projects< / a >
< article class = "project-detail" >
< div class = "project-detail-header" >
2026-06-19 00:09:34 +02:00
< h1 class = "project-detail-title" > {{ render_title(project['title']) }}< / h1 >
2026-05-11 07:02:06 +02:00
< div class = "project-status {% if project.get('status') == 'Released' %}released{% else %}dev{% endif %}" >
● {{ project.get('status', 'In Development') }}
< / div >
< / div >
2026-06-09 06:41:27 +02:00
{% if is_private or read_only %}
< div class = "project-detail-meta" >
{% if is_private %}< span class = "badge badge-type" > Private< / span > {% endif %}
{% if read_only %}< span class = "badge badge-type" > Read-only< / span > {% endif %}
< / div >
{% endif %}
2026-05-11 07:02:06 +02:00
< div class = "project-detail-meta" >
2026-05-12 15:07:34 +02:00
< span class = "badge badge-type" > {{ project.get('project_type', 'software').replace('_', ' ') }}< / span >
2026-05-11 07:02:06 +02:00
{% if project.get('release_date') %}
2026-06-09 00:30:25 +02:00
< span > 📅 Released: {{ format_date(project['release_date']) }}< / span >
2026-05-11 07:02:06 +02:00
{% endif %}
{% if project.get('demo_date') %}
2026-06-09 00:30:25 +02:00
< span > 🎭 Demo: {{ format_date(project['demo_date']) }}< / span >
2026-05-11 07:02:06 +02:00
{% endif %}
< / div >
2026-06-09 16:06:02 +02:00
{% if forked_from %}
< div class = "project-detail-meta" >
2026-06-19 00:09:34 +02:00
< span > ⑂ Forked from < a href = "/projects/{{ forked_from['slug'] }}" > {{ render_title(forked_from['title']) }}< / a > < / span >
2026-06-09 16:06:02 +02:00
< / div >
{% endif %}
2026-05-11 07:02:06 +02:00
< div class = "project-detail-author" >
{% set _size = 32 %}{% set _size_class = "sm" %}{% set _user = author %}{% include "_avatar_link.html" %}
< div >
2026-06-02 23:17:51 +02:00
{% set _user = author %}{% set _class = none %}{% include "_user_link.html" %}
feat: add alt text extraction from URLs for images and improve CSS variable usage for progress bars
Add `_alt_from_url` helper in Python rendering and `altFromUrl` in JS ContentRenderer to generate descriptive alt attributes from image filenames, replacing empty alt strings. Update image embed templates and Avatar component to use derived alt text. Migrate inline `style.width` assignments to CSS custom properties (`--poll-pct`, `--hud-xp`, `--quest-pct`, `--bar-pct`, `--quota-pct`) across PollManager, GameFarm, and profile/quest CSS for better maintainability. Introduce new admin, docs, projects, services, and base CSS utility classes. Add "Code Farm" docs page entry.
2026-06-23 01:13:48 +02:00
< span class = "meta-muted" > · Level {{ author.get('level', 1) if author else 1 }}< / span >
2026-05-11 07:02:06 +02:00
< / div >
< / div >
2026-06-19 00:09:34 +02:00
< div class = "project-detail-desc rendered-content" > {{ render_content(project.get('description', '')) }}< / div >
2026-05-11 07:02:06 +02:00
2026-05-12 15:07:34 +02:00
{% if attachments %}
{% include "_attachment_display.html" %}
{% endif %}
2026-05-11 07:02:06 +02:00
{% if platforms %}
feat: add alt text extraction from URLs for images and improve CSS variable usage for progress bars
Add `_alt_from_url` helper in Python rendering and `altFromUrl` in JS ContentRenderer to generate descriptive alt attributes from image filenames, replacing empty alt strings. Update image embed templates and Avatar component to use derived alt text. Migrate inline `style.width` assignments to CSS custom properties (`--poll-pct`, `--hud-xp`, `--quest-pct`, `--bar-pct`, `--quota-pct`) across PollManager, GameFarm, and profile/quest CSS for better maintainability. Introduce new admin, docs, projects, services, and base CSS utility classes. Add "Code Farm" docs page entry.
2026-06-23 01:13:48 +02:00
< div class = "project-platforms" >
< h4 class = "project-section-label" > Platforms< / h4 >
2026-05-11 07:02:06 +02:00
< div class = "project-card-platforms" >
{% for plat in platforms %}
< span class = "platform-tag" > {{ plat.strip() }}< / span >
{% endfor %}
< / div >
< / div >
{% endif %}
< div class = "project-detail-actions" >
2026-06-16 00:44:12 +02:00
< a href = "/projects/{{ project['slug'] or project['uid'] }}/files" class = "project-star-btn" > < span class = "icon" > 📁 < / span > < span class = "label" > Files ({{ file_count }} files)< / span > < / a >
< button type = "button" class = "project-star-btn" data-share = "/projects/{{ project['slug'] or project['uid'] }}" > < span class = "icon" > 🔗 < / span > < span class = "label" > Share< / span > < / button >
2026-05-11 07:02:06 +02:00
{% if user %}
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 _type = "project" %}{% set _uid = project['uid'] %}{% set _my_vote = my_vote %}{% set _count = star_count %}{% set _btn_class = "project-star-btn" %}{% include "_star_vote.html" %}
2026-05-11 07:02:06 +02:00
{% endif %}
2026-06-06 16:31:42 +02:00
{% set _type = "project" %}{% set _uid = project['uid'] %}{% set _bookmarked = bookmarked %}{% include "_bookmark_button.html" %}
{% set _type = "project" %}{% set _uid = project['uid'] %}{% set _reactions = reactions %}{% include "_reaction_bar.html" %}
2026-06-19 10:06:09 +02:00
< button type = "button" class = "project-star-btn project-actions-more" aria-haspopup = "menu" aria-expanded = "false" aria-label = "More actions" > < span class = "icon" > ⋯ < / span > < span class = "label" > More< / span > < / button >
2026-06-11 00:17:25 +02:00
< div class = "project-actions-overflow" hidden >
{% if is_admin(user) %}
< a href = "/projects/{{ project['slug'] or project['uid'] }}/containers" data-menu-action data-menu-icon = "🖥️" data-menu-label = "Containers" > Containers< / a >
{% endif %}
< button type = "button" data-zip-download = "/projects/{{ project['slug'] or project['uid'] }}/zip" data-menu-action data-menu-icon = "📦" data-menu-label = "Download zip" > Download zip< / button >
{% if user %}
< button type = "button" data-fork-project = "/projects/{{ project['slug'] or project['uid'] }}/fork" data-fork-name = "{{ project['title'] }}" data-menu-action data-menu-icon = "⑂" data-menu-label = "Fork" > Fork< / button >
{% endif %}
{% if is_owner %}
< button type = "button" data-modal = "edit-project-modal" data-menu-action data-menu-icon = "✏️" data-menu-label = "Edit" > Edit< / button >
< form method = "POST" action = "/projects/{{ project['slug'] or project['uid'] }}/private" >
< input type = "hidden" name = "value" value = "{{ 0 if is_private else 1 }}" >
< button type = "submit" data-confirm-danger data-confirm = "{% if is_private %}Make this project public? Everyone will be able to see the project and all its files.{% else %}Make this project private? Only you and administrators will be able to see it.{% endif %}" data-menu-action data-menu-icon = "{% if is_private %}🔓{% else %}🔒{% endif %}" data-menu-label = "{% if is_private %}Make public{% else %}Make private{% endif %}" > {% if is_private %}Make public{% else %}Make private{% endif %}< / button >
< / form >
< form method = "POST" action = "/projects/{{ project['slug'] or project['uid'] }}/readonly" >
< input type = "hidden" name = "value" value = "{{ 0 if read_only else 1 }}" >
< button type = "submit" data-confirm-danger data-confirm = "{% if read_only %}Allow file changes again for this project?{% else %}Make this project read-only? Files become immutable until you turn this off.{% endif %}" data-menu-action data-menu-icon = "{% if read_only %}📝{% else %}🚫{% endif %}" data-menu-label = "{% if read_only %}Allow edits{% else %}Make read-only{% endif %}" > {% if read_only %}Allow edits{% else %}Make read-only{% endif %}< / button >
< / form >
feat: add admin/internal database API with CRUD, read-only query, and natural-language SQL endpoints
Add a new `/dbapi` router package providing a generic database API over `dataset`, restricted to admin sessions, admin API keys, and the internal gateway key. Includes:
- `tables.py`: list all tables and inspect table schemas
- `crud.py`: full CRUD operations (GET, POST, PATCH, DELETE) with soft-delete awareness, born-live inserts, `?include_deleted`, `.../restore`, and `?hard=true` purge
- `query.py`: validated read-only SELECT execution via sqlglot parsing, classification, and EXPLAIN dry-run; async query jobs with WebSocket streaming via `DbApiJobService`
- `nl.py`: natural-language-to-SQL conversion using the platform AI gateway with re-prompting until validation passes
Also register `DbApiJobService` and `PubSubService` in the service manager, add `DBAPI_DIR` to config data paths, and force cleartext `http://` connections to HTTP/1.1 in `curl_transport` to fix large request failures against uvicorn's HTTP/1.1-only internal gateway.
2026-06-15 01:00:30 +02:00
{% endif %}
{% if is_owner or is_admin(user) %}
2026-06-11 00:17:25 +02:00
< form method = "POST" action = "/projects/delete/{{ project['slug'] or project['uid'] }}" >
< button type = "submit" data-confirm-danger data-confirm = "Delete this project?" data-menu-action data-menu-icon = "🗑️" data-menu-label = "Delete" > Delete< / button >
< / form >
{% endif %}
< / div >
2026-05-11 07:02:06 +02:00
< / div >
< / article >
2026-05-11 20:49:45 +02:00
2026-06-11 00:17:25 +02:00
{% if is_owner %}
{% call modal('edit-project-modal', 'Edit Project') %}
< form method = "POST" action = "/projects/edit/{{ project['slug'] or project['uid'] }}" >
< div class = "auth-field auth-field-gap" >
< label for = "edit-project-title" > Title< / label >
< input type = "text" id = "edit-project-title" name = "title" required maxlength = "200" value = "{{ project['title'] }}" >
< / div >
< div class = "auth-field auth-field-gap" >
< label for = "edit-project-description" > Description< / label >
< textarea id = "edit-project-description" name = "description" required maxlength = "5000" class = "min-h-100" data-mention > {{ project.get('description', '') }}< / textarea >
< / div >
< div class = "grid-2col" >
< div class = "auth-field" >
< label for = "edit-project-release_date" > Release Date< / label >
< input type = "text" id = "edit-project-release_date" name = "release_date" placeholder = "DD/MM/YYYY" pattern = "\d{2}/\d{2}/\d{4}" inputmode = "numeric" autocomplete = "off" value = "{{ format_date(project.get('release_date', '')) }}" >
< / div >
< div class = "auth-field" >
< label for = "edit-project-demo_date" > Demo Date< / label >
< input type = "text" id = "edit-project-demo_date" name = "demo_date" placeholder = "DD/MM/YYYY" pattern = "\d{2}/\d{2}/\d{4}" inputmode = "numeric" autocomplete = "off" value = "{{ format_date(project.get('demo_date', '')) }}" >
< / div >
< / div >
< div class = "auth-field auth-field-gap" >
< label > Type< / label >
2026-06-19 10:06:09 +02:00
< div class = "flex-wrap-gap" role = "group" aria-label = "Type" >
2026-06-11 00:17:25 +02:00
{% for t in [('game', 'Game'), ('game_asset', 'Game Asset'), ('software', 'Software'), ('mobile_app', 'Mobile App'), ('website', 'Website')] %}
< label class = "topic-label" >
< input type = "radio" name = "project_type" value = "{{ t[0] }}" { % if t [ 0 ] = = project . get ( ' project_type ' , ' software ' ) % } checked { % endif % } class = "topic-radio" >
{{ t[1] }}
< / label >
{% endfor %}
< / div >
< / div >
< div class = "auth-field auth-field-gap" >
< label > Status< / label >
2026-06-19 10:06:09 +02:00
< div class = "flex-wrap-gap" role = "group" aria-label = "Status" >
2026-06-11 00:17:25 +02:00
< label class = "topic-label" >
< input type = "radio" name = "status" value = "In Development" { % if project . get ( ' status ' , ' In Development ' ) ! = ' Released ' % } checked { % endif % } class = "topic-radio" > In Development
< / label >
< label class = "topic-label" >
< input type = "radio" name = "status" value = "Released" { % if project . get ( ' status ' ) = = ' Released ' % } checked { % endif % } class = "topic-radio" > Released
< / label >
< / div >
< / div >
< div class = "auth-field auth-field-gap-last" >
< label > Platforms< / label >
< div id = "platforms-tags" class = "flex-wrap-gap-bottom" > < / div >
2026-06-19 10:06:09 +02:00
< input type = "text" id = "platforms-input" class = "input-sm" placeholder = "Add platform (press Enter)" aria-label = "Add platform" >
2026-06-11 00:17:25 +02:00
< input type = "hidden" id = "platforms" name = "platforms" value = "{{ project.get('platforms', '') }}" >
< div class = "platform-presets-wrap" >
{% for p in ['PC', 'Windows', 'Mac', 'Linux', 'iOS', 'Android', 'Web', 'Console'] %}
< button type = "button" class = "platform-preset" data-platform = "{{ p }}" > {{ p }}< / button >
{% endfor %}
< / div >
< / div >
< div class = "modal-footer" >
< button type = "button" class = "modal-close btn btn-secondary" > Cancel< / button >
< button type = "submit" class = "btn btn-primary" > < span class = "icon" > 💾 < / span > Save Changes< / button >
< / div >
< / form >
{% endcall %}
{% endif %}
2026-05-11 20:49:45 +02:00
{% with target_uid=project['uid'], target_type="project" %}
{% include "_comment_section.html" %}
{% endwith %}
2026-05-11 07:02:06 +02:00
< / div >
{% endblock %}
2026-06-11 00:17:25 +02:00
{% block extra_js %}
< script type = "module" >
2026-06-14 02:16:22 +02:00
import { ProjectActionsMenu } from "{{ static_url('/static/js/ProjectActionsMenu.js') }}";
2026-06-11 00:17:25 +02:00
const actions = document.querySelector(".project-detail-actions");
if (actions) {
new ProjectActionsMenu(actions);
}
< / script >
{% endblock %}