Files
gandalf/templates/cutsheet.html
jaredandClaude Opus 5.5 a9de175d90
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (flake8) (push) Successful in 1m0s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m9s
Lint / Python (flake8) (pull_request) Successful in 57s
Test / Python Tests (pytest) (push) Successful in 1m14s
Lint / JS (eslint) (pull_request) Successful in 8s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 56s
Test / Python Tests (pytest) (pull_request) Successful in 1m5s
Rack: ports-facing separate from posts, three views, ZimaBoard 832 shelves to scale
Gear on the rear posts usually still faces the front (the StarTech's RPDU
and ZimaBoard shelf), so "which posts" and "which way the ports face" are
now separate fields (mount, facing). The page has three views: Front (front
posts), Rear posts (rear-post gear seen from the front, i.e. what is behind
the front gear) and Rear (from behind). An item seen from the side it
doesn't face shows its back. Facing defaults to front, so existing items
keep their meaning.

Shelves are drawn at true scale with their contents ordered by a new
"Position on shelf". The IceWhale ZimaBoard 832 is traced from IceWhale's
labelled photo of its port end (ZimaDocs): Mini-DP, two RJ45-over-USB 3.0
stacks with link/activity LEDs, DC jack, gunmetal case, gold PCB edge and
black base. Its RJ45 link lights follow the host's physical NICs and the
status glow follows the host.

New cutsheet_items columns facing and slot, added on first use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 00:12:07 -05:00

383 lines
18 KiB
HTML

{% extends "base.html" %}
{% block title %}Cutsheet{% 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>
{% 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>
<p class="g-section-more"><a href="/rack">Open the rack drawing →</a></p>
</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>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-device">Gandalf device</label>
<input class="lt-input" id="cs-item-device" name="device" maxlength="255" list="cs-known-devices"
placeholder="same as the name">
<datalist id="cs-known-devices"></datalist>
<div class="lt-field-hint">Which live device this is, when the name above is different (e.g. name "UDM-Pro", device "Dream Machine Pro"). Its faceplate, ports and status then show on the rack.</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-chassis">Front drawing</label>
<select class="lt-select" id="cs-item-chassis" name="chassis"><option value="">Automatic (faceplate or plain plate)</option></select>
<div class="lt-field-hint">For gear Gandalf has no live faceplate for: the NVR, or a server's case.</div>
</div>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-item-rack">Rack</label>
<input class="lt-input" id="cs-item-rack" name="rack" maxlength="100" list="cs-rack-list" placeholder="first rack">
<datalist id="cs-rack-list"></datalist>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-mount">Mounted on</label>
<select class="lt-select" id="cs-item-mount" name="mount">
<option value="front">Front posts</option>
<option value="rear">Rear posts</option>
<option value="full">Both (full depth)</option>
</select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-facing">Ports face</label>
<select class="lt-select" id="cs-item-facing" name="facing">
<option value="front">Front</option>
<option value="rear">Rear</option>
</select>
</div>
</div>
<div class="form-row">
<div class="lt-form-group">
<label class="lt-label" for="cs-item-shelf">On shelf</label>
<select class="lt-select" id="cs-item-shelf" name="shelf"><option value="">Not on a shelf</option></select>
</div>
<div class="lt-form-group">
<label class="lt-label" for="cs-item-slot">Position on shelf</label>
<input class="lt-input" id="cs-item-slot" name="slot" type="number" min="1" max="99" placeholder="1 = leftmost">
</div>
</div>
<div class="lt-field-hint cs-shelf-hint">Gear on a shelf (mini PCs, desk switches) takes its place and side from the shelf: leave its rack unit empty. On a 4-post rack a front and a rear item can share a unit; a full-depth one (a server) takes it on both sides.</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 %}