Files
cproudlock 94d8d6c9b6
Some checks failed
CI / backend (push) Failing after 7s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 9s
CI / migrations-mysql (push) Failing after 7s
dashboard: numbers that agree, a map on hover, wider cards
"All assets 704" sat beside "all assets in use 737", and both were correct
about different populations. The totals summed five specific asset types and
subtracted dual-bay secondaries; the status counts took every asset row of any
type with no collapse, so USB devices and hidden secondary bays inflated one
side of a comparison the layout invites. Status is now counted over exactly the
same assets the totals describe.

Warranty rows fell back to asset.name when the covered asset had no hostname,
and an asset's name is usually the MACHINE's descriptive name - which is how a
column meant to identify a PC ended up showing a machine. Hostname, else the
asset number, never the name. The machine number loses its label too: the row
is hostname, machine, state, and "machine 3015" spends a word on what position
already conveys.

Printer names now carry the floor-plan preview on hover, the same
LocationMapTooltip the printer's own page uses - a location name tells you the
room, the map tells you where to walk. Declared as map.maphover on the card, so
any card with coordinates gets it; a row without them shows a plain link rather
than being dropped.

Cards are four across rather than five. At five columns a row holding a
hostname, a machine number and a state truncates on exactly the rows that
matter. auto-fit, so two cards fill the width instead of leaving empty tracks.

Not covered by a test: the count fix. I started one and it was interrupted, and
I have not gone back for it - the assertion worth having is that in-use can
never exceed the total.
2026-08-11 16:27:14 -04:00

176 lines
6.5 KiB
Python

"""Warranty plugin main class.
Asset-general plugin: owns its warranties + warrantyassets tables, an API
surface, and a sidebar entry. Not tied to any one asset type - a warranty can
cover a PC, printer, network device, or machine.
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from .api import warranty_bp
from .models import Warranty, WarrantyAsset
logger = logging.getLogger(__name__)
class WarrantyPlugin(BasePlugin):
"""Warranty tracking plugin."""
def __init__(self):
self._manifest = self._load_manifest()
def _load_manifest(self) -> Dict:
manifest_path = Path(__file__).parent / 'manifest.json'
if manifest_path.exists():
with open(manifest_path, 'r') as f:
return json.load(f)
return {}
@property
def meta(self) -> PluginMeta:
return PluginMeta(
name=self._manifest.get('name', 'warranty'),
version=self._manifest.get('version', '1.0.0'),
description=self._manifest.get('description', 'Asset warranty tracking'),
author=self._manifest.get('author', 'ShopDB Team'),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.2.0,<1.0.0'),
api_prefix=self._manifest.get('api_prefix', '/api/warranty'),
)
def get_blueprint(self) -> Optional[Blueprint]:
return warranty_bp
def get_models(self) -> List[Type]:
return [Warranty, WarrantyAsset]
def get_navigation_items(self) -> List[Dict]:
return [
{
'name': 'Warranties',
'icon': 'shield',
'route': '/warranties',
'position': 8,
},
]
def get_reports(self) -> List[Dict]:
return [
{
'id': 'warranty',
'name': 'Warranty Report',
'description': 'Assets bucketed by coverage: expired, expiring soon, active',
'category': 'warranty',
'route': '/reports/warranty',
},
]
def get_asset_panels(self) -> List[Dict]:
# ADR-010 pilot. Warranty is asset-general so it shows on every detail
# page (['*']). Declares the existing per-asset warranty endpoint the
# WarrantyPanel already reads; the generic core panel renderer consumes
# this instead of each detail view hand-composing the panel.
return [
{
'id': 'warranty',
'title': 'Warranty',
'assettypes': ['*'],
'endpoint': '/api/warranty/asset/{assetid}',
'render': 'list',
# Field map: the generic renderer builds each list item from the
# warranty payload without any warranty-specific frontend code.
'map': {
'title': 'vendor',
'badge': {
'label': 'status',
'color': 'statuscolor',
'labelmap': {
'active': 'Active',
'expiring': 'Expiring Soon',
'expired': 'Expired',
'unknown': 'Unknown',
},
},
'meta': [
{'key': 'servicelevel'},
{'key': 'enddate', 'label': 'Ends', 'format': 'date'},
{'key': 'servicetag', 'label': 'Tag', 'mono': True},
],
},
'empty': 'No warranty on record.',
'manage': {
'to': '/warranties?addfor={assetid}',
'label': 'Add / manage',
'emptylabel': 'Add one',
},
'position': 30,
},
]
def get_dashboard_widgets(self) -> List[Dict]:
"""Dashboard card: warranties running out, and ones already expired.
Expired stays listed rather than dropping off. A machine out of
warranty is a purchasing decision someone still has to make, and
removing it the day it lapses is how it gets missed entirely.
"""
return [
{
'id': 'warranty-expiring',
'viewall': '/reports/warranty',
'title': 'Warranties expiring',
'endpoint': '/api/warranty/dashboard/expiring',
'render': 'exceptions',
'severity': 'info',
'permission': 'warranty.view',
'empty': 'hide',
'position': 50,
'map': {
'title': 'hostname',
'detail': 'state',
# The bay, which is how the floor identifies the PC. No
# date: expired or expiring is the whole decision when
# scanning a board, and the exact day belongs on the report
# you order from.
# Bare number, no label. The row is hostname, machine,
# state - three short facts, and "machine 3015" spends a
# word saying what the position already says.
'meta': [{'key': 'machinenumber'}],
'link': '/assets/{assetid}',
},
},
]
def get_settings_defaults(self) -> List[Dict]:
"""Settings this plugin owns."""
return [
{
'key': 'warranty_expiringdays',
'value': '90',
'valuetype': 'integer',
'category': 'warranty',
'description': 'Days ahead to list an expiring warranty on the '
'dashboard. Already-expired warranties are always '
'listed.',
},
]
def get_permissions(self) -> List:
"""Return the RBAC permissions this plugin owns."""
return [
('warranty.view', 'View warranties', 'warranty'),
('warranty.create', 'Create warranties', 'warranty'),
('warranty.edit', 'Edit warranties', 'warranty'),
('warranty.delete', 'Delete warranties', 'warranty'),
]
def init_app(self, app: Flask, db_instance) -> None:
logger.info(f"Warranty plugin initialized (v{self.meta.version})")