Files
bookapp/templates/project.html
2026-02-04 20:19:07 -05:00

578 lines
32 KiB
HTML

{% extends "base.html" %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<div class="d-flex align-items-center">
<h1 class="mb-0 me-3">{{ project.name }}</h1>
{% if not locked %}
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editProjectModal"><i class="fas fa-edit"></i></button>
{% endif %}
<button class="btn btn-sm btn-outline-info ms-2" data-bs-toggle="modal" data-bs-target="#cloneProjectModal" title="Clone/Fork Project" data-bs-toggle="tooltip">
<i class="fas fa-code-branch"></i>
</button>
</div>
<div class="mt-2">
<span class="badge bg-secondary">{{ bible.project_metadata.genre }}</span>
<span class="badge bg-info text-dark">{{ bible.project_metadata.target_audience }}</span>
</div>
</div>
<div>
<form action="/project/{{ project.id }}/run" method="POST" class="d-inline">
<button class="btn btn-success shadow px-4 py-2" {% if active_run and active_run.status in ['running', 'queued'] %}disabled{% endif %} data-bs-toggle="tooltip" title="Start the AI writer. It will write the next book in the plan.">
<i class="fas fa-play me-2"></i>{{ 'Generating...' if runs and runs[0].status in ['running', 'queued'] else 'Generate New Book' }}
</button>
</form>
{% if runs and runs[0].status in ['running', 'queued'] %}
<form action="/run/{{ runs[0].id }}/stop" method="POST" class="d-inline ms-2">
<button class="btn btn-danger shadow px-3 py-2" title="Stop/Cancel Run" onclick="return confirm('Are you sure you want to stop this job? If the server restarted, this will simply unlock the UI.')">
<i class="fas fa-stop"></i>
</button>
</form>
{% endif %}
</div>
</div>
<!-- Workflow Help -->
<div class="alert alert-light border shadow-sm mb-4">
<div class="d-flex align-items-center">
<i class="fas fa-info-circle text-primary fa-2x me-3"></i>
<div>
<strong>Workflow:</strong>
1. Review the <a href="#bible-section">World Bible</a> below. &nbsp;&nbsp;
2. Click <span class="badge bg-success">Generate New Book</span>. &nbsp;&nbsp;
3. When finished, <a href="#latest-run">Download</a> the files or click <span class="badge bg-primary">Read & Edit</span> to refine the text.
</div>
</div>
</div>
<!-- LATEST RUN CARD -->
<div class="card mb-4 border-0 shadow-sm" id="latest-run">
<div class="card-header bg-white border-bottom-0 pt-4 px-4">
<h4 class="card-title"><i class="fas fa-bolt text-warning me-2"></i>Active Run (ID: {{ active_run.id if active_run else '-' }})</h4>
</div>
<div class="card-body px-4 pb-4">
{% if active_run %}
<div class="d-flex align-items-center mb-3">
<span class="badge bg-{{ 'success' if active_run.status == 'completed' else 'warning' if active_run.status in ['running', 'queued'] else 'danger' if active_run.status in ['failed', 'cancelled', 'interrupted'] else 'secondary' }} fs-6 me-3 status-badge-{{ active_run.id }}">
{{ active_run.status|upper }}
</span>
<span class="text-muted small">Run #{{ active_run.id }} started at {{ active_run.start_time.strftime('%Y-%m-%d %H:%M') }}</span>
<span class="ms-auto text-muted fw-bold">Cost: $<span class="cost-{{ active_run.id }}">{{ "%.4f"|format(active_run.cost) }}</span></span>
</div>
<!-- DOWNLOADS -->
{% if artifacts or cover_image %}
<div class="row mt-3">
{% if cover_image %}
<div class="col-md-4 text-center mb-3">
<div class="card shadow-sm">
<img src="/project/{{ active_run.id }}/download?file={{ cover_image }}&t={{ active_run.end_time.timestamp() if active_run.end_time else 0 }}" class="card-img-top" alt="Book Cover">
</div>
</div>
{% endif %}
<div class="col-md-{{ '8' if cover_image else '12' }}">
<div class="alert alert-{{ 'success' if active_run.status == 'completed' else 'warning' }} h-100">
<h5 class="alert-heading"><i class="fas fa-{{ 'check-circle' if active_run.status == 'completed' else 'folder-open' }} me-2"></i>{{ 'Book Generated!' if active_run.status == 'completed' else 'Files Available' }}</h5>
<p>{{ 'Your manuscript and ebook files are ready.' if active_run.status == 'completed' else 'Files generated during this run (may be partial).' }}</p>
<hr>
<div class="d-flex flex-wrap gap-2">
{% for file in artifacts %}
<a href="/project/{{ active_run.id }}/download?file={{ file.path }}" class="btn btn-success">
<i class="fas fa-download me-1"></i> Download {{ file.type }}
</a>
{% endfor %}
<button class="btn btn-outline-dark" data-bs-toggle="modal" data-bs-target="#regenerateModal" data-bs-toggle="tooltip" title="Re-create the cover art or re-compile the EPUB without rewriting the text.">
<i class="fas fa-paint-brush me-1"></i> Regenerate Cover / Files
</button>
</div>
{% if blurb_content %}
<div class="card mt-3 border-0 bg-light">
<div class="card-body">
<h6 class="card-title fw-bold text-muted">Back Cover Blurb</h6>
<p class="card-text fst-italic">{{ blurb_content }}</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
<!-- STATUS BAR -->
{% if active_run and active_run.status in ['running', 'queued'] %}
<div class="card bg-light border-0 mb-3" id="statusBar">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="spinner-border text-primary spinner-border-sm me-2" role="status"></div>
<strong class="text-primary" id="statusPhase">Initializing...</strong>
</div>
<h5 class="card-title mb-3" id="statusMessage">Preparing environment...</h5>
<div class="progress" style="height: 10px;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: 0%"></div>
</div>
<small class="text-muted mt-2 d-block" id="statusTime"></small>
</div>
</div>
{% endif %}
<!-- CONSOLE -->
<div class="mt-3">
<button class="btn btn-sm btn-link text-decoration-none p-0 mb-2" type="button" data-bs-toggle="collapse" data-bs-target="#consoleCollapse">
<i class="fas fa-terminal me-1"></i> {{ 'Hide' if active_run.status in ['running', 'queued'] else 'Show' }} Console Logs
</button>
<div class="collapse {{ 'show' if active_run.status in ['running', 'queued'] else '' }}" id="consoleCollapse">
<div id="consoleOutput" class="console-log bg-dark text-success p-3 rounded" style="height: 300px; overflow-y: auto; font-family: monospace; font-size: 0.85rem;">Loading logs...</div>
</div>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<p>No books generated yet.</p>
<p>Click the <strong>Generate New Book</strong> button to start writing.</p>
</div>
{% endif %}
</div>
</div>
<!-- RUN HISTORY -->
<div class="card mb-4 border-0 shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-history me-2"></i>Run History</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Date</th>
<th>Status</th>
<th>Cost</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for r in runs %}
<tr class="{{ 'table-primary' if active_run and r.id == active_run.id else '' }}">
<td>#{{ r.id }}</td>
<td>{{ r.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<span class="badge bg-{{ 'success' if r.status == 'completed' else 'warning' if r.status in ['running', 'queued'] else 'danger' if r.status in ['failed', 'cancelled', 'interrupted'] else 'secondary' }}">
{{ r.status }}
</span>
</td>
<td>${{ "%.4f"|format(r.cost) }}</td>
<td>
<a href="/project/{{ project.id }}/run/{{ r.id }}" class="btn btn-sm btn-outline-primary">
{{ 'View Active' if active_run and r.id == active_run.id else 'View' }}
</a>
{% if r.status in ['failed', 'cancelled', 'interrupted'] %}
<form action="/run/{{ r.id }}/restart" method="POST" class="d-inline ms-1">
<input type="hidden" name="mode" value="resume">
<button class="btn btn-sm btn-outline-success" title="Resume from last step">
<i class="fas fa-play"></i>
</button>
</form>
{% endif %}
{% if r.status not in ['running', 'queued'] %}
<form action="/run/{{ r.id }}/restart" method="POST" class="d-inline ms-1" onsubmit="return confirm('This will delete all files for this run and start over. Are you sure?');">
<input type="hidden" name="mode" value="restart_clean">
<button class="btn btn-sm btn-outline-danger" title="Re-run (Wipe & Restart)">
<i class="fas fa-redo"></i>
</button>
</form>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted">No runs found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- SERIES OVERVIEW -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="mb-0">
<i class="fas fa-{{ 'layer-group' if bible.project_metadata.is_series else 'book' }} me-2"></i>
{{ 'Series Overview' if bible.project_metadata.is_series else 'Book Overview' }}
</h4>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addBookModal">
<i class="fas fa-plus me-1"></i> {{ 'Add Book' if bible.project_metadata.is_series else 'Extend to Series' }}
</button>
</div>
<div class="row flex-nowrap overflow-auto pb-3" style="gap: 1rem;">
{% for book in bible.books %}
<div class="col-md-4 col-lg-3" style="min-width: 300px;">
<div class="card h-100 shadow-sm border-top border-4 {{ 'border-success' if generated_books.get(book.book_number) else 'border-secondary' }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge bg-light text-dark border">Book {{ book.book_number }}</span>
{% if generated_books.get(book.book_number) %}
<div class="btn-group">
<button type="button" class="btn btn-sm btn-success dropdown-toggle" data-bs-toggle="dropdown">
<i class="fas fa-check me-1"></i>Generated
</button>
<ul class="dropdown-menu">
{% set gb = generated_books.get(book.book_number) %}
{% if gb.epub %}<li><a class="dropdown-item" href="/project/{{ gb.run_id }}/download?file={{ gb.epub }}"><i class="fas fa-file-epub me-2"></i>Download EPUB</a></li>{% endif %}
{% if gb.docx %}<li><a class="dropdown-item" href="/project/{{ gb.run_id }}/download?file={{ gb.docx }}"><i class="fas fa-file-word me-2"></i>Download DOCX</a></li>{% endif %}
</ul>
</div>
{% else %}
<span class="badge bg-secondary">Planned</span>
{% endif %}
</div>
<h5 class="card-title text-truncate" title="{{ book.title }}">{{ book.title }}</h5>
<p class="card-text small text-muted" style="height: 60px; overflow: hidden;">
{{ book.manual_instruction or "No summary provided." }}
</p>
<div class="d-flex justify-content-between mt-3">
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#editBookModal{{ book.book_number }}" title="Edit Details">
<i class="fas fa-edit"></i> Edit
</button>
{% if not locked %}
{% if not generated_books.get(book.book_number) %}
<form action="/project/{{ project.id }}/delete_book/{{ book.book_number }}" method="POST" onsubmit="return confirm('Remove this book from the plan?');">
<button class="btn btn-sm btn-outline-danger"><i class="fas fa-trash"></i></button>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
<!-- Edit Book Modal (Specific to this book) -->
<div class="modal fade" id="editBookModal{{ book.book_number }}" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/book/{{ book.book_number }}/update" method="POST">
<div class="modal-header">
<h5 class="modal-title">Edit Book {{ book.book_number }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Title</label>
<input type="text" name="title" class="form-control" value="{{ book.title }}" {% if locked %}disabled{% endif %}>
</div>
<div class="mb-3">
<label class="form-label">Plot Summary / Instruction</label>
<textarea name="instruction" class="form-control" rows="6" {% if locked %}disabled{% endif %}>{{ book.manual_instruction }}</textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
{% if not locked %}
<button type="submit" class="btn btn-primary">Save Changes</button>
{% endif %}
</div>
</form>
</div>
</div>
</div>
{% endfor %}
<!-- Add Book Card -->
{% if not locked %}
<div class="col-md-4 col-lg-3" style="min-width: 200px;">
<div class="card h-100 border-dashed d-flex align-items-center justify-content-center bg-light" style="border: 2px dashed #ccc; cursor: pointer;" data-bs-toggle="modal" data-bs-target="#addBookModal">
<div class="text-center text-muted py-5">
<i class="fas fa-plus-circle fa-2x mb-2"></i>
<br>Add Next Book
</div>
</div>
</div>
{% endif %}
</div>
</div>
<!-- WORLD BIBLE & LINKED SERIES -->
<div class="row mb-4" id="bible-section">
<div class="col-md-12">
<div class="card shadow-sm">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-globe me-2"></i>World Bible & Characters</h5>
<div>
<a href="/project/{{ project.id }}/review" class="btn btn-sm btn-outline-info me-1">
<i class="fas fa-list-alt me-1"></i> Full Review
</a>
{% if not locked %}
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#importCharModal" data-bs-toggle="tooltip" title="Import characters from another project to create a shared universe.">
<i class="fas fa-link me-1"></i> Link / Import Series
</button>
<button class="btn btn-sm btn-outline-primary ms-1" data-bs-toggle="modal" data-bs-target="#refineBibleModal" data-bs-toggle="tooltip" title="Use AI to bulk-edit characters or plot points based on your instructions.">
<i class="fas fa-magic me-1"></i> Refine
</button>
{% endif %}
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4 border-end">
<h6 class="text-muted text-uppercase small fw-bold mb-3">Project Metadata</h6>
<dl class="row small mb-0">
<dt class="col-4">Author</dt><dd class="col-8">{{ bible.project_metadata.author }}</dd>
<dt class="col-4">Genre</dt><dd class="col-8">{{ bible.project_metadata.genre }}</dd>
<dt class="col-4">Tone</dt><dd class="col-8">{{ bible.project_metadata.style.tone }}</dd>
<dt class="col-4">Series</dt><dd class="col-8">{{ 'Yes' if bible.project_metadata.is_series else 'No' }}</dd>
</dl>
</div>
<div class="col-md-8">
<h6 class="text-muted text-uppercase small fw-bold mb-3">Characters ({{ bible.characters|length }})</h6>
<div class="d-flex flex-wrap gap-2">
{% for c in bible.characters %}
<span class="badge bg-light text-dark border" title="{{ c.description }}">
<i class="fas fa-user me-1 text-secondary"></i> {{ c.name }}
<span class="text-muted fw-normal">| {{ c.role }}</span>
</span>
{% else %}
<span class="text-muted small">No characters defined.</span>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editProjectModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/update" method="POST">
<div class="modal-header">
<h5 class="modal-title">Edit Project Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3"><label class="form-label">Book Title</label><input type="text" name="title" class="form-control" value="{{ bible.project_metadata.title }}" required></div>
<div class="mb-3"><label class="form-label">Author Name</label><input type="text" name="author" class="form-control" value="{{ bible.project_metadata.author }}" required></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
<!-- Clone Project Modal -->
<div class="modal fade" id="cloneProjectModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/clone" method="POST" onsubmit="showLoading(this)">
<div class="modal-header">
<h5 class="modal-title">Clone & Modify Project</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Create a new project based on this one, with AI modifications.</p>
<div class="mb-3"><label class="form-label">New Project Name</label><input type="text" name="new_name" class="form-control" value="{{ project.name }} (Copy)" required></div>
<div class="mb-3"><label class="form-label">AI Instructions (Optional)</label><textarea name="instruction" class="form-control" rows="3" placeholder="e.g. 'Change the genre to Sci-Fi', 'Make the protagonist a villain', 'Rewrite as a comedy'."></textarea></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-info">Clone Project</button>
</div>
</form>
</div>
</div>
<!-- Add Book Modal -->
<div class="modal fade" id="addBookModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/add_book" method="POST">
<div class="modal-header">
<h5 class="modal-title">Add Book to Series</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Book Title</label>
<input type="text" name="title" class="form-control" placeholder="e.g. The Two Towers" required>
</div>
<div class="mb-3">
<label class="form-label">Plot Summary / Instruction</label>
<textarea name="instruction" class="form-control" rows="4" placeholder="Briefly describe the plot. Mention if it follows the main cast or shifts to side characters/new settings."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Book</button>
</div>
</form>
</div>
</div>
<!-- Import Characters Modal -->
<div class="modal fade" id="importCharModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/import_characters" method="POST">
<div class="modal-header">
<h5 class="modal-title">Link Related Series</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Import characters from another project to establish a shared universe.</p>
<div class="mb-3">
<label class="form-label">Source Project</label>
<select name="source_project_id" class="form-select" required>
<option value="">-- Select Project --</option>
{% for p in other_projects %}
<option value="{{ p.id }}">{{ p.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Import Characters</button>
</div>
</form>
</div>
</div>
<!-- Regenerate/Feedback Modal -->
<div class="modal fade" id="regenerateModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ active_run.id if active_run else 0 }}/regenerate_artifacts" method="POST">
<div class="modal-header">
<h5 class="modal-title">Update Files & Cover</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">This will regenerate the EPUB/DOCX files with any metadata changes. You can also provide feedback to fix the cover.</p>
<div class="mb-3">
<label class="form-label">Cover Feedback (Optional)</label>
<textarea name="feedback" class="form-control" rows="3" placeholder="e.g. 'Change the title color to gold', 'The text is hard to read', or 'I don't like the image, make it darker'."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">Regenerate</button>
</div>
</form>
</div>
</div>
<!-- Refine Bible Modal -->
<div class="modal fade" id="refineBibleModal" tabindex="-1">
<div class="modal-dialog">
<form class="modal-content" action="/project/{{ project.id }}/refine_bible" method="POST">
<div class="modal-header">
<h5 class="modal-title">Refine Bible with AI</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small">Ask the AI to change characters, plot points, or settings.</p>
<div class="mb-3">
<label class="form-label">Instruction</label>
<textarea name="instruction" class="form-control" rows="3" placeholder="e.g. 'Change the ending of Book 1', 'Add a character named Bob', 'Make the tone darker'." required></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Bible</button>
</div>
</form>
</div>
</div>
<!-- Full Bible JSON Modal -->
<div class="modal fade" id="fullBibleModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Full Bible JSON</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body bg-light">
<pre class="small">{{ bible | tojson(indent=2) }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let activeInterval = null;
// Only auto-poll if we have a latest run
let currentRunId = {{ active_run.id if active_run else 'null' }};
const initialRunStatus = "{{ active_run.status if active_run else '' }}";
function fetchLog() {
if (!currentRunId) return;
fetch(`/run/${currentRunId}/status`)
.then(response => response.json())
.then(data => {
const consoleDiv = document.getElementById('consoleOutput');
if (consoleDiv) {
consoleDiv.innerText = data.log || "Waiting for logs...";
consoleDiv.scrollTop = consoleDiv.scrollHeight;
}
// Update Sidebar Badge & Cost
const badge = document.querySelector(`.status-badge-${currentRunId}`);
if (badge) {
badge.innerText = data.status.toUpperCase();
badge.className = `badge fs-6 me-3 status-badge-${currentRunId} bg-${data.status === 'completed' ? 'success' : (data.status === 'running' || data.status === 'queued') ? 'warning' : (data.status === 'failed' || data.status === 'interrupted' || data.status === 'cancelled') ? 'danger' : 'secondary'}`;
}
const costSpan = document.querySelector(`.cost-${currentRunId}`);
if (costSpan) costSpan.innerText = parseFloat(data.cost).toFixed(4);
// Update Progress Bar Width
const progBar = document.getElementById('progressBar');
if (progBar && data.percent !== undefined) {
progBar.style.width = data.percent + "%";
}
// Update Status Bar
if (data.progress && data.progress.message) {
const phaseEl = document.getElementById('statusPhase');
const msgEl = document.getElementById('statusMessage');
const timeEl = document.getElementById('statusTime');
if (phaseEl) {
// Map phases to icons
const icons = {
"WRITER": "✍️", "ARCHITECT": "🏗️", "MARKETING": "🎨",
"ENRICHER": "🧠", "SYSTEM": "⚙️", "TIMING": "⏱️", "TRACKER": "🔎"
};
const icon = icons[data.progress.phase] || "🔄";
phaseEl.innerText = `${icon} ${data.progress.phase}`;
}
if (msgEl) msgEl.innerText = data.progress.message;
if (timeEl) {
const date = new Date(data.progress.timestamp * 1000);
timeEl.innerText = "Last update: " + date.toLocaleTimeString();
}
}
// If running, keep polling
if (data.status === 'running' || data.status === 'queued') {
if (!activeInterval) activeInterval = setInterval(fetchLog, 2000);
} else {
if (activeInterval) clearInterval(activeInterval);
activeInterval = null;
// Reload if we were polling (watched it finish) OR if page loaded as running but is now done
if (initialRunStatus === 'running' || initialRunStatus === 'queued') {
window.location.reload();
}
}
});
}
{% if active_run %}
fetchLog();
{% endif %}
</script>
{% endblock %}