fix: correct typo in progress tracking variable name from Progss to Progress
This commit is contained in:
@@ -10,6 +10,9 @@
|
||||
<a href="/admin/users" class="admin-nav-link {% if admin_section == 'users' %}active{% endif %}">
|
||||
<span class="admin-nav-icon">👥</span> Users
|
||||
</a>
|
||||
<a href="/admin/services" class="admin-nav-link {% if admin_section == 'services' %}active{% endif %}">
|
||||
<span class="admin-nav-icon">⚙️</span> Services
|
||||
</a>
|
||||
<a href="/admin/settings" class="admin-nav-link {% if admin_section == 'settings' %}active{% endif %}">
|
||||
<span class="admin-nav-icon">⚙️</span> Settings
|
||||
</a>
|
||||
|
||||
@@ -91,6 +91,15 @@
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner">
|
||||
<span class="footer-copy">© DevPlace</span>
|
||||
<nav class="footer-links">
|
||||
<a href="/bugs">Bug Report</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="/static/css/feed.css">
|
||||
<style>
|
||||
.bugs-layout {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.bugs-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bugs-header h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bug-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bug-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.bug-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.bug-status {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.bug-status.open {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
.bug-status.closed {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: var(--success);
|
||||
}
|
||||
.bug-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.bug-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="bugs-layout">
|
||||
<div class="bugs-header">
|
||||
<h1>Bug Reports</h1>
|
||||
{% if user %}
|
||||
<button class="btn btn-primary btn-sm" data-modal="create-bug-modal">Report Bug</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bugs-list">
|
||||
{% for item in bugs %}
|
||||
<div class="bug-card fade-in">
|
||||
<div class="bug-card-header">
|
||||
{% set _user = item.author %}{% set _size = 24 %}{% set _size_class = "sm" %}{% include "_avatar_link.html" %}
|
||||
<span class="bug-title">{{ item.bug['title'] }}</span>
|
||||
<span class="bug-status {{ item.bug['status'] }}">{{ item.bug['status'] }}</span>
|
||||
</div>
|
||||
<div class="bug-desc">{{ item.bug['description'][:300] }}</div>
|
||||
<div class="bug-meta">{{ item.author['username'] if item.author else 'Unknown' }} · {{ item.time_ago }}</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">No bug reports yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if user %}
|
||||
<div class="modal-overlay" id="create-bug-modal">
|
||||
<div class="card" style="width: 100%; max-width: 560px; margin: 1rem;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="font-size: 1.125rem; font-weight: 700;">Report a Bug</h3>
|
||||
<button type="button" class="modal-close btn-ghost btn-icon" style="font-size: 1.25rem;">×</button>
|
||||
</div>
|
||||
<form method="POST" action="/bugs/create">
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label for="bug-title">Title</label>
|
||||
<input type="text" id="bug-title" name="title" required maxlength="200" placeholder="Brief description of the issue">
|
||||
</div>
|
||||
<div class="auth-field" style="margin-bottom: 1rem;">
|
||||
<label for="bug-description">Description</label>
|
||||
<textarea id="bug-description" name="description" required maxlength="5000" placeholder="Detailed steps to reproduce..." style="min-height: 120px;"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
|
||||
<button type="button" class="modal-close btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Submit Report</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -32,6 +32,10 @@
|
||||
<span class="dot"></span>
|
||||
Fun
|
||||
</a>
|
||||
<a href="/feed?topic=signals" class="feed-category {% if current_topic == 'signals' %}active{% endif %}" data-topic="signals">
|
||||
<span class="dot"></span>
|
||||
Signals
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -86,6 +90,15 @@
|
||||
</a>
|
||||
<button class="post-action-btn">🔗 Share</button>
|
||||
</div>
|
||||
|
||||
{% if user %}
|
||||
<form class="feed-comment-form" method="POST" action="/comments/create">
|
||||
<input type="hidden" name="post_uid" value="{{ item.post['uid'] }}">
|
||||
<img src="{{ avatar_url('multiavatar', user['username'], 24) }}" class="avatar-img" style="width: 24px; height: 24px; border-radius: 50%;" alt="" loading="lazy">
|
||||
<input type="text" name="content" placeholder="Your opinion goes here..." maxlength="1000" autocomplete="off">
|
||||
<button type="submit" class="feed-comment-submit">Post</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% else %}
|
||||
<div class="empty-state">No posts yet. Be the first!</div>
|
||||
@@ -105,8 +118,8 @@
|
||||
<h4>{{ daily_topic.get('title', '') }}</h4>
|
||||
<p>{{ daily_topic.get('summary', '') }}</p>
|
||||
<div class="topic-links">
|
||||
<a href="#">Read More</a>
|
||||
<a href="#">Source</a>
|
||||
<a href="/feed">Read More</a>
|
||||
<a href="/feed">Source</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -154,7 +167,7 @@
|
||||
|
||||
<form id="create-post-form" method="POST" action="/posts/create" enctype="multipart/form-data">
|
||||
<div style="display: flex; gap: 0.375rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
{% for t in ['devlog', 'showcase', 'question', 'rant', 'fun', 'random'] %}
|
||||
{% for t in ['devlog', 'showcase', 'question', 'rant', 'fun', 'random', 'signals'] %}
|
||||
<label style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; cursor: pointer; background: var(--bg-card-hover); transition: all 0.2s;">
|
||||
<input type="radio" name="topic" value="{{ t }}" {% if t == 'random' %}checked{% endif %} style="width: auto; accent-color: var(--accent);">
|
||||
{{ t|capitalize }}
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
<h3>{{ daily_topic.get('title', '') }}</h3>
|
||||
<p>{{ daily_topic.get('summary', '') }}</p>
|
||||
<div class="landing-daily-topic-links">
|
||||
<a href="#">Read More</a>
|
||||
<a href="#">Source</a>
|
||||
<a href="/feed">Read More</a>
|
||||
<a href="/feed">Source</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<label>
|
||||
<input type="checkbox" name="remember_me"> Remember me
|
||||
</label>
|
||||
<a href="#">Forgot your password?</a>
|
||||
<a href="/auth/forgot-password">Forgot your password?</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="auth-submit">Sign in</button>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="/static/css/feed.css">
|
||||
<link rel="stylesheet" href="/static/css/post.css">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
@@ -37,6 +38,7 @@
|
||||
</form>
|
||||
<button class="post-action-btn">🔗 Share</button>
|
||||
{% if user and post['user_uid'] == user['uid'] %}
|
||||
<button class="post-action-btn" data-modal="edit-post-modal">Edit</button>
|
||||
<form method="POST" action="/posts/delete/{{ post['uid'] }}" style="display:inline;">
|
||||
<button type="submit" class="post-action-btn" onclick="return confirm('Delete this post?')">Delete</button>
|
||||
</form>
|
||||
@@ -44,6 +46,39 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{% if user and post['user_uid'] == user['uid'] %}
|
||||
<div class="modal-overlay" id="edit-post-modal">
|
||||
<div class="card" style="width: 100%; max-width: 560px; margin: 1rem; max-height: 90vh; overflow-y: auto;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<h3 style="font-size: 1.125rem; font-weight: 700;">Edit Post</h3>
|
||||
<button type="button" class="modal-close btn-ghost btn-icon" style="font-size: 1.25rem;">×</button>
|
||||
</div>
|
||||
<form method="POST" action="/posts/edit/{{ post['uid'] }}">
|
||||
<div style="display: flex; gap: 0.375rem; flex-wrap: wrap; margin-bottom: 1rem;">
|
||||
{% for t in topics %}
|
||||
<label style="display: flex; align-items: center; gap: 0.25rem; padding: 0.375rem 0.75rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; cursor: pointer; background: var(--bg-card-hover); transition: all 0.2s;">
|
||||
<input type="radio" name="topic" value="{{ t }}" {% if t == post['topic'] %}checked{% endif %} style="width: auto; accent-color: var(--accent);">
|
||||
{{ t|capitalize }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label for="edit-title">Title</label>
|
||||
<input type="text" id="edit-title" name="title" maxlength="500" value="{{ post.get('title', '') }}">
|
||||
</div>
|
||||
<div class="auth-field" style="margin-bottom: 0.75rem;">
|
||||
<label for="edit-content">Content</label>
|
||||
<textarea id="edit-content" name="content" required maxlength="2000" style="min-height: 200px;">{{ post['content'] }}</textarea>
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
|
||||
<button type="button" class="modal-close btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="comments-section">
|
||||
<h3>Comments</h3>
|
||||
|
||||
@@ -61,7 +96,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="comment-body">
|
||||
<div class="comment-body" data-comment-uid="{{ item.comment['uid'] }}">
|
||||
<div class="comment-header">
|
||||
<a href="/profile/{{ item.author['username'] if item.author else '#' }}">
|
||||
<img src="{{ avatar_url('multiavatar', item.author['username'] if item.author else '?', 24) }}" class="avatar-img" style="width: 24px; height: 24px; border-radius: 50%;" alt="{{ item.author['username'] if item.author else '?' }}" loading="lazy">
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
</div>
|
||||
|
||||
{% if user and user['uid'] != profile_user['uid'] %}
|
||||
<div style="margin-top: 1rem;">
|
||||
<div style="margin-top: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
|
||||
<a href="/messages?with_uid={{ profile_user['uid'] }}" class="btn btn-secondary btn-sm" style="width: 100%; justify-content: center;">Send Message</a>
|
||||
{% if is_following %}
|
||||
<form method="POST" action="/follow/unfollow/{{ profile_user['username'] }}">
|
||||
<button type="submit" class="btn btn-secondary btn-sm" style="width: 100%;">Unfollow</button>
|
||||
@@ -171,7 +172,7 @@
|
||||
{% if item.post.get('title') %}
|
||||
<h3 class="post-title">{{ item.post['title'] }}</h3>
|
||||
{% endif %}
|
||||
<div class="post-content rendered-content" data-render>{{ item.post['content'][:200] }}{% if item.post['content']|length > 200 %}...{% endif %}</div>
|
||||
<div class="post-content rendered-content" data-render>{{ item.post['content'][:300] }}{% if item.post['content']|length > 300 %}...{% endif %}</div>
|
||||
<div class="post-actions">
|
||||
<span class="post-action-btn" style="cursor: default;">+{{ item.post.get('stars', 0) }}</span>
|
||||
<span class="post-action-btn" style="cursor: default;">💬 {{ item.comment_count }}</span>
|
||||
@@ -183,7 +184,7 @@
|
||||
{% endfor %}
|
||||
{% elif current_tab == 'projects' %}
|
||||
{% for p in projects %}
|
||||
<div class="project-card fade-in">
|
||||
<a href="/projects/{{ p['uid'] }}" class="project-card fade-in" style="display: block; text-decoration: none; color: inherit;">
|
||||
<div class="project-card-header">
|
||||
<h3 class="project-card-title">{{ p['title'] }}</h3>
|
||||
</div>
|
||||
@@ -201,7 +202,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="empty-state">No projects yet.</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
{% extends "base.html" %}
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="/static/css/projects.css">
|
||||
<style>
|
||||
.project-detail-page {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.project-detail {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.project-detail-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.project-detail-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.project-detail-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.project-detail-meta span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.project-detail-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.project-detail-author a {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.project-detail-desc {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.project-detail-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.project-star-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.project-star-btn:hover {
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--warning);
|
||||
}
|
||||
</style>
|
||||
{% 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">
|
||||
<h1 class="project-detail-title">{{ project['title'] }}</h1>
|
||||
<div class="project-status {% if project.get('status') == 'Released' %}released{% else %}dev{% endif %}">
|
||||
● {{ project.get('status', 'In Development') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-detail-meta">
|
||||
<span class="badge" style="background: var(--border); color: var(--text-muted); text-transform: capitalize;">{{ project.get('project_type', 'software').replace('_', ' ') }}</span>
|
||||
{% if project.get('release_date') %}
|
||||
<span>📅 Released: {{ project['release_date'] }}</span>
|
||||
{% endif %}
|
||||
{% if project.get('demo_date') %}
|
||||
<span>🎭 Demo: {{ project['demo_date'] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="project-detail-author">
|
||||
{% set _size = 32 %}{% set _size_class = "sm" %}{% set _user = author %}{% include "_avatar_link.html" %}
|
||||
<div>
|
||||
<a href="/profile/{{ author['username'] if author else '#' }}">{{ author['username'] if author else 'Unknown' }}</a>
|
||||
{% if author and author.get('role') %}
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted);">· {{ author['role'] }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-detail-desc rendered-content" data-render>{{ project.get('description', '') }}</div>
|
||||
|
||||
{% if platforms %}
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h4 style="font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.5rem;">Platforms</h4>
|
||||
<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">
|
||||
{% if user %}
|
||||
<form method="POST" action="/votes/project/{{ project['uid'] }}" style="display:inline;">
|
||||
<input type="hidden" name="value" value="1">
|
||||
<button type="submit" class="project-star-btn">☆ {{ star_count }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if is_owner %}
|
||||
<form method="POST" action="/projects/delete/{{ project['uid'] }}" style="display:inline;">
|
||||
<button type="submit" class="project-star-btn" onclick="return confirm('Delete this project?')">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<div class="projects-grid">
|
||||
{% for project in projects %}
|
||||
<div class="project-card fade-in">
|
||||
<a href="/projects/{{ project['uid'] }}" class="project-card fade-in" style="display: block; text-decoration: none; color: inherit;">
|
||||
<div class="project-card-header">
|
||||
<h3 class="project-card-title">{{ project['title'] }}</h3>
|
||||
<form method="POST" action="/votes/project/{{ project['uid'] }}" style="display:inline;">
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="empty-state" style="grid-column: 1 / -1;">No projects found. Create one!</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "admin_base.html" %}
|
||||
{% block extra_head %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="/static/css/services.css">
|
||||
{% endblock %}
|
||||
{% block admin_content %}
|
||||
<div class="services-page">
|
||||
<h1>Services</h1>
|
||||
<div class="services-list" id="services-list">
|
||||
{% for svc in services %}
|
||||
<div class="service-card" data-service="{{ svc.name }}">
|
||||
<div class="service-header">
|
||||
<span class="service-name">{{ svc.name }}</span>
|
||||
<span class="service-status {{ svc.status }}">{{ svc.status }}</span>
|
||||
</div>
|
||||
<div class="service-meta">
|
||||
{% if svc.uptime %}
|
||||
<span class="meta-item">Uptime: {{ svc.uptime }}</span>
|
||||
{% endif %}
|
||||
{% if svc.last_run %}
|
||||
<span class="meta-item">Last run: {{ svc.last_run[:19] }}</span>
|
||||
{% endif %}
|
||||
{% if svc.next_run %}
|
||||
<span class="meta-item">Next run: {{ svc.next_run[:19] }}</span>
|
||||
{% endif %}
|
||||
<span class="meta-item">Interval: every {{ svc.interval_seconds }}s</span>
|
||||
</div>
|
||||
<div class="service-log">
|
||||
<div class="log-header">Log (last 20 lines)</div>
|
||||
<pre class="log-output">{% for line in svc.log_buffer %}{{ line }}
|
||||
{% else %}No log entries yet.
|
||||
{% endfor %}</pre>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="no-services">No services registered.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block extra_js %}
|
||||
<script type="module">
|
||||
const POLL_INTERVAL = 5000;
|
||||
|
||||
async function pollServices() {
|
||||
try {
|
||||
const resp = await fetch('/admin/services/data');
|
||||
const data = await resp.json();
|
||||
const container = document.getElementById('services-list');
|
||||
if (!container) return;
|
||||
for (const svc of data.services) {
|
||||
const card = container.querySelector(`.service-card[data-service="${svc.name}"]`);
|
||||
if (!card) continue;
|
||||
card.querySelector('.service-status').textContent = svc.status;
|
||||
card.querySelector('.service-status').className = 'service-status ' + svc.status;
|
||||
const metaItems = card.querySelectorAll('.meta-item');
|
||||
if (metaItems.length >= 3) {
|
||||
metaItems[0].textContent = 'Uptime: ' + (svc.uptime || '-');
|
||||
metaItems[1].textContent = 'Last run: ' + (svc.last_run ? svc.last_run.slice(0, 19) : '-');
|
||||
metaItems[2].textContent = 'Next run: ' + (svc.next_run ? svc.next_run.slice(0, 19) : '-');
|
||||
}
|
||||
const logPre = card.querySelector('.log-output');
|
||||
if (logPre) {
|
||||
logPre.textContent = svc.log_buffer.map(l => l + '\n').join('') || 'No log entries yet.\n';
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// silently ignore poll errors
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(pollServices, POLL_INTERVAL);
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user