Files
agentic-mobile-control/api/templates/runtime_console/tasks.html
T
q792602257andClaude Opus 4.6 e00c50e703 feat(api): server-rendered Jinja2 Runtime console at /ui/
Replaces the separate Vue/Vite `console/` SPA with a same-origin,
server-rendered console built on a module-level Jinja2 Environment
with select_autoescape(["html","xml"]).

- Add api/console_web.py with /ui/ routes (dashboard, tasks, task
  detail/timeline, config) and a _status_fragment polled every 10s.
- Refactor api/console.py into a typed ConsoleService shared by the
  JSON and HTML routers so validation/persistence cannot drift.
- Remove RUNTIME_CONSOLE_STATIC_DIR, SpaStaticFiles, and the wildcard
  CORS middleware from api/rest.py; GET / now redirects to /ui/.
- Delete the top-level console/ project; add jinja2 and python-multipart
  as direct dependencies and ship templates/CSS/JS via package-data.
- Add 31 tests (XSS probes, PRG flows, fragment refresh, no-static-dir
  and no-CORS regressions, wheel-packaging smoke test).

/console/* JSON endpoints remain unchanged. The console keeps the
trusted-network-only boundary; auth/CSRF is intentionally deferred.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-15 08:03:13 +08:00

63 lines
1.8 KiB
HTML

{% extends "base.html" %}
{% block title %}Tasks &middot; Apex Console{% endblock %}
{% block body %}
<header class="topbar">
<div>
<h1>Tasks</h1>
<p>{{ tasks|length }} task(s) match the current filters</p>
</div>
</header>
<section class="panel task-browser">
<div class="section-title">
<h2>Task List</h2>
</div>
<form class="filters" method="get" action="{{ url_for('runtime_console_tasks') }}">
<label>
Device
<select name="device_id">
<option value="">All devices</option>
{% for device in devices %}
<option
value="{{ device.id }}"
{% if device.id == selected_device_id %}selected{% endif %}
>{{ device.name or device.id }}</option>
{% endfor %}
</select>
</label>
<label>
Status
<select name="status">
<option value="">All statuses</option>
{% for status_name in task_statuses %}
<option
value="{{ status_name }}"
{% if status_name == selected_status %}selected{% endif %}
>{{ status_name }}</option>
{% endfor %}
</select>
</label>
<noscript><button class="icon-text-button" type="submit"><span>Apply</span></button></noscript>
</form>
{% if not tasks %}
<div class="empty-state compact">
<span>No tasks match the current filters.</span>
</div>
{% else %}
{% for task in tasks %}
<a
class="task-row"
href="{{ url_for('runtime_console_task_detail', task_id=task.id) }}"
>
<span class="task-goal">{{ task.goal }}</span>
<span class="task-meta">
{{ device_names.get(task.device_id, task.device_id) }}
</span>
<span class="status-pill {{ task.status }}">{{ task.status }}</span>
</a>
{% endfor %}
{% endif %}
</section>
{% endblock %}