"""Read-only Gateway inventory lookup for reviewed asset creation.""" import asyncio import json from typing import Literal from urllib.parse import quote import aiohttp from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field from psycopg2.extras import Json from app.core.database import execute_query from app.core.config import settings from app.products.backend.router import _apigw_base_url, _apigw_headers router = APIRouter(prefix='/hardware-intake/inventory') def inventory_item(record): raw = record.get('json_data', record.get('hardware_data')) or {} if isinstance(raw, str): raw = json.loads(raw) hardware = raw.get('hardware') or {} return dict(id=record['id'], name=record.get('computer_name') or '', brand=record.get('manufacturer') or hardware.get('manufacturer') or '', model=record.get('model') or hardware.get('model') or '', serial_number=record.get('serial_number') or '', os=record.get('os_caption') or (raw.get('os') or {}).get('caption') or '', cpu=record.get('cpu_name') or (hardware.get('cpu') or {}).get('name') or '', ram_gb=record.get('ram_gb', hardware.get('ram_gb')), last_seen=record.get('last_seen') or record.get('collected_at'), hardware_specs={'gateway_inventory': {'id':record['id'], 'collected_at':record.get('last_seen') or record.get('collected_at'), 'data':raw, 'name':record.get('computer_name'), 'os':record.get('os_caption') or (raw.get('os') or {}).get('caption'), 'cpu':record.get('cpu_name') or (hardware.get('cpu') or {}).get('name'), 'ram_gb':record.get('ram_gb', hardware.get('ram_gb'))}}) @router.get('') async def search_inventory(q: str = Query('', max_length=160), by: Literal['name', 'serial'] = 'name', page: int = Query(1, ge=1)): path = '/api/inventory' params = {'per_page': 20, 'page': page} if by == 'serial': if not q.strip(): raise HTTPException(422, 'Indtast et serienummer') path += '/serial/' + quote(q.strip(), safe='') params = {} elif q.strip(): params['computer_name'] = q.strip() headers = _apigw_headers() try: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=settings.APIGW_TIMEOUT_SECONDS)) as session: async with session.get(_apigw_base_url() + path, headers=headers, params=params, allow_redirects=False) as response: if response.status == 404 and by == 'serial': record = await find_inventory_in_list(session, headers, serial=q.strip()) return {'items': [inventory_item(record)] if record else [], 'pagination': {}} if response.status != 200: raise HTTPException(502, 'API Gateway kunne ikke levere inventory. Kontrollér adgang og tilgængelighed.') payload = await response.json() records = [payload] if by == 'serial' else payload['data'] return {'items': [inventory_item(record) for record in records], 'pagination': payload.get('pagination', {})} except (aiohttp.ClientError, asyncio.TimeoutError, ValueError, KeyError, TypeError, AttributeError): raise HTTPException(502, 'Inventory kunne ikke læses fra API Gateway') from None class InventoryPairInput(BaseModel): inventory_id: int = Field(gt=0) async def find_inventory_in_list(session, headers, *, inventory_id=None, serial=None): """Some deployed Gateways expose the list but lack working detail routes.""" page = 1 while page <= 1000: async with session.get(f'{_apigw_base_url()}/api/inventory', headers=headers, params={'per_page': 100, 'page': page}, allow_redirects=False) as response: if response.status != 200: raise HTTPException(502, 'Inventory-listen kunne ikke hentes fra API Gateway') payload = await response.json() for record in payload['data']: if inventory_id is not None and str(record['id']) == str(inventory_id): return record if serial is not None and str(record.get('serial_number') or '').strip().casefold() == serial.casefold(): return record total_pages = int(payload['pagination']['total_pages']) if page >= total_pages: return None page += 1 raise HTTPException(502, 'Inventory-listen er for stor til at gennemføre opslaget') async def fetch_inventory_record(inventory_id: int): headers = _apigw_headers() try: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=settings.APIGW_TIMEOUT_SECONDS)) as session: async with session.get(f'{_apigw_base_url()}/api/inventory/{inventory_id}', headers=headers, allow_redirects=False) as response: if response.status == 404: record = await find_inventory_in_list(session, headers, inventory_id=inventory_id) if record is None: raise HTTPException(404, 'Inventory-maskinen findes ikke længere. Søg igen for at opdatere listen.') return inventory_item(record) if response.status != 200: raise HTTPException(502, 'Inventory-maskinen kunne ikke hentes fra API Gateway') return inventory_item(await response.json()) except (aiohttp.ClientError, asyncio.TimeoutError, ValueError, KeyError, TypeError, AttributeError): raise HTTPException(502, 'Inventory kunne ikke læses fra API Gateway') from None @router.post('/{hardware_id}/pair') async def pair_inventory(hardware_id: int, payload: InventoryPairInput): if not execute_query('SELECT id FROM hardware_assets WHERE id=%s AND deleted_at IS NULL', (hardware_id,)): raise HTTPException(404, 'Hardware findes ikke') item = await fetch_inventory_record(payload.inventory_id) # Merge in PostgreSQL so concurrent updates and acquisition/ESET/UISP data survive. rows = execute_query("""UPDATE hardware_assets SET hardware_specs = COALESCE(hardware_specs, '{}'::jsonb) || %s::jsonb, updated_at = NOW() WHERE id=%s AND deleted_at IS NULL RETURNING id""", (Json(item['hardware_specs']), hardware_id)) if not rows: raise HTTPException(404, 'Hardware findes ikke') return {'hardware_id': hardware_id, 'inventory': item} @router.delete('/{hardware_id}/pair') async def unpair_inventory(hardware_id: int): rows = execute_query("""UPDATE hardware_assets SET hardware_specs = COALESCE(hardware_specs, '{}'::jsonb) - 'gateway_inventory', updated_at = NOW() WHERE id=%s AND deleted_at IS NULL RETURNING id""", (hardware_id,)) if not rows: raise HTTPException(404, 'Hardware findes ikke') return {'hardware_id': hardware_id, 'unpaired': True}