Files

338 lines
16 KiB
HTML
Raw Permalink Normal View History

{% extends "base.html" %}
{% block title %}Cutsheet – GANDALF{% endblock %}
{% block content %}
<div class="lt-page-header">
<div>
<h1 class="lt-page-title">Cutsheet</h1>
<p class="g-page-sub">Where every cable is meant to go, checked against what Gandalf sees on the network.</p>
</div>
<div class="cs-actions no-print">
{% if can_edit %}
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm" data-action="new-cable">+ Cable</button>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="new-item">+ Panel / item</button>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-modal-open="cs-import-modal">Import CSV</button>
{% endif %}
<a class="lt-btn lt-btn-secondary lt-btn-sm" href="/api/cutsheet/cables.csv">Export CSV</a>
</div>
</div>
<div id="cs-summary" class="cs-summary" aria-live="polite"></div>
<section class="g-section" id="cs-cables-section">
<div class="g-section-header">
<h2 class="g-section-title">Cables</h2>
<span class="g-section-badge" id="cs-cable-count">0</span>
</div>
<div class="cs-filters no-print">
<input type="search" class="lt-input" id="cs-filter" placeholder="Filter by label, device, port or note" aria-label="Filter cables">
<select class="lt-select" id="cs-status-filter" aria-label="Filter by status">
<option value="">All statuses</option>
<option value="installed">Installed</option>
<option value="planned">Planned</option>
<option value="dark">Dark</option>
<option value="retired">Retired</option>
</select>
<label class="cs-check"><input type="checkbox" id="cs-problems-only"> Problems only</label>
</div>
<div id="cs-cables"></div>
</section>
<section class="g-section no-print" id="cs-suggest-section">
<div class="g-section-header">
<h2 class="g-section-title">Undocumented links</h2>
<span class="g-section-badge" id="cs-suggest-count">0</span>
</div>
<p class="cs-hint">Switch ports with a live link that no cable record mentions. Adding one pre-fills what Gandalf already knows; insert the patch panel hop if the cable runs through one.</p>
<div id="cs-suggestions"></div>
</section>
<section class="g-section" id="cs-panels-section">
<div class="g-section-header">
<h2 class="g-section-title">Patch panels</h2>
</div>
2026-11-10 16:54:57 -05:00
{% if can_edit %}
<div class="lt-frame cs-trace no-print" id="cs-trace">
<span class="lt-frame-bl">&#x255A;</span><span class="lt-frame-br">&#x255D;</span>
<div class="lt-section-header">Trace a cord</div>
<div class="lt-section-body">
<p class="cs-hint">Find which switch port a panel port's cord goes to: start, then unplug that cord at the panel.
Gandalf watches every switch port and shows the one that drops. Plug it back in afterwards. The device on it
loses its link for those few seconds.</p>
<div class="cs-trace-form">
<div class="lt-form-group">
<label class="lt-label" for="cs-trace-panel">Panel</label>
<select class="lt-select" id="cs-trace-panel"></select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-trace-port">Port</label>
<input class="lt-input" id="cs-trace-port" type="number" min="1" max="512" value="1">
</div>
<div class="lt-form-group cs-trace-buttons">
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm" data-action="trace-start">Start</button>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="trace-stop" hidden>Stop</button>
</div>
</div>
<div id="cs-trace-status" class="cs-trace-status" aria-live="polite"></div>
</div>
</div>
{% endif %}
<div id="cs-panels"></div>
</section>
<section class="g-section" id="cs-labels-section">
<div class="g-section-header">
<h2 class="g-section-title">Labels to make</h2>
<span class="g-section-badge" id="cs-label-count">0</span>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm no-print" data-action="print-labels">Print list</button>
</div>
<p class="cs-hint">Type these on the label maker. Cable IDs go on both ends of the cord; panel labels go on the port. Tick each one once it's stuck on.</p>
<div id="cs-labels"></div>
</section>
<section class="g-section" id="cs-rack-section">
<div class="g-section-header">
<h2 class="g-section-title">Rack</h2>
</div>
<div id="cs-rack"></div>
</section>
<section class="g-section" id="cs-items-section">
<div class="g-section-header">
<h2 class="g-section-title">Inventory</h2>
<span class="g-section-badge" id="cs-item-count">0</span>
</div>
<p class="cs-hint">Things Gandalf can't discover: patch panels, wall jacks, shelves, and rack positions for devices.</p>
<div id="cs-items"></div>
</section>
<section class="g-section no-print" id="cs-history-section">
<div class="g-section-header">
<h2 class="g-section-title">History</h2>
</div>
<div id="cs-history"></div>
</section>
<datalist id="cs-device-list"></datalist>
<datalist id="cs-a-port-list"></datalist>
<datalist id="cs-b-port-list"></datalist>
{% if can_edit %}
<!-- Cable editor -->
<div id="cs-cable-modal" class="lt-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="cs-cable-title" aria-hidden="true">
<div class="lt-modal cs-modal-wide">
<div class="lt-modal-header">
<h3 class="lt-modal-title" id="cs-cable-title">Cable</h3>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="cs-cable-form" autocomplete="off">
<div class="lt-modal-body">
<input type="hidden" name="id">
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-label">Cable ID</label>
<input class="lt-input" id="cs-label" name="label" maxlength="32">
<div class="lt-field-hint">Blank if the cable has no ID yet. Next free:
<span id="cs-next-label"></span> <button type="button" class="cs-link-btn" data-action="use-next-label">use</button></div>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-status">Status</label>
<select class="lt-select" id="cs-status" name="status">
<option value="installed">Installed</option>
<option value="planned">Planned</option>
<option value="dark">Dark (run, not in use)</option>
<option value="retired">Retired</option>
</select>
</div>
</div>
<fieldset class="cs-end">
<legend>From</legend>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-a-device">Device or item <span class="required">*</span></label>
<input class="lt-input" id="cs-a-device" name="a_device" list="cs-device-list" required maxlength="100">
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-a-port">Port</label>
<input class="lt-input" id="cs-a-port" name="a_port" list="cs-a-port-list" maxlength="64">
</div>
<div class="lt-form-group cs-side" data-end="a">
<label class="lt-label" for="cs-a-side">Side</label>
<select class="lt-select" id="cs-a-side" name="a_side">
<option value="front">Front</option>
<option value="rear">Rear</option>
</select>
</div>
</div>
</fieldset>
<fieldset class="cs-end">
<legend>To</legend>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-b-device">Device or item</label>
<input class="lt-input" id="cs-b-device" name="b_device" list="cs-device-list" maxlength="100"
placeholder="blank = unterminated">
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-b-port">Port</label>
<input class="lt-input" id="cs-b-port" name="b_port" list="cs-b-port-list" maxlength="64">
</div>
<div class="lt-form-group cs-side" data-end="b">
<label class="lt-label" for="cs-b-side">Side</label>
<select class="lt-select" id="cs-b-side" name="b_side">
<option value="front">Front</option>
<option value="rear">Rear</option>
</select>
</div>
</div>
</fieldset>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-type">Type</label>
<select class="lt-select" id="cs-type" name="cable_type"></select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-length">Length (m)</label>
<input class="lt-input" id="cs-length" name="length_m" type="number" min="0.01" max="999" step="0.01">
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-color">Color</label>
<input class="lt-input" id="cs-color" name="color" maxlength="32">
</div>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-notes">Notes</label>
<textarea class="lt-input" id="cs-notes" name="notes" rows="2" maxlength="2000"></textarea>
</div>
<label class="cs-check"><input type="checkbox" name="label_applied"> Label is on the cable</label>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-danger cs-delete" data-action="delete-cable">Delete</button>
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
<button type="submit" class="lt-btn lt-btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- Item editor -->
<div id="cs-item-modal" class="lt-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="cs-item-title" aria-hidden="true">
<div class="lt-modal">
<div class="lt-modal-header">
<h3 class="lt-modal-title" id="cs-item-title">Item</h3>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="cs-item-form" autocomplete="off">
<div class="lt-modal-body">
<input type="hidden" name="id">
<div class="lt-form-group">
<label class="lt-label" for="cs-item-name">Name <span class="required">*</span></label>
<input class="lt-input" id="cs-item-name" name="name" required maxlength="100" list="cs-device-list">
<div class="lt-field-hint">For a device's rack position, use its exact Gandalf name.</div>
</div>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-item-kind">Kind</label>
<select class="lt-select" id="cs-item-kind" name="kind">
<option value="patch_panel">Patch panel</option>
<option value="wall_jack">Wall jack</option>
<option value="device">Device</option>
<option value="shelf">Shelf</option>
<option value="other">Other</option>
</select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-ports">Ports</label>
<input class="lt-input" id="cs-item-ports" name="port_count" type="number" min="1" max="512">
</div>
</div>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-item-u">Rack unit (bottom)</label>
<input class="lt-input" id="cs-item-u" name="rack_u" type="number" min="1" max="60">
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-h">Height (U)</label>
<input class="lt-input" id="cs-item-h" name="rack_height" type="number" min="1" max="20">
</div>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-location">Location</label>
<input class="lt-input" id="cs-item-location" name="location" maxlength="255" placeholder="e.g. rack, office wall">
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-notes">Notes</label>
<textarea class="lt-input" id="cs-item-notes" name="notes" rows="2" maxlength="2000"></textarea>
</div>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-danger cs-delete" data-action="delete-item">Delete</button>
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
<button type="submit" class="lt-btn lt-btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- Panel port label -->
<div id="cs-port-modal" class="lt-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="cs-port-title" aria-hidden="true">
<div class="lt-modal">
<div class="lt-modal-header">
<h3 class="lt-modal-title" id="cs-port-title">Port label</h3>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="cs-port-form" autocomplete="off">
<div class="lt-modal-body">
<input type="hidden" name="item_id">
<input type="hidden" name="port">
<div class="lt-form-group">
<label class="lt-label" for="cs-port-label">Label</label>
<input class="lt-input" id="cs-port-label" name="label" maxlength="64" placeholder="e.g. GUEST BR">
<div class="lt-field-hint">Usually where the port's rear run goes. Blank removes it.</div>
</div>
<label class="cs-check"><input type="checkbox" name="applied"> Label is on the panel</label>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
<button type="submit" class="lt-btn lt-btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- CSV import -->
<div id="cs-import-modal" class="lt-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="cs-import-title" aria-hidden="true">
<div class="lt-modal cs-modal-wide">
<div class="lt-modal-header">
<h3 class="lt-modal-title" id="cs-import-title">Import cables from CSV</h3>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="cs-import-form">
<div class="lt-modal-body">
<p class="cs-hint">Same columns as Export CSV. Rows with a cable ID that already exists update that cable; other rows are added. If any row is invalid, nothing is imported.</p>
<div class="lt-form-group">
<label class="lt-label" for="cs-import-file">CSV file</label>
<input class="lt-input" id="cs-import-file" type="file" accept=".csv,text/csv">
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-import-text">Or paste CSV</label>
<textarea class="lt-input cs-mono" id="cs-import-text" name="csv" rows="8"></textarea>
</div>
<div id="cs-import-errors" class="cs-errors" role="alert"></div>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-secondary" data-modal-close>Cancel</button>
<button type="button" class="lt-btn lt-btn-secondary" data-action="import-check">Check</button>
<button type="submit" class="lt-btn lt-btn-primary">Import</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>const CS_CAN_EDIT = {{ 'true' if can_edit else 'false' }};</script>
<script src="{{ url_for('static', filename='cutsheet.js') }}"></script>
{% endblock %}