bmc_hub/app/routers/anydesk.py
Christian bc504b9257 feat: Add subscription management functionality and AnyDesk API integration
- Implemented subscription creation, updating, and rendering in script_9.js.
- Added functions for handling subscription line items, product selection, and total calculations.
- Integrated AnyDesk API for session management in test_anydesk.py.
- Created REST client test requests for API endpoints in api.http.
- Developed a script to check ESET machine status and save details in tmp_check_eset_machine.py.
2026-03-30 07:50:15 +02:00

823 lines
30 KiB
Python

"""
AnyDesk Remote Support Router
REST API endpoints for managing remote support sessions
"""
import logging
import json
from uuid import uuid4
from typing import Optional
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from app.models.schemas import (
AnyDeskSessionCreate,
AnyDeskSession,
AnyDeskSessionDetail,
AnyDeskSessionHistory,
AnyDeskSessionWithWorklog
)
from app.services.anydesk import AnyDeskService
from app.core.database import execute_query
logger = logging.getLogger(__name__)
router = APIRouter()
anydesk_service = AnyDeskService()
# =====================================================
# Session Management Endpoints
# =====================================================
@router.post("/anydesk/start-session", response_model=AnyDeskSession, tags=["Remote Support"])
async def start_remote_session(session_data: AnyDeskSessionCreate):
"""
Start a new AnyDesk remote support session
- **customer_id**: Required - Customer to provide support to
- **contact_id**: Optional - Specific contact person
- **sag_id**: Optional - Link to case/ticket for time tracking
- **description**: Optional - Purpose of session
- **created_by_user_id**: Optional - User initiating session
Returns session details with access link for sharing with customer
"""
try:
logger.info(f"🔗 Starting AnyDesk session for customer {session_data.customer_id}")
# Verify customer exists
cust_query = "SELECT id FROM customers WHERE id = %s"
customer = execute_query(cust_query, (session_data.customer_id,))
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Verify contact exists if provided
if session_data.contact_id:
contact_query = "SELECT id FROM contacts WHERE id = %s"
contact = execute_query(contact_query, (session_data.contact_id,))
if not contact:
raise HTTPException(status_code=404, detail="Contact not found")
# Verify sag exists if provided
if session_data.sag_id:
sag_query = "SELECT id FROM sag_sager WHERE id = %s"
sag = execute_query(sag_query, (session_data.sag_id,))
if not sag:
raise HTTPException(status_code=404, detail="Case not found")
# Create session via AnyDesk service
result = await anydesk_service.create_session(
customer_id=session_data.customer_id,
contact_id=session_data.contact_id,
sag_id=session_data.sag_id,
description=session_data.description,
created_by_user_id=session_data.created_by_user_id
)
if "error" in result:
raise HTTPException(status_code=400, detail=result["error"])
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Error starting session: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/anydesk/sessions/{session_id}", response_model=AnyDeskSessionDetail, tags=["Remote Support"])
async def get_session_details(session_id: int):
"""
Get details of a specific AnyDesk session
Includes current status, duration, and linked entities (contact, customer, case)
"""
try:
query = """
SELECT
s.id, s.anydesk_session_id, s.customer_id, s.contact_id, s.sag_id,
s.session_link, s.status, s.started_at, s.ended_at, s.duration_minutes,
s.created_by_user_id, s.created_at, s.updated_at,
c.first_name || ' ' || c.last_name as contact_name,
cust.name as customer_name,
sag.titel as sag_title,
u.full_name as created_by_user_name,
s.device_info, s.metadata
FROM anydesk_sessions s
LEFT JOIN contacts c ON s.contact_id = c.id
LEFT JOIN customers cust ON s.customer_id = cust.id
LEFT JOIN sag_sager sag ON s.sag_id = sag.id
LEFT JOIN users u ON s.created_by_user_id = u.user_id
WHERE s.id = %s
"""
result = execute_query(query, (session_id,))
if not result:
raise HTTPException(status_code=404, detail="Session not found")
session = result[0]
return AnyDeskSessionDetail(**session)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching session: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/anydesk/sessions/{session_id}/end", tags=["Remote Support"])
async def end_remote_session(session_id: int):
"""
End a remote support session and calculate duration
Returns completed session with duration in minutes and hours
"""
try:
logger.info(f"🛑 Ending AnyDesk session {session_id}")
result = await anydesk_service.end_session(session_id)
if "error" in result:
raise HTTPException(status_code=400, detail=result["error"])
return JSONResponse(content=result)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error ending session: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/anydesk/sessions/{session_id}", tags=["Remote Support"])
async def update_session(session_id: int, data: dict):
"""
Update a session — assign/re-assign to a sag, contact, or add notes.
Accepted fields: sag_id, contact_id, customer_id, notes, status
"""
try:
allowed = {"sag_id", "contact_id", "customer_id", "notes", "status"}
updates = {k: v for k, v in data.items() if k in allowed}
if not updates:
raise HTTPException(status_code=400, detail="No valid fields to update")
# Verify session exists
existing = execute_query("SELECT id FROM anydesk_sessions WHERE id = %s", (session_id,))
if not existing:
raise HTTPException(status_code=404, detail="Session not found")
# Verify sag exists if provided
if "sag_id" in updates and updates["sag_id"] is not None:
sag = execute_query("SELECT id FROM sag_sager WHERE id = %s", (updates["sag_id"],))
if not sag:
raise HTTPException(status_code=404, detail="Case not found")
set_clauses = ", ".join([f"{k} = %s" for k in updates])
params = list(updates.values()) + [session_id]
query = f"""
UPDATE anydesk_sessions
SET {set_clauses}, updated_at = NOW()
WHERE id = %s
RETURNING id, anydesk_session_id, customer_id, contact_id, sag_id,
session_link, status, started_at, ended_at, duration_minutes,
created_by_user_id, created_at, updated_at
"""
result = execute_query(query, tuple(params))
if not result:
raise HTTPException(status_code=500, detail="Update failed")
logger.info(f"✅ Updated AnyDesk session {session_id}: {list(updates.keys())}")
return result[0]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating session: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/anydesk/register-manual-session", tags=["Remote Support"])
async def register_manual_session(data: dict):
"""
Register a manual AnyDesk support session directly on a case.
Expected payload:
- customer_id (required)
- sag_id (required)
- anydesk_id (required)
- assisted_device (required)
- device_type (required, e.g. placebo/desktop/server)
- contact_id (optional)
- notes (optional)
- created_by_user_id (optional)
"""
try:
customer_id = data.get("customer_id")
sag_id = data.get("sag_id")
contact_id = data.get("contact_id")
created_by_user_id = data.get("created_by_user_id")
anydesk_id = str(data.get("anydesk_id") or "").strip()
assisted_device = str(data.get("assisted_device") or "").strip()
device_type = str(data.get("device_type") or "placebo").strip().lower()
notes = str(data.get("notes") or "").strip()
if not customer_id:
raise HTTPException(status_code=400, detail="customer_id is required")
if not sag_id:
raise HTTPException(status_code=400, detail="sag_id is required")
if not anydesk_id:
raise HTTPException(status_code=400, detail="anydesk_id is required")
if not assisted_device:
raise HTTPException(status_code=400, detail="assisted_device is required")
customer = execute_query("SELECT id FROM customers WHERE id = %s", (customer_id,))
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
sag = execute_query("SELECT id FROM sag_sager WHERE id = %s AND deleted_at IS NULL", (sag_id,))
if not sag:
raise HTTPException(status_code=404, detail="Case not found")
if contact_id:
contact = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,))
if not contact:
raise HTTPException(status_code=404, detail="Contact not found")
manual_external_id = f"manual-{uuid4().hex[:12]}"
is_placeholder = device_type in {"placebo", "placeholder", "ukendt", "unknown"}
device_info = {
"to_id": anydesk_id,
"customer_machine_id": anydesk_id,
"assisted_device_name": assisted_device,
"assisted_device_type": device_type,
"is_placeholder_device": is_placeholder,
"source": "manual_case_registration"
}
metadata = {
"notes": notes,
"source": "manual_case_registration"
}
insert_q = """
INSERT INTO anydesk_sessions (
anydesk_session_id,
contact_id,
customer_id,
sag_id,
session_link,
device_info,
created_by_user_id,
started_at,
ended_at,
duration_minutes,
status,
metadata,
created_at,
updated_at
)
VALUES (
%s,
%s,
%s,
%s,
NULL,
%s::jsonb,
%s,
NOW(),
NOW(),
0,
'completed',
%s::jsonb,
NOW(),
NOW()
)
RETURNING id, anydesk_session_id, customer_id, contact_id, sag_id, status, started_at
"""
created = execute_query(
insert_q,
(
manual_external_id,
contact_id,
customer_id,
sag_id,
json.dumps(device_info),
created_by_user_id,
json.dumps(metadata),
),
)
if not created:
raise HTTPException(status_code=500, detail="Failed to register session")
comment_lines = [
f"🖥️ AnyDesk session registreret manuelt (AnyDesk ID: {anydesk_id})",
f"Enhed: {assisted_device}",
f"Type: {device_type}",
]
if notes:
comment_lines.append(f"Notat: {notes}")
if is_placeholder:
comment_lines.append("Info: Enhedstype er placeholder og kan linkes til hardware senere.")
execute_query(
"""
INSERT INTO sag_kommentarer (sag_id, forfatter, indhold, er_system_besked)
VALUES (%s, %s, %s, %s)
""",
(sag_id, "System", "\n".join(comment_lines), True),
)
logger.info("✅ Manual AnyDesk session registered for case %s", sag_id)
return {"ok": True, "session": created[0]}
except HTTPException:
raise
except Exception as e:
logger.error("Error registering manual AnyDesk session: %s", str(e))
raise HTTPException(status_code=500, detail=str(e))
@router.get("/anydesk/sessions", response_model=AnyDeskSessionHistory, tags=["Remote Support"])
async def get_session_history(
contact_id: Optional[int] = None,
customer_id: Optional[int] = None,
sag_id: Optional[int] = None,
limit: int = 50,
offset: int = 0
):
"""
Get session history filtered by contact, customer, or case
At least one filter parameter should be provided.
Results are paginated and sorted by date (newest first).
- **contact_id**: Get all sessions for a specific contact
- **customer_id**: Get all sessions for a company
- **sag_id**: Get sessions linked to a specific case
- **limit**: Number of sessions per page (default 50, max 100)
- **offset**: Pagination offset
"""
try:
if limit > 100:
limit = 100
result = await anydesk_service.get_session_history(
contact_id=contact_id,
customer_id=customer_id,
sag_id=sag_id,
limit=limit,
offset=offset
)
if "error" in result:
raise HTTPException(status_code=400, detail=result["error"])
# Enrich session data with contact/customer/user names
enriched_sessions = []
for session in result.get("sessions", []):
enriched_sessions.append(AnyDeskSessionDetail(**session))
return AnyDeskSessionHistory(
sessions=enriched_sessions,
total=result["total"],
limit=limit,
offset=offset
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching session history: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# =====================================================
# Worklog Integration Endpoints
# =====================================================
@router.get("/anydesk/sessions/{session_id}/worklog-suggestion", tags=["Remote Support", "Time Tracking"])
async def suggest_worklog_from_session(session_id: int):
"""
Get suggested worklog entry from a completed session
Calculates billable hours from session duration and provides
a pre-filled worklog suggestion for review/approval.
The worklog still needs to be created separately via the
timetracking/worklog endpoints after user approval.
"""
try:
# Get session
query = """
SELECT id, duration_minutes, customer_id, sag_id, contact_id,
started_at, ended_at, status
FROM anydesk_sessions
WHERE id = %s
"""
result = execute_query(query, (session_id,))
if not result:
raise HTTPException(status_code=404, detail="Session not found")
session = result[0]
if session["status"] != "completed":
raise HTTPException(
status_code=400,
detail=f"Cannot suggest worklog for non-completed session (status: {session['status']})"
)
if not session["duration_minutes"]:
raise HTTPException(
status_code=400,
detail="Session duration not calculated yet"
)
# Build worklog suggestion
duration_hours = round(session["duration_minutes"] / 60, 2)
suggestion = {
"session_id": session_id,
"duration_minutes": session["duration_minutes"],
"duration_hours": duration_hours,
"start_time": str(session["started_at"]),
"end_time": str(session["ended_at"]),
"description": f"Remote support session via AnyDesk",
"work_type": "remote_support",
"billable": True,
"linked_to": {
"customer_id": session["customer_id"],
"contact_id": session["contact_id"],
"sag_id": session["sag_id"]
}
}
logger.info(f"Generated worklog suggestion for session {session_id}: {duration_hours}h")
return JSONResponse(content=suggestion)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error generating worklog suggestion: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# =====================================================
# Status & Analytics Endpoints
# =====================================================
@router.get("/anydesk/stats", tags=["Remote Support", "Analytics"])
async def get_anydesk_stats():
"""
Get AnyDesk integration statistics
- Total sessions today/this week/this month
- Active sessions count
- Average session duration
- Most supported customers
"""
try:
stats = {
"sessions_today": 0,
"sessions_this_week": 0,
"sessions_this_month": 0,
"active_sessions": 0,
"average_duration_minutes": 0,
"total_support_hours": 0
}
# Get today's sessions
query = """
SELECT COUNT(*) as count FROM anydesk_sessions
WHERE DATE(started_at) = CURRENT_DATE
"""
result = execute_query(query)
stats["sessions_today"] = result[0]["count"] if result else 0
# Get this week's sessions
query = """
SELECT COUNT(*) as count FROM anydesk_sessions
WHERE started_at >= CURRENT_DATE - INTERVAL '7 days'
"""
result = execute_query(query)
stats["sessions_this_week"] = result[0]["count"] if result else 0
# Get this month's sessions
query = """
SELECT COUNT(*) as count FROM anydesk_sessions
WHERE started_at >= DATE_TRUNC('month', CURRENT_DATE)
"""
result = execute_query(query)
stats["sessions_this_month"] = result[0]["count"] if result else 0
# Get active sessions
query = """
SELECT COUNT(*) as count FROM anydesk_sessions
WHERE status = 'active'
"""
result = execute_query(query)
stats["active_sessions"] = result[0]["count"] if result else 0
# Get average duration
query = """
SELECT AVG(duration_minutes) as avg_duration FROM anydesk_sessions
WHERE status = 'completed' AND duration_minutes IS NOT NULL
"""
result = execute_query(query)
stats["average_duration_minutes"] = round(result[0]["avg_duration"], 1) if result and result[0]["avg_duration"] else 0
# Get total support hours this month
query = """
SELECT SUM(duration_minutes) as total_minutes FROM anydesk_sessions
WHERE status = 'completed'
AND started_at >= DATE_TRUNC('month', CURRENT_DATE)
"""
result = execute_query(query)
total_minutes = result[0]["total_minutes"] if result and result[0]["total_minutes"] else 0
stats["total_support_hours"] = round(total_minutes / 60, 2)
return JSONResponse(content=stats)
except Exception as e:
logger.error(f"Error calculating stats: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/anydesk/health", tags=["Remote Support", "Health"])
async def anydesk_health_check():
"""
Health check for AnyDesk integration
Returns configuration status, API connectivity, and last sync time
"""
creds = anydesk_service._get_credentials()
return JSONResponse(content={
"service": "AnyDesk Remote Support",
"status": "operational",
"configured": bool(creds["api_token"] and creds["license_id"]),
"dry_run_mode": creds["dry_run"],
"read_only_mode": creds["read_only"],
"auto_start_enabled": anydesk_service.auto_start
})
@router.post("/anydesk/fetch-from-api", tags=["Remote Support"])
async def fetch_sessions_from_anydesk(
days: int = 30,
limit: int = 1000,
after: Optional[str] = None,
before: Optional[str] = None,
):
"""
Pull session log from the live AnyDesk REST API and import into local DB.
- **days**: How many days back to fetch (default 30)
- **limit**: Max entries to fetch (default 1000)
- **after**: Override start as ISO-8601 timestamp (e.g. 2024-01-01T00:00:00Z)
- **before**: Override end as ISO-8601 timestamp
Requires `dry_run=false` in AnyDesk settings.
Auth uses HMAC-SHA1 (AnyDesk native format), not Bearer token.
Returns count of newly imported and updated sessions.
"""
try:
result = await anydesk_service.fetch_sessions_from_api(
days=days,
limit=limit,
after=after,
before=before,
)
if "error" in result:
raise HTTPException(status_code=400, detail=result["error"])
return JSONResponse(content=result)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching from AnyDesk API: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# =====================================================
# Sessions Dashboard Endpoints
# =====================================================
@router.get("/anydesk/sessions-overview", tags=["Remote Support"])
async def sessions_overview(
days: int = 90,
unregistered_only: bool = False,
limit: int = 200,
offset: int = 0,
):
"""
Enriched session list for the dashboard page.
Joins hardware_assets (via remote_id/anydesk_id), contacts, customers, sag.
"""
try:
where = "WHERE s.started_at >= NOW() - INTERVAL '%s days'" % int(days)
if unregistered_only:
where += " AND s.sag_id IS NULL AND s.contact_id IS NULL AND s.hardware_asset_id IS NULL"
query = f"""
SELECT
s.id,
s.anydesk_session_id,
s.started_at,
s.ended_at,
s.duration_minutes,
s.status,
s.notes,
-- linked hardware
s.hardware_asset_id,
ha.brand AS hw_brand,
ha.model AS hw_model,
ha.anydesk_id AS hw_anydesk_id,
ha.current_owner_customer_id AS hw_customer_id,
-- linked contact
s.contact_id,
c.first_name || ' ' || c.last_name AS contact_name,
c.email AS contact_email,
-- linked customer
s.customer_id,
cust.name AS customer_name,
-- linked sag
s.sag_id,
sag.titel AS sag_titel,
sag.status AS sag_status,
-- raw remote_id from import
(s.device_info->>'remote_id')::TEXT AS remote_id,
(s.device_info->>'remote_alias')::TEXT AS remote_alias,
(s.device_info->>'from_id')::TEXT AS technician_id,
(s.device_info->>'to_id')::TEXT AS customer_machine_id,
(s.device_info->>'local_alias')::TEXT AS customer_alias,
-- technician resolved from user_anydesk_ids
tech_u.user_id AS tech_user_id,
COALESCE(tech_u.full_name, tech_u.username) AS tech_name
FROM anydesk_sessions s
LEFT JOIN hardware_assets ha ON s.hardware_asset_id = ha.id
LEFT JOIN contacts c ON s.contact_id = c.id
LEFT JOIN customers cust ON s.customer_id = cust.id
LEFT JOIN sag_sager sag ON s.sag_id = sag.id
LEFT JOIN user_anydesk_ids uad
ON regexp_replace(COALESCE(uad.anydesk_id, ''), '[^0-9]', '', 'g') =
regexp_replace(COALESCE((s.device_info->>'from_id')::TEXT, ''), '[^0-9]', '', 'g')
LEFT JOIN users tech_u ON tech_u.user_id = uad.user_id
{where}
ORDER BY s.started_at DESC
LIMIT {int(limit)} OFFSET {int(offset)}
"""
rows = execute_query(query)
# count
count_q = f"SELECT COUNT(*) AS total FROM anydesk_sessions s {where}"
total = (execute_query(count_q) or [{"total": 0}])[0]["total"]
sessions = []
for r in (rows or []):
sessions.append({
"id": r["id"],
"anydesk_session_id": r["anydesk_session_id"],
"started_at": str(r["started_at"]) if r["started_at"] else None,
"ended_at": str(r["ended_at"]) if r["ended_at"] else None,
"duration_minutes": r["duration_minutes"],
"status": r["status"],
"notes": r["notes"],
"remote_id": r["remote_id"],
"remote_alias": r["remote_alias"],
"technician_id": r["technician_id"], # from.cid — teknikkerens maskine
"technician_name": r["tech_name"], # resolved from user_anydesk_ids
"customer_machine_id": r["customer_machine_id"], # to.cid — kundens maskine
"customer_alias": r["customer_alias"],
"hardware": {
"id": r["hardware_asset_id"],
"brand": r["hw_brand"],
"model": r["hw_model"],
"anydesk_id": r["hw_anydesk_id"],
"customer_id": r["hw_customer_id"],
} if r["hardware_asset_id"] else None,
"contact": {
"id": r["contact_id"],
"name": r["contact_name"],
"email": r["contact_email"],
} if r["contact_id"] else None,
"customer": {
"id": r["customer_id"],
"name": r["customer_name"],
} if r["customer_id"] else None,
"sag": {
"id": r["sag_id"],
"titel": r["sag_titel"],
"status": r["sag_status"],
} if r["sag_id"] else None,
})
return JSONResponse(content={"sessions": sessions, "total": total, "limit": limit, "offset": offset})
except Exception as e:
logger.error(f"Error in sessions_overview: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/anydesk/auto-link", tags=["Remote Support"])
async def auto_link_sessions():
"""
Auto-link unlinked sessions to hardware_assets via anydesk_id match,
and carry over contact/customer from the hardware asset.
Returns count of newly linked sessions.
"""
try:
linked = 0
# Match sessions to hardware_assets where to_id (customer machine) = anydesk_id
# NOTE: from.cid is the TECHNICIAN's machine, to.cid is the CUSTOMER's machine
result = execute_query("""
UPDATE anydesk_sessions s
SET
hardware_asset_id = ha.id,
customer_id = COALESCE(s.customer_id, ha.current_owner_customer_id),
updated_at = NOW()
FROM hardware_assets ha
WHERE ha.anydesk_id IS NOT NULL
AND ha.anydesk_id != ''
AND (
(s.device_info->>'to_id') = ha.anydesk_id
OR
-- fallback: older imports without to_id — try remote_id only if it differs from technicians' known IDs
(s.device_info->>'to_id' IS NULL AND (s.device_info->>'remote_id') = ha.anydesk_id)
)
AND s.hardware_asset_id IS NULL
RETURNING s.id
""")
linked = len(result) if result else 0
logger.info(f"✅ Auto-linked {linked} AnyDesk sessions to hardware assets")
return JSONResponse(content={"linked": linked})
except Exception as e:
logger.error(f"Error in auto_link_sessions: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/anydesk/sessions/{session_id}/link", tags=["Remote Support"])
async def link_session(
session_id: int,
sag_id: Optional[int] = None,
contact_id: Optional[int] = None,
customer_id: Optional[int] = None,
hardware_asset_id: Optional[int] = None,
notes: Optional[str] = None,
):
"""
Manually link a session to sag, contact, customer, hardware, or add notes.
"""
try:
sets = ["updated_at = NOW()"]
params = []
if sag_id is not None:
sets.append("sag_id = %s"); params.append(sag_id)
if contact_id is not None:
sets.append("contact_id = %s"); params.append(contact_id)
if customer_id is not None:
sets.append("customer_id = %s"); params.append(customer_id)
if hardware_asset_id is not None:
sets.append("hardware_asset_id = %s"); params.append(hardware_asset_id)
if notes is not None:
sets.append("notes = %s"); params.append(notes)
if len(sets) == 1:
return JSONResponse(content={"message": "no changes"})
params.append(session_id)
execute_query(
f"UPDATE anydesk_sessions SET {', '.join(sets)} WHERE id = %s",
tuple(params)
)
return JSONResponse(content={"ok": True})
except Exception as e:
logger.error(f"Error linking session {session_id}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/anydesk/hardware-assets", tags=["Remote Support"])
async def anydesk_hardware_list():
"""List all hardware assets that have an anydesk_id (for linking dropdown)"""
try:
rows = execute_query("""
SELECT ha.id, ha.brand, ha.model, ha.anydesk_id, ha.serial_number,
ha.current_owner_customer_id AS customer_id, cust.name AS customer_name
FROM hardware_assets ha
LEFT JOIN customers cust ON ha.current_owner_customer_id = cust.id
WHERE ha.deleted_at IS NULL
ORDER BY ha.brand, ha.model
""")
return JSONResponse(content={"assets": rows or []})
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))