Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 50s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / Python Security (bandit) (push) Successful in 50s
Test / Python Tests (pytest) (push) Successful in 1m2s
Lint / Python (flake8) (pull_request) Successful in 52s
Lint / JS (eslint) (pull_request) Successful in 7s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m42s
Test / Python Tests (pytest) (pull_request) Successful in 2m2s
The rack front is drawn from the rack units on cutsheet items. Switches with a faceplate use it, with live port state; patch panels get a generic panel with port labels; everything else is a labelled plate. Each documented port shows its cutsheet state, and hovering a port lights up every port on the same cable run (through patch panels) and names the cables. Clicking opens the switch port or the cable. Overlapping items and items past the top of the rack are listed instead of drawn over another; where two claim the same top unit, the first one recorded wins. rack.units sets the height (default 42). /api/rack builds on the cutsheet view; cutsheet.rack_layout and cutsheet.port_marks hold the placement and per-port logic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
339 lines
16 KiB
HTML
339 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">╚</span><span class="lt-frame-br">╝</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">✕</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">✕</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">✕</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">✕</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 %}
|