Updates all docs, UI copy, release checker, proto go_package, container image refs, and tests to point at codeberg.org/towerops-agent/towerops-agent. Also deletes stray empty package.json/package-lock.json (Phoenix uses esbuild — no npm) and an unused test_encode_decode.exs scratch file.
379 lines
15 KiB
Text
379 lines
15 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
>
|
|
<div class="max-w-2xl mx-auto py-8">
|
|
<%!-- Step indicator --%>
|
|
<nav class="mb-8">
|
|
<ol class="flex items-center gap-2">
|
|
<%= for step <- @steps do %>
|
|
<li class="flex items-center gap-2">
|
|
<button
|
|
phx-click="go_to_step"
|
|
phx-value-step={step}
|
|
class={[
|
|
"flex items-center gap-1.5 text-sm font-medium px-2 py-1 rounded transition",
|
|
if(step == @step,
|
|
do: "text-indigo-600 dark:text-indigo-400 bg-indigo-50 dark:bg-indigo-900/30",
|
|
else:
|
|
if(step_index(step) < step_index(@step),
|
|
do:
|
|
"text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white",
|
|
else: "text-gray-400 dark:text-gray-500"
|
|
)
|
|
)
|
|
]}
|
|
>
|
|
<span class={[
|
|
"flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold",
|
|
if(step == @step,
|
|
do: "bg-indigo-600 text-white dark:bg-indigo-500",
|
|
else:
|
|
if(step_index(step) < step_index(@step),
|
|
do: "bg-gray-400 dark:bg-gray-500 text-white",
|
|
else: "bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400"
|
|
)
|
|
)
|
|
]}>
|
|
<%= if step_index(step) < step_index(@step) do %>
|
|
<.icon name="hero-check-mini" class="h-3 w-3" />
|
|
<% else %>
|
|
{step_index(step) + 1}
|
|
<% end %>
|
|
</span>
|
|
{step_label(step)}
|
|
</button>
|
|
<%= if step != List.last(@steps) do %>
|
|
<.icon
|
|
name="hero-chevron-right-mini"
|
|
class="h-4 w-4 text-gray-300 dark:text-gray-600"
|
|
/>
|
|
<% end %>
|
|
</li>
|
|
<% end %>
|
|
</ol>
|
|
</nav>
|
|
|
|
<%!-- Step content --%>
|
|
<div class="bg-white dark:bg-white/5 rounded-lg border border-gray-200 dark:border-white/10 p-6">
|
|
<%= case @step do %>
|
|
<% :snmp -> %>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
|
{t("SNMP Configuration")}
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
{t("Set organization-wide SNMP defaults. Can be overridden per-site or per-device.")}
|
|
</p>
|
|
|
|
<.form for={@form} phx-change="validate_snmp" phx-submit="save_snmp">
|
|
<div class="space-y-4">
|
|
<.input
|
|
field={@form[:snmp_version]}
|
|
type="select"
|
|
label={t("SNMP Version")}
|
|
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
|
|
/>
|
|
|
|
<%= if @form[:snmp_version].value in ["1", "2c"] do %>
|
|
<.input
|
|
field={@form[:snmp_community]}
|
|
type="text"
|
|
label={t("Community String")}
|
|
placeholder="public"
|
|
/>
|
|
<% end %>
|
|
|
|
<%= if @form[:snmp_version].value == "3" do %>
|
|
<.input
|
|
field={@form[:snmpv3_username]}
|
|
type="text"
|
|
label={t("Username")}
|
|
/>
|
|
<.input
|
|
field={@form[:snmpv3_auth_protocol]}
|
|
type="select"
|
|
label={t("Auth Protocol")}
|
|
prompt="None"
|
|
options={["MD5", "SHA", "SHA-224", "SHA-256", "SHA-384", "SHA-512"]}
|
|
/>
|
|
<.input
|
|
field={@form[:snmpv3_auth_password]}
|
|
type="password"
|
|
label={t("Auth Password")}
|
|
/>
|
|
<.input
|
|
field={@form[:snmpv3_priv_protocol]}
|
|
type="select"
|
|
label={t("Privacy Protocol")}
|
|
prompt="None"
|
|
options={["DES", "AES", "AES-192", "AES-256", "AES-256-C"]}
|
|
/>
|
|
<.input
|
|
field={@form[:snmpv3_priv_password]}
|
|
type="password"
|
|
label={t("Privacy Password")}
|
|
/>
|
|
<% end %>
|
|
|
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
|
|
<button
|
|
type="button"
|
|
phx-click="skip"
|
|
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
{t("Skip")}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{t("Save & Continue")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
<% :site -> %>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
|
{t("Add Your First Site")}
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
{t("Sites represent physical locations — tower sites, POPs, data centers, etc.")}
|
|
</p>
|
|
|
|
<.form for={@site_form} phx-change="validate_site" phx-submit="save_site">
|
|
<div class="space-y-4">
|
|
<.input
|
|
field={@site_form[:name]}
|
|
type="text"
|
|
label={t("Name")}
|
|
placeholder="e.g. Main Tower"
|
|
required
|
|
/>
|
|
<.input
|
|
field={@site_form[:address]}
|
|
type="text"
|
|
label={t("Address")}
|
|
placeholder="123 Tower Rd"
|
|
/>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<.input
|
|
field={@site_form[:latitude]}
|
|
type="number"
|
|
label={t("Latitude")}
|
|
step="any"
|
|
placeholder="38.8977"
|
|
/>
|
|
<.input
|
|
field={@site_form[:longitude]}
|
|
type="number"
|
|
label={t("Longitude")}
|
|
step="any"
|
|
placeholder="-77.0365"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
|
|
<button
|
|
type="button"
|
|
phx-click="skip"
|
|
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
{t("Skip")}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{t("Create Site & Continue")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
<% :billing -> %>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
|
{t("Connect a Billing Platform")}
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
{t(
|
|
"Optional. Sync subscriber data for outage impact analysis and inventory reconciliation."
|
|
)}
|
|
</p>
|
|
|
|
<%= if @selected_provider == nil do %>
|
|
<div class="grid grid-cols-2 gap-3 mb-6">
|
|
<%= for provider <- @billing_providers do %>
|
|
<button
|
|
type="button"
|
|
phx-click="select_provider"
|
|
phx-value-provider={provider.id}
|
|
class="flex items-center gap-3 p-4 rounded-lg border border-gray-200 dark:border-white/10 hover:border-indigo-400 dark:hover:border-indigo-500 hover:bg-gray-50 dark:hover:bg-white/5 transition text-left"
|
|
>
|
|
<.icon name={provider.icon} class="h-6 w-6 text-gray-400 dark:text-gray-500" />
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
|
{provider.name}
|
|
</span>
|
|
</button>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<div class="mb-4">
|
|
<button
|
|
type="button"
|
|
phx-click="select_provider"
|
|
phx-value-provider=""
|
|
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
← {t("Back to providers")}
|
|
</button>
|
|
</div>
|
|
|
|
<.form
|
|
for={@integration_form}
|
|
phx-change="validate_integration"
|
|
phx-submit="save_integration"
|
|
>
|
|
<div class="space-y-4">
|
|
<%= case @selected_provider do %>
|
|
<% "sonar" -> %>
|
|
<.input
|
|
name="integration[instance_url]"
|
|
type="text"
|
|
label={t("Instance URL")}
|
|
placeholder="https://myisp.sonar.software"
|
|
value=""
|
|
/>
|
|
<.input
|
|
name="integration[api_token]"
|
|
type="password"
|
|
label={t("API Token")}
|
|
value=""
|
|
/>
|
|
<% "splynx" -> %>
|
|
<.input
|
|
name="integration[instance_url]"
|
|
type="text"
|
|
label={t("Instance URL")}
|
|
placeholder="https://myisp.splynx.app"
|
|
value=""
|
|
/>
|
|
<.input name="integration[api_key]" type="text" label={t("API Key")} value="" />
|
|
<.input
|
|
name="integration[api_secret]"
|
|
type="password"
|
|
label={t("API Secret")}
|
|
value=""
|
|
/>
|
|
<% _ -> %>
|
|
<.input
|
|
name="integration[api_key]"
|
|
type="password"
|
|
label={t("API Key")}
|
|
value=""
|
|
/>
|
|
<% end %>
|
|
|
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
|
|
<button
|
|
type="button"
|
|
phx-click="skip"
|
|
class="text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
{t("Skip")}
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{t("Save & Continue")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</.form>
|
|
<% end %>
|
|
|
|
<%= if @selected_provider == nil do %>
|
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-white/10">
|
|
<button
|
|
type="button"
|
|
phx-click="skip"
|
|
class="text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
|
|
>
|
|
{t("Skip — I'll set this up later")}
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
<% :agent -> %>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-1">
|
|
{t("Deploy Agent")}
|
|
</h2>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
{t(
|
|
"The TowerOps agent polls your network devices via SNMP. Deploy it on a host with network access to your infrastructure."
|
|
)}
|
|
</p>
|
|
|
|
<%= if @agent_token do %>
|
|
<div class="space-y-4">
|
|
<%= if @agent_token_value do %>
|
|
<div class="rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-3">
|
|
<p class="text-sm text-amber-800 dark:text-amber-200">
|
|
<.icon name="hero-exclamation-triangle" class="h-4 w-4 inline -mt-0.5" />
|
|
{t("Save this token — it won't be shown again.")}
|
|
</p>
|
|
<code class="mt-2 block text-xs font-mono bg-amber-100 dark:bg-amber-900/40 rounded p-2 break-all select-all text-amber-900 dark:text-amber-100">
|
|
{@agent_token_value}
|
|
</code>
|
|
</div>
|
|
<% else %>
|
|
<div class="rounded-md bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 p-3">
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
{t("Agent:")}
|
|
<span class="font-medium text-gray-900 dark:text-white">
|
|
{@agent_token.name}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
|
|
{t("Docker")}
|
|
</h3>
|
|
<pre class="text-xs font-mono bg-gray-900 text-green-400 rounded-lg p-4 overflow-x-auto select-all"><code>docker run -d \
|
|
--name towerops-agent \
|
|
--restart unless-stopped \
|
|
-e TOWEROPS_TOKEN=<%= if @agent_token_value, do: @agent_token_value, else: "<your-token>" %> \
|
|
-e TOWEROPS_SERVER=<%= ToweropsWeb.Endpoint.url() %> \
|
|
--network host \
|
|
codeberg.org/towerops-agent/towerops-agent:latest</code></pre>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
|
|
{t("Systemd")}
|
|
</h3>
|
|
<pre class="text-xs font-mono bg-gray-900 text-green-400 rounded-lg p-4 overflow-x-auto select-all"><code>curl -sSL https://install.towerops.net | \
|
|
TOWEROPS_TOKEN=<%= if @agent_token_value, do: @agent_token_value, else: "<your-token>" %> \
|
|
TOWEROPS_SERVER=<%= ToweropsWeb.Endpoint.url() %> \
|
|
bash</code></pre>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
{t("Failed to create agent token. You can create one later in Settings → Agents.")}
|
|
</p>
|
|
<% end %>
|
|
|
|
<div class="flex items-center justify-end pt-6 border-t border-gray-200 dark:border-white/10 mt-6">
|
|
<button
|
|
type="button"
|
|
phx-click="finish"
|
|
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
|
|
>
|
|
{t("Done — Go to Dashboard")}
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</Layouts.authenticated>
|