feat: project overview + timeline + docs center

- services/task_project_linker.py: task<->project mapping (title keywords + workspace_path)
- routes/pages.py: /timeline, /docs, /docs/<path>, enhanced /projects/<name> and /tasks/<id>
- templates: timeline.html, docs.html, doc_view.html (new), project_detail.html + task_detail.html (overhauled)
- config.py: DOCS_DIRS + ProjectConfig color
- base.html: Timeline + Docs nav links
This commit is contained in:
hermes-dashboard
2026-08-17 20:00:37 +08:00
parent 1e5e30ffcb
commit c0cbd683f4
11 changed files with 813 additions and 16 deletions

95
templates/timeline.html Normal file
View File

@@ -0,0 +1,95 @@
{% extends "base.html" %}
{% block title %}Timeline - Hermes{% endblock %}
{% block content %}
<h2>🕐 Full Task Timeline</h2>
<div class="card">
<h3>Summary</h3>
<p>
<strong>Total:</strong> {{ stats.total }} |
<strong>Done:</strong> <span class="status-badge status-done">{{ stats.done }}</span> |
<strong>Blocked:</strong> <span class="status-badge status-blocked">{{ stats.blocked }}</span> |
<strong>Running:</strong> <span class="status-badge status-running">{{ stats.running }}</span> |
<strong>Ready:</strong> <span class="status-badge status-ready">{{ stats.ready }}</span> |
<strong>Failed:</strong> <span class="status-badge status-failed">{{ stats.failed }}</span>
</p>
</div>
{% for pname, tasks in by_project.items() %}
{% if tasks %}
<div class="card mt-20">
<h3>
{% if pname != 'unassigned' %}
<a href="/projects/{{ pname }}" style="color: {{ proj_colors.get(pname, '#4a9eff') }};">📦 {{ pname }}</a>
{% else %}
<span class="text-muted">📦 {{ pname }}</span>
{% endif %}
<span class="text-small text-muted">({{ tasks|length }} tasks)</span>
</h3>
{% set ns = namespace(min_ts=99999999999, max_ts=0) %}
{% for t in tasks %}
{% if t.created_at and t.created_at < ns.min_ts %}{% set ns.min_ts = t.created_at %}{% endif %}
{% if t.completed_at and t.completed_at > ns.max_ts %}{% set ns.max_ts = t.completed_at %}{% endif %}
{% if t.created_at and not t.completed_at and t.created_at > ns.max_ts %}{% set ns.max_ts = t.created_at %}{% endif %}
{% endfor %}
{% set span = (ns.max_ts - ns.min_ts) if ns.max_ts > ns.min_ts else 1 %}
{% set status_color = {'done': '#4aff4a', 'blocked': '#ff4a4a', 'running': '#4a9eff', 'ready': '#aaaaaa', 'failed': '#ff9e4a'} %}
<div style="background:#1a1a1a;padding:10px;border-radius:3px;margin-bottom:15px;">
{% for t in tasks[:20] %}
{% if t.created_at %}
{% set left = ((t.created_at - ns.min_ts) / span * 100)|round(2) %}
{% set width = (((t.completed_at or t.created_at) - t.created_at) / span * 100)|round(2) %}
{% if width < 0.5 %}{% set width = 0.5 %}{% endif %}
{% set bar_color = status_color.get(t.status, '#888') %}
<div style="position:relative;height:18px;margin-bottom:2px;">
<a href="/tasks/{{ t.task_id }}" style="display:block;position:absolute;left:{{ left }}%;width:{{ width }}%;height:100%;background:{{ bar_color }};opacity:0.75;border-radius:2px;text-decoration:none;" title="[{{ t.status }}] {{ t.title[:60] }}"></a>
</div>
{% endif %}
{% endfor %}
<div class="text-small text-muted" style="margin-top:5px;">
{% if ns.min_ts < 99999999999 %}{{ ns.min_ts|int|dt }} {{ ns.max_ts|int|dt }}{% endif %}
</div>
</div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Status</th>
<th>Created</th>
<th>Duration</th>
<th>Outcome</th>
</tr>
</thead>
<tbody>
{% for t in tasks %}
<tr>
<td class="text-small"><a href="/tasks/{{ t.task_id }}">{{ t.task_id }}</a></td>
<td>{{ t.title[:80] }}</td>
<td><span class="status-badge status-{{ t.status }}">{{ t.status }}</span></td>
<td class="text-small">{% if t.created_at %}{{ t.created_at|int|dt }}{% endif %}</td>
<td class="text-small">
{% if t.duration_s %}
{% if t.duration_s < 60 %}{{ t.duration_s }}s
{% elif t.duration_s < 3600 %}{{ (t.duration_s / 60)|round(1) }}m
{% else %}{{ (t.duration_s / 3600)|round(1) }}h{% endif %}
{% else %}-{% endif %}
</td>
<td class="text-small text-muted">{{ t.outcome_summary[:80] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endfor %}
<p class="mt-20">
<a href="/" class="btn">← Back to Dashboard</a>
</p>
{% endblock %}