format
This commit is contained in:
parent
35fc993db0
commit
a028695d3b
11 changed files with 221 additions and 112 deletions
|
|
@ -20,7 +20,7 @@ defmodule Towerops.Gaiia.Webhooks do
|
|||
@spec verify_signature(binary(), binary(), binary(), binary()) :: boolean()
|
||||
def verify_signature(body, timestamp, signature, secret) do
|
||||
signed_payload = timestamp <> "." <> body
|
||||
expected = :crypto.mac(:hmac, :sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
expected = :hmac |> :crypto.mac(:sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
Plug.Crypto.secure_compare(expected, signature)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Towerops.Geocoding do
|
||||
@moduledoc """
|
||||
Service for geocoding addresses using Google Maps Geocoding API.
|
||||
|
||||
|
||||
Supports both system-wide API keys and organization-specific encrypted API keys.
|
||||
"""
|
||||
|
||||
|
|
@ -11,23 +11,20 @@ defmodule Towerops.Geocoding do
|
|||
|
||||
@doc """
|
||||
Geocode an address to latitude/longitude coordinates.
|
||||
|
||||
|
||||
## Parameters
|
||||
|
||||
|
||||
- `address`: The address string to geocode
|
||||
|
||||
|
||||
## Returns
|
||||
|
||||
|
||||
- `{:ok, %{latitude: float, longitude: float, formatted_address: string}}` on success
|
||||
- `{:error, reason}` on failure
|
||||
"""
|
||||
def geocode(address) when is_binary(address) do
|
||||
with {:ok, api_key} <- get_api_key(),
|
||||
{:ok, response} <- make_geocoding_request(address, api_key),
|
||||
{:ok, result} <- parse_geocoding_response(response) do
|
||||
{:ok, result}
|
||||
else
|
||||
{:error, reason} -> {:error, reason}
|
||||
{:ok, response} <- make_geocoding_request(address, api_key) do
|
||||
parse_geocoding_response(response)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -69,11 +66,12 @@ defmodule Towerops.Geocoding do
|
|||
defp parse_geocoding_response(%{"status" => "OK", "results" => [result | _]}) do
|
||||
with %{"geometry" => %{"location" => %{"lat" => lat, "lng" => lng}}} <- result,
|
||||
%{"formatted_address" => formatted_address} <- result do
|
||||
{:ok, %{
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
formatted_address: formatted_address
|
||||
}}
|
||||
{:ok,
|
||||
%{
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
formatted_address: formatted_address
|
||||
}}
|
||||
else
|
||||
_ -> {:error, "Invalid response format from Google Maps API"}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ defmodule Towerops.Workers.GaiiaInsightWorker do
|
|||
"dedup_key" => "untracked",
|
||||
"finding_type" => "untracked",
|
||||
"count" => count,
|
||||
"device_ids" => untracked_devices |> Enum.map(& &1.device.id),
|
||||
"device_ids" => Enum.map(untracked_devices, & &1.device.id),
|
||||
"device_names" => untracked_devices |> Enum.take(10) |> Enum.map(& &1.device.name)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -93,8 +93,8 @@ defmodule ToweropsWeb.MarketingLayouts do
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Developers Column -->
|
||||
|
||||
<!-- Developers Column -->
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-900 mb-4">Developers</h3>
|
||||
<ul class="space-y-3">
|
||||
|
|
@ -124,8 +124,8 @@ defmodule ToweropsWeb.MarketingLayouts do
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Copyright and Legal -->
|
||||
|
||||
<!-- Copyright and Legal -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="flex flex-col items-start gap-4 sm:flex-row sm:justify-between sm:items-end">
|
||||
<div class="flex flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -56,9 +56,8 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
|
|||
case get_req_header(conn, "x-gaiia-webhook-signature") do
|
||||
[header] ->
|
||||
with {:ok, timestamp, signature} <- parse_signature_header(header),
|
||||
:ok <- check_timestamp(timestamp),
|
||||
:ok <- check_signature(timestamp, raw_body, secret, signature) do
|
||||
:ok
|
||||
:ok <- check_timestamp(timestamp) do
|
||||
check_signature(timestamp, raw_body, secret, signature)
|
||||
end
|
||||
|
||||
_ ->
|
||||
|
|
@ -101,7 +100,7 @@ defmodule ToweropsWeb.Api.V1.GaiiaWebhookController do
|
|||
|
||||
defp check_signature(timestamp, raw_body, secret, expected) do
|
||||
signed_payload = timestamp <> "." <> raw_body
|
||||
computed = :crypto.mac(:hmac, :sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
computed = :hmac |> :crypto.mac(:sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
|
||||
if Plug.Crypto.secure_compare(computed, expected) do
|
||||
:ok
|
||||
|
|
|
|||
|
|
@ -2321,20 +2321,38 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
Network Insights provides proactive observations about your network health, gathered automatically from all connected data sources.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">How It Works</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
How It Works
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Towerops continuously analyzes data from SNMP polling, Preseem, Gaiia, and other integrations to surface actionable findings. Insights are categorized by source, urgency, and type so you can focus on what matters.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Insight Types</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Insight Types
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li><strong>Reconciliation Findings</strong> — Devices that exist in Towerops but not in Gaiia (or vice versa), data mismatches between systems</li>
|
||||
<li><strong>Performance Anomalies</strong> — Devices with unusual metric patterns detected by Preseem or SNMP baselines</li>
|
||||
<li><strong>Configuration Drift</strong> — Detected changes to device configurations that may affect performance</li>
|
||||
<li><strong>Capacity Warnings</strong> — Access points or links approaching utilization thresholds</li>
|
||||
<li>
|
||||
<strong>Reconciliation Findings</strong>
|
||||
— Devices that exist in Towerops but not in Gaiia (or vice versa), data mismatches between systems
|
||||
</li>
|
||||
<li>
|
||||
<strong>Performance Anomalies</strong>
|
||||
— Devices with unusual metric patterns detected by Preseem or SNMP baselines
|
||||
</li>
|
||||
<li>
|
||||
<strong>Configuration Drift</strong>
|
||||
— Detected changes to device configurations that may affect performance
|
||||
</li>
|
||||
<li>
|
||||
<strong>Capacity Warnings</strong>
|
||||
— Access points or links approaching utilization thresholds
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Filtering & Management</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Filtering & Management
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Use the filter bar to narrow insights by status (active/dismissed), source (Preseem, Gaiia, SNMP, system), and urgency level. Each insight shows affected devices as clickable links so you can drill directly into the device detail page.
|
||||
</p>
|
||||
|
|
@ -2344,13 +2362,15 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Tip:</strong> Insights run on a nightly schedule. Connect your Gaiia and Preseem integrations in
|
||||
<.link navigate={~p"/help?section=integrations"} class="underline">Organization Settings</.link>
|
||||
<strong>Tip:</strong>
|
||||
Insights run on a nightly schedule. Connect your Gaiia and Preseem integrations in
|
||||
<.link navigate={~p"/help?section=integrations"} class="underline">
|
||||
Organization Settings
|
||||
</.link>
|
||||
to get the most comprehensive insights.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% "network-map" -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
|
|
@ -2360,29 +2380,46 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
The Network Map provides a geographic view of your sites on an interactive map. Sites with latitude and longitude coordinates are displayed as markers that you can click to view details.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Adding Location Data</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Adding Location Data
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Each site can have an address and/or latitude/longitude coordinates. You can set these in two ways:
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li><strong>Manual entry</strong> — Enter latitude and longitude directly in the site edit form</li>
|
||||
<li><strong>Geocoding</strong> — Enter a street address and click "Geocode" to automatically look up the coordinates using Google Maps</li>
|
||||
<li>
|
||||
<strong>Manual entry</strong>
|
||||
— Enter latitude and longitude directly in the site edit form
|
||||
</li>
|
||||
<li>
|
||||
<strong>Geocoding</strong>
|
||||
— Enter a street address and click "Geocode" to automatically look up the coordinates using Google Maps
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Geocoding Setup</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Geocoding Setup
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Address-to-coordinate conversion requires a Google Maps Geocoding API key. Your administrator sets this as the <code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">GOOGLE_MAPS_API_KEY</code> environment variable on the server. The map display itself uses OpenStreetMap and requires no API key.
|
||||
Address-to-coordinate conversion requires a Google Maps Geocoding API key. Your administrator sets this as the
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
GOOGLE_MAPS_API_KEY
|
||||
</code>
|
||||
environment variable on the server. The map display itself uses OpenStreetMap and requires no API key.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Map Features</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Map Features
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>Interactive pan and zoom with OpenStreetMap tiles</li>
|
||||
<li>Click any site marker to see site name, device count, and a link to the site detail page</li>
|
||||
<li>
|
||||
Click any site marker to see site name, device count, and a link to the site detail page
|
||||
</li>
|
||||
<li>Auto-fits the map to show all your sites</li>
|
||||
<li>Summary stats showing total sites, mapped sites, and total devices</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% "api-tokens" -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
|
|
@ -2392,7 +2429,9 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
API tokens allow external applications and scripts to authenticate with the Towerops REST and GraphQL APIs. Each token is scoped to an organization and can be managed from your user settings.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Creating a Token</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Creating a Token
|
||||
</h3>
|
||||
<ol class="list-decimal list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>Go to <strong>User Settings → API Tokens</strong></li>
|
||||
<li>Click <strong>"Create API Token"</strong></li>
|
||||
|
|
@ -2400,17 +2439,27 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
<li>Copy the token immediately — it will only be shown once</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Using a Token</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Using a Token
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
Include the token in the <code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">Authorization</code> header:
|
||||
Include the token in the
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
Authorization
|
||||
</code>
|
||||
header:
|
||||
</p>
|
||||
<div class="rounded-lg bg-gray-900 p-4 mb-4">
|
||||
<pre class="text-sm text-green-400 overflow-x-auto"><code>Authorization: Bearer your-api-token-here</code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Security Best Practices</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Security Best Practices
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li>Create separate tokens for each integration — don't reuse tokens across services</li>
|
||||
<li>
|
||||
Create separate tokens for each integration — don't reuse tokens across services
|
||||
</li>
|
||||
<li>Revoke tokens you no longer need</li>
|
||||
<li>Never commit tokens to version control</li>
|
||||
<li>Store tokens in environment variables or a secrets manager</li>
|
||||
|
|
@ -2419,41 +2468,68 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>See also:</strong>
|
||||
<.link navigate={~p"/help?section=rest-api"} class="underline">REST API</.link> and
|
||||
<.link navigate={~p"/help?section=graphql-api"} class="underline">GraphQL API</.link>
|
||||
<.link navigate={~p"/help?section=rest-api"} class="underline">REST API</.link>
|
||||
and
|
||||
<.link navigate={~p"/help?section=graphql-api"} class="underline">
|
||||
GraphQL API
|
||||
</.link>
|
||||
for endpoint documentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% "rest-api" -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
REST API
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
The Towerops REST API provides programmatic access to your monitoring data. All endpoints are under <code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">/api/v1/</code> and require a valid API token.
|
||||
The Towerops REST API provides programmatic access to your monitoring data. All endpoints are under
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
/api/v1/
|
||||
</code>
|
||||
and require a valid API token.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Available Endpoints</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Available Endpoints
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li><strong>Devices</strong> — List, view, create, and update monitored devices</li>
|
||||
<li><strong>Alerts</strong> — Query active and historical alerts, acknowledge, and resolve</li>
|
||||
<li>
|
||||
<strong>Devices</strong> — List, view, create, and update monitored devices
|
||||
</li>
|
||||
<li>
|
||||
<strong>Alerts</strong>
|
||||
— Query active and historical alerts, acknowledge, and resolve
|
||||
</li>
|
||||
<li><strong>Sites</strong> — Manage sites and their device assignments</li>
|
||||
<li><strong>Agents</strong> — List remote pollers and their status</li>
|
||||
<li><strong>Check Results</strong> — Query monitoring check results and metrics</li>
|
||||
<li><strong>Organization Settings</strong> — Read and update SNMP/MikroTik configuration</li>
|
||||
<li>
|
||||
<strong>Check Results</strong> — Query monitoring check results and metrics
|
||||
</li>
|
||||
<li>
|
||||
<strong>Organization Settings</strong>
|
||||
— Read and update SNMP/MikroTik configuration
|
||||
</li>
|
||||
<li><strong>Members & Invitations</strong> — Manage organization membership</li>
|
||||
<li><strong>Integrations</strong> — Configure Preseem, Gaiia, and PagerDuty</li>
|
||||
<li><strong>Activity Feed</strong> — Recent events and changes across the organization</li>
|
||||
<li>
|
||||
<strong>Activity Feed</strong>
|
||||
— Recent events and changes across the organization
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Quick Example</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Quick Example
|
||||
</h3>
|
||||
<div class="rounded-lg bg-gray-900 p-4 mb-4">
|
||||
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw("curl -H \"Authorization: Bearer YOUR_TOKEN\" \\\n https://app.towerops.net/api/v1/devices") %></code></pre>
|
||||
</div>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
All responses use <code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700"><%= raw("{\"data\": ...}") %></code> format. Results are scoped to the organization associated with your API token.
|
||||
All responses use
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
{raw("{\"data\": ...}")}
|
||||
</code>
|
||||
format. Results are scoped to the organization associated with your API token.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
|
|
@ -2464,52 +2540,84 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% "graphql-api" -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
GraphQL API
|
||||
</h2>
|
||||
<p class="text-gray-600 dark:text-gray-400 mb-4">
|
||||
The GraphQL API lets you query exactly the data you need in a single request. The endpoint is <code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">POST /api/graphql</code> and uses the same API token authentication as the REST API.
|
||||
The GraphQL API lets you query exactly the data you need in a single request. The endpoint is
|
||||
<code class="rounded bg-gray-100 px-1.5 py-0.5 text-sm font-mono dark:bg-gray-700">
|
||||
POST /api/graphql
|
||||
</code>
|
||||
and uses the same API token authentication as the REST API.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Why GraphQL?</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Why GraphQL?
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li><strong>Fetch related data in one query</strong> — Get devices with their sites, alerts, and latest metrics in a single request</li>
|
||||
<li>
|
||||
<strong>Fetch related data in one query</strong>
|
||||
— Get devices with their sites, alerts, and latest metrics in a single request
|
||||
</li>
|
||||
<li><strong>No over-fetching</strong> — Request only the fields you need</li>
|
||||
<li><strong>Introspection</strong> — The schema is self-documenting; use any GraphQL client to explore available types and fields</li>
|
||||
<li>
|
||||
<strong>Introspection</strong>
|
||||
— The schema is self-documenting; use any GraphQL client to explore available types and fields
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Quick Example</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Quick Example
|
||||
</h3>
|
||||
<div class="rounded-lg bg-gray-900 p-4 mb-4">
|
||||
<pre class="text-sm text-green-400 overflow-x-auto"><code><%= raw("curl -X POST https://app.towerops.net/api/graphql \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"query\": \"{ devices { id name ipAddress status } }\"}'") %></code></pre>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Available Queries</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Available Queries
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li><strong>devices</strong> — List devices with filtering and nested site/alert data</li>
|
||||
<li><strong>device(id)</strong> — Single device with full detail including sensors and interfaces</li>
|
||||
<li>
|
||||
<strong>devices</strong>
|
||||
— List devices with filtering and nested site/alert data
|
||||
</li>
|
||||
<li>
|
||||
<strong>device(id)</strong>
|
||||
— Single device with full detail including sensors and interfaces
|
||||
</li>
|
||||
<li><strong>sites</strong> — Sites with nested device lists</li>
|
||||
<li><strong>alerts</strong> — Active and historical alerts with device context</li>
|
||||
<li>
|
||||
<strong>alerts</strong> — Active and historical alerts with device context
|
||||
</li>
|
||||
<li><strong>agents</strong> — Remote poller agents and their assignments</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">Mutations</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Mutations
|
||||
</h3>
|
||||
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 space-y-2 mb-4">
|
||||
<li><strong>createDevice</strong> / <strong>updateDevice</strong> — Manage devices</li>
|
||||
<li><strong>acknowledgeAlert</strong> / <strong>resolveAlert</strong> — Alert lifecycle management</li>
|
||||
<li>
|
||||
<strong>createDevice</strong> / <strong>updateDevice</strong> — Manage devices
|
||||
</li>
|
||||
<li>
|
||||
<strong>acknowledgeAlert</strong>
|
||||
/ <strong>resolveAlert</strong>
|
||||
— Alert lifecycle management
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-blue-50 dark:bg-blue-900/20 p-4">
|
||||
<p class="text-sm text-blue-700 dark:text-blue-300">
|
||||
<strong>Full reference:</strong>
|
||||
<a href="/docs/graphql" class="underline font-medium">GraphQL API Documentation →</a>
|
||||
<a href="/docs/graphql" class="underline font-medium">
|
||||
GraphQL API Documentation →
|
||||
</a>
|
||||
— complete schema reference with query examples and variable usage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% _ -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
|
|
|
|||
|
|
@ -284,7 +284,9 @@
|
|||
<%= for {name, idx} <- Enum.with_index(insight.metadata["device_names"]) do %>
|
||||
<%= if insight.metadata["device_ids"] && Enum.at(insight.metadata["device_ids"], idx) do %>
|
||||
<.link
|
||||
navigate={~p"/devices/#{Enum.at(insight.metadata["device_ids"], idx)}"}
|
||||
navigate={
|
||||
~p"/devices/#{Enum.at(insight.metadata["device_ids"], idx)}"
|
||||
}
|
||||
class="inline-flex items-center rounded-md bg-white px-2 py-0.5 text-xs font-medium text-indigo-700 ring-1 ring-inset ring-indigo-700/10 hover:bg-indigo-50 dark:bg-indigo-900/20 dark:text-indigo-400 dark:ring-indigo-400/20 dark:hover:bg-indigo-900/40"
|
||||
>
|
||||
{name || "Unnamed"}
|
||||
|
|
|
|||
|
|
@ -41,22 +41,24 @@ defmodule ToweropsWeb.MapLive.Index do
|
|||
sites = Sites.list_organization_sites(organization.id)
|
||||
|
||||
# Filter sites that have geographic coordinates
|
||||
sites_with_coords = Enum.filter(sites, fn site ->
|
||||
site.latitude != nil && site.longitude != nil
|
||||
end)
|
||||
sites_with_coords =
|
||||
Enum.filter(sites, fn site ->
|
||||
site.latitude != nil && site.longitude != nil
|
||||
end)
|
||||
|
||||
# Prepare sites data for the map
|
||||
sites_data = Enum.map(sites_with_coords, fn site ->
|
||||
%{
|
||||
id: site.id,
|
||||
name: site.name,
|
||||
description: site.description,
|
||||
address: site.address,
|
||||
latitude: site.latitude,
|
||||
longitude: site.longitude,
|
||||
device_count: length(site.device || [])
|
||||
}
|
||||
end)
|
||||
sites_data =
|
||||
Enum.map(sites_with_coords, fn site ->
|
||||
%{
|
||||
id: site.id,
|
||||
name: site.name,
|
||||
description: site.description,
|
||||
address: site.address,
|
||||
latitude: site.latitude,
|
||||
longitude: site.longitude,
|
||||
device_count: length(site.device || [])
|
||||
}
|
||||
end)
|
||||
|
||||
assign(socket, :sites, sites_data)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -35,13 +35,13 @@
|
|||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Showing <%= length(@sites) %> sites with coordinates
|
||||
Showing {length(@sites)} sites with coordinates
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Container -->
|
||||
<!-- Map Container -->
|
||||
<div
|
||||
id="leaflet-map"
|
||||
phx-hook="SitesMap"
|
||||
|
|
@ -59,8 +59,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats and Site List -->
|
||||
|
||||
<!-- Stats and Site List -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Site Statistics -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10 p-6">
|
||||
|
|
@ -69,20 +69,20 @@
|
|||
<div>
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Sites</div>
|
||||
<div class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<%= Sites.count_organization_sites(@organization.id) %>
|
||||
{Sites.count_organization_sites(@organization.id)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Geocoded Sites</div>
|
||||
<div class="text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
<%= length(@sites) %>
|
||||
{length(@sites)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Coverage</div>
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<%= if Sites.count_organization_sites(@organization.id) > 0 do %>
|
||||
<%= round(length(@sites) / Sites.count_organization_sites(@organization.id) * 100) %>%
|
||||
{round(length(@sites) / Sites.count_organization_sites(@organization.id) * 100)}%
|
||||
<% else %>
|
||||
0%
|
||||
<% end %>
|
||||
|
|
@ -90,8 +90,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sites with Coordinates -->
|
||||
|
||||
<!-- Sites with Coordinates -->
|
||||
<div class="lg:col-span-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-white/10">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
|
|
@ -107,26 +107,26 @@
|
|||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center">
|
||||
<h4 class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
<%= site.name %>
|
||||
{site.name}
|
||||
</h4>
|
||||
<%= if site.device_count > 0 do %>
|
||||
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||
<%= site.device_count %> devices
|
||||
{site.device_count} devices
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= if site.address do %>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= site.address %>
|
||||
{site.address}
|
||||
</p>
|
||||
<% end %>
|
||||
<%= if site.description do %>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= site.description %>
|
||||
{site.description}
|
||||
</p>
|
||||
<% end %>
|
||||
<p class="mt-1 text-xs text-gray-400">
|
||||
<%= Float.round(site.latitude, 4) %>, <%= Float.round(site.longitude, 4) %>
|
||||
{Float.round(site.latitude, 4)}, {Float.round(site.longitude, 4)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
|
|
@ -184,4 +184,4 @@
|
|||
document.head.appendChild(js);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Geographic Location Section -->
|
||||
|
||||
<!-- Geographic Location Section -->
|
||||
<div class="mt-6 border-t pt-6">
|
||||
<h3 class="text-base font-medium mb-4">
|
||||
Geographic Location
|
||||
|
|
@ -148,8 +148,8 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Preview -->
|
||||
|
||||
<!-- Map Preview -->
|
||||
<%= if @form[:latitude].value && @form[:longitude].value do %>
|
||||
<div class="mt-4">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ defmodule Towerops.Gaiia.WebhooksTest do
|
|||
body = ~s({"event":"account.created"})
|
||||
timestamp = "1492774577"
|
||||
signed_payload = timestamp <> "." <> body
|
||||
signature = :crypto.mac(:hmac, :sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
signature = :hmac |> :crypto.mac(:sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
|
||||
assert Webhooks.verify_signature(body, timestamp, signature, secret) == true
|
||||
end
|
||||
|
|
@ -269,7 +269,7 @@ defmodule Towerops.Gaiia.WebhooksTest do
|
|||
body = ~s({"event":"account.created"})
|
||||
timestamp = "1492774577"
|
||||
signed_payload = timestamp <> "." <> body
|
||||
signature = :crypto.mac(:hmac, :sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
signature = :hmac |> :crypto.mac(:sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
tampered_body = ~s({"event":"account.deleted"})
|
||||
|
||||
assert Webhooks.verify_signature(tampered_body, timestamp, signature, secret) == false
|
||||
|
|
@ -280,7 +280,7 @@ defmodule Towerops.Gaiia.WebhooksTest do
|
|||
body = ~s({"event":"account.created"})
|
||||
timestamp = "1492774577"
|
||||
signed_payload = timestamp <> "." <> body
|
||||
signature = :crypto.mac(:hmac, :sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
signature = :hmac |> :crypto.mac(:sha256, secret, signed_payload) |> Base.encode16(case: :lower)
|
||||
|
||||
assert Webhooks.verify_signature(body, "9999999999", signature, secret) == false
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue