Files
gandalf/templates/cutsheet.html
T
jaredandClaude Opus 5.5 52cfea5439
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
Cutsheet: show the next free cable ID as a hint, not a placeholder
A placeholder of C01 in the empty ID field looked like a real value.

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

312 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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">&#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 %}