Templates: - mission_timeline_preview: client-side rendering with lane stacking, wheel zoom, drag pan, percentage-based layout, event dedup, scale labels - asset_detail: dock/undock modals with time selection, future event warning, Mission Label column replacing Location/State - asset_entry_form: all_locations expanded to 72 items (draft board hierarchy), all_states dynamic from DB, event point dropdown uses same list - Remove sim_time from all template url_for links - base.html: remove Today button JS: - app.js: sim_time controls use POST /api/v1/sim-time API instead of URL param Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
433 lines
22 KiB
HTML
433 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="ph-row">
|
|
<div class="ph">
|
|
<div class="ey">Mission Asset</div>
|
|
<h1>{{ asset.name }}</h1>
|
|
<p>{{ asset.asset_type }} · {{ 'Retired' if asset.is_retired else 'Active' }}</p>
|
|
</div>
|
|
<div style="display:flex;gap:6px">
|
|
<a class="b" href="{{ url_for('web.asset_edit', asset_id=asset.id
|
|
) }}">Edit Asset</a>
|
|
<a class="b" href="{{ url_for('web.asset_list'
|
|
) }}">← Back</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ad-grid">
|
|
<div>
|
|
<!-- Info cards -->
|
|
<div class="mg" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
|
|
<div class="mc"><div class="l">Type</div><div class="v" style="font-size:.95rem">{{ asset.asset_type }}</div></div>
|
|
<div class="mc"><div class="l">Location</div><div class="v" style="font-size:.95rem">{{ asset_summary.current_location or '—' }}</div></div>
|
|
<div class="mc"><div class="l">Mission</div><div class="v" style="font-size:.85rem">{{ asset_summary.current_mission or '—' }}</div></div>
|
|
<div class="mc"><div class="l">Entries</div><div class="v" style="font-size:.95rem">{{ total_entry_count }}</div></div>
|
|
</div>
|
|
|
|
<!-- Docking section -->
|
|
<div class="ad-dock">
|
|
<div>
|
|
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
|
|
<span>Docking Target</span>
|
|
<button class="b sm ac" type="button" onclick="openDockModal('dock_to_target')">+ Target</button>
|
|
</div>
|
|
<div class="ad-dock-box">
|
|
{% if asset_summary.docking_target %}
|
|
<div class="ad-dock-item">
|
|
<span>
|
|
<a href="{{ url_for('web.asset_detail', asset_id=asset_summary.docking_target.id
|
|
) }}" style="color:var(--accent)">{{ asset_summary.docking_target.name }}</a>
|
|
<span class="mt" style="margin-left:6px">{{ asset_summary.docking_target.current_location }}</span>
|
|
<span class="mt" style="margin-left:6px;color:var(--muted);font-size:.66rem">since {{ asset_summary.docking_target.docked_at }}</span>
|
|
</span>
|
|
<a class="ad-undock" onclick="openUndockModal('{{ asset_summary.docking_target.event_id }}', '{{ asset_summary.docking_target.undocked_at or '' }}')">× Remove</a>
|
|
</div>
|
|
{% else %}
|
|
<div style="padding:6px 10px;color:var(--mut);font-size:.72rem">Not docked to any asset</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
|
|
<span>Docked Vehicles</span>
|
|
<button class="b sm ac" type="button" onclick="openDockModal('dock_vehicle')">+ Dock</button>
|
|
</div>
|
|
<div class="ad-dock-box">
|
|
{% if asset_summary.docked_vehicles %}
|
|
{% for v in asset_summary.docked_vehicles %}
|
|
<div class="ad-dock-item">
|
|
<span>
|
|
{% if v.id %}
|
|
<a href="{{ url_for('web.asset_detail', asset_id=v.id
|
|
) }}" style="color:var(--accent)">{{ v.name }}</a>
|
|
{% else %}
|
|
<strong style="color:var(--txt)">{{ v.name }}</strong>
|
|
{% endif %}
|
|
<span class="mt">since {{ v.docked_at }}</span>
|
|
</span>
|
|
<a class="ad-undock" onclick="openUndockModal('{{ v.event_id }}', '{{ v.undocked_at or '' }}')">× Remove</a>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div style="padding:6px 10px;color:var(--mut);font-size:.72rem">No vehicles docked</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Log entries -->
|
|
<div class="stl" style="display:flex;align-items:center;justify-content:space-between">
|
|
<span>Log Entries</span>
|
|
<div style="display:flex;gap:4px">
|
|
<button class="b sm ac" onclick="openNewLog('state')">+ State Interval</button>
|
|
<button class="b sm" onclick="openNewLog('event')">+ Event Point</button>
|
|
</div>
|
|
</div>
|
|
<div class="fb">
|
|
<input type="text" id="ad-lq" placeholder="Search logs...">
|
|
<select id="ad-lf">
|
|
<option value="">All Types</option>
|
|
<option value="state">State Interval</option>
|
|
<option value="event">Event Point</option>
|
|
</select>
|
|
</div>
|
|
<table>
|
|
<thead><tr><th>Date</th><th>Type</th><th>Title</th><th>Mission Label</th><th>Action</th></tr></thead>
|
|
<tbody>
|
|
{% for entry in entries %}
|
|
<tr data-log-row data-log-type="{{ entry.mode }}" data-log-text="{{ (entry.title ~ ' ' ~ entry.summary ~ ' ' ~ entry.mission_label)|lower|e }}">
|
|
<td class="mt">{{ entry.timestamp }}{% if entry.end_timestamp %} → {{ entry.end_timestamp }}{% endif %}</td>
|
|
<td><span style="font-size:.64rem;padding:2px 5px;border-radius:3px;background:{{ 'rgba(59,130,246,.1);color:var(--accent)' if entry.mode == 'state' else 'rgba(234,179,8,.08);color:var(--amber)' }}">{{ entry.mode_label }}</span></td>
|
|
<td>{{ entry.title }}{% for node in entry.state_nodes %}<div class="mt" style="font-size:.62rem">→ {{ node.title }} ({{ node.at }}{% if node.detail %} · {{ node.detail }}{% endif %})</div>{% endfor %}</td>
|
|
<td class="mt">{{ entry.mission_label }}</td>
|
|
<td><div class="act-btn">
|
|
<a href="{{ entry.edit_url }}">Edit</a>
|
|
<form method="post" action="{{ url_for('web.asset_entry_delete', asset_id=asset.id, entry_id=entry.source_entry_id) }}" style="display:inline" onsubmit="return confirm('Delete?')">
|
|
<input type="hidden" name="sim_time" value="{{ simulation_time_input }}">
|
|
<button class="danger-link" type="submit" style="font-size:.66rem">Del</button>
|
|
</form>
|
|
</div></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if entry_total_pages > 1 %}
|
|
<div class="pg-bar">
|
|
{% if entry_has_prev %}<a href="{{ entry_prev_url }}">← Prev</a>{% endif %}
|
|
{% for link in entry_page_links %}
|
|
{% if link.is_current %}<span class="cur">{{ link.page }}</span>
|
|
{% elif link.is_ellipsis %}<span>...</span>
|
|
{% else %}<a href="{{ link.url }}">{{ link.page }}</a>{% endif %}
|
|
{% endfor %}
|
|
{% if entry_has_next %}<a href="{{ entry_next_url }}">Next →</a>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<div class="stl">Upcoming Events</div>
|
|
<div class="evl" style="margin-bottom:14px">
|
|
{% for ev in future_events %}
|
|
<div class="evr"><span class="ed">{{ ev.date }}</span>{{ ev.title }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="stl">Quick Info</div>
|
|
<div style="background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;font-size:.72rem;color:var(--mut)">
|
|
<div style="margin-bottom:4px"><strong style="color:var(--txt)">Retired:</strong> {{ 'Yes' if asset.is_retired else 'No' }}</div>
|
|
<div style="margin-bottom:4px"><strong style="color:var(--txt)">Program:</strong> {{ asset.program or '—' }}</div>
|
|
<div><strong style="color:var(--txt)">Home Region:</strong> {{ asset.home_region or '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ DOCK MODAL ═══ -->
|
|
<div class="modal-bg" id="dock-modal">
|
|
<div class="modal">
|
|
<h2 id="dock-modal-title">Dock Vehicle</h2>
|
|
<div class="fg">
|
|
<input type="hidden" id="dock-mode" value="dock_vehicle">
|
|
<label><span id="dock-select-label">Vehicle</span><select id="dock-vehicle">
|
|
<option value="">Select existing asset...</option>
|
|
{% for a in all_assets %}{% if a.id != asset.id %}
|
|
<option value="{{ a.id }}">{{ a.name }}</option>
|
|
{% endif %}{% endfor %}
|
|
<option value="_custom">Custom name...</option>
|
|
</select></label>
|
|
<input type="text" id="dock-custom" placeholder="Enter vehicle name..." style="display:none">
|
|
<label>Dock Time<input type="datetime-local" id="dock-time" value="{{ simulation_time_input }}"></label>
|
|
<label>Note<textarea id="dock-note" rows="2" placeholder="Optional"></textarea></label>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="b" onclick="closeModal('dock-modal')">Cancel</button>
|
|
<button class="b ac" onclick="saveDock()">Dock</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ UNDOCK MODAL ═══ -->
|
|
<div class="modal-bg" id="undock-modal">
|
|
<div class="modal">
|
|
<h2>Remove Docking</h2>
|
|
<div class="fg">
|
|
<input type="hidden" id="undock-event-id" value="">
|
|
<input type="hidden" id="undock-has-future" value="">
|
|
<div id="undock-future-warn" style="display:none;padding:8px 12px;margin-bottom:10px;border-radius:4px;background:rgba(234,179,8,.1);border:1px solid rgba(234,179,8,.3);color:var(--amber);font-size:.78rem">
|
|
This docking already has a scheduled undock at <strong id="undock-future-time"></strong>.
|
|
</div>
|
|
<div id="undock-normal-form">
|
|
<label>Undock Time<input type="datetime-local" id="undock-time" value="{{ simulation_time_input }}"></label>
|
|
<label>Note<textarea id="undock-note" rows="2" placeholder="Optional"></textarea></label>
|
|
</div>
|
|
</div>
|
|
<div class="btn-row" id="undock-btns-normal" style="display:none">
|
|
<button class="b" onclick="closeModal('undock-modal')">Cancel</button>
|
|
<button class="b ac" onclick="saveUndock()">Remove</button>
|
|
</div>
|
|
<div class="btn-row" id="undock-btns-future" style="display:none">
|
|
<button class="b" onclick="closeModal('undock-modal')">Cancel</button>
|
|
<button class="b" onclick="deleteFutureUndock()">Delete Future Undock</button>
|
|
<button class="b ac" onclick="editFutureUndock()">Edit</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══ LOG MODAL ═══ -->
|
|
<div class="modal-bg" id="log-modal">
|
|
<div class="modal" style="width:440px;max-height:80vh;overflow-y:auto">
|
|
<h2 id="log-modal-title">New Log Entry</h2>
|
|
<div class="fg">
|
|
<input type="hidden" id="log-type" value="state">
|
|
<label>Title<input type="text" id="log-title" placeholder="Entry title"></label>
|
|
<label id="log-start-wrap">Start At<input type="datetime-local" id="log-start" step="60"></label>
|
|
<label id="log-end-wrap">End At<input type="datetime-local" id="log-end" step="60"></label>
|
|
<div id="log-state-wrap">
|
|
<label>State<select id="log-state">
|
|
<option value="">—</option>
|
|
<option>Transfer</option><option>Exploration</option><option>Maintenance</option>
|
|
<option>Docked</option><option>In Transit</option>
|
|
</select></label>
|
|
</div>
|
|
<label>Location<input type="text" id="log-location" placeholder="LEO / Mars Orbit / etc."></label>
|
|
<label>Summary<textarea id="log-summary" rows="2" placeholder="Optional"></textarea></label>
|
|
<div id="log-nodes-wrap">
|
|
<hr style="border:none;border-top:1px solid var(--border);margin:6px 0">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
|
<span style="font-size:.7rem;color:var(--mut)">State Nodes (0-N)</span>
|
|
<button class="b sm" type="button" onclick="addNodeRow()" style="font-size:.62rem">+ Add Node</button>
|
|
</div>
|
|
<div id="nodes-list"></div>
|
|
</div>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="b" onclick="closeModal('log-modal')">Cancel</button>
|
|
<button class="b ac" onclick="saveLog()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const ASSET_ID = '{{ asset.id }}';
|
|
const SIM_TIME = '{{ simulation_time_input }}';
|
|
|
|
// === DOCKING ===
|
|
function openDockModal(mode='dock_vehicle') {
|
|
document.getElementById('dock-mode').value = mode;
|
|
document.getElementById('dock-modal-title').textContent = mode === 'dock_to_target' ? 'Dock To Target' : 'Dock Vehicle';
|
|
document.getElementById('dock-select-label').textContent = mode === 'dock_to_target' ? 'Target Asset' : 'Vehicle';
|
|
document.getElementById('dock-vehicle').value = '';
|
|
document.getElementById('dock-custom').style.display = 'none';
|
|
document.querySelector('#dock-vehicle option[value="_custom"]').style.display = mode === 'dock_to_target' ? 'none' : '';
|
|
document.getElementById('dock-note').value = '';
|
|
document.getElementById('dock-time').value = SIM_TIME;
|
|
openModal('dock-modal');
|
|
}
|
|
document.getElementById('dock-vehicle').addEventListener('change', function() {
|
|
document.getElementById('dock-custom').style.display = this.value === '_custom' && document.getElementById('dock-mode').value !== 'dock_to_target' ? 'block' : 'none';
|
|
});
|
|
async function saveDock() {
|
|
const mode = document.getElementById('dock-mode').value;
|
|
const vehicleId = document.getElementById('dock-vehicle').value;
|
|
const customName = document.getElementById('dock-custom').value;
|
|
const note = document.getElementById('dock-note').value;
|
|
const dockTime = document.getElementById('dock-time').value;
|
|
if (!vehicleId) return;
|
|
const name = vehicleId === '_custom' ? customName : document.getElementById('dock-vehicle').selectedOptions[0]?.text;
|
|
if (!name) return;
|
|
const resp = await fetch(`/api/v1/assets/${ASSET_ID}/dock`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
mode: mode,
|
|
vehicle_id: mode === 'dock_vehicle' && vehicleId !== '_custom' ? vehicleId : null,
|
|
target_asset_id: mode === 'dock_to_target' ? vehicleId : null,
|
|
vehicle_name: name,
|
|
note: note,
|
|
dock_time: dockTime,
|
|
sim_time: SIM_TIME
|
|
})
|
|
});
|
|
if (resp.ok) { closeModal('dock-modal'); location.reload(); }
|
|
}
|
|
|
|
// === UNDOCKING ===
|
|
function openUndockModal(eventId, futureUndockAt) {
|
|
document.getElementById('undock-event-id').value = eventId;
|
|
document.getElementById('undock-time').value = SIM_TIME;
|
|
document.getElementById('undock-note').value = '';
|
|
const hasFuture = futureUndockAt && futureUndockAt.trim() !== '';
|
|
document.getElementById('undock-has-future').value = hasFuture ? futureUndockAt : '';
|
|
if (hasFuture) {
|
|
document.getElementById('undock-future-time').textContent = futureUndockAt;
|
|
document.getElementById('undock-future-warn').style.display = 'block';
|
|
document.getElementById('undock-normal-form').style.display = 'none';
|
|
document.getElementById('undock-btns-normal').style.display = 'none';
|
|
document.getElementById('undock-btns-future').style.display = 'flex';
|
|
} else {
|
|
document.getElementById('undock-future-warn').style.display = 'none';
|
|
document.getElementById('undock-normal-form').style.display = 'block';
|
|
document.getElementById('undock-btns-normal').style.display = 'flex';
|
|
document.getElementById('undock-btns-future').style.display = 'none';
|
|
}
|
|
openModal('undock-modal');
|
|
}
|
|
async function saveUndock() {
|
|
const eventId = document.getElementById('undock-event-id').value;
|
|
const undockTime = document.getElementById('undock-time').value;
|
|
const note = document.getElementById('undock-note').value;
|
|
const hasFuture = document.getElementById('undock-has-future').value;
|
|
const resp = await fetch(`/api/v1/assets/${ASSET_ID}/undock`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
event_id: eventId,
|
|
action: hasFuture ? 'edit_future' : 'undock',
|
|
undock_time: undockTime,
|
|
note: note,
|
|
sim_time: SIM_TIME
|
|
})
|
|
});
|
|
if (resp.ok) { closeModal('undock-modal'); location.reload(); }
|
|
}
|
|
async function deleteFutureUndock() {
|
|
const eventId = document.getElementById('undock-event-id').value;
|
|
const resp = await fetch(`/api/v1/assets/${ASSET_ID}/undock`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
event_id: eventId,
|
|
action: 'delete_future',
|
|
sim_time: SIM_TIME
|
|
})
|
|
});
|
|
if (resp.ok) { closeModal('undock-modal'); location.reload(); }
|
|
}
|
|
function editFutureUndock() {
|
|
const futureTime = document.getElementById('undock-has-future').value;
|
|
document.getElementById('undock-future-warn').style.display = 'none';
|
|
document.getElementById('undock-normal-form').style.display = 'block';
|
|
document.getElementById('undock-time').value = futureTime;
|
|
document.getElementById('undock-btns-future').style.display = 'none';
|
|
document.getElementById('undock-btns-normal').style.display = 'flex';
|
|
}
|
|
|
|
// === LOG MODAL (multi-node) ===
|
|
let logNodeData = [], editLogId = null;
|
|
function escapeAttr(value) {
|
|
return String(value || '').replace(/[&<>"']/g, (ch) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
|
}
|
|
function addNodeRow(title='', at='', detail='', stateLabel='', previousLocation='', transitLocation='', targetLocation='') {
|
|
logNodeData.push({title, at, detail, stateLabel, previousLocation, transitLocation, targetLocation});
|
|
renderNodeRows();
|
|
}
|
|
function removeNodeRow(i) { logNodeData.splice(i, 1); renderNodeRows(); }
|
|
function renderNodeRows() {
|
|
document.getElementById('nodes-list').innerHTML = logNodeData.length ? logNodeData.map((n, i) =>
|
|
`<div style="background:var(--surface2);border:1px solid var(--border);border-radius:4px;padding:6px 8px;margin-bottom:4px">
|
|
<div style="display:flex;gap:6px;align-items:center">
|
|
<input type="text" value="${escapeAttr(n.title)}" placeholder="Node title" oninput="logNodeData[${i}].title=this.value" style="flex:1;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem">
|
|
<input type="datetime-local" value="${escapeAttr(n.at)}" oninput="logNodeData[${i}].at=this.value" step="60" style="width:140px;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem">
|
|
<button class="b sm" type="button" onclick="removeNodeRow(${i})" style="color:var(--red);font-size:.6rem">✕</button>
|
|
</div>
|
|
<input type="text" value="${escapeAttr(n.detail)}" placeholder="Detail (optional)" oninput="logNodeData[${i}].detail=this.value" style="width:100%;padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem;margin-top:3px">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-top:4px">
|
|
<input type="text" value="${escapeAttr(n.stateLabel)}" placeholder="State" oninput="logNodeData[${i}].stateLabel=this.value" style="padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem">
|
|
<input type="text" value="${escapeAttr(n.targetLocation)}" placeholder="Target location" oninput="logNodeData[${i}].targetLocation=this.value" style="padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem">
|
|
<input type="text" value="${escapeAttr(n.previousLocation)}" placeholder="Previous location" oninput="logNodeData[${i}].previousLocation=this.value" style="padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem">
|
|
<input type="text" value="${escapeAttr(n.transitLocation)}" placeholder="Transit location" oninput="logNodeData[${i}].transitLocation=this.value" style="padding:3px 6px;border-radius:3px;background:var(--bg);color:var(--text);border:1px solid var(--border);font-size:.7rem">
|
|
</div>
|
|
</div>`
|
|
).join('') : '<div style="font-size:.68rem;color:var(--mut);padding:4px 0">No nodes. Click "+ Add Node" to add.</div>';
|
|
}
|
|
function openNewLog(type) {
|
|
editLogId = null; logNodeData = [];
|
|
document.getElementById('log-type').value = type;
|
|
document.getElementById('log-title').value = '';
|
|
document.getElementById('log-start').value = SIM_TIME;
|
|
document.getElementById('log-end').value = '';
|
|
document.getElementById('log-state').value = '';
|
|
document.getElementById('log-location').value = '';
|
|
document.getElementById('log-summary').value = '';
|
|
document.getElementById('log-start-wrap').style.display = 'block';
|
|
document.getElementById('log-end-wrap').style.display = type === 'state' ? 'block' : 'none';
|
|
document.getElementById('log-state-wrap').style.display = type === 'state' ? 'block' : 'none';
|
|
document.getElementById('log-nodes-wrap').style.display = type === 'state' ? 'block' : 'none';
|
|
renderNodeRows();
|
|
document.getElementById('log-modal-title').textContent = 'New Log Entry';
|
|
openModal('log-modal');
|
|
}
|
|
async function saveLog() {
|
|
const type = document.getElementById('log-type').value;
|
|
const title = document.getElementById('log-title').value;
|
|
if (!title) { alert('Title required'); return; }
|
|
const stateLabel = document.getElementById('log-state').value || null;
|
|
const targetLocation = document.getElementById('log-location').value || null;
|
|
const enteredNodes = logNodeData.filter(n => n.title).map(n => ({
|
|
title: n.title,
|
|
at_time: n.at || null,
|
|
detail: n.detail || null,
|
|
state_label: n.stateLabel || stateLabel,
|
|
previous_location: n.previousLocation || null,
|
|
transit_location: n.transitLocation || null,
|
|
target_location: n.targetLocation || targetLocation
|
|
}));
|
|
const stateNodes = type === 'state' && enteredNodes.length === 0 && (stateLabel || targetLocation)
|
|
? [{title: title, at_time: document.getElementById('log-start').value || SIM_TIME, detail: null, state_label: stateLabel, target_location: targetLocation}]
|
|
: enteredNodes;
|
|
const data = {
|
|
entry_kind: type, title: title,
|
|
start_at: document.getElementById('log-start').value || null,
|
|
end_at: type === 'state' ? (document.getElementById('log-end').value || null) : null,
|
|
state_label: type === 'state' ? stateLabel : null,
|
|
location: targetLocation,
|
|
summary: document.getElementById('log-summary').value || null,
|
|
sim_time: SIM_TIME,
|
|
state_nodes: stateNodes
|
|
};
|
|
const url = editLogId ? `/assets/${ASSET_ID}/entries/${editLogId}/edit` : `/assets/${ASSET_ID}/entries/new`;
|
|
const resp = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(data)
|
|
});
|
|
if (resp.ok) { closeModal('log-modal'); location.reload(); }
|
|
else { const err = await resp.json(); alert(err.error || 'Save failed'); }
|
|
}
|
|
|
|
function filterAssetLogs() {
|
|
const query = (document.getElementById('ad-lq')?.value || '').trim().toLowerCase();
|
|
const type = document.getElementById('ad-lf')?.value || '';
|
|
document.querySelectorAll('[data-log-row]').forEach((row) => {
|
|
const matchesType = !type || row.dataset.logType === type;
|
|
const matchesQuery = !query || (row.dataset.logText || '').includes(query);
|
|
row.style.display = matchesType && matchesQuery ? '' : 'none';
|
|
});
|
|
}
|
|
document.getElementById('ad-lq')?.addEventListener('input', filterAssetLogs);
|
|
document.getElementById('ad-lf')?.addEventListener('change', filterAssetLogs);
|
|
</script>
|
|
{% endblock %}
|