Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
1633 lines
63 KiB
Markdown
1633 lines
63 KiB
Markdown
# Asset Detail View Enhancement Plan
|
|
|
|
## Vision Overview
|
|
|
|
Transform the asset detail view from a basic information display into a comprehensive RMM-integrated dashboard that surfaces critical operational data, enables quick actions, and provides at-a-glance status indicators for technicians.
|
|
|
|
### Key Changes Summary
|
|
- Redesigned header with RMM status badge and quick action buttons
|
|
- New key metrics summary banner (health, tickets, security, warranty)
|
|
- Two-column dashboard layout with live RMM vitals
|
|
- Enhanced tabbed navigation with expanded capabilities
|
|
- Deep integration with NinjaOne (and future RMM providers)
|
|
|
|
---
|
|
|
|
## Part 1: Backend Updates
|
|
|
|
### 1.1 New API Endpoints
|
|
|
|
#### 1.1.1 Asset RMM Data Endpoint (Database-Cached Approach)
|
|
**File**: `ee/server/src/app/api/assets/[assetId]/rmm/route.ts`
|
|
|
|
Create an endpoint that returns RMM data from our database (populated during sync) with an optional refresh capability:
|
|
|
|
```typescript
|
|
GET /api/assets/[assetId]/rmm?refresh=false
|
|
Response: {
|
|
provider: 'ninjaone' | 'datto' | 'connectwise_automate',
|
|
agent_status: 'online' | 'offline' | 'unknown',
|
|
last_check_in: ISO8601,
|
|
last_rmm_sync_at: ISO8601, // When we last synced this data
|
|
current_user: string | null,
|
|
uptime_seconds: number | null,
|
|
lan_ip: string | null,
|
|
wan_ip: string | null,
|
|
cpu_utilization_percent: number | null,
|
|
memory_utilization_percent: number | null,
|
|
memory_used_gb: number | null,
|
|
memory_total_gb: number | null,
|
|
remote_control_url: string | null,
|
|
storage: Array<{
|
|
name: string,
|
|
total_gb: number,
|
|
free_gb: number,
|
|
utilization_percent: number
|
|
}>
|
|
}
|
|
|
|
POST /api/assets/[assetId]/rmm/refresh
|
|
// Triggers a single-device sync from RMM and returns updated data
|
|
```
|
|
|
|
**Implementation Steps**:
|
|
1. Create GET route that reads RMM data from workstation_assets/server_assets tables
|
|
2. Include `last_rmm_sync_at` timestamp so UI can show "as of X" indicator
|
|
3. Create POST refresh route that calls sync engine for single device
|
|
4. Map database fields to standardized RMM response format
|
|
5. Handle missing RMM data gracefully (asset not linked to RMM)
|
|
|
|
#### 1.1.2 Asset Summary Metrics Endpoint
|
|
**File**: `server/src/app/api/assets/[assetId]/summary/route.ts`
|
|
|
|
Create endpoint for the key metrics banner:
|
|
|
|
```typescript
|
|
GET /api/assets/[assetId]/summary
|
|
Response: {
|
|
health_status: 'healthy' | 'warning' | 'critical' | 'unknown',
|
|
health_reason: string | null,
|
|
open_tickets_count: number,
|
|
security_status: 'secure' | 'at_risk' | 'critical',
|
|
security_issues: string[], // e.g., ["3 Critical OS Patches missing"]
|
|
warranty_days_remaining: number | null,
|
|
warranty_status: 'active' | 'expiring_soon' | 'expired' | 'unknown'
|
|
}
|
|
```
|
|
|
|
**Implementation Steps**:
|
|
1. Query open tickets associated with asset
|
|
2. Calculate health status from RMM alerts (severity-based)
|
|
3. Calculate security status from patch/antivirus data
|
|
4. Compute warranty days remaining from warranty_end_date
|
|
|
|
#### 1.1.3 Remote Control URL Generation
|
|
**File**: `ee/server/src/lib/integrations/ninjaone/remoteControl.ts`
|
|
|
|
```typescript
|
|
getRemoteControlUrl(deviceId: string): Promise<string | null>
|
|
```
|
|
|
|
**Implementation Steps**:
|
|
1. Research NinjaOne API for remote session initiation
|
|
2. Generate deep-link URL or session token for remote access
|
|
3. Handle cases where remote control is unavailable
|
|
|
|
---
|
|
|
|
### 1.2 Database Schema Updates
|
|
|
|
#### 1.2.1 New Fields for Workstation/Server Assets (RMM Data Cache)
|
|
**Migration**: `20251201000001_add_asset_rmm_cached_fields.cjs`
|
|
|
|
Add columns to `workstation_assets` and `server_assets` to cache RMM data during sync.
|
|
This allows instant display of "as of last sync" data without live API calls:
|
|
|
|
```sql
|
|
-- Current user logged into the device
|
|
ALTER TABLE workstation_assets ADD COLUMN current_user VARCHAR(255);
|
|
ALTER TABLE server_assets ADD COLUMN current_user VARCHAR(255);
|
|
|
|
-- Uptime in seconds (synced from RMM)
|
|
ALTER TABLE workstation_assets ADD COLUMN uptime_seconds BIGINT;
|
|
ALTER TABLE server_assets ADD COLUMN uptime_seconds BIGINT;
|
|
|
|
-- Network addresses (can change, synced from RMM)
|
|
ALTER TABLE workstation_assets ADD COLUMN lan_ip VARCHAR(45);
|
|
ALTER TABLE workstation_assets ADD COLUMN wan_ip VARCHAR(45);
|
|
ALTER TABLE server_assets ADD COLUMN lan_ip VARCHAR(45);
|
|
ALTER TABLE server_assets ADD COLUMN wan_ip VARCHAR(45);
|
|
|
|
-- CPU utilization (percentage, 0-100)
|
|
ALTER TABLE workstation_assets ADD COLUMN cpu_utilization_percent NUMERIC(5,2);
|
|
ALTER TABLE server_assets ADD COLUMN cpu_utilization_percent NUMERIC(5,2);
|
|
|
|
-- Memory utilization
|
|
ALTER TABLE workstation_assets ADD COLUMN memory_used_gb NUMERIC(10,2);
|
|
ALTER TABLE server_assets ADD COLUMN memory_used_gb NUMERIC(10,2);
|
|
-- Note: memory_usage_percent already exists on server_assets, add to workstation:
|
|
ALTER TABLE workstation_assets ADD COLUMN memory_usage_percent NUMERIC(5,2);
|
|
|
|
-- Storage info (array of drive details, synced from RMM)
|
|
-- Note: disk_usage already exists on server_assets as JSONB, add to workstation:
|
|
ALTER TABLE workstation_assets ADD COLUMN disk_usage JSONB;
|
|
-- Format: [{ "name": "Macintosh HD", "total_gb": 1850, "free_gb": 1200 }, ...]
|
|
|
|
-- Last reboot timestamp (more precise than just uptime)
|
|
-- Note: last_reboot_at already exists, ensure it's being synced
|
|
```
|
|
|
|
#### 1.2.2 Asset Notes Document Reference
|
|
**Migration**: `20251201000002_add_notes_document_id_to_assets.cjs`
|
|
|
|
Following the same pattern as company notes, add a document reference to assets.
|
|
This enables rich BlockNote-formatted notes with the existing document system:
|
|
|
|
```sql
|
|
-- Add notes_document_id to assets table (1:1 relationship with documents)
|
|
ALTER TABLE assets ADD COLUMN notes_document_id UUID;
|
|
|
|
-- Add composite foreign key for tenant isolation
|
|
ALTER TABLE assets ADD CONSTRAINT fk_assets_notes_document
|
|
FOREIGN KEY (tenant, notes_document_id)
|
|
REFERENCES documents(tenant, document_id)
|
|
ON DELETE SET NULL;
|
|
|
|
-- Index for efficient lookups
|
|
CREATE INDEX idx_assets_notes_document ON assets(tenant, notes_document_id)
|
|
WHERE notes_document_id IS NOT NULL;
|
|
```
|
|
|
|
**How This Works** (following company notes pattern):
|
|
1. When user creates a note, we create a document in `documents` table
|
|
2. Store BlockNote JSON in `document_block_content.block_data`
|
|
3. Link document to asset via `assets.notes_document_id`
|
|
4. Use existing `createBlockDocument()`, `getBlockContent()`, `updateBlockContent()` actions
|
|
5. Render with existing `TextEditor` component (BlockNote)
|
|
|
|
#### 1.2.3 Normalized Software Inventory Tables
|
|
**Migration**: `20251201000003_create_software_inventory_tables.cjs`
|
|
|
|
Replace the JSONB `installed_software` column with normalized tables for better querying,
|
|
reporting, and future features (license tracking, vulnerability matching).
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- SOFTWARE CATALOG: Canonical list of software (deduplicated per tenant)
|
|
-- ============================================================================
|
|
CREATE TABLE software_catalog (
|
|
software_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant UUID NOT NULL REFERENCES tenants(tenant),
|
|
|
|
-- Identification
|
|
name VARCHAR(500) NOT NULL, -- Software name (e.g., "Google Chrome")
|
|
publisher VARCHAR(255), -- Publisher (e.g., "Google LLC")
|
|
normalized_name VARCHAR(500), -- Lowercase, trimmed for matching
|
|
|
|
-- Classification
|
|
category VARCHAR(100), -- e.g., "Browser", "Productivity", "Security", "Development"
|
|
software_type VARCHAR(50) DEFAULT 'application', -- 'application', 'driver', 'update', 'system'
|
|
|
|
-- Management flags
|
|
is_managed BOOLEAN DEFAULT FALSE, -- Tracked for patching/licensing
|
|
is_security_relevant BOOLEAN DEFAULT FALSE, -- Antivirus, firewall, etc.
|
|
|
|
-- Metadata
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Ensure unique software per tenant (by normalized name + publisher)
|
|
UNIQUE(tenant, normalized_name, publisher)
|
|
);
|
|
|
|
CREATE INDEX idx_software_catalog_tenant ON software_catalog(tenant);
|
|
CREATE INDEX idx_software_catalog_name ON software_catalog(tenant, normalized_name);
|
|
CREATE INDEX idx_software_catalog_publisher ON software_catalog(tenant, publisher);
|
|
CREATE INDEX idx_software_catalog_category ON software_catalog(tenant, category);
|
|
|
|
-- RLS
|
|
ALTER TABLE software_catalog ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation ON software_catalog
|
|
USING (tenant = current_setting('app.current_tenant')::UUID);
|
|
|
|
-- ============================================================================
|
|
-- ASSET SOFTWARE: Junction table linking assets to installed software
|
|
-- ============================================================================
|
|
CREATE TABLE asset_software (
|
|
tenant UUID NOT NULL REFERENCES tenants(tenant),
|
|
asset_id UUID NOT NULL,
|
|
software_id UUID NOT NULL REFERENCES software_catalog(software_id) ON DELETE CASCADE,
|
|
|
|
-- Installation details
|
|
version VARCHAR(100), -- Installed version
|
|
install_date DATE, -- When it was installed (from RMM)
|
|
install_path TEXT, -- Installation location
|
|
size_bytes BIGINT, -- Size on disk
|
|
|
|
-- Sync tracking
|
|
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- When we first detected it
|
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Updated each sync
|
|
|
|
-- Status
|
|
is_current BOOLEAN DEFAULT TRUE, -- FALSE = was uninstalled (soft delete)
|
|
uninstalled_at TIMESTAMPTZ, -- When we detected removal
|
|
|
|
-- Composite primary key
|
|
PRIMARY KEY (tenant, asset_id, software_id),
|
|
|
|
-- Foreign key to assets
|
|
FOREIGN KEY (tenant, asset_id) REFERENCES assets(tenant, asset_id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Query patterns we need to optimize:
|
|
-- 1. "Show all software on asset X" (asset detail page)
|
|
CREATE INDEX idx_asset_software_asset ON asset_software(tenant, asset_id)
|
|
WHERE is_current = TRUE;
|
|
|
|
-- 2. "Find all assets with software Y installed" (fleet search)
|
|
CREATE INDEX idx_asset_software_software ON asset_software(tenant, software_id)
|
|
WHERE is_current = TRUE;
|
|
|
|
-- 3. "Show recently installed software" (audit/reporting)
|
|
CREATE INDEX idx_asset_software_first_seen ON asset_software(tenant, first_seen_at DESC);
|
|
|
|
-- 4. "Show uninstalled software" (change tracking)
|
|
CREATE INDEX idx_asset_software_uninstalled ON asset_software(tenant, uninstalled_at DESC)
|
|
WHERE is_current = FALSE;
|
|
|
|
-- RLS
|
|
ALTER TABLE asset_software ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation ON asset_software
|
|
USING (tenant = current_setting('app.current_tenant')::UUID);
|
|
|
|
-- ============================================================================
|
|
-- MIGRATION: Move existing JSONB data to normalized tables
|
|
-- ============================================================================
|
|
-- This will be handled in the migration script:
|
|
-- 1. For each asset with installed_software JSONB:
|
|
-- a. Parse the JSON array
|
|
-- b. For each software item:
|
|
-- - Find or create entry in software_catalog (match on normalized_name + publisher)
|
|
-- - Create entry in asset_software with version, install_date, etc.
|
|
-- 2. After migration, drop the installed_software columns (or keep for rollback)
|
|
|
|
-- ============================================================================
|
|
-- HELPER VIEW: Denormalized view for easy querying
|
|
-- ============================================================================
|
|
CREATE VIEW v_asset_software_details AS
|
|
SELECT
|
|
asw.tenant,
|
|
asw.asset_id,
|
|
a.name AS asset_name,
|
|
a.asset_type,
|
|
a.client_id,
|
|
sc.software_id,
|
|
sc.name AS software_name,
|
|
sc.publisher,
|
|
sc.category,
|
|
sc.is_managed,
|
|
sc.is_security_relevant,
|
|
asw.version,
|
|
asw.install_date,
|
|
asw.size_bytes,
|
|
asw.first_seen_at,
|
|
asw.last_seen_at,
|
|
asw.is_current
|
|
FROM asset_software asw
|
|
JOIN software_catalog sc ON sc.software_id = asw.software_id
|
|
JOIN assets a ON a.tenant = asw.tenant AND a.asset_id = asw.asset_id;
|
|
```
|
|
|
|
**Key Design Decisions:**
|
|
|
|
1. **Normalized name matching**: Store `normalized_name` (lowercase, trimmed) to handle variations like "Google Chrome" vs "google chrome" vs " Google Chrome "
|
|
|
|
2. **Soft delete for uninstalls**: When software disappears from a sync, set `is_current = FALSE` and `uninstalled_at`. This preserves history and enables "what changed" reporting.
|
|
|
|
3. **Publisher in unique constraint**: "Chrome" from "Google LLC" is different from a hypothetical "Chrome" from another publisher.
|
|
|
|
4. **Category field**: Allows filtering by type (browsers, security tools, etc.) - can be populated manually or via heuristics.
|
|
|
|
5. **is_managed flag**: Marks software that should be tracked for patching/licensing.
|
|
|
|
---
|
|
|
|
### 1.3 Sync Engine Enhancements
|
|
|
|
#### 1.3.1 Enhanced Device Data Mapping
|
|
**File**: `ee/server/src/lib/integrations/ninjaone/mappers/deviceMapper.ts`
|
|
|
|
Update mapper to extract all cached RMM fields during sync:
|
|
|
|
```typescript
|
|
// Add to mapDeviceToAsset():
|
|
current_user: device.lastLoggedInUser || null,
|
|
uptime_seconds: device.uptimeSeconds || null,
|
|
lan_ip: extractPrimaryLanIp(device.networkInterfaces),
|
|
wan_ip: device.publicIP || null,
|
|
cpu_utilization_percent: device.system?.cpuUsage || null,
|
|
memory_usage_percent: device.system?.memoryUsage || null,
|
|
memory_used_gb: calculateMemoryUsedGb(device.system),
|
|
disk_usage: mapDiskUsage(device.volumes), // [{ name, total_gb, free_gb }]
|
|
```
|
|
|
|
**Helper Functions to Add**:
|
|
```typescript
|
|
function extractPrimaryLanIp(networkInterfaces: NinjaOneNetworkInterface[]): string | null {
|
|
// Find primary non-virtual interface with IPv4 address
|
|
}
|
|
|
|
function calculateMemoryUsedGb(system: NinjaOneSystemInfo): number | null {
|
|
if (!system?.totalMemory || !system?.availableMemory) return null;
|
|
return (system.totalMemory - system.availableMemory) / (1024 * 1024 * 1024);
|
|
}
|
|
|
|
function mapDiskUsage(volumes: NinjaOneVolume[]): DiskUsageEntry[] {
|
|
return volumes.map(v => ({
|
|
name: v.name || v.deviceName,
|
|
total_gb: v.capacity / (1024 * 1024 * 1024),
|
|
free_gb: v.freeSpace / (1024 * 1024 * 1024),
|
|
}));
|
|
}
|
|
```
|
|
|
|
#### 1.3.2 Single-Device Refresh Sync
|
|
**File**: `ee/server/src/lib/integrations/ninjaone/sync/syncEngine.ts`
|
|
|
|
Add method to refresh a single device on-demand (for manual refresh button):
|
|
|
|
```typescript
|
|
async syncSingleDeviceById(assetId: string): Promise<SyncResult> {
|
|
// 1. Look up asset to get rmm_device_id
|
|
// 2. Call NinjaOne API for single device detail
|
|
// 3. Update workstation_assets/server_assets with new data
|
|
// 4. Update assets.last_rmm_sync_at timestamp
|
|
// 5. Return updated data
|
|
}
|
|
```
|
|
|
|
#### 1.3.3 Software Inventory Sync (Refactored for Normalized Tables)
|
|
**File**: `ee/server/src/lib/integrations/ninjaone/sync/softwareSync.ts`
|
|
|
|
Refactor the existing software sync to use the new normalized tables instead of JSONB.
|
|
|
|
```typescript
|
|
interface SoftwareSyncContext {
|
|
tenant: string;
|
|
assetId: string;
|
|
syncTimestamp: Date;
|
|
}
|
|
|
|
async function syncAssetSoftware(
|
|
ctx: SoftwareSyncContext,
|
|
ninjaSoftware: NinjaOneSoftware[]
|
|
): Promise<void> {
|
|
const { tenant, assetId, syncTimestamp } = ctx;
|
|
|
|
// 1. Get current software IDs for this asset
|
|
const currentSoftwareIds = await knex('asset_software')
|
|
.where({ tenant, asset_id: assetId, is_current: true })
|
|
.pluck('software_id');
|
|
|
|
const seenSoftwareIds = new Set<string>();
|
|
|
|
// 2. Process each software item from NinjaOne
|
|
for (const sw of ninjaSoftware) {
|
|
// Find or create catalog entry
|
|
const softwareId = await findOrCreateSoftwareCatalogEntry(tenant, {
|
|
name: sw.name,
|
|
publisher: sw.publisher,
|
|
});
|
|
|
|
seenSoftwareIds.add(softwareId);
|
|
|
|
// Upsert asset_software record
|
|
await knex('asset_software')
|
|
.insert({
|
|
tenant,
|
|
asset_id: assetId,
|
|
software_id: softwareId,
|
|
version: sw.version,
|
|
install_date: sw.installDate,
|
|
install_path: sw.location,
|
|
size_bytes: sw.size,
|
|
first_seen_at: syncTimestamp,
|
|
last_seen_at: syncTimestamp,
|
|
is_current: true,
|
|
uninstalled_at: null,
|
|
})
|
|
.onConflict(['tenant', 'asset_id', 'software_id'])
|
|
.merge({
|
|
version: sw.version, // Version may have changed
|
|
last_seen_at: syncTimestamp, // Always update last seen
|
|
is_current: true, // Re-mark as current if previously uninstalled
|
|
uninstalled_at: null,
|
|
});
|
|
}
|
|
|
|
// 3. Mark software no longer present as uninstalled
|
|
const removedSoftwareIds = currentSoftwareIds.filter(
|
|
id => !seenSoftwareIds.has(id)
|
|
);
|
|
|
|
if (removedSoftwareIds.length > 0) {
|
|
await knex('asset_software')
|
|
.where({ tenant, asset_id: assetId })
|
|
.whereIn('software_id', removedSoftwareIds)
|
|
.update({
|
|
is_current: false,
|
|
uninstalled_at: syncTimestamp,
|
|
});
|
|
}
|
|
}
|
|
|
|
async function findOrCreateSoftwareCatalogEntry(
|
|
tenant: string,
|
|
software: { name: string; publisher?: string }
|
|
): Promise<string> {
|
|
const normalizedName = software.name.toLowerCase().trim();
|
|
const publisher = software.publisher?.trim() || null;
|
|
|
|
// Try to find existing entry
|
|
const existing = await knex('software_catalog')
|
|
.where({ tenant, normalized_name: normalizedName, publisher })
|
|
.first();
|
|
|
|
if (existing) {
|
|
return existing.software_id;
|
|
}
|
|
|
|
// Create new entry
|
|
const [entry] = await knex('software_catalog')
|
|
.insert({
|
|
tenant,
|
|
name: software.name.trim(),
|
|
normalized_name: normalizedName,
|
|
publisher,
|
|
category: inferSoftwareCategory(software.name), // Optional heuristic
|
|
})
|
|
.returning('software_id');
|
|
|
|
return entry.software_id;
|
|
}
|
|
|
|
// Optional: Simple heuristic to auto-categorize common software
|
|
function inferSoftwareCategory(name: string): string | null {
|
|
const lower = name.toLowerCase();
|
|
if (/chrome|firefox|safari|edge|opera|brave/.test(lower)) return 'Browser';
|
|
if (/office|word|excel|powerpoint|outlook/.test(lower)) return 'Productivity';
|
|
if (/visual studio|vscode|intellij|xcode|android studio/.test(lower)) return 'Development';
|
|
if (/antivirus|defender|norton|mcafee|sentinelone|crowdstrike/.test(lower)) return 'Security';
|
|
if (/zoom|teams|slack|discord/.test(lower)) return 'Communication';
|
|
if (/adobe|photoshop|illustrator|acrobat/.test(lower)) return 'Creative';
|
|
return null;
|
|
}
|
|
```
|
|
|
|
**Key Changes from Current Implementation:**
|
|
|
|
1. **No more JSONB**: Software stored in relational tables
|
|
2. **Deduplication**: Same software across assets shares one catalog entry
|
|
3. **Change tracking**: `is_current` and `uninstalled_at` track install/uninstall events
|
|
4. **Upsert logic**: Handles version updates and reinstalls gracefully
|
|
5. **Category inference**: Optional auto-categorization for common software
|
|
|
|
---
|
|
|
|
### 1.4 Service Layer Updates
|
|
|
|
#### 1.4.1 Asset Actions Enhancement
|
|
**File**: `server/src/lib/actions/asset-actions/assetActions.ts`
|
|
|
|
Add new server actions:
|
|
|
|
```typescript
|
|
// Get asset summary metrics (health, tickets, security, warranty)
|
|
export async function getAssetSummaryMetrics(assetId: string): Promise<AssetSummaryMetrics>
|
|
```
|
|
|
|
#### 1.4.2 Asset Notes Actions (Using Document System)
|
|
**File**: `server/src/lib/actions/asset-actions/assetNoteActions.ts` (new file)
|
|
|
|
Following the company notes pattern from `ClientDetails.tsx`:
|
|
|
|
```typescript
|
|
// Load note content for an asset
|
|
export async function getAssetNoteContent(assetId: string): Promise<{
|
|
document: Document | null;
|
|
blockData: PartialBlock[] | null;
|
|
}> {
|
|
// 1. Fetch asset to get notes_document_id
|
|
// 2. If exists, call getDocument() and getBlockContent()
|
|
// 3. Parse block_data JSON and return
|
|
}
|
|
|
|
// Save note content (create or update)
|
|
export async function saveAssetNote(
|
|
assetId: string,
|
|
blockData: PartialBlock[],
|
|
userId: string
|
|
): Promise<{ document_id: string }> {
|
|
const asset = await getAsset(assetId);
|
|
|
|
if (asset.notes_document_id) {
|
|
// Update existing document
|
|
await updateBlockContent(asset.notes_document_id, {
|
|
block_data: JSON.stringify(blockData),
|
|
user_id: userId
|
|
});
|
|
return { document_id: asset.notes_document_id };
|
|
} else {
|
|
// Create new document and link to asset
|
|
const { document_id } = await createBlockDocument({
|
|
document_name: `${asset.name} Notes`,
|
|
user_id: userId,
|
|
block_data: JSON.stringify(blockData),
|
|
entityId: assetId,
|
|
entityType: 'asset'
|
|
});
|
|
|
|
// Update asset with notes_document_id
|
|
await updateAsset(assetId, { notes_document_id: document_id });
|
|
return { document_id };
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 1.4.3 RMM Actions
|
|
**File**: `ee/server/src/lib/actions/asset-actions/rmmActions.ts` (new file)
|
|
|
|
```typescript
|
|
// Get cached RMM data from database (fast, no API call)
|
|
export async function getAssetRmmData(assetId: string): Promise<RmmCachedData | null>
|
|
|
|
// Trigger single-device sync and return updated data
|
|
export async function refreshAssetRmmData(assetId: string): Promise<RmmCachedData | null>
|
|
|
|
// Get remote control URL
|
|
export async function getAssetRemoteControlUrl(assetId: string): Promise<string | null>
|
|
|
|
// Trigger RMM actions (for Actions dropdown)
|
|
export async function triggerRmmReboot(assetId: string): Promise<{ success: boolean, message: string }>
|
|
export async function triggerRmmScript(assetId: string, scriptId: string): Promise<{ success: boolean, jobId: string }>
|
|
```
|
|
|
|
---
|
|
|
|
### 1.5 Interface Updates
|
|
|
|
#### 1.5.1 New Types
|
|
**File**: `server/src/interfaces/asset.interfaces.tsx`
|
|
|
|
```typescript
|
|
export interface AssetSummaryMetrics {
|
|
health_status: 'healthy' | 'warning' | 'critical' | 'unknown';
|
|
health_reason: string | null;
|
|
open_tickets_count: number;
|
|
security_status: 'secure' | 'at_risk' | 'critical';
|
|
security_issues: string[];
|
|
warranty_days_remaining: number | null;
|
|
warranty_status: 'active' | 'expiring_soon' | 'expired' | 'unknown';
|
|
}
|
|
|
|
// Add to Asset interface:
|
|
export interface Asset {
|
|
// ... existing fields ...
|
|
notes_document_id?: string | null; // Reference to document for BlockNote notes
|
|
}
|
|
```
|
|
|
|
**File**: `ee/server/src/interfaces/rmm.interfaces.ts`
|
|
|
|
```typescript
|
|
// Cached RMM data from database (populated during sync)
|
|
export interface RmmCachedData {
|
|
provider: RmmProvider;
|
|
agent_status: 'online' | 'offline' | 'unknown';
|
|
last_check_in: string | null;
|
|
last_rmm_sync_at: string | null; // When we last synced from RMM
|
|
current_user: string | null;
|
|
uptime_seconds: number | null;
|
|
lan_ip: string | null;
|
|
wan_ip: string | null;
|
|
cpu_utilization_percent: number | null;
|
|
memory_utilization_percent: number | null;
|
|
memory_used_gb: number | null;
|
|
memory_total_gb: number | null;
|
|
storage: RmmStorageInfo[];
|
|
}
|
|
|
|
export interface RmmStorageInfo {
|
|
name: string;
|
|
total_gb: number;
|
|
free_gb: number;
|
|
utilization_percent: number; // Calculated: (total - free) / total * 100
|
|
}
|
|
|
|
// Workstation extension fields for RMM cache
|
|
export interface RmmWorkstationCacheFields {
|
|
current_user: string | null;
|
|
uptime_seconds: number | null;
|
|
lan_ip: string | null;
|
|
wan_ip: string | null;
|
|
cpu_utilization_percent: number | null;
|
|
memory_usage_percent: number | null;
|
|
memory_used_gb: number | null;
|
|
disk_usage: RmmStorageInfo[] | null;
|
|
}
|
|
```
|
|
|
|
**File**: `server/src/interfaces/software.interfaces.ts` (new file)
|
|
|
|
```typescript
|
|
// Canonical software entry (deduplicated per tenant)
|
|
export interface SoftwareCatalogEntry {
|
|
software_id: string;
|
|
tenant: string;
|
|
name: string;
|
|
publisher: string | null;
|
|
normalized_name: string;
|
|
category: string | null; // 'Browser', 'Security', 'Productivity', etc.
|
|
software_type: 'application' | 'driver' | 'update' | 'system';
|
|
is_managed: boolean; // Tracked for patching/licensing
|
|
is_security_relevant: boolean; // Antivirus, firewall, etc.
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
// Software installed on a specific asset
|
|
export interface AssetSoftwareInstall {
|
|
tenant: string;
|
|
asset_id: string;
|
|
software_id: string;
|
|
version: string | null;
|
|
install_date: string | null;
|
|
install_path: string | null;
|
|
size_bytes: number | null;
|
|
first_seen_at: string;
|
|
last_seen_at: string;
|
|
is_current: boolean;
|
|
uninstalled_at: string | null;
|
|
|
|
// Joined from software_catalog (when needed)
|
|
software?: SoftwareCatalogEntry;
|
|
}
|
|
|
|
// For display in asset detail software tab
|
|
export interface AssetSoftwareDisplayItem {
|
|
software_id: string;
|
|
name: string;
|
|
publisher: string | null;
|
|
category: string | null;
|
|
version: string | null;
|
|
install_date: string | null;
|
|
size_bytes: number | null;
|
|
first_seen_at: string;
|
|
is_current: boolean;
|
|
}
|
|
|
|
// For fleet-wide software search results
|
|
export interface SoftwareSearchResult {
|
|
software_id: string;
|
|
name: string;
|
|
publisher: string | null;
|
|
category: string | null;
|
|
install_count: number; // How many assets have this installed
|
|
assets: Array<{
|
|
asset_id: string;
|
|
asset_name: string;
|
|
client_id: string;
|
|
client_name: string;
|
|
version: string | null;
|
|
}>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 1.6 Backend Task Checklist
|
|
|
|
**Database & Migrations:**
|
|
- [ ] Create migration for RMM cached fields on workstation/server tables (cpu, memory, IPs, disk_usage)
|
|
- [ ] Create migration to add `notes_document_id` to assets table with FK to documents
|
|
- [ ] Create migration for normalized software tables (`software_catalog`, `asset_software`)
|
|
- [ ] Create migration to populate normalized tables from existing JSONB data
|
|
- [ ] Create helper view `v_asset_software_details`
|
|
|
|
**Sync Engine:**
|
|
- [ ] Update device mapper to extract current_user, uptime, IPs, CPU, memory, disk_usage
|
|
- [ ] Add `syncSingleDeviceById()` method for on-demand refresh
|
|
- [ ] Ensure all new fields are populated during full/incremental sync
|
|
- [ ] Refactor `softwareSync.ts` to use normalized tables instead of JSONB
|
|
- [ ] Implement `findOrCreateSoftwareCatalogEntry()` with deduplication
|
|
- [ ] Implement soft-delete for uninstalled software (`is_current = false`)
|
|
- [ ] Add optional category inference for common software
|
|
|
|
**API Endpoints:**
|
|
- [ ] Create `GET /api/assets/[assetId]/rmm` endpoint (reads from DB cache)
|
|
- [ ] Create `POST /api/assets/[assetId]/rmm/refresh` endpoint (triggers single-device sync)
|
|
- [ ] Create `GET /api/assets/[assetId]/summary` endpoint
|
|
- [ ] Create `GET /api/assets/[assetId]/software` endpoint (paginated, filterable)
|
|
- [ ] Create `GET /api/software/search` endpoint (fleet-wide software search)
|
|
|
|
**Server Actions:**
|
|
- [ ] Create `assetNoteActions.ts` with `getAssetNoteContent()` and `saveAssetNote()`
|
|
- [ ] Create `rmmActions.ts` with `getAssetRmmData()` and `refreshAssetRmmData()`
|
|
- [ ] Add `getAssetSummaryMetrics()` server action
|
|
- [ ] Add `getAssetRemoteControlUrl()` action
|
|
- [ ] Create `softwareActions.ts` with `getAssetSoftware()`, `searchSoftwareFleetWide()`
|
|
|
|
**Interface Updates:**
|
|
- [ ] Add `notes_document_id` to Asset interface
|
|
- [ ] Add `RmmCachedData` and `RmmWorkstationCacheFields` interfaces
|
|
- [ ] Add `AssetSummaryMetrics` interface
|
|
- [ ] Add `SoftwareCatalogEntry` and `AssetSoftwareInstall` interfaces
|
|
|
|
**RMM Integration:**
|
|
- [ ] Research and implement NinjaOne remote control URL generation
|
|
- [ ] Add NinjaOne API methods for script execution (for Actions menu)
|
|
|
|
**Testing:**
|
|
- [ ] Add unit tests for new endpoints
|
|
- [ ] Add integration tests for RMM data sync and refresh
|
|
- [ ] Add tests for software sync with normalized tables
|
|
- [ ] Add tests for software deduplication logic
|
|
|
|
---
|
|
|
|
## Part 2: UI Updates
|
|
|
|
### 2.1 Component Architecture
|
|
|
|
#### New Component Structure
|
|
```
|
|
server/src/components/assets/
|
|
├── AssetDetailView.tsx # New: Full-page detail view (replaces drawer for RMM assets)
|
|
├── AssetDetailHeader.tsx # New: Header with status badge and action buttons
|
|
├── AssetMetricsBanner.tsx # New: Key metrics summary banner
|
|
├── AssetDashboardGrid.tsx # New: Two-column dashboard layout
|
|
├── panels/
|
|
│ ├── RmmVitalsPanel.tsx # New: Live RMM connectivity data
|
|
│ ├── HardwareSpecsPanel.tsx # New: Enhanced hardware with utilization bars
|
|
│ ├── SecurityPatchingPanel.tsx # New: Security & patching status
|
|
│ ├── AssetInfoPanel.tsx # New: Static asset info & lifecycle
|
|
│ └── AssetNotesPanel.tsx # New: Technician notes
|
|
├── tabs/
|
|
│ ├── ServiceHistoryTab.tsx # Enhanced: Ticket history
|
|
│ ├── SoftwareInventoryTab.tsx # Enhanced: Searchable software list
|
|
│ ├── MaintenanceSchedulesTab.tsx # Enhanced: Schedule list view
|
|
│ ├── RelatedAssetsTab.tsx # Existing: Related assets
|
|
│ ├── DocumentsPasswordsTab.tsx # Existing: Documents
|
|
│ └── AuditLogTab.tsx # New: Asset change audit trail
|
|
└── shared/
|
|
├── StatusBadge.tsx # New: Reusable status badges
|
|
├── UtilizationBar.tsx # New: Visual utilization bars
|
|
└── CopyableField.tsx # New: Field with copy-to-clipboard
|
|
```
|
|
|
|
---
|
|
|
|
### 2.2 Header & Quick Actions Bar
|
|
|
|
#### 2.2.1 AssetDetailHeader Component
|
|
**File**: `server/src/components/assets/AssetDetailHeader.tsx`
|
|
|
|
**Layout**:
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ [Icon] Asset Name [● Online - NinjaOne] [Remote] [Ticket] [▾]│
|
|
│ Asset Tag: NINJA-K93H2QG3GH │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**Features**:
|
|
- Asset type icon (Mac, Windows, Server, etc.)
|
|
- Large asset name with RMM status badge
|
|
- Status badge color: green (online), gray (offline), orange (unknown)
|
|
- Badge shows provider name (NinjaOne, Datto, etc.)
|
|
- Asset tag displayed subtly below name
|
|
- Action buttons right-aligned:
|
|
- **Remote Control** (blue, prominent) - opens RMM remote session
|
|
- **Create Ticket** - existing functionality, pre-fills asset
|
|
- **Actions** dropdown:
|
|
- Run RMM Script (submenu with available scripts)
|
|
- Edit Asset
|
|
- Reboot Device (RMM action)
|
|
- Archive Asset
|
|
- Delete Asset
|
|
|
|
---
|
|
|
|
### 2.3 Key Metrics Summary Banner
|
|
|
|
#### 2.3.1 AssetMetricsBanner Component
|
|
**File**: `server/src/components/assets/AssetMetricsBanner.tsx`
|
|
|
|
**Layout**:
|
|
```
|
|
┌────────────────┬────────────────┬────────────────┬────────────────┐
|
|
│ Health Status │ Open Tickets │ Security Status│ Warranty │
|
|
│ [✓ Healthy] │ [2 Active] │ [⚠ 3 Missing] │ [142 Days] │
|
|
└────────────────┴────────────────┴────────────────┴────────────────┘
|
|
```
|
|
|
|
**Features**:
|
|
- Four equal-width panels in horizontal layout
|
|
- Each panel has:
|
|
- Label (small, gray text)
|
|
- Value with status icon
|
|
- Color coding based on status
|
|
- **Health Status**:
|
|
- Green checkmark for healthy
|
|
- Yellow warning for issues
|
|
- Red X for critical
|
|
- Tooltip with reason if unhealthy
|
|
- **Open Tickets**:
|
|
- Clickable, navigates to tickets tab
|
|
- Shows count with "Active" label
|
|
- **Security Status**:
|
|
- Green: "Secure"
|
|
- Yellow: "X Missing Patches" with warning icon
|
|
- Red: "Critical" for antivirus issues
|
|
- **Warranty**:
|
|
- Green: > 90 days remaining
|
|
- Yellow: 30-90 days ("Expiring Soon")
|
|
- Red: < 30 days or expired
|
|
- Gray: Unknown/not set
|
|
|
|
---
|
|
|
|
### 2.4 Main Dashboard Grid
|
|
|
|
#### 2.4.1 AssetDashboardGrid Component
|
|
**File**: `server/src/components/assets/AssetDashboardGrid.tsx`
|
|
|
|
**Layout**:
|
|
```
|
|
┌─────────────────────────────────────┬──────────────────────────┐
|
|
│ RMM Vitals & Connectivity │ Asset Info & Lifecycle │
|
|
│ (Panel A) │ (Panel D) │
|
|
├─────────────────────────────────────┤ │
|
|
│ Hardware Specifications │ │
|
|
│ (Panel B) ├──────────────────────────┤
|
|
├─────────────────────────────────────┤ Notes & Quick Info │
|
|
│ Security & Patching │ (Panel E) │
|
|
│ (Panel C) │ │
|
|
└─────────────────────────────────────┴──────────────────────────┘
|
|
```
|
|
|
|
**Implementation**:
|
|
- CSS Grid with 2 columns (2fr 1fr ratio)
|
|
- Left column: 3 stacked panels
|
|
- Right column: 2 panels (Info taller, Notes shorter)
|
|
- Responsive: Stack vertically on mobile
|
|
|
|
---
|
|
|
|
### 2.5 Dashboard Panels
|
|
|
|
#### 2.5.1 RmmVitalsPanel
|
|
**File**: `server/src/components/assets/panels/RmmVitalsPanel.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
RMM Vitals & Connectivity [🔄 Refresh]
|
|
─────────────────────────────────────────────────────────────────────
|
|
Agent Status: Online (Last check-in: 2 minutes ago)
|
|
Current User: j.appleseed
|
|
Uptime: 4 days, 12 hours, 33 minutes
|
|
Last RMM Sync: Today, 10:45 AM ← "as of" indicator
|
|
Network: LAN IP: 192.168.1.45 [📋] | WAN IP: 45.12.123.99 [📋]
|
|
```
|
|
|
|
**Features**:
|
|
- Data loaded from database cache (instant display on page load)
|
|
- **"Last RMM Sync"** shows when data was last refreshed from NinjaOne
|
|
- **Refresh button** triggers `POST /api/assets/[assetId]/rmm/refresh`
|
|
- Shows loading spinner during refresh
|
|
- Updates all displayed data on completion
|
|
- Status indicator with time since last check-in
|
|
- Copy-to-clipboard buttons for IPs
|
|
- Graceful degradation when RMM data unavailable (show "Not connected to RMM")
|
|
- Loading skeleton on initial page load
|
|
|
|
#### 2.5.2 HardwareSpecsPanel
|
|
**File**: `server/src/components/assets/panels/HardwareSpecsPanel.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
Hardware Specifications
|
|
───────────────────────
|
|
CPU: Apple M4 Max (12 Cores) | Utilization: [██████────] 45%
|
|
RAM: 32GB Unified Memory | Utilization: [████████──] 78% (25GB Used)
|
|
Storage: Macintosh HD (NVMe): 1.85TB Total | [██████████] 1.2TB Free
|
|
GPU: Apple M4 Max (38-core GPU)
|
|
```
|
|
|
|
**Features**:
|
|
- Visual utilization bars (animated)
|
|
- Color-coded utilization (green < 70%, yellow 70-90%, red > 90%)
|
|
- Multiple storage drives if applicable
|
|
- Tooltips with exact values
|
|
- Show "N/A" gracefully for missing data
|
|
|
|
#### 2.5.3 SecurityPatchingPanel
|
|
**File**: `server/src/components/assets/panels/SecurityPatchingPanel.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
Security & Patching
|
|
───────────────────
|
|
OS Version: macOS Sonoma 14.5 (Latest Build)
|
|
Antivirus: SentinelOne [✓ Installed & Running] | Last Scan: Today, 3:00 AM
|
|
Patch Status: [⚠ At Risk] - 3 Critical OS Patches missing.
|
|
Firewall: [✓ On]
|
|
```
|
|
|
|
**Features**:
|
|
- Status indicators with icons (checkmarks, warnings)
|
|
- Color-coded status badges
|
|
- Patch count with severity breakdown
|
|
- Click patch status to see details (modal or expand)
|
|
- Antivirus product name and status
|
|
|
|
#### 2.5.4 AssetInfoPanel
|
|
**File**: `server/src/components/assets/panels/AssetInfoPanel.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
Asset Info & Lifecycle
|
|
──────────────────────
|
|
Client: Emerald City →
|
|
Location: HQ - Floor 2, Design Dept.
|
|
Model: MacBook Pro 16-inch (M4 Max, Late 2024)
|
|
Serial: K93H2QG3GH
|
|
Purchase Date: 01/15/2025
|
|
Warranty End: 01/15/2026 (AppleCare+)
|
|
```
|
|
|
|
**Features**:
|
|
- Client name is a link to client dashboard
|
|
- All static data from asset record
|
|
- Serial number with copy button
|
|
- Warranty with status indicator
|
|
|
|
#### 2.5.5 AssetNotesPanel
|
|
**File**: `server/src/components/assets/panels/AssetNotesPanel.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
Notes & Quick Info [Save]
|
|
──────────────────────────────────────────────────────────────────
|
|
┌────────────────────────────────────────────────────────────────┐
|
|
│ [BlockNote Editor] │
|
|
│ │
|
|
│ • User is VIP, handle with care. │
|
|
│ • Known issue with USB-C port 2. - @t.smith │
|
|
│ │
|
|
│ Last updated: Today, 2:30 PM by j.smith │
|
|
└────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**Features** (following company notes pattern):
|
|
- Uses existing `TextEditor` component (BlockNote) for rich formatting
|
|
- Supports all BlockNote features: headings, lists, bold, italic, etc.
|
|
- **@mentions** supported (triggers user search, sends notifications)
|
|
- **Auto-save** on content change (debounced) or explicit Save button
|
|
- Document stored in `documents` + `document_block_content` tables
|
|
- Linked to asset via `assets.notes_document_id`
|
|
- Shows "Add a note..." placeholder when no note exists
|
|
- Displays last updated timestamp and author from document metadata
|
|
|
|
**Implementation** (mirrors `ClientDetails.tsx` notes handling):
|
|
```typescript
|
|
// Load note on mount
|
|
useEffect(() => {
|
|
if (asset.notes_document_id) {
|
|
const content = await getBlockContent(asset.notes_document_id);
|
|
setCurrentContent(content.block_data);
|
|
}
|
|
}, [asset.notes_document_id]);
|
|
|
|
// Save note
|
|
const handleSaveNote = async () => {
|
|
await saveAssetNote(asset.asset_id, currentContent, currentUser.user_id);
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
### 2.6 Tabbed Navigation
|
|
|
|
#### 2.6.1 Enhanced Tab Structure
|
|
**File**: `server/src/components/assets/AssetDetailTabs.tsx`
|
|
|
|
**Tabs**:
|
|
1. **Service History (Tickets)** - Default active
|
|
2. **Software Inventory**
|
|
3. **Maintenance Schedules**
|
|
4. **Related Assets**
|
|
5. **Documents & Passwords**
|
|
6. **Audit Log**
|
|
|
|
#### 2.6.2 ServiceHistoryTab
|
|
**File**: `server/src/components/assets/tabs/ServiceHistoryTab.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
┌──────────┬──────────────────────────────────┬────────────┬─────────────┐
|
|
│ Ticket ID│ Subject │ Status │ Date Closed │
|
|
├──────────┼──────────────────────────────────┼────────────┼─────────────┤
|
|
│ #12345 │ Outlook Crashing │ In Progress│ N/A │
|
|
│ #12300 │ New User Setup │ Closed │ 10/25/2025 │
|
|
│ #12289 │ RAM Upgrade Request │ Closed │ 10/20/2025 │
|
|
└──────────┴──────────────────────────────────┴────────────┴─────────────┘
|
|
```
|
|
|
|
**Features**:
|
|
- Sortable columns
|
|
- Ticket ID links to ticket detail
|
|
- Status with color-coded badge
|
|
- Pagination for long histories
|
|
- "Create Ticket" button in tab header
|
|
|
|
#### 2.6.3 SoftwareInventoryTab
|
|
**File**: `server/src/components/assets/tabs/SoftwareInventoryTab.tsx`
|
|
|
|
**Content** (using normalized tables):
|
|
```
|
|
┌────────────────────────┬──────────┬─────────────────┬────────────┬─────────────┐
|
|
│ Name │ Version │ Publisher │ Category │ First Seen │
|
|
├────────────────────────┼──────────┼─────────────────┼────────────┼─────────────┤
|
|
│ Google Chrome │ 120.0.1 │ Google LLC │ Browser │ 10/15/2025 │
|
|
│ Microsoft Office 365 │ 16.80 │ Microsoft │ Productivi │ 01/15/2025 │
|
|
│ SentinelOne Agent │ 23.1.2 │ SentinelOne │ Security │ 01/15/2025 │
|
|
│ Visual Studio Code │ 1.85.0 │ Microsoft │ Developmen │ 09/01/2025 │
|
|
└────────────────────────┴──────────┴─────────────────┴────────────┴─────────────┘
|
|
[Show uninstalled software ☐]
|
|
```
|
|
|
|
**Features**:
|
|
- Queries `asset_software` + `software_catalog` (fast, indexed)
|
|
- Searchable by name, publisher
|
|
- Sortable by any column
|
|
- **Filter by category** dropdown (Browser, Security, Productivity, etc.)
|
|
- **Toggle "Show uninstalled"** to see software that was removed
|
|
- Version change indicator (if version differs from last sync)
|
|
- First seen date (when we first detected installation)
|
|
- Export to CSV option
|
|
|
|
**Data Hook**:
|
|
```typescript
|
|
function useAssetSoftware(assetId: string, options: {
|
|
search?: string;
|
|
category?: string;
|
|
showUninstalled?: boolean;
|
|
}) {
|
|
return useQuery({
|
|
queryKey: ['asset', assetId, 'software', options],
|
|
queryFn: () => getAssetSoftware(assetId, options),
|
|
});
|
|
}
|
|
```
|
|
|
|
#### 2.6.4 MaintenanceSchedulesTab
|
|
**File**: `server/src/components/assets/tabs/MaintenanceSchedulesTab.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
Upcoming
|
|
────────
|
|
[📅] Quarterly Disk Cleanup Due: 11/01/2025 [Mark Complete]
|
|
[📅] Annual Security Audit Due: 01/15/2026 [Mark Complete]
|
|
|
|
Past Maintenance
|
|
────────────────
|
|
[✓] Monthly Backup Verification Completed: 10/01/2025 by j.smith
|
|
[✓] Quarterly Disk Cleanup Completed: 08/01/2025 by t.jones
|
|
```
|
|
|
|
**Features**:
|
|
- Upcoming vs completed sections
|
|
- Due date with color coding (overdue = red)
|
|
- Mark complete action
|
|
- Link to schedule recurring tasks
|
|
|
|
#### 2.6.5 AuditLogTab (New)
|
|
**File**: `server/src/components/assets/tabs/AuditLogTab.tsx`
|
|
|
|
**Content**:
|
|
```
|
|
Timeline
|
|
────────
|
|
Today, 10:45 AM │ RMM Sync System synced device data from NinjaOne
|
|
Yesterday, 2:30 PM │ Field Updated Location changed from "Floor 1" to "Floor 2"
|
|
│ by j.smith
|
|
10/25/2025 │ Ticket Closed #12300 "New User Setup" resolved
|
|
10/20/2025 │ Asset Created Asset created by t.jones
|
|
```
|
|
|
|
**Features**:
|
|
- Timeline view of asset changes
|
|
- WHO/WHAT/WHEN for each entry
|
|
- Filter by change type
|
|
- Pagination for long histories
|
|
|
|
---
|
|
|
|
### 2.7 Shared Components
|
|
|
|
#### 2.7.1 StatusBadge
|
|
**File**: `server/src/components/assets/shared/StatusBadge.tsx`
|
|
|
|
```typescript
|
|
interface StatusBadgeProps {
|
|
status: 'online' | 'offline' | 'healthy' | 'warning' | 'critical' | 'unknown';
|
|
provider?: string; // e.g., "NinjaOne"
|
|
size?: 'sm' | 'md' | 'lg';
|
|
}
|
|
```
|
|
|
|
#### 2.7.2 UtilizationBar
|
|
**File**: `server/src/components/assets/shared/UtilizationBar.tsx`
|
|
|
|
```typescript
|
|
interface UtilizationBarProps {
|
|
value: number; // 0-100
|
|
label?: string; // e.g., "45%"
|
|
showLabel?: boolean;
|
|
colorThresholds?: { warning: number; critical: number };
|
|
}
|
|
```
|
|
|
|
#### 2.7.3 CopyableField
|
|
**File**: `server/src/components/assets/shared/CopyableField.tsx`
|
|
|
|
```typescript
|
|
interface CopyableFieldProps {
|
|
label: string;
|
|
value: string;
|
|
showCopyButton?: boolean;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.8 State Management & Data Fetching
|
|
|
|
#### 2.8.1 Asset Detail Data Hook
|
|
**File**: `server/src/hooks/useAssetDetail.ts`
|
|
|
|
```typescript
|
|
function useAssetDetail(assetId: string) {
|
|
// Fetch base asset data (includes notes_document_id)
|
|
const { data: asset, isLoading: assetLoading } = useQuery({
|
|
queryKey: ['asset', assetId],
|
|
queryFn: () => getAsset(assetId),
|
|
})
|
|
|
|
// Fetch summary metrics
|
|
const { data: metrics, isLoading: metricsLoading } = useQuery({
|
|
queryKey: ['asset', assetId, 'summary'],
|
|
queryFn: () => getAssetSummaryMetrics(assetId),
|
|
})
|
|
|
|
// Fetch cached RMM data from database (fast, no external API call)
|
|
const { data: rmmData, isLoading: rmmLoading, refetch: refetchRmm } = useQuery({
|
|
queryKey: ['asset', assetId, 'rmm'],
|
|
queryFn: () => getAssetRmmData(assetId),
|
|
// No automatic polling - data comes from our DB cache
|
|
// User can manually refresh via button
|
|
})
|
|
|
|
// Manual refresh function
|
|
const refreshRmmData = useMutation({
|
|
mutationFn: () => refreshAssetRmmData(assetId),
|
|
onSuccess: () => refetchRmm(),
|
|
})
|
|
|
|
return {
|
|
asset,
|
|
metrics,
|
|
rmmData,
|
|
isLoading: assetLoading || metricsLoading,
|
|
refreshRmmData: refreshRmmData.mutate,
|
|
isRefreshing: refreshRmmData.isPending,
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 2.8.2 Asset Notes Hook (Using Document System)
|
|
**File**: `server/src/hooks/useAssetNotes.ts`
|
|
|
|
```typescript
|
|
function useAssetNotes(assetId: string, notesDocumentId: string | null) {
|
|
// Fetch note content from document system
|
|
const { data: noteContent, isLoading } = useQuery({
|
|
queryKey: ['asset', assetId, 'notes'],
|
|
queryFn: () => getAssetNoteContent(assetId),
|
|
enabled: !!assetId,
|
|
})
|
|
|
|
// Save note mutation
|
|
const { mutate: saveNote, isPending: isSaving } = useMutation({
|
|
mutationFn: (blockData: PartialBlock[]) =>
|
|
saveAssetNote(assetId, blockData, currentUser.user_id),
|
|
onSuccess: () => {
|
|
// Invalidate asset query to get updated notes_document_id
|
|
queryClient.invalidateQueries({ queryKey: ['asset', assetId] });
|
|
},
|
|
})
|
|
|
|
return {
|
|
noteContent: noteContent?.blockData,
|
|
noteDocument: noteContent?.document,
|
|
isLoading,
|
|
saveNote,
|
|
isSaving,
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2.9 UI Task Checklist
|
|
|
|
**Core Layout Components:**
|
|
- [ ] Create AssetDetailView component (full-page layout)
|
|
- [ ] Create AssetDetailHeader with status badge and action buttons
|
|
- [ ] Create AssetMetricsBanner with four metric panels
|
|
- [ ] Create AssetDashboardGrid with responsive two-column layout
|
|
|
|
**Dashboard Panels:**
|
|
- [ ] Create RmmVitalsPanel with cached data display and refresh button
|
|
- [ ] Create HardwareSpecsPanel with utilization bars (uses cached disk_usage, cpu, memory)
|
|
- [ ] Create SecurityPatchingPanel with status indicators
|
|
- [ ] Create AssetInfoPanel with client link
|
|
- [ ] Create AssetNotesPanel with BlockNote editor (uses TextEditor component)
|
|
|
|
**Tab Components:**
|
|
- [ ] Enhance ServiceHistoryTab with sortable columns
|
|
- [ ] Create SoftwareInventoryTab with search/filter
|
|
- [ ] Enhance MaintenanceSchedulesTab with list view
|
|
- [ ] Create AuditLogTab with timeline view
|
|
|
|
**Shared Components:**
|
|
- [ ] Create StatusBadge shared component
|
|
- [ ] Create UtilizationBar shared component
|
|
- [ ] Create CopyableField shared component
|
|
|
|
**Hooks & State:**
|
|
- [ ] Create useAssetDetail hook (fetches asset, metrics, cached RMM data)
|
|
- [ ] Add manual refresh functionality to useAssetDetail
|
|
- [ ] Create useAssetNotes hook (integrates with document system)
|
|
|
|
**Polish & Testing:**
|
|
- [ ] Add loading skeletons for all panels
|
|
- [ ] Add error states for failed data fetching
|
|
- [ ] Implement responsive design for mobile/tablet
|
|
- [ ] Add keyboard navigation for accessibility
|
|
- [ ] Write component tests
|
|
- [ ] Update existing drawer to use new components (or replace entirely)
|
|
|
|
---
|
|
|
|
## Implementation Order
|
|
|
|
### Phase 1: Database Foundation
|
|
1. Database migrations:
|
|
- RMM cached fields on workstation/server tables
|
|
- `notes_document_id` on assets table
|
|
- Normalized software tables (`software_catalog`, `asset_software`)
|
|
- Helper view `v_asset_software_details`
|
|
2. Data migration script: Populate normalized tables from existing JSONB
|
|
3. Interface/type updates (Asset, RmmCachedData, SoftwareCatalogEntry, etc.)
|
|
|
|
### Phase 2: Sync Engine Updates
|
|
1. Device mapper enhancements for all cached RMM fields
|
|
2. Single-device refresh sync method (`syncSingleDeviceById`)
|
|
3. Refactor `softwareSync.ts` to use normalized tables
|
|
4. Implement `findOrCreateSoftwareCatalogEntry()` with deduplication
|
|
5. Implement soft-delete for uninstalled software
|
|
6. Add category inference for common software
|
|
|
|
### Phase 3: Backend API & Actions
|
|
1. `assetNoteActions.ts` (uses existing document system)
|
|
2. `rmmActions.ts` (getAssetRmmData, refreshAssetRmmData)
|
|
3. `softwareActions.ts` (getAssetSoftware, searchSoftwareFleetWide)
|
|
4. Summary metrics server action
|
|
5. RMM data endpoints (GET cached, POST refresh)
|
|
6. Software endpoints (asset software list, fleet search)
|
|
|
|
### Phase 4: Core UI Components
|
|
1. Shared components (StatusBadge, UtilizationBar, CopyableField)
|
|
2. Data fetching hooks (useAssetDetail, useAssetNotes, useAssetSoftware)
|
|
3. AssetDetailHeader
|
|
4. AssetMetricsBanner
|
|
|
|
### Phase 5: Dashboard Panels
|
|
1. AssetInfoPanel (uses existing data)
|
|
2. AssetNotesPanel (with BlockNote editor)
|
|
3. HardwareSpecsPanel (uses cached data)
|
|
4. SecurityPatchingPanel
|
|
5. RmmVitalsPanel (with refresh button)
|
|
|
|
### Phase 6: Tabbed Content
|
|
1. ServiceHistoryTab (enhance existing)
|
|
2. SoftwareInventoryTab (queries normalized tables, searchable, filterable by category)
|
|
3. MaintenanceSchedulesTab (enhance existing)
|
|
4. AuditLogTab (new)
|
|
|
|
### Phase 7: Integration & Polish
|
|
1. AssetDashboardGrid layout
|
|
2. AssetDetailView full page
|
|
3. Action button functionality (remote control, etc.)
|
|
4. Responsive design
|
|
5. Testing and refinement
|
|
6. Deprecate/remove JSONB `installed_software` columns (after validation)
|
|
|
|
---
|
|
|
|
## Dependencies & Considerations
|
|
|
|
### External Dependencies
|
|
- NinjaOne API access for device detail data
|
|
- Remote control URL generation (research needed)
|
|
- Script execution API (for Actions menu)
|
|
|
|
### Existing Code Impact
|
|
- `AssetDetailDrawer.tsx` - May be replaced or heavily modified
|
|
- `AssetDetails.tsx` - Components will be restructured
|
|
- `AssetForm.tsx` - May need updates for new fields
|
|
- Sync engine - Enhanced to populate all cached fields
|
|
|
|
### Leveraging Existing Systems
|
|
- **Document System**: Asset notes use existing `documents` + `document_block_content` tables
|
|
- **BlockNote Editor**: Use existing `TextEditor` component with @mentions
|
|
- **Document Actions**: Use existing `createBlockDocument()`, `getBlockContent()`, `updateBlockContent()`
|
|
- **Company Notes Pattern**: Follow same flow as `ClientDetails.tsx`
|
|
|
|
### EE vs CE Split
|
|
- RMM-specific panels (RmmVitalsPanel, SecurityPatchingPanel) are EE-only
|
|
- Base asset info and notes are CE (notes use CE document system)
|
|
- Graceful degradation when RMM not connected
|
|
|
|
### Performance Considerations
|
|
- **Database-cached RMM data**: Instant page load, no external API calls on view
|
|
- **Manual refresh**: User-triggered sync for fresh data when needed
|
|
- **Automatic sync**: Background job keeps cache reasonably fresh
|
|
- Use React Query for efficient data fetching and cache invalidation
|
|
- Lazy load tab content
|
|
- Skeleton loading states for perceived performance
|
|
|
|
---
|
|
|
|
## Task Breakdown
|
|
|
|
### Backend Phases & Tasks
|
|
|
|
#### Phase B1: Database Migrations
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B1.1 | Create migration for RMM cached fields (current_user, uptime, IPs, CPU, memory, disk_usage) on workstation_assets and server_assets | §1.2.1 |
|
|
| B1.2 | Create migration to add `notes_document_id` to assets table with FK to documents | §1.2.2 |
|
|
| B1.3 | Create migration for `software_catalog` table | §1.2.3 |
|
|
| B1.4 | Create migration for `asset_software` junction table | §1.2.3 |
|
|
| B1.5 | Create helper view `v_asset_software_details` | §1.2.3 |
|
|
| B1.6 | Create data migration script to populate normalized tables from existing JSONB | §1.2.3 |
|
|
|
|
#### Phase B2: Interface & Type Definitions
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B2.1 | Add `notes_document_id` to Asset interface | §1.5.1 |
|
|
| B2.2 | Create `RmmCachedData` interface | §1.5.1 |
|
|
| B2.3 | Create `RmmStorageInfo` interface | §1.5.1 |
|
|
| B2.4 | Create `RmmWorkstationCacheFields` interface | §1.5.1 |
|
|
| B2.5 | Create `AssetSummaryMetrics` interface | §1.5.1 |
|
|
| B2.6 | Create `software.interfaces.ts` with SoftwareCatalogEntry, AssetSoftwareInstall, etc. | §1.5.1 |
|
|
|
|
#### Phase B3: Sync Engine Updates
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B3.1 | Update device mapper to extract current_user, uptime_seconds | §1.3.1 |
|
|
| B3.2 | Update device mapper to extract lan_ip, wan_ip | §1.3.1 |
|
|
| B3.3 | Update device mapper to extract cpu_utilization_percent, memory stats | §1.3.1 |
|
|
| B3.4 | Update device mapper to extract disk_usage array | §1.3.1 |
|
|
| B3.5 | Add helper functions: extractPrimaryLanIp, calculateMemoryUsedGb, mapDiskUsage | §1.3.1 |
|
|
| B3.6 | Add `syncSingleDeviceById()` method for on-demand refresh | §1.3.2 |
|
|
| B3.7 | Refactor `softwareSync.ts` to use normalized tables | §1.3.3 |
|
|
| B3.8 | Implement `findOrCreateSoftwareCatalogEntry()` with deduplication | §1.3.3 |
|
|
| B3.9 | Implement soft-delete for uninstalled software (is_current = false) | §1.3.3 |
|
|
| B3.10 | Add `inferSoftwareCategory()` for auto-categorization | §1.3.3 |
|
|
|
|
#### Phase B4: Server Actions
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B4.1 | Create `assetNoteActions.ts` with `getAssetNoteContent()` | §1.4.2 |
|
|
| B4.2 | Add `saveAssetNote()` to assetNoteActions.ts | §1.4.2 |
|
|
| B4.3 | Create `rmmActions.ts` with `getAssetRmmData()` | §1.4.3 |
|
|
| B4.4 | Add `refreshAssetRmmData()` to rmmActions.ts | §1.4.3 |
|
|
| B4.5 | Add `getAssetRemoteControlUrl()` to rmmActions.ts | §1.4.3 |
|
|
| B4.6 | Add `triggerRmmReboot()` and `triggerRmmScript()` to rmmActions.ts | §1.4.3 |
|
|
| B4.7 | Add `getAssetSummaryMetrics()` server action | §1.4.1 |
|
|
| B4.8 | Create `softwareActions.ts` with `getAssetSoftware()` | §1.6 (checklist) |
|
|
| B4.9 | Add `searchSoftwareFleetWide()` to softwareActions.ts | §1.6 (checklist) |
|
|
|
|
#### Phase B5: API Endpoints
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B5.1 | Create `GET /api/assets/[assetId]/rmm` endpoint (reads from DB cache) | §1.1.1 |
|
|
| B5.2 | Create `POST /api/assets/[assetId]/rmm/refresh` endpoint | §1.1.1 |
|
|
| B5.3 | Create `GET /api/assets/[assetId]/summary` endpoint | §1.1.2 |
|
|
| B5.4 | Create `GET /api/assets/[assetId]/software` endpoint (paginated, filterable) | §1.6 (checklist) |
|
|
| B5.5 | Create `GET /api/software/search` endpoint (fleet-wide search) | §1.6 (checklist) |
|
|
|
|
#### Phase B6: RMM Integration Research
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B6.1 | Research NinjaOne API for remote control URL generation | §1.1.3 |
|
|
| B6.2 | Implement remote control URL generation | §1.1.3 |
|
|
| B6.3 | Research NinjaOne API for script execution | §1.6 (checklist) |
|
|
| B6.4 | Implement script execution actions | §1.4.3 |
|
|
|
|
#### Phase B7: Backend Testing
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B7.1 | Add unit tests for RMM data endpoints | §1.6 (checklist) |
|
|
| B7.2 | Add unit tests for summary metrics endpoint | §1.6 (checklist) |
|
|
| B7.3 | Add integration tests for RMM data sync and refresh | §1.6 (checklist) |
|
|
| B7.4 | Add tests for software sync with normalized tables | §1.6 (checklist) |
|
|
| B7.5 | Add tests for software deduplication logic | §1.6 (checklist) |
|
|
| B7.6 | Add tests for asset note actions | §1.6 (checklist) |
|
|
|
|
#### Phase B8: Cleanup
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| B8.1 | Validate normalized software data matches JSONB data | §1.2.3 |
|
|
| B8.2 | Deprecate/remove JSONB `installed_software` columns | Implementation Order |
|
|
|
|
---
|
|
|
|
### Frontend Phases & Tasks
|
|
|
|
#### Phase F1: Shared Components
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F1.1 | Create `StatusBadge` component (online/offline/healthy/warning/critical) | §2.7.1 |
|
|
| F1.2 | Create `UtilizationBar` component with color thresholds | §2.7.2 |
|
|
| F1.3 | Create `CopyableField` component with clipboard button | §2.7.3 |
|
|
|
|
#### Phase F2: Data Fetching Hooks
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F2.1 | Create `useAssetDetail` hook (fetches asset, metrics, cached RMM data) | §2.8.1 |
|
|
| F2.2 | Add manual refresh functionality to useAssetDetail | §2.8.1 |
|
|
| F2.3 | Create `useAssetNotes` hook (integrates with document system) | §2.8.2 |
|
|
| F2.4 | Create `useAssetSoftware` hook (paginated, filterable) | §2.6.3 |
|
|
|
|
#### Phase F3: Header & Metrics Banner
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F3.1 | Create `AssetDetailHeader` component structure | §2.2.1 |
|
|
| F3.2 | Add asset type icon and name display | §2.2.1 |
|
|
| F3.3 | Add RMM status badge (Online/Offline - Provider) | §2.2.1 |
|
|
| F3.4 | Add Remote Control button (links to RMM) | §2.2.1 |
|
|
| F3.5 | Add Create Ticket button | §2.2.1 |
|
|
| F3.6 | Add Actions dropdown (Edit, Reboot, Run Script, Archive, Delete) | §2.2.1 |
|
|
| F3.7 | Create `AssetMetricsBanner` component structure | §2.3.1 |
|
|
| F3.8 | Add Health Status panel with tooltip | §2.3.1 |
|
|
| F3.9 | Add Open Tickets panel (clickable) | §2.3.1 |
|
|
| F3.10 | Add Security Status panel | §2.3.1 |
|
|
| F3.11 | Add Warranty panel with color-coded countdown | §2.3.1 |
|
|
|
|
#### Phase F4: Dashboard Panels - Left Column
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F4.1 | Create `RmmVitalsPanel` component structure | §2.5.1 |
|
|
| F4.2 | Add agent status with last check-in time | §2.5.1 |
|
|
| F4.3 | Add current user display | §2.5.1 |
|
|
| F4.4 | Add uptime display (formatted) | §2.5.1 |
|
|
| F4.5 | Add last RMM sync timestamp ("as of" indicator) | §2.5.1 |
|
|
| F4.6 | Add network IPs with copy buttons | §2.5.1 |
|
|
| F4.7 | Add Refresh button with loading state | §2.5.1 |
|
|
| F4.8 | Create `HardwareSpecsPanel` component | §2.5.2 |
|
|
| F4.9 | Add CPU info with utilization bar | §2.5.2 |
|
|
| F4.10 | Add RAM info with utilization bar | §2.5.2 |
|
|
| F4.11 | Add storage drives with utilization bars | §2.5.2 |
|
|
| F4.12 | Add GPU info | §2.5.2 |
|
|
| F4.13 | Create `SecurityPatchingPanel` component | §2.5.3 |
|
|
| F4.14 | Add OS version display | §2.5.3 |
|
|
| F4.15 | Add antivirus status with product name | §2.5.3 |
|
|
| F4.16 | Add patch status with severity breakdown | §2.5.3 |
|
|
| F4.17 | Add firewall status | §2.5.3 |
|
|
|
|
#### Phase F5: Dashboard Panels - Right Column
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F5.1 | Create `AssetInfoPanel` component | §2.5.4 |
|
|
| F5.2 | Add client name with link to dashboard | §2.5.4 |
|
|
| F5.3 | Add location display | §2.5.4 |
|
|
| F5.4 | Add model display | §2.5.4 |
|
|
| F5.5 | Add serial number with copy button | §2.5.4 |
|
|
| F5.6 | Add purchase date display | §2.5.4 |
|
|
| F5.7 | Add warranty end date with status indicator | §2.5.4 |
|
|
| F5.8 | Create `AssetNotesPanel` component | §2.5.5 |
|
|
| F5.9 | Integrate BlockNote TextEditor for notes | §2.5.5 |
|
|
| F5.10 | Add save functionality (auto-save or button) | §2.5.5 |
|
|
| F5.11 | Add last updated timestamp and author | §2.5.5 |
|
|
| F5.12 | Handle empty state ("Add a note...") | §2.5.5 |
|
|
|
|
#### Phase F6: Dashboard Layout
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F6.1 | Create `AssetDashboardGrid` component with CSS Grid | §2.4.1 |
|
|
| F6.2 | Implement two-column layout (2fr 1fr) | §2.4.1 |
|
|
| F6.3 | Add responsive stacking for mobile | §2.4.1 |
|
|
|
|
#### Phase F7: Tabbed Content
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F7.1 | Create `AssetDetailTabs` container component | §2.6.1 |
|
|
| F7.2 | Enhance `ServiceHistoryTab` with sortable columns | §2.6.2 |
|
|
| F7.3 | Add ticket ID links to ticket detail | §2.6.2 |
|
|
| F7.4 | Add status badges and pagination | §2.6.2 |
|
|
| F7.5 | Create `SoftwareInventoryTab` component | §2.6.3 |
|
|
| F7.6 | Add search input for software name/publisher | §2.6.3 |
|
|
| F7.7 | Add category filter dropdown | §2.6.3 |
|
|
| F7.8 | Add "Show uninstalled" toggle | §2.6.3 |
|
|
| F7.9 | Add sortable columns and pagination | §2.6.3 |
|
|
| F7.10 | Add CSV export option | §2.6.3 |
|
|
| F7.11 | Enhance `MaintenanceSchedulesTab` with list view | §2.6.4 |
|
|
| F7.12 | Add upcoming vs completed sections | §2.6.4 |
|
|
| F7.13 | Add mark complete action | §2.6.4 |
|
|
| F7.14 | Create `AuditLogTab` component | §2.6.5 |
|
|
| F7.15 | Add timeline view with WHO/WHAT/WHEN | §2.6.5 |
|
|
| F7.16 | Add change type filter | §2.6.5 |
|
|
|
|
#### Phase F8: Full Page Assembly
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F8.1 | Create `AssetDetailView` full-page component | §2.1 |
|
|
| F8.2 | Integrate header, metrics banner, dashboard grid, and tabs | §2.1 |
|
|
| F8.3 | Add loading skeletons for all panels | §2.9 (checklist) |
|
|
| F8.4 | Add error states for failed data fetching | §2.9 (checklist) |
|
|
| F8.5 | Wire up action buttons (remote control, create ticket, etc.) | §2.2.1 |
|
|
|
|
#### Phase F9: Polish & Accessibility
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F9.1 | Implement responsive design for tablet | §2.9 (checklist) |
|
|
| F9.2 | Implement responsive design for mobile | §2.9 (checklist) |
|
|
| F9.3 | Add keyboard navigation | §2.9 (checklist) |
|
|
| F9.4 | Add ARIA labels and roles | §2.9 (checklist) |
|
|
| F9.5 | Update existing drawer to use new components (or replace) | §2.9 (checklist) |
|
|
|
|
#### Phase F10: Frontend Testing
|
|
| Task | Description | Plan Reference |
|
|
|------|-------------|----------------|
|
|
| F10.1 | Write tests for shared components | §2.9 (checklist) |
|
|
| F10.2 | Write tests for data hooks | §2.9 (checklist) |
|
|
| F10.3 | Write tests for dashboard panels | §2.9 (checklist) |
|
|
| F10.4 | Write tests for tab components | §2.9 (checklist) |
|
|
| F10.5 | Write integration tests for full page | §2.9 (checklist) |
|
|
|
|
---
|
|
|
|
## Task Summary
|
|
|
|
### Backend: 8 Phases, 44 Tasks
|
|
| Phase | Name | Tasks |
|
|
|-------|------|-------|
|
|
| B1 | Database Migrations | 6 |
|
|
| B2 | Interface & Type Definitions | 6 |
|
|
| B3 | Sync Engine Updates | 10 |
|
|
| B4 | Server Actions | 9 |
|
|
| B5 | API Endpoints | 5 |
|
|
| B6 | RMM Integration Research | 4 |
|
|
| B7 | Backend Testing | 6 |
|
|
| B8 | Cleanup | 2 |
|
|
|
|
### Frontend: 10 Phases, 55 Tasks
|
|
| Phase | Name | Tasks |
|
|
|-------|------|-------|
|
|
| F1 | Shared Components | 3 |
|
|
| F2 | Data Fetching Hooks | 4 |
|
|
| F3 | Header & Metrics Banner | 11 |
|
|
| F4 | Dashboard Panels - Left Column | 17 |
|
|
| F5 | Dashboard Panels - Right Column | 12 |
|
|
| F6 | Dashboard Layout | 3 |
|
|
| F7 | Tabbed Content | 16 |
|
|
| F8 | Full Page Assembly | 5 |
|
|
| F9 | Polish & Accessibility | 5 |
|
|
| F10 | Frontend Testing | 5 |
|
|
|
|
### Recommended Execution Order
|
|
1. **B1** → **B2** → **B3** (Database and sync engine foundation)
|
|
2. **B4** → **B5** (Server actions and API endpoints)
|
|
3. **F1** → **F2** (Shared components and hooks - can start once B4/B5 are done)
|
|
4. **F3** → **F4** → **F5** → **F6** (UI components in parallel with backend)
|
|
5. **B6** (RMM integration research - can be parallel)
|
|
6. **F7** → **F8** (Tabs and full page assembly)
|
|
7. **B7** → **F9** → **F10** (Testing and polish)
|
|
8. **B8** (Cleanup after everything is validated)
|