towerops/lib/towerops_web/controllers/api_docs_html/index.html.heex
Graham McIntire d7741cdc0e
feat: implement unified checks system (Phase 1-4)
This commit implements the unified checks architecture that consolidates
SNMP monitoring with HTTP/TCP/DNS checks under a single "check" abstraction,
enabling consistent graphing, alerting, and management across all check types.

Database Changes:
- Add source_type and source_id to checks table for tracking auto-discovered
  vs manually created checks
- Add value field to check_results for storing numeric sensor readings
- Maintain backward compatibility with existing check_results data

New SNMP Executors:
- SnmpSensorExecutor: Poll sensor OIDs and return formatted values with
  status determination (OK/WARNING/CRITICAL based on limits)
- SnmpInterfaceExecutor: Poll interface stats (bandwidth, packets, errors)
- SnmpProcessorExecutor: Poll CPU/processor usage
- SnmpStorageExecutor: Poll disk/memory usage with percentage calculations

Check Execution Worker:
- CheckExecutorWorker: Unified Oban worker that dispatches to appropriate
  executor based on check_type (snmp_sensor, snmp_interface, http, tcp, etc.)
- Self-schedules next execution with distributed polling offsets
- Records results in check_results TimescaleDB hypertable
- Updates check state (OK/WARNING/CRITICAL/UNKNOWN)

Discovery Integration:
- Auto-creates checks during SNMP discovery for sensors, interfaces,
  processors, and storage
- Links checks to source entities via source_id for data lookup
- Enables/disables checks based on discovery results

UI Enhancements:
- Checks tab on device detail page with grouped display
- FormComponent for adding manual HTTP/TCP/DNS checks
- Empty state with "Run Discovery" prompt
- Check status badges and last checked times

Graphing:
- Update GraphLive to accept check_id parameter
- Query check_results table for time-series data
- Support all check types (SNMP, HTTP response times, etc.)

Testing:
- Comprehensive test suite for SnmpSensorExecutor (5 tests)
- Test suite for CheckExecutorWorker (7 tests)
- Test coverage for discovery check creation (6 tests)
- Remove deprecated monitoring_test.exs testing old API

Bug Fixes:
- Fix SNMP executors reading credentials from correct Device schema fields
  (device.snmp_version instead of device.snmp_device.version)
- Update agent channel test to query MonitoringCheck table directly

Code Quality:
- Extract add_snmp_credentials helper to reduce cyclomatic complexity
- Use map-based lookups for sensor formatting and check type grouping
- Apply pattern matching in dispatcher to reduce complexity
- All credo checks passing with no issues

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-12 16:58:40 -06:00

1742 lines
71 KiB
Text

<!-- Sidebar -->
<div class="hidden lg:fixed lg:inset-y-0 lg:z-40 lg:flex lg:w-72 lg:flex-col xl:w-80">
<div class="flex grow flex-col gap-y-5 overflow-y-auto border-r border-zinc-900/10 px-6 pt-4 pb-8 dark:border-white/10">
<!-- Logo -->
<div class="flex h-16 shrink-0 items-center">
<.link navigate={~p"/"} class="text-2xl font-bold text-zinc-900 dark:text-white">
Towerops API
</.link>
</div>
<!-- Navigation -->
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-7">
<!-- Guides Section -->
<li>
<h2 class="text-xs font-semibold text-zinc-900 dark:text-white">Guides</h2>
<ul role="list" class="mt-3 space-y-1 border-l border-zinc-900/10 dark:border-white/5">
<li>
<a
href="#introduction"
data-nav-link="introduction"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Introduction
</a>
</li>
<li>
<a
href="#authentication"
data-nav-link="authentication"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Authentication
</a>
</li>
<li>
<a
href="#errors"
data-nav-link="errors"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Errors
</a>
</li>
</ul>
</li>
<!-- Resources Section -->
<li>
<h2 class="text-xs font-semibold text-zinc-900 dark:text-white">Resources</h2>
<ul role="list" class="mt-3 space-y-1 border-l border-zinc-900/10 dark:border-white/5">
<li>
<a
href="#sites"
data-nav-link="sites"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Sites
</a>
</li>
<li>
<a
href="#devices"
data-nav-link="devices"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Devices
</a>
</li>
<li>
<a
href="#account-data"
data-nav-link="account-data"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Account Data (GDPR)
</a>
</li>
<li>
<a
href="#graphs"
data-nav-link="graphs"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Graphs & Live Polling
</a>
</li>
</ul>
</li>
<!-- Tools Section -->
<li>
<h2 class="text-xs font-semibold text-zinc-900 dark:text-white">Tools</h2>
<ul role="list" class="mt-3 space-y-1 border-l border-zinc-900/10 dark:border-white/5">
<li>
<a
href="#terraform"
data-nav-link="terraform"
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Terraform Provider
</a>
</li>
</ul>
</li>
</ul>
</nav>
</div>
</div>
<!-- Main content -->
<div class="lg:ml-72 xl:ml-80">
<div class="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
<main class="flex-auto max-w-7xl">
<!-- Introduction -->
<section id="introduction" class="scroll-mt-24 mb-16">
<h1 class="text-4xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-5xl">
Towerops API Reference
</h1>
<p class="mt-4 text-lg text-gray-600 dark:text-gray-400">
Use the Towerops API to manage your network monitoring infrastructure programmatically. Create and monitor sites, devices, and alerts.
</p>
</section>
<!-- Authentication -->
<section id="authentication" class="scroll-mt-24 mb-16">
<h2 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
Authentication
</h2>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
All API requests require authentication using an API token. Include your token in the Authorization header:
</p>
<div class="mt-6 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl https://towerops.net/api/v1/sites \\
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
API tokens are created and managed in your organization settings. Each token is scoped to a single organization.
</p>
</section>
<!-- Errors -->
<section id="errors" class="scroll-mt-24 mb-16">
<h2 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
Errors
</h2>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
The API uses conventional HTTP response codes to indicate success or failure:
</p>
<div class="mt-6">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th class="py-3 text-left text-sm font-semibold text-zinc-900 dark:text-white">
Code
</th>
<th class="py-3 text-left text-sm font-semibold text-zinc-900 dark:text-white">
Description
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<tr>
<td class="py-3 text-sm font-mono text-emerald-600 dark:text-emerald-400">200</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">Success</td>
</tr>
<tr>
<td class="py-3 text-sm font-mono text-emerald-600 dark:text-emerald-400">201</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">
Created successfully
</td>
</tr>
<tr>
<td class="py-3 text-sm font-mono text-red-600 dark:text-red-400">400</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">
Bad request - missing or invalid parameters
</td>
</tr>
<tr>
<td class="py-3 text-sm font-mono text-red-600 dark:text-red-400">401</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">
Unauthorized - invalid or missing API token
</td>
</tr>
<tr>
<td class="py-3 text-sm font-mono text-red-600 dark:text-red-400">403</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">
Forbidden - you don't have access to this resource
</td>
</tr>
<tr>
<td class="py-3 text-sm font-mono text-red-600 dark:text-red-400">404</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">
Not found - resource doesn't exist
</td>
</tr>
<tr>
<td class="py-3 text-sm font-mono text-red-600 dark:text-red-400">422</td>
<td class="py-3 text-sm text-gray-600 dark:text-gray-400">
Unprocessable entity - validation errors
</td>
</tr>
</tbody>
</table>
</div>
</section>
<hr class="my-16 border-gray-200 dark:border-white/10" />
<!-- Sites Resource -->
<section id="sites" class="scroll-mt-24 mb-16">
<h2 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
Sites
</h2>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
Sites represent physical locations where your network devices are deployed. Each site can have multiple devices and may have default SNMP configuration.
</p>
<!-- Site Model -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">The site model</h3>
<div class="mt-6">
<dl class="divide-y divide-gray-200 dark:divide-white/10">
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
id
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Unique identifier for the site (UUID).
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
name
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
The name of the site.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
location
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Physical location or address of the site.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_community
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Default SNMP community string for devices at this site (optional).
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
inserted_at
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
timestamp
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Timestamp when the site was created.
</dd>
</div>
</dl>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- List all sites -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">List all sites</h3>
<span class="inline-flex items-center rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/30">
GET
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">/api/v1/sites</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<!-- Left column - description -->
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Lists all sites for the authenticated organization.
</p>
</div>
<!-- Right column - code example -->
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl -G https://towerops.net/api/v1/sites \\
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"sites": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Main Office",
"location": "New York, NY",
"snmp_community": "public",
"inserted_at": "2026-01-15T19:44:25Z"
}
]
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Create a site -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Create a site</h3>
<span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
POST
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">/api/v1/sites</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<!-- Left column -->
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Creates a new site for the authenticated organization.
</p>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Required parameters
</h4>
<dl class="mt-3 divide-y divide-gray-200 dark:divide-white/10">
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
name
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
The name of the site.
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
location
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Physical location of the site.
</dd>
</div>
</dl>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Optional parameters
</h4>
<dl class="mt-3 divide-y divide-gray-200 dark:divide-white/10">
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_community
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Default SNMP community string for devices at this site.
</dd>
</div>
</dl>
</div>
<!-- Right column -->
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl https://towerops.net/api/v1/sites \
-H "Authorization: Bearer #{@sample_token}" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "Main Office",
"location": "New York, NY",
"snmp_community": "public"
}
}'
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Main Office",
"location": "New York, NY",
"snmp_community": "public",
"inserted_at": "2026-01-15T19:44:25Z"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Get a site -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Retrieve a site</h3>
<span class="inline-flex items-center rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/30">
GET
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/sites/:id
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Retrieves a single site by ID.
</p>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl https://towerops.net/api/v1/sites/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Main Office",
"location": "New York, NY",
"snmp_community": "public",
"inserted_at": "2026-01-15T19:44:25Z"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Update a site -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Update a site</h3>
<span class="inline-flex items-center rounded-md bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/30">
PATCH
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/sites/:id
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Updates an existing site. Only provided fields will be updated.
</p>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl -X PATCH https://towerops.net/api/v1/sites/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer #{@sample_token}" \
-H "Content-Type: application/json" \
-d '{
"site": {
"name": "Updated Office Name"
}
}'
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Updated Office Name",
"location": "New York, NY",
"snmp_community": "public",
"inserted_at": "2026-01-15T19:44:25Z"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Delete a site -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Delete a site</h3>
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/30">
DELETE
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/sites/:id
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Deletes a site. Note: This will also delete all devices associated with the site.
</p>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl -X DELETE https://towerops.net/api/v1/sites/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"success": true
}
""") %></code></pre>
</div>
</div>
</div>
</div>
</section>
<hr class="my-16 border-gray-200 dark:border-white/10" />
<!-- Devices Resource -->
<section id="devices" class="scroll-mt-24 mb-16">
<h2 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
Devices
</h2>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
Devices are network equipment monitored by Towerops. Each device belongs to a site and can be monitored via ICMP ping and SNMP.
</p>
<!-- Device Model -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">The device model</h3>
<div class="mt-6">
<dl class="divide-y divide-gray-200 dark:divide-white/10">
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
id
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Unique identifier for the device (UUID).
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
name
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
The name of the device.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
ip_address
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
IP address of the device.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
site_id
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
ID of the site this device belongs to.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
description
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Optional description of the device.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
monitoring_enabled
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
boolean
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Whether monitoring is enabled for this device.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
check_interval_seconds
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
integer
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
How often to poll this device (in seconds).
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_enabled
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
boolean
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Whether SNMP monitoring is enabled.
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_version
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
SNMP version to use (e.g., "2c").
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_port
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
integer
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
SNMP port (default: 161).
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
inserted_at
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
timestamp
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Timestamp when the device was created.
</dd>
</div>
</dl>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- List all devices -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">List all devices</h3>
<span class="inline-flex items-center rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/30">
GET
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/devices
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Lists all devices for the authenticated organization.
</p>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Optional parameters
</h4>
<dl class="mt-3 divide-y divide-gray-200 dark:divide-white/10">
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
site_id
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Filter devices by site ID.
</dd>
</div>
</dl>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl -G https://towerops.net/api/v1/devices \
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"devices": [
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Core Router",
"ip_address": "192.168.1.1",
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"monitoring_enabled": true,
"snmp_enabled": true,
"inserted_at": "2026-01-15T19:44:25Z"
}
]
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Create a device -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Create a device</h3>
<span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
POST
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/devices
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Creates a new device.
</p>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Required parameters
</h4>
<dl class="mt-3 divide-y divide-gray-200 dark:divide-white/10">
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
site_id
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
ID of the site this device belongs to.
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
name
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
The name of the device.
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
ip_address
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
IP address of the device.
</dd>
</div>
</dl>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Optional parameters
</h4>
<dl class="mt-3 divide-y divide-gray-200 dark:divide-white/10">
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
description
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Description of the device.
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
monitoring_enabled
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
boolean
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Enable monitoring (default: true).
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_enabled
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
boolean
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Enable SNMP monitoring (default: true).
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_version
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
SNMP version (default: "2c").
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_community
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
string
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
SNMP community string (inherits from site/org if not set).
</dd>
</div>
<div class="py-3">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
snmp_port
<span class="ml-2 font-sans text-xs font-normal text-gray-500 dark:text-gray-400">
integer
</span>
</dt>
<dd class="mt-1 text-sm text-gray-600 dark:text-gray-400">
SNMP port (default: 161).
</dd>
</div>
</dl>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl https://towerops.net/api/v1/devices \
-H "Authorization: Bearer #{@sample_token}" \
-H "Content-Type: application/json" \
-d '{
"device": {
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Core Router",
"ip_address": "192.168.1.1",
"description": "Main router",
"snmp_community": "public"
}
}'
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Core Router",
"ip_address": "192.168.1.1",
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"monitoring_enabled": true,
"snmp_enabled": true,
"inserted_at": "2026-01-15T19:44:25Z"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Get a device -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Retrieve a device</h3>
<span class="inline-flex items-center rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/30">
GET
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/devices/:id
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Retrieves a single device by ID, including all configuration details.
</p>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl https://towerops.net/api/v1/devices/650e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Core Router",
"ip_address": "192.168.1.1",
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Main router",
"monitoring_enabled": true,
"check_interval_seconds": 300,
"snmp_enabled": true,
"snmp_version": "2c",
"snmp_port": 161,
"inserted_at": "2026-01-15T19:44:25Z"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Update a device -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Update a device</h3>
<span class="inline-flex items-center rounded-md bg-amber-50 px-2 py-1 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/30">
PATCH
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/devices/:id
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Updates an existing device. Only provided fields will be updated.
</p>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl -X PATCH https://towerops.net/api/v1/devices/650e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer #{@sample_token}" \
-H "Content-Type: application/json" \
-d '{
"device": {
"name": "Updated Router Name",
"monitoring_enabled": false
}
}'
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Updated Router Name",
"ip_address": "192.168.1.1",
"site_id": "550e8400-e29b-41d4-a716-446655440000",
"description": "Main router",
"monitoring_enabled": false,
"check_interval_seconds": 300,
"snmp_enabled": true,
"snmp_version": "2c",
"snmp_port": 161,
"inserted_at": "2026-01-15T19:44:25Z"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Delete a device -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Delete a device</h3>
<span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/30">
DELETE
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/devices/:id
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Deletes a device. Note: This will also delete all monitoring data associated with the device.
</p>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
curl -X DELETE https://towerops.net/api/v1/devices/650e8400-e29b-41d4-a716-446655440001 \
-H "Authorization: Bearer #{@sample_token}"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw("""
{
"success": true
}
""") %></code></pre>
</div>
</div>
</div>
</div>
</section>
<hr class="my-16 border-gray-200 dark:border-white/10" />
<!-- Account Data Resource (GDPR) -->
<section id="account-data" class="scroll-mt-24 mb-16">
<h2 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
Account Data (GDPR)
</h2>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
GDPR Right to Access endpoint that allows users to download all their personal data in JSON format. This endpoint complies with Article 15 of GDPR.
</p>
<div class="mt-6 rounded-lg bg-amber-50 dark:bg-amber-900/10 p-4 border border-amber-200 dark:border-amber-800">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-amber-400" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
Authentication Difference
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
This endpoint requires <strong>browser session authentication</strong>, not API tokens. Users must be logged into their Towerops account via the web interface to access this endpoint.
</p>
</div>
</div>
</div>
</div>
<!-- Export all account data -->
<div class="mt-12">
<div class="flex items-center gap-x-3">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">
Export all account data
</h3>
<span class="inline-flex items-center rounded-md bg-emerald-50 px-2 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-400 dark:ring-emerald-400/30">
GET
</span>
<span class="text-sm font-mono text-gray-600 dark:text-gray-400">
/api/v1/account/data
</span>
</div>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<!-- Left column - description -->
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
Downloads all personal data associated with the authenticated user account as a JSON file. This includes:
</p>
<ul class="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400 list-disc list-inside">
<li>Profile information (email, name, timezone)</li>
<li>Organizations and membership roles</li>
<li>Devices across all organizations</li>
<li>Recent alerts (last 90 days)</li>
<li>Audit logs (up to 1,000 most recent)</li>
</ul>
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/10 p-4 border border-blue-200 dark:border-blue-800">
<p class="text-sm text-blue-700 dark:text-blue-300">
<strong>Privacy Note:</strong>
Sensitive data like SNMP community strings are redacted from the export for security.
</p>
</div>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Authentication
</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Requires active browser session with CSRF token. Cannot be accessed via API tokens.
</p>
</div>
<!-- Right column - code example -->
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Request (Browser)</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw(~S"""
# This endpoint requires browser authentication
# Access via logged-in browser session:
GET https://towerops.net/api/v1/account/data
# Or via curl with session cookie:
curl https://towerops.net/api/v1/account/data \
-H "Cookie: _towerops_key=YOUR_SESSION_COOKIE"
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Response</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw(~S"""
{
"profile": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe",
"timezone": "America/New_York",
"is_superuser": false,
"confirmed_at": "2026-01-15T10:30:00Z",
"inserted_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-01-20T14:22:00Z"
},
"credentials": [
{
"id": "750e8400-e29b-41d4-a716-446655440002",
"label": "MacBook Pro Touch ID",
"public_key_spki": "MFkwEwYHKoZIzj0CAQYIKoZI...",
"last_used_at": "2026-01-28T12:15:00Z",
"inserted_at": "2026-01-15T10:35:00Z"
}
],
"organizations": [
{
"id": "850e8400-e29b-41d4-a716-446655440003",
"name": "Acme Networks",
"slug": "acme-networks",
"role": "owner",
"inserted_at": "2026-01-15T10:32:00Z",
"updated_at": "2026-01-15T10:32:00Z"
}
],
"devices": [
{
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Core Router",
"ip_address": "192.168.1.1",
"snmp_community": "[REDACTED]",
"monitoring_enabled": true,
"snmp_enabled": true,
"status": "up",
"organization": {
"id": "850e8400-e29b-41d4-a716-446655440003",
"name": "Acme Networks"
},
"site": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Main Office"
},
"inserted_at": "2026-01-16T09:00:00Z",
"updated_at": "2026-01-28T08:30:00Z"
}
],
"alerts": [
{
"id": "950e8400-e29b-41d4-a716-446655440004",
"alert_type": "equipment_down",
"severity": "critical",
"message": "Device is not responding to ping",
"triggered_at": "2026-01-27T15:45:00Z",
"resolved_at": "2026-01-27T16:10:00Z",
"acknowledged_at": null,
"device": {
"id": "650e8400-e29b-41d4-a716-446655440001",
"name": "Core Router"
},
"inserted_at": "2026-01-27T15:45:00Z"
}
],
"audit_logs": [
{
"id": "150e8400-e29b-41d4-a716-446655440005",
"action": "user.login",
"actor_email": "user@example.com",
"target_user_id": "550e8400-e29b-41d4-a716-446655440000",
"metadata": {
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0..."
},
"inserted_at": "2026-01-28T08:00:00Z"
}
],
"export_info": {
"exported_at": "2026-01-28T19:50:00Z",
"format": "JSON",
"gdpr_article": "Article 15 - Right to Access"
}
}
""") %></code></pre>
</div>
<div class="mt-4 rounded-lg bg-gray-900 dark:bg-gray-950 p-4">
<p class="text-xs font-medium text-zinc-400 mb-2">Response Headers</p>
<pre class="text-sm text-zinc-100 overflow-x-auto"><code><%= raw(~S"""
Content-Type: application/json
Content-Disposition: attachment; filename="towerops-data-{user_id}-{timestamp}.json"
""") %></code></pre>
</div>
</div>
</div>
</div>
</section>
<hr class="my-16 border-gray-200 dark:border-white/10" />
<!-- Terraform Provider Section -->
<section id="terraform" class="scroll-mt-24 mb-16">
<h2 class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">
Terraform Provider
</h2>
<p class="mt-4 text-base text-gray-600 dark:text-gray-400">
Manage your Towerops infrastructure as code using our official Terraform provider. Automate site and device provisioning, version control your network configuration, and integrate Towerops into your existing infrastructure workflows.
</p>
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/10 p-4 border border-blue-200 dark:border-blue-800">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
Infrastructure as Code
</h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>
The Terraform provider uses the same REST API documented above. All resource operations require API token authentication (not browser sessions).
</p>
</div>
</div>
</div>
</div>
<!-- What is Terraform -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">What is Terraform?</h3>
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">
<a
href="https://www.terraform.io/"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
Terraform
</a>
is an infrastructure as code tool that lets you define both cloud and on-premise resources in human-readable configuration files that you can version, reuse, and share. With the Towerops Terraform provider, you can:
</p>
<ul class="mt-4 space-y-2 text-sm text-gray-600 dark:text-gray-400 list-disc list-inside">
<li>Define sites and devices in declarative configuration files</li>
<li>Version control your network monitoring infrastructure</li>
<li>Automate device provisioning and updates</li>
<li>Integrate with CI/CD pipelines for automated deployments</li>
<li>Ensure consistent configuration across environments</li>
<li>Track infrastructure changes with git history</li>
</ul>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Getting Started -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Getting Started</h3>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<!-- Left column - description -->
<div>
<h4 class="text-sm font-semibold text-zinc-900 dark:text-white">Installation</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
The provider is available on the <a
href="https://registry.terraform.io/providers/towerops-app/towerops"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:underline"
>Terraform Registry</a>. Terraform will automatically download it when you run <code class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs font-mono">terraform init</code>.
</p>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Requirements
</h4>
<ul class="mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400 list-disc list-inside">
<li>Terraform >= 1.0</li>
<li>Towerops API token (create in Organization Settings)</li>
</ul>
</div>
<!-- Right column - code example -->
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Configuration</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw(~S"""
terraform {
required_providers {
towerops = {
source = "towerops-app/towerops"
version = "~> 0.1"
}
}
}
variable "towerops_api_token" {
type = string
sensitive = true
}
provider "towerops" {
token = var.towerops_api_token
api_url = "https://towerops.net"
}
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Example Usage -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Example Usage</h3>
<div class="mt-6 grid grid-cols-1 gap-x-12 gap-y-8 lg:grid-cols-2">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">
This example creates a site and two devices. Terraform will automatically manage the lifecycle of these resources, creating, updating, or deleting them as needed to match your configuration.
</p>
<h4 class="mt-6 text-sm font-semibold text-zinc-900 dark:text-white">
Key Features
</h4>
<ul class="mt-3 space-y-2 text-sm text-gray-600 dark:text-gray-400 list-disc list-inside">
<li>Automatic dependency management (devices depend on sites)</li>
<li>State tracking to detect configuration drift</li>
<li>Plan preview before making changes</li>
<li>Rollback support via version control</li>
</ul>
</div>
<div>
<div class="overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">main.tf</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw(~S"""
resource "towerops_site" "main_office" {
name = "Main Office"
location = "New York, NY"
}
resource "towerops_device" "core_router" {
site_id = towerops_site.main_office.id
name = "Core Router"
ip_address = "192.168.1.1"
description = "Primary gateway"
snmp_enabled = true
snmp_version = "2c"
}
resource "towerops_device" "backup_router" {
site_id = towerops_site.main_office.id
name = "Backup Router"
ip_address = "192.168.1.2"
description = "Failover gateway"
snmp_enabled = true
}
""") %></code></pre>
</div>
<div class="mt-4 overflow-hidden rounded-lg bg-gray-900 dark:bg-gray-950">
<div class="border-b border-zinc-800 px-4 py-2">
<p class="text-xs font-medium text-zinc-400">Commands</p>
</div>
<pre class="p-4 text-sm text-zinc-100 overflow-x-auto"><code><%= raw(~S"""
# Initialize and download provider
terraform init
# Preview changes
terraform plan
# Apply configuration
terraform apply
# Destroy all resources
terraform destroy
""") %></code></pre>
</div>
</div>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Available Resources -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Available Resources</h3>
<div class="mt-6">
<dl class="divide-y divide-gray-200 dark:divide-white/10">
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
towerops_site
</dt>
<dd class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Manages a physical site/location. Supports name, location, and SNMP community string configuration.
</dd>
<dd class="mt-2">
<a
href="https://registry.terraform.io/providers/towerops-app/towerops/latest/docs/resources/site"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
View documentation →
</a>
</dd>
</div>
<div class="py-4">
<dt class="text-sm font-mono font-medium text-zinc-900 dark:text-white">
towerops_device
</dt>
<dd class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Manages network equipment at a site. Supports IP address, SNMP configuration, monitoring settings, and more.
</dd>
<dd class="mt-2">
<a
href="https://registry.terraform.io/providers/towerops-app/towerops/latest/docs/resources/device"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
View documentation →
</a>
</dd>
</div>
</dl>
</div>
</div>
<hr class="my-12 border-gray-200 dark:border-white/10" />
<!-- Documentation Links -->
<div class="mt-12">
<h3 class="text-xl font-semibold text-zinc-900 dark:text-white">Full Documentation</h3>
<div class="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2">
<a
href="https://registry.terraform.io/providers/towerops-app/towerops/latest/docs"
target="_blank"
rel="noopener noreferrer"
class="group relative rounded-lg border border-gray-200 dark:border-white/10 p-6 hover:border-blue-500 dark:hover:border-blue-400 transition"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-base font-semibold text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400">
Provider Documentation
</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Complete reference for all resources, data sources, and provider configuration options.
</p>
</div>
<svg
class="h-5 w-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 ml-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z"
clip-rule="evenodd"
/>
</svg>
</div>
</a>
<a
href="https://github.com/towerops-app/terraform-provider-towerops"
target="_blank"
rel="noopener noreferrer"
class="group relative rounded-lg border border-gray-200 dark:border-white/10 p-6 hover:border-blue-500 dark:hover:border-blue-400 transition"
>
<div class="flex items-start justify-between">
<div class="flex-1">
<h4 class="text-base font-semibold text-zinc-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400">
GitHub Repository
</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Source code, examples, and issue tracking for the Terraform provider.
</p>
</div>
<svg
class="h-5 w-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 ml-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z"
clip-rule="evenodd"
/>
</svg>
</div>
</a>
</div>
</div>
<div class="mt-12 rounded-lg bg-gray-50 dark:bg-gray-900/50 p-6 border border-gray-200 dark:border-white/10">
<h4 class="text-sm font-semibold text-zinc-900 dark:text-white">Need Help?</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
For questions about the Terraform provider, please open an issue on
<a
href="https://github.com/towerops-app/terraform-provider-towerops/issues"
target="_blank"
rel="noopener noreferrer"
class="text-blue-600 dark:text-blue-400 hover:underline"
>
GitHub
</a>
or contact us at <a
href="mailto:hi@towerops.net"
class="text-blue-600 dark:text-blue-400 hover:underline"
>hi@towerops.net</a>.
</p>
</div>
</section>
<!-- Footer -->
<footer class="mt-24 border-t border-gray-200 dark:border-white/10 pt-8 pb-16">
<p class="text-sm text-gray-600 dark:text-gray-400">
© 2026 Towerops. All rights reserved.
</p>
</footer>
</main>
</div>
</div>
<script>
// Update active navigation link based on scroll position
document.addEventListener('DOMContentLoaded', function() {
const sections = document.querySelectorAll('section[id]');
const navLinks = document.querySelectorAll('.nav-link');
const activeClasses = ['text-emerald-500', 'dark:text-emerald-400', 'font-medium', 'border-l-2', 'border-emerald-500', '-ml-px'];
const inactiveClasses = ['text-gray-600', 'dark:text-gray-400'];
function setActiveLink(id) {
navLinks.forEach(link => {
const linkId = link.getAttribute('data-nav-link');
if (linkId === id) {
// Remove inactive classes
inactiveClasses.forEach(cls => link.classList.remove(cls));
// Add active classes
activeClasses.forEach(cls => link.classList.add(cls));
} else {
// Remove active classes
activeClasses.forEach(cls => link.classList.remove(cls));
// Add inactive classes
inactiveClasses.forEach(cls => link.classList.add(cls));
}
});
}
// Use IntersectionObserver to track visible sections
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setActiveLink(entry.target.id);
}
});
}, {
rootMargin: '-20% 0px -70% 0px', // Trigger when section is near top
threshold: 0
});
sections.forEach(section => observer.observe(section));
// Also update on hash change (when clicking nav links)
window.addEventListener('hashchange', function() {
const hash = window.location.hash.slice(1);
if (hash) {
setActiveLink(hash);
}
});
// Set initial active state
const initialHash = window.location.hash.slice(1) || 'introduction';
setActiveLink(initialHash);
});
</script>