## Summary Closes #5. - Composer can **Send now** without a schedule time (Schedule and Save draft still available) - Account names wrap and show the platform id so a bad OAuth label is still readable - Social accounts page can rename the display name; re-auth keeps a custom name instead of overwriting it with a placeholder - Worker polls `dispatch_due` every 15s so scheduled campaigns and social posts fire without a manual command; local `docker compose up` now starts the worker ## Test plan - [ ] Open Social → Compose: confirm **Send now**, **Schedule**, and **Save draft** buttons (no combined Save / publish) - [ ] Send now with a connected account and caption → post queues immediately, no datetime required - [ ] Schedule with a datetime → post is scheduled; Schedule with a blank time → validation error - [ ] Composer account list shows full name plus external id, wrapping if the name is long - [ ] Social accounts → rename a messy label → new name shows in composer without reconnecting - [ ] Re-authorize an account that was renamed → custom name is kept - [ ] Rebuild and start compose (`docker compose up --build`) so the **worker** service is running - [ ] Schedule a social post ~2 minutes ahead → after due time, worker logs `Enqueued N due item(s).` and the post leaves Scheduled (published or failed) - [ ] `docker compose logs -f worker` shows `Polling due scheduled work every 15s.` Reviewed-on: #6
59 lines
2.3 KiB
HTML
59 lines
2.3 KiB
HTML
{% extends "portal_base.html" %}
|
|
{% block title %}Post · Portal{% endblock %}
|
|
{% block topbar_title %}Social post{% endblock %}
|
|
{% block portal_content %}
|
|
<div class="toolbar">
|
|
<span class="badge badge-{{ post.status }}">{{ post.get_status_display }}</span>
|
|
<a class="btn btn-sm btn-ghost" href="{% url 'social:post_list' %}">← All posts</a>
|
|
</div>
|
|
<div class="split">
|
|
<div class="panel">
|
|
<div class="panel-h"><h2>Caption</h2></div>
|
|
<div class="panel-b">
|
|
{% if post.media %}
|
|
<div style="display:flex;flex-wrap:wrap;gap:10px;margin-bottom:12px">
|
|
{% for item in post.media %}
|
|
{% if item.type == "video" %}
|
|
<video src="{{ item.url }}" controls playsinline style="max-width:100%;max-height:280px;border-radius:8px"></video>
|
|
{% else %}
|
|
<img src="{{ item.url }}" alt="{{ item.filename|default:'Image' }}" style="max-width:100%;max-height:280px;border-radius:8px;object-fit:contain">
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
<div class="preview-pane" style="white-space:pre-wrap">{{ post.body }}</div>
|
|
{% if post.scheduled_for %}
|
|
<p class="hint-block">Scheduled {{ post.scheduled_for|date:"M j, Y g:i A" }}</p>
|
|
{% endif %}
|
|
{% if post.ollama_prompt %}
|
|
<p class="hint-block">AI prompt: {{ post.ollama_prompt }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="panel">
|
|
<div class="panel-h"><h2>Targets</h2></div>
|
|
<div class="panel-b" style="padding:0">
|
|
<table class="table">
|
|
<thead><tr><th>Account</th><th>Platform</th><th>Status</th></tr></thead>
|
|
<tbody>
|
|
{% for target in post.targets.all %}
|
|
<tr>
|
|
<td>
|
|
{{ target.account.display_name }}
|
|
{% if target.account.external_id %}
|
|
<br><span class="muted" style="overflow-wrap:anywhere">{{ target.account.external_id }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ target.get_platform_display }}</td>
|
|
<td><span class="badge badge-{{ target.status }}">{{ target.get_status_display }}</span></td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr><td colspan="3" class="empty-state">No targets attached.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|