SQLAlchemy's text() interprets :name::type as two named parameters.
Fixes syntax errors in link discovery, signal history, and SNMP profile
CRUD that caused 500 errors at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SQLAlchemy's text() interprets ::jsonb as a named parameter binding.
Use CAST(:profile_data AS jsonb) to avoid the collision.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds SNMP Device Profiles card to SettingsPage for discoverability.
Adds device_count correlated subquery to profile list SQL and schema
field so the frontend profile cards show accurate device counts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Flatten tree for tanstack/react-virtual virtualization
- Expand/collapse branch nodes with chevron toggle
- Checkbox selection for leaf nodes (selectable OIDs)
- Search filter by name or OID substring
- Expand all / collapse all toolbar buttons
- ARIA tree roles for accessibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add gosmi v1.0.4 dependency for MIB parsing
- Create poller/cmd/mib-parser/ with main.go and tree.go
- CLI accepts MIB file path and optional --search-path
- Outputs JSON OID tree with oid, name, description, type, access, status, children
- Errors output as JSON {"error":"..."} to stdout (exit 0) for Python backend
- Panic recovery wraps gosmi LoadModule for malformed MIBs
- Parent-child tree built from OID hierarchy with numeric sort
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /snmp-profiles/parse-mib: upload MIB file, subprocess-call tod-mib-parser, return OID tree JSON
- POST /snmp-profiles/{id}/test: test profile connectivity via NATS discovery probe to poller
- New snmp_proxy service module following routeros_proxy.py lazy NATS pattern
- Pydantic schemas: MIBParseResponse, ProfileTestRequest, ProfileTestResponse, ProfileTestOIDResult
- MIB_PARSER_PATH config setting with /app/tod-mib-parser default
- MIB parse errors return 422, not 500; temp file cleanup in finally block
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Shows assigned SNMP profile name with system badge indicator
- Shows profile description when available
- Returns null when no snmpProfileId is assigned
- Fetches profile data via snmpProfilesApi.get
- Foundation for Phase 20 custom OID charting (PROF-03)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Credential profile dropdown filtered by device type (routeros/snmp)
- IP textarea parses one-per-line IPv4 addresses with deduplication
- Optional hostname prefix generates numbered names (e.g., tower-ap-01)
- SNMP variant shows SNMP port and device profile selector
- RouterOS variant shows API port and TLS API port fields
- Results display with per-device CheckCircle2/XCircle success/failure icons
- Calls devicesApi.bulkAddWithProfile for backend bulk add endpoint
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add DeviceTypeIcon component (Router for RouterOS, Network for SNMP)
- Add Type column to desktop table between Status and Hostname
- Add type icon to mobile DeviceCard view
- Show em-dash for RouterOS version on SNMP devices
- Add device type filter dropdown to DeviceFilters (All / RouterOS / SNMP)
- Pass device_type through URL search params to API query
- Update colSpan from 11 to 12 for empty/loading states
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Always show three-tab layout (RouterOS, SNMP, VPN) instead of conditional two-tab
- RouterOS tab: credential profile toggle (profile mode vs manual credentials)
- SNMP tab: version selector (v2c/v3), credential profile, device profile, port
- Both tabs have "Add Multiple" toggle to switch to BulkAddForm
- VPN tab renders existing VpnOnboardingWizard unchanged
- All form state resets on dialog close
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create settings.credentials.tsx route using dot-notation pattern matching api-keys
- RBAC guard restricts access to tenant_admin and above
- Super admin org selector integration for multi-tenant support
- Add Credential Profiles card to Settings page under Device Credentials section
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Create CredentialProfilesPage component with full CRUD for RouterOS and SNMP profiles
- Add credentialProfilesApi types and client to api.ts (blocking dependency from 19-01)
- Profile list grouped by type with device count, edit, and delete actions
- Create/edit dialog with conditional fields per credential type
- SNMPv3 form shows auth/privacy fields based on security_level selection
- Delete confirmation with 409 error handling for linked devices
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SNMP collector was missing the per-device Redis lock that prevents
duplicate polls across pods. Rather than adding the lock to each
collector individually, lift it into runDeviceLoop so ALL collector
types (RouterOS and SNMP) are protected uniformly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Additive field with omitempty tag for SNMP device identification
- Existing RouterOS events produce identical JSON (field not set)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SNMPCollector implements poller.Collector interface
- Profile-driven OID collection: scalars via Get, tables via Walk/BulkWalk
- BulkWalk wrapped in withTimeout to prevent indefinite hangs
- SNMPv1 uses Walk, v2c/v3 uses BulkWalk (MaxRepetitions=10)
- Safety valve: walkTable aborts at 10,000 PDUs per walk
- Counter delta computation via CounterCache for rate metrics
- Standard metrics routed to DeviceMetricsEvent (interfaces, health)
- Custom metrics routed to SNMPMetricsEvent (snmp_custom)
- DeviceStatusEvent published with online/offline status
- Each poll group collects independently (partial failure tolerant)
- Credential resolution via GetRawCredentials + ParseSNMPCredentials
- ifXTable Counter64 data supersedes ifTable Counter32 via PreferOver
- ssCpuIdle invert_percent transform for CPU load fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- NATS request-reply on device.discover.snmp with queue group discover-workers
- Probes sysObjectID.0, sysDescr.0, sysName.0 via SNMP GET
- Credentials from request payload (not database), never logged
- 5-second timeout on both connect and GET operations
- Supports SNMP v1, v2c, and v3 with all auth/priv protocols
- Builds gosnmp client inline to avoid snmp->bus import cycle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Subscribe/unsubscribe lifecycle
- Invalid JSON returns error response
- Missing ip_address returns descriptive error
- Response JSON field names match spec
- Invalid SNMP version rejected
- Default port 161 when zero
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Implement computeCounterDelta for Counter32/Counter64 with wraparound handling
- Sanity threshold discards deltas > 90% of max value (device reset detection)
- CounterCache uses Redis MGET/MSET pipelining for efficient state persistence
- Counter keys use "snmp:counter:{device_id}:{oid}" format with 600s TTL
- Add SNMPMetricsEvent and SNMPMetricEntry structs to bus package
- Add PublishSNMPMetrics publishing to "device.metrics.snmp_custom.{device_id}"
- Full test coverage: 10 counter tests including miniredis integration
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Install gosnmp v1.43.2 as direct dependency
- Create snmp package with SNMPConfig, CompiledProfile, PollGroup types
- Implement BuildSNMPClient for v1, v2c, and v3 (all security levels)
- Map auth protocols (MD5, SHA, SHA224-512) and priv protocols (DES, AES128-256)
- MaxRepetitions set to 10 (not gosnmp default 50) for embedded devices
- Full test coverage: 9 tests covering all SNMP versions and protocol mappings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /tenants/{tenant_id}/devices/bulk endpoint with rate limiting
- bulk_add_with_profile service validates profile ownership and type compatibility
- Duplicate IP check prevents adding same IP twice in one tenant
- TCP reachability check for RouterOS devices, skipped for SNMP (UDP)
- Per-device result reporting with partial success support
- Device model updated with device_type, snmp_port, snmp_version, snmp_profile_id columns
- Audit logging for bulk add operations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- BulkAddWithProfileRequest with credential_profile_id, device_type, defaults
- BulkAddDeviceEntry with IP address validation
- BulkAddDefaults for type-appropriate port/TLS defaults
- BulkAddDeviceResult and BulkAddWithProfileResult for per-device reporting
- Existing BulkAddRequest preserved for backward compatibility
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Service with CRUD + Transit encryption for all new credential writes
- Router with 6 endpoints under /tenants/{tenant_id}/credential-profiles
- Delete returns HTTP 409 with device_count when devices reference profile
- Registered credential_profiles_router in main.py
- DeviceUpdate schema accepts optional credential_profile_id
- update_device validates profile belongs to tenant before assigning
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SQLAlchemy model mapping to credential_profiles table (migration 037)
- CredentialProfileCreate with model_validator enforcing per-type required fields
- CredentialProfileUpdate with conditional validation on type change
- CredentialProfileResponse without any credential fields (write-only)
- Device model updated with credential_profile_id FK and relationship
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add _insert_snmp_custom_metrics handler for custom SNMP OID events
- Insert all 9 columns into snmp_metrics hypertable
- Change unknown metric types from ACK to NAK for redelivery safety
- Prevents permanent data loss during deployment ordering mismatches
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add collectors map[string]Collector field to Scheduler struct
- Register RouterOSCollector for "routeros" inside NewScheduler
- Replace direct PollDevice call with collector dispatch by dev.DeviceType
- Default empty DeviceType to "routeros" for backward compatibility
- Log error and exit device loop for unknown device types
- Circuit breaker logic unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- GetRawCredentials resolves credentials: device transit, device legacy, profile transit, profile legacy
- Cache key includes source (device/profile) to prevent cross-source poisoning
- GetCredentials is now a backward-compatible wrapper calling GetRawCredentials + ParseRouterOSCredentials
- Add DecryptRaw to device package for raw byte decryption without JSON parsing
- Invalidate clears both parsed and raw cache entries
- All existing callers (PollDevice, CmdResponder, TunnelResponder, BackupResponder, SSHRelay) unchanged
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SNMPCredential struct with v1/v2c/v3 field support
- ParseRouterOSCredentials handles typed and legacy no-type-field JSON
- ParseSNMPCredentials handles snmp_v1, snmp_v2c, snmp_v3 types
- credentialEnvelope for type-agnostic type field peeking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Assert DeviceType defaults to "routeros" via COALESCE
- Assert SNMPPort defaults to 161 via COALESCE
- Assert SNMPVersion, SNMPProfileID, CredentialProfileID are nil for
existing RouterOS devices without profile links
- Assert ProfileEncryptedCredentials and ProfileEncryptedCredentialsTransit
are nil when no credential profile is linked
- Update test schema with device_type, snmp_port, snmp_version,
snmp_profile_id, credential_profile_id columns
- Add credential_profiles table to test schema for LEFT JOIN
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add 7 new fields to store.Device: DeviceType, SNMPPort, SNMPVersion,
SNMPProfileID, CredentialProfileID, ProfileEncryptedCredentials,
ProfileEncryptedCredentialsTransit
- Update FetchDevices query with LEFT JOIN credential_profiles and
expanded WHERE clause (credential_profile_id IS NOT NULL)
- Update GetDevice query with same JOIN and new columns
- COALESCE defaults: device_type='routeros', snmp_port=161
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- devices table: device_type (default 'routeros'), snmp_port (default 161),
snmp_version, snmp_profile_id FK -> snmp_profiles, credential_profile_id
FK -> credential_profiles, with lock_timeout = 3s for safe ALTER
- snmp_metrics: hypertable with 90-day retention, composite index on
(device_id, metric_name, time DESC), RLS with tenant isolation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- credential_profiles: UUID PK, tenant_id FK with CASCADE, credential_type,
encrypted credential fields, unique(tenant_id, name), RLS, poller_user GRANT
- snmp_profiles: UUID PK, nullable tenant_id for system profiles, profile_data
JSONB, partial unique indexes for tenant vs system name uniqueness, RLS with
system profile visibility to all tenants, poller_user GRANT
- 6 system seed profiles: generic-snmp, network-switch, network-router,
wireless-ap, ups-device, mikrotik-snmp with full OID collection definitions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Expanded SSH now uses left: var(--sidebar-width) instead of inset-4,
so it fills the content area without covering the sidebar or header.
Styled header/buttons to match Warm Precision.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Go Postgres driver defaults to requiring TLS. Container-to-container
Postgres doesn't have TLS configured. Without sslmode=disable the
poller crashes in a restart loop on fresh installs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The wizard previously hardcoded https:// for APP_BASE_URL and
CORS_ORIGINS. LAN and dev deployments without TLS need http:// or
browsers silently drop Secure cookies, causing login to fail.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>