Files
shopdb-flask/plugins/warranty/plugin.py
Cam Proudlock 870e068c44 Rebuild the dashboard around what needs a person
The board was one card per source in a fixed grid: every source got the same
small box whatever it had to say (warranties showed 5 of 50), and a box per
source flattened priority, while the largest type on the page belonged to counts
that need nobody.

Now a severity rollup, one worst-first queue of everything needing a person, and
a row of context beneath. The queue is full width, capped per source so a noisy
check cannot bury a rare one, and names what did not fit with a link to it. Chips
filter the queue by check as well as by severity; both narrow what is already
loaded, so the counts and rows cannot disagree.

Dropped: an 'All assets' tile shown beside its own four addends, a printer strip
carrying the same count a third time, three cards whose job was to display 0, and
a Recent Devices table listing five machines all 'In Use'.

Contract 0.26.0 adds optional 'shortname' on dashboard cards - a title is written
as a heading and ran wider than the hostname it qualified as a row badge.

Also: the supplies card reported 'Low Supplies 0' when Zabbix was unreachable,
which reads as reassurance rather than the truth that nothing could see the
cartridges. It now distinguishes unreachable (says so) from unconfigured (stays
quiet - the site declined the feature) from a real zero.

Full suite 2182 passed, frontend 314 passed.
2026-08-29 21:35:03 -04:00

179 lines
6.6 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',
'permission': 'warranty.view',
'position': 9,
},
]
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',
'permission': 'warranty.view',
},
]
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',
'shortname': 'Warranty',
'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})")