wulf-pulse/autotask-app/app/addigy-devices/page.tsx
Lorentz Hinrichsen f429f3af54 Add Addigy API integration and Docker deployment with Redis caching
- Implemented complete Addigy API v2 client with authentication via x-api-key
- Added device and policy endpoints with automatic org ID resolution
- Created field mapping from snake_case to Title Case for UI compatibility
- Handles nested 'facts' response structure from Addigy devices API
- Added comprehensive API documentation in ADDIGY_API_GUIDE.md

- Multi-stage Dockerfile with optimized production build
- Custom ports: App on 3100, Redis on 6380 (avoids conflicts)
- Docker Compose orchestration with health checks
- Standalone Next.js output for smaller container images
- Non-root user execution for security

- Implemented Redis caching layer for API responses
- 5-minute TTL with graceful fallback if Redis unavailable
- Cache key structure: service:entity:filter1:filter2
- Applied to Addigy devices endpoint with cache hit/miss logging

- Fixed TypeScript strict mode errors for production builds
- Added null safety checks with optional chaining throughout API routes
- Wrapped useSearchParams in Suspense boundary for Next.js 15+ compatibility
- Fixed type assertions for dynamic API responses
- Corrected Set<string> type mismatches in device comparison logic

- Created DOCKER_README.md with complete deployment guide
- Updated ADDIGY_API_GUIDE.md with real-world API patterns
- Documented response structures, field mappings, and troubleshooting

- Next.js 16.0.0 with Turbopack
- Redis 7 with AOF persistence
- Podman/Docker compatible
- TypeScript strict mode compliant
2025-10-28 22:49:08 -04:00

193 lines
7.7 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { AddigyDevice } from '@/lib/types/addigy';
export default function AddigyDevicesPage() {
const [devices, setDevices] = useState<AddigyDevice[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filterOnline, setFilterOnline] = useState(false);
useEffect(() => {
fetchDevices();
}, [filterOnline]);
const fetchDevices = async () => {
setLoading(true);
setError(null);
try {
const url = filterOnline
? '/api/addigy-devices?online=true'
: '/api/addigy-devices';
const response = await fetch(url);
const result = await response.json();
if (result.success) {
setDevices(result.data);
} else {
setError(result.error || 'Failed to fetch devices');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
return (
<div className="container mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Addigy Devices</h1>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={filterOnline}
onChange={(e) => setFilterOnline(e.target.checked)}
className="w-4 h-4"
/>
<span>Online Only</span>
</label>
<button
onClick={fetchDevices}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Refresh'}
</button>
</div>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded mb-4">
<strong>Error:</strong> {error}
</div>
)}
{loading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
<p className="mt-4 text-gray-600">Loading devices...</p>
</div>
) : (
<>
<div className="mb-4 text-gray-600">
Found {devices.length} device{devices.length !== 1 ? 's' : ''}
</div>
<div className="bg-white shadow-md rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Device Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Model
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
OS Version
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Current User
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Free Disk
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Security
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{devices.map((device) => (
<tr key={device.agentid} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{device['Device Name']}
</div>
<div className="text-xs text-gray-500">
{device['Serial Number'] || 'N/A'}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{device['Device Model Name'] || 'Unknown'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{device['MAC OS X Version'] ||
device['iOS Version'] ||
'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{device['Current User'] || 'N/A'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
device.online
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{device.online ? 'Online' : 'Offline'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
{device['Free Disk Percentage'] !== undefined ? (
<div className="flex items-center">
<span
className={`${
device['Free Disk Percentage'] < 20
? 'text-red-600'
: device['Free Disk Percentage'] < 40
? 'text-yellow-600'
: 'text-green-600'
}`}
>
{device['Free Disk Percentage']}%
</span>
</div>
) : (
'N/A'
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<div className="flex flex-col gap-1">
<span
className={`text-xs ${
device['Firewall Enabled']
? 'text-green-600'
: 'text-red-600'
}`}
>
FW: {device['Firewall Enabled'] ? '✓' : '✗'}
</span>
<span
className={`text-xs ${
device['FileVault Enabled']
? 'text-green-600'
: 'text-red-600'
}`}
>
FV: {device['FileVault Enabled'] ? '✓' : '✗'}
</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
)}
</div>
);
}