Files
gandalf/templates/cutsheet.html
jaredandClaude Opus 5.5 c262e5ecc8
Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 54s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 1m1s
Lint / JS (eslint) (pull_request) Successful in 19s
Test / Python Tests (pytest) (push) Successful in 2m6s
Security / Python Security (bandit) (pull_request) Successful in 1m18s
Lint / Python (flake8) (pull_request) Successful in 53s
Test / Python Tests (pytest) (pull_request) Successful in 1m1s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Inventory links, cutsheet marks on faceplates, and single page titles
Device pages get an Inventory button: it opens the device's HomeBox record
once an admin pastes its URL (stored in the new device_links table; only
URLs on inventory.base_url are accepted), and searches the inventory for
the model until then. Tickets link the record too.

Faceplate ports with a documented cable get a corner dot: green matches,
red mismatch, grey documented but unconfirmed. The port's tooltip names
the cable, and a legend toggle (remembered per browser) hides them.

Page titles no longer repeat GANDALF ("Alert History - GANDALF - GANDALF").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 20:47:14 -05:00

338 lines
16 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>
</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 %}