Lint / JS (eslint) (push) Successful in 7s
Lint / Python (flake8) (push) Successful in 48s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Lint / Python (flake8) (pull_request) Successful in 55s
Lint / JS (eslint) (pull_request) Successful in 14s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / Python Security (bandit) (pull_request) Successful in 1m45s
Security / Python Security (bandit) (push) Successful in 1m22s
Test / Python Tests (pytest) (pull_request) Successful in 1m6s
Test / Python Tests (pytest) (push) Successful in 1m0s
A placeholder of C01 in the empty ID field looked like a real value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
312 lines
14 KiB
HTML
312 lines
14 KiB
HTML
{% 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>
|
||
<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">✕</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 %}
|