Merge remote-tracking branch 'origin/main' into wilm

# Conflicts:
#	shopdb/__init__.py
This commit is contained in:
benjamin.baker
2026-08-28 09:30:35 -04:00
112 changed files with 5502 additions and 604 deletions

View File

@@ -47,7 +47,7 @@ def _upsert_primary_ip(asset, ip):
# =============================================================================
@network_bp.route('/types', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_network_device_types():
"""List all network device types."""
page, per_page = get_pagination_params(request)
@@ -69,7 +69,7 @@ def list_network_device_types():
@network_bp.route('/types/<int:type_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device_type(type_id: int):
"""Get a single network device type."""
t = db.session.get(NetworkDeviceType, type_id)
@@ -85,7 +85,6 @@ def get_network_device_type(type_id: int):
@network_bp.route('/types', methods=['POST'])
@jwt_required()
@require_permission('network.create')
def create_network_device_type():
"""Create a new network device type."""
@@ -123,7 +122,6 @@ def create_network_device_type():
@network_bp.route('/types/<int:type_id>', methods=['PUT'])
@jwt_required()
@require_permission('network.edit')
def update_network_device_type(type_id: int):
"""Update a network device type."""
@@ -158,7 +156,6 @@ def update_network_device_type(type_id: int):
@network_bp.route('/types/<int:type_id>', methods=['DELETE'])
@jwt_required()
@require_permission('network.delete')
def delete_network_device_type(type_id: int):
"""Delete a network device type. Refused if any device still uses it."""
@@ -179,7 +176,7 @@ def delete_network_device_type(type_id: int):
# =============================================================================
@network_bp.route('', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_network_devices():
"""
List all network devices with filtering and pagination.
@@ -287,7 +284,7 @@ def list_network_devices():
@network_bp.route('/<int:device_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device(device_id: int):
"""Get a single network device with full details."""
netdev = db.session.get(NetworkDevice, device_id)
@@ -307,7 +304,7 @@ def get_network_device(device_id: int):
@network_bp.route('/by-asset/<int:asset_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device_by_asset(asset_id: int):
"""Get network device data by asset ID."""
netdev = NetworkDevice.query.filter_by(assetid=asset_id).first()
@@ -327,7 +324,7 @@ def get_network_device_by_asset(asset_id: int):
@network_bp.route('/by-hostname/<hostname>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_network_device_by_hostname(hostname: str):
"""Get network device by hostname."""
netdev = NetworkDevice.query.filter_by(hostname=hostname).first()
@@ -347,7 +344,6 @@ def get_network_device_by_hostname(hostname: str):
@network_bp.route('', methods=['POST'])
@jwt_required()
@require_permission('network.create')
def create_network_device():
"""
@@ -464,7 +460,6 @@ def create_network_device():
@network_bp.route('/<int:device_id>', methods=['PUT'])
@jwt_required()
@require_permission('network.edit')
def update_network_device(device_id: int):
"""Update network device (both Asset and NetworkDevice records)."""
@@ -559,7 +554,6 @@ def update_network_device(device_id: int):
@network_bp.route('/<int:device_id>', methods=['DELETE'])
@jwt_required()
@require_permission('network.delete')
def delete_network_device(device_id: int):
"""Delete (soft delete) network device."""
@@ -589,7 +583,7 @@ def delete_network_device(device_id: int):
# =============================================================================
@network_bp.route('/dashboard/summary', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def dashboard_summary():
"""Get network device dashboard summary data."""
# Total active network devices
@@ -637,7 +631,7 @@ def dashboard_summary():
# =============================================================================
@network_bp.route('/vlans', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_vlans():
"""List all VLANs with filtering and pagination."""
page, per_page = get_pagination_params(request)
@@ -670,7 +664,7 @@ def list_vlans():
@network_bp.route('/vlans/<int:vlan_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_vlan(vlan_id: int):
"""Get a single VLAN with its subnets."""
vlan = db.session.get(VLAN, vlan_id)
@@ -690,7 +684,6 @@ def get_vlan(vlan_id: int):
@network_bp.route('/vlans', methods=['POST'])
@jwt_required()
@require_permission('network.create')
def create_vlan():
"""Create a new VLAN."""
@@ -732,7 +725,6 @@ def create_vlan():
@network_bp.route('/vlans/<int:vlan_id>', methods=['PUT'])
@jwt_required()
@require_permission('network.edit')
def update_vlan(vlan_id: int):
"""Update a VLAN."""
@@ -778,7 +770,6 @@ def update_vlan(vlan_id: int):
@network_bp.route('/vlans/<int:vlan_id>', methods=['DELETE'])
@jwt_required()
@require_permission('network.delete')
def delete_vlan(vlan_id: int):
"""Delete (soft delete) a VLAN."""
@@ -815,7 +806,7 @@ def delete_vlan(vlan_id: int):
# =============================================================================
@network_bp.route('/subnets', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def list_subnets():
"""List all subnets with filtering and pagination."""
page, per_page = get_pagination_params(request)
@@ -856,7 +847,7 @@ def list_subnets():
@network_bp.route('/subnets/<int:subnet_id>', methods=['GET'])
@jwt_required(optional=True)
@require_permission('network.view')
def get_subnet(subnet_id: int):
"""Get a single subnet plus the network devices whose primary IP falls in
its CIDR range."""
@@ -932,7 +923,6 @@ def _asset_detail_url(assettype, assetid):
@network_bp.route('/subnets', methods=['POST'])
@jwt_required()
@require_permission('network.create')
def create_subnet():
"""Create a new subnet."""
@@ -998,7 +988,6 @@ def create_subnet():
@network_bp.route('/subnets/<int:subnet_id>', methods=['PUT'])
@jwt_required()
@require_permission('network.edit')
def update_subnet(subnet_id: int):
"""Update a subnet."""
@@ -1049,7 +1038,6 @@ def update_subnet(subnet_id: int):
@network_bp.route('/subnets/<int:subnet_id>', methods=['DELETE'])
@jwt_required()
@require_permission('network.delete')
def delete_subnet(subnet_id: int):
"""Delete (soft delete) a subnet."""

View File

@@ -1,5 +1,25 @@
<template>
<div class="detail-page" v-if="device">
<!--
The same page-header + header-actions block every other asset detail page
uses. This page had no header at all: Edit was tucked inside the hero
title row and there was no Print Label, no Retire and no way back to the
list, so managing a network device meant looking somewhere different from
every other asset type.
-->
<div class="page-header">
<h2>Network Device Details</h2>
<div class="header-actions">
<router-link :to="`/print/asset-label/network_device/${deviceId}`"
class="btn btn-secondary" target="_blank">Print Label</router-link>
<router-link v-if="authStore.isAuthenticated" :to="`/network/${deviceId}/edit`"
class="btn btn-primary">Edit</router-link>
<button v-if="authStore.isAuthenticated" @click="confirmRetire"
class="btn btn-danger">Retire</button>
<router-link to="/network" class="btn btn-secondary">Back to List</router-link>
</div>
</div>
<div class="hero-card">
<div class="hero-image">
<img v-if="device.networkdevice?.imageurl" :src="withBase(device.networkdevice.imageurl)" alt="Model photo" />
@@ -10,13 +30,6 @@
<div class="hero-content">
<div class="hero-title-row">
<h1 class="hero-title">{{ device.networkdevice?.hostname || device.name || device.assetnumber }}</h1>
<router-link
v-if="authStore.isAuthenticated"
:to="`/network/${deviceId}/edit`"
class="btn btn-secondary"
>
Edit
</router-link>
</div>
<div class="hero-meta">
<span class="badge" :style="colorStyle(device.statuscolor)">
@@ -184,18 +197,9 @@
</div>
</div>
<!-- Actions -->
<div class="action-bar" v-if="authStore.isAuthenticated">
<router-link :to="`/print/asset-label/network_device/${deviceId}`" class="btn btn-secondary" target="_blank">
Print Label
</router-link>
<router-link :to="`/network/${deviceId}/edit`" class="btn btn-primary">
Edit Device
</router-link>
<button @click="confirmDelete" class="btn btn-danger">
Delete Device
</button>
</div>
<!-- The manage actions live in the page header, where every other asset
detail page puts them. They used to sit down here instead, which is
why finding them on a network device meant looking somewhere else. -->
<!-- Audit Footer -->
<div class="audit-footer">
@@ -228,6 +232,7 @@ import PluginAssetPanels from '@/components/PluginAssetPanels.vue'
import { useWarrantyBadge } from '@/composables/warrantyBadge'
import { useIdentifierFlags } from '@/composables/identifierSettings'
import { useToast } from '@/composables/toast'
import { apiError } from '@/utils/apiError'
import { setRecordTitle } from '@/composables/pageTitle'
const toast = useToast()
@@ -293,17 +298,24 @@ function formatDate(dateStr) {
})
}
async function confirmDelete() {
if (confirm(`Are you sure you want to delete ${device.value.networkdevice?.hostname || device.value.assetnumber}?`)) {
try {
await networkApi.delete(deviceId)
router.push('/network')
} catch (error) {
console.error('Error deleting device:', error)
toast.error('Failed to delete device')
}
// Soft delete, like every other asset type: the record leaves the lists while
// its history and relationships stay put, and it can be restored by setting
// isactive back. Called Retire rather than Delete for exactly that reason.
async function confirmRetire () {
const label = device.value?.networkdevice?.hostname
|| device.value?.name || device.value?.assetnumber || 'this device'
if (!confirm(`Retire ${label}?\n\nIt will no longer appear in ShopDB. `
+ `History and relationships are kept, and it can be restored.`)) return
try {
await networkApi.delete(deviceId)
toast.success(`Retired ${label}`)
router.push('/network')
} catch (error) {
console.error('Error retiring network device:', error)
toast.error(apiError(error, 'Failed to retire network device'))
}
}
</script>
<style scoped>

View File

@@ -207,7 +207,9 @@ class NetworkPlugin(BasePlugin):
'name': 'Network',
'icon': 'network-wired',
'route': '/network',
'permission': 'network.view',
'position': 18,
'section': 'assets',
},
]