From 5ce0ab61b6a71e8a9b32dcebf05e4ef828d90074 Mon Sep 17 00:00:00 2001
From: Tom M0LTE
Date: Thu, 11 Jun 2026 10:20:56 +0000
Subject: [PATCH 1/3] Config: env values are deployment-managed; derive
callsign from the host node
Two startup-config behaviours for the pdn supervised-app deployment, both inert for standalone installs:
1. Set DAPPS_* env vars are now re-applied at every start, not just first start. A stored row that differs from its set env var is updated and logged as deployment-managed. Unset env vars never touch stored config, so the standalone /Setup // /Config flow is unchanged, and seeding once via env then unsetting keeps dashboard control as before. /Settings and /Setup badge env-managed fields ("managed by environment") and render them read-only.
2. When the stored callsign is absent or the N0CALL placeholder and the pdn host injects PDN_NODE_CALLSIGN, the callsign is derived as -, where Ssid is a new seeded option (DAPPS_SSID) defaulting to "7" - proposing the convention that DAPPS resides at SSID -7 of the node callsign. Any SSID on PDN_NODE_CALLSIGN is stripped first. An explicit DAPPS_CALLSIGN or a real stored callsign always wins. /Setup prefills the derived value when applicable.
Also: the release job now stamps pdn-app.yaml's version to the release tag and attaches it as a release asset so pdn's build can fetch manifest + binary from one release; pdn-app.yaml's seeding notes updated to match (no DAPPS_CALLSIGN pre-set needed under pdn any more); the seed call in Program.cs now gets a bootstrap console logger so these decisions are visible in the journal.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.github/workflows/ci.yml | 14 +-
pdn-app.yaml | 28 +-
src/dapps/dapps.core.tests/DbStartupTests.cs | 165 +++++++++++-
src/dapps/dapps.core/Pages/Settings.cshtml | 40 +++
src/dapps/dapps.core/Pages/Settings.cshtml.cs | 14 +
src/dapps/dapps.core/Pages/Setup.cshtml | 36 ++-
src/dapps/dapps.core/Pages/Setup.cshtml.cs | 15 +-
src/dapps/dapps.core/Program.cs | 11 +-
src/dapps/dapps.core/Services/DbStartup.cs | 252 ++++++++++++++----
9 files changed, 495 insertions(+), 80 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a89a886..edf36c8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -247,7 +247,18 @@ jobs:
path: dist
merge-multiple: true
+ - name: Stamp manifest version
+ # pdn-app.yaml's `version:` field is informational (pdn shows it
+ # in its UI) and should track the release tag. Stamp it into the
+ # copy we attach so the asset always matches the release it
+ # ships with; the repo copy stays as-is.
+ run: |
+ sed -i 's/^version: .*/version: "${{ needs.test.outputs.version }}"/' pdn-app.yaml
+ grep '^version:' pdn-app.yaml
+
- name: Create GitHub Release
+ # pdn-app.yaml rides along as a release asset so the pdn build
+ # can fetch manifest + binary from the same release.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -256,4 +267,5 @@ jobs:
--title "$tag" \
--target "${{ github.sha }}" \
--generate-notes \
- dist/*
+ dist/* \
+ pdn-app.yaml
diff --git a/pdn-app.yaml b/pdn-app.yaml
index 8a35fb8..4157577 100644
--- a/pdn-app.yaml
+++ b/pdn-app.yaml
@@ -6,9 +6,9 @@
# deb from this repo's published release binary) or /var/lib/packetnet/apps/dapps
# (owner-installed), and the app stays OFF until the node owner enables it.
#
-# A future dapps release workflow should attach this file as a release asset
-# (next to the dapps-linux-* binaries) so pdn's build can fetch manifest +
-# binary together from one release instead of carrying an interim copy.
+# The release workflow attaches this file as a release asset (next to the
+# dapps-linux-* binaries, with `version:` stamped to the release tag) so pdn's
+# build can fetch manifest + binary together from one release.
manifest: 1
id: dapps # must equal the package directory name
name: DAPPS
@@ -21,20 +21,24 @@ capabilities: [network, web] # binds its own callsigns over RHPv2; serves its o
# DAPPS is not a console verb; it speaks RHPv2 to the node and binds its own
# callsigns.
#
-# FIRST-START SEEDING — read before enabling: DAPPS seeds its config from
-# DAPPS_* env vars ONLY on first start (a fresh data/dapps.db in the working
-# directory). Once the db exists, env vars stop mattering — later changes go
-# through the DAPPS dashboard's /Setup // /Config pages, or by deleting the db
-# to re-seed. In particular the owner MUST supply their callsign via pdn's
-# `apps:` override environment BEFORE the first start (there is no sensible
-# default — DAPPS boots in setup-required mode with the N0CALL placeholder and
-# refuses to bind bearers until a real callsign is configured):
+# CONFIG SEEDING — how DAPPS_* env vars behave: every set DAPPS_* env var is
+# deployment-managed config — it is applied at EVERY start (re-applied over
+# whatever the dashboard stored), not just the first. Unset variables never
+# touch stored config: anything not pinned below stays editable live through
+# the DAPPS dashboard's /Setup // /Config pages, exactly as standalone.
+#
+# CALLSIGN — no pre-set needed: pdn injects PDN_NODE_CALLSIGN, and while no
+# real callsign is configured DAPPS derives its own identity as
+# - (DAPPS_SSID defaults to 7, so node M0XYZ
+# gets DAPPS at M0XYZ-7). The owner can still pin a different identity via
+# pdn's `apps:` override environment — an explicit DAPPS_CALLSIGN (or a real
+# callsign saved via the dashboard) always wins over derivation:
#
# apps:
# - id: dapps
# enabled: true
# environment:
-# DAPPS_CALLSIGN: M0XYZ-7
+# DAPPS_CALLSIGN: M0XYZ-9 # optional — omit to accept -7
#
# The working directory defaults to the app's state dir, so the database lands
# in /var/lib/packetnet/apps/dapps (DAPPS resolves it cwd-relative).
diff --git a/src/dapps/dapps.core.tests/DbStartupTests.cs b/src/dapps/dapps.core.tests/DbStartupTests.cs
index beaafc9..c3be90a 100644
--- a/src/dapps/dapps.core.tests/DbStartupTests.cs
+++ b/src/dapps/dapps.core.tests/DbStartupTests.cs
@@ -7,10 +7,12 @@
namespace dapps.core.tests;
///
-/// DbStartup.EnsureSchemaAndSeed is the system's first-run config seam:
-/// it creates schema, seeds defaults from env vars (Plan C2), and
-/// refuses to start on a placeholder callsign. These tests drive each
-/// path against a fresh SQLite file.
+/// DbStartup.EnsureSchemaAndSeed is the system's startup config seam:
+/// it creates schema, seeds defaults from env vars (Plan C2),
+/// re-applies set env vars at every start (deployment-managed config),
+/// derives a callsign from a pdn host's PDN_NODE_CALLSIGN, and warns
+/// on a placeholder callsign. These tests drive each path against a
+/// fresh SQLite file.
///
[Collection(SqliteOverridePathCollection.Name)]
public sealed class DbStartupTests : IAsyncLifetime
@@ -25,6 +27,8 @@ public sealed class DbStartupTests : IAsyncLifetime
"DAPPS_DEFAULT_BEARER_PORT",
"DAPPS_CALLSIGN",
"DAPPS_MQTT_PORT",
+ "DAPPS_SSID",
+ "PDN_NODE_CALLSIGN",
];
public ValueTask InitializeAsync()
@@ -103,21 +107,166 @@ public void EnsureSchemaAndSeed_AllEnvVars_SeedsEachOption()
}
[Fact]
- public void EnsureSchemaAndSeed_ExistingRow_NotOverwrittenByEnv()
+ public void EnsureSchemaAndSeed_ExistingRow_EnvSet_AppliedAtEveryStart()
{
- // Pre-seed a manually-configured callsign as if /Config POST had set it.
+ // Deployment-managed config: a SET env var wins over the stored
+ // row at every start, not just the first - the pdn supervised-
+ // app case, where the host's app config is authoritative.
+ using (var c = DbInfo.GetConnection())
+ {
+ c.CreateTable();
+ c.Insert(new DbSystemOption { Option = "Callsign", Value = "M0LTE-3" });
+ }
+ Environment.SetEnvironmentVariable("DAPPS_CALLSIGN", "G0TST-7");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var conn = DbInfo.GetConnection();
+ conn.Find("Callsign")!.Value.Should().Be("G0TST-7",
+ "a set env var is deployment-managed and re-applied over the stored row at every start");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_ExistingRow_NoEnv_LeftAlone()
+ {
+ // The standalone flow: no DAPPS_* env set, so the stored
+ // (dashboard-configured) value must survive every restart
+ // byte-for-byte.
using (var c = DbInfo.GetConnection())
{
c.CreateTable();
c.Insert(new DbSystemOption { Option = "Callsign", Value = "M0LTE-3" });
}
- Environment.SetEnvironmentVariable("DAPPS_CALLSIGN", "DIFFERENT-CALL");
DbStartup.EnsureSchemaAndSeed();
using var conn = DbInfo.GetConnection();
conn.Find("Callsign")!.Value.Should().Be("M0LTE-3",
- "existing rows MUST NOT be overwritten by env vars on subsequent starts");
+ "unset env vars must never touch stored config");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_SeedOnceViaEnvThenUnset_DashboardEditSticks()
+ {
+ // A standalone operator who seeds once via env then unsets it
+ // keeps dashboard control exactly as before this change.
+ Environment.SetEnvironmentVariable("DAPPS_CALLSIGN", "G0TST");
+ DbStartup.EnsureSchemaAndSeed();
+ Environment.SetEnvironmentVariable("DAPPS_CALLSIGN", null);
+
+ // Restart without the env var: seeded value sticks.
+ DbStartup.EnsureSchemaAndSeed();
+ using (var c = DbInfo.GetConnection())
+ {
+ c.Find("Callsign")!.Value.Should().Be("G0TST");
+ // Dashboard edit (as /Config POST would persist it).
+ c.Execute("update systemoptions set value=? where option=?", "M0LTE-3", "Callsign");
+ }
+
+ // Next restart: the edit survives.
+ DbStartup.EnsureSchemaAndSeed();
+ using var conn = DbInfo.GetConnection();
+ conn.Find("Callsign")!.Value.Should().Be("M0LTE-3");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_PdnNodeCallsign_DerivesCallsignWithDefaultSsid()
+ {
+ // pdn-hosted fresh install: no DAPPS_CALLSIGN, host injects
+ // PDN_NODE_CALLSIGN -> DAPPS takes up residence at SSID -7 of
+ // the node callsign.
+ Environment.SetEnvironmentVariable("PDN_NODE_CALLSIGN", "M9YYY");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var c = DbInfo.GetConnection();
+ c.Find("Callsign")!.Value.Should().Be("M9YYY-7");
+ c.Find("Ssid")!.Value.Should().Be("7", "the SSID knob is seeded alongside");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_PdnNodeCallsignWithSsid_StripsItBeforeComposing()
+ {
+ Environment.SetEnvironmentVariable("PDN_NODE_CALLSIGN", "m9yyy-2");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var c = DbInfo.GetConnection();
+ c.Find("Callsign")!.Value.Should().Be("M9YYY-7",
+ "the node's own SSID is stripped (and the base upper-cased) before composing");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_DappsSsidEnv_OverridesDerivationSsid()
+ {
+ Environment.SetEnvironmentVariable("PDN_NODE_CALLSIGN", "M9YYY");
+ Environment.SetEnvironmentVariable("DAPPS_SSID", "4");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var c = DbInfo.GetConnection();
+ c.Find("Callsign")!.Value.Should().Be("M9YYY-4");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_ExplicitDappsCallsign_WinsOverDerivation()
+ {
+ Environment.SetEnvironmentVariable("PDN_NODE_CALLSIGN", "M9YYY");
+ Environment.SetEnvironmentVariable("DAPPS_CALLSIGN", "G0TST-1");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var c = DbInfo.GetConnection();
+ c.Find("Callsign")!.Value.Should().Be("G0TST-1",
+ "an explicit DAPPS_CALLSIGN always wins over derivation");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_RealStoredCallsign_WinsOverDerivation()
+ {
+ using (var c = DbInfo.GetConnection())
+ {
+ c.CreateTable();
+ c.Insert(new DbSystemOption { Option = "Callsign", Value = "M0LTE-3" });
+ }
+ Environment.SetEnvironmentVariable("PDN_NODE_CALLSIGN", "M9YYY");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var conn = DbInfo.GetConnection();
+ conn.Find("Callsign")!.Value.Should().Be("M0LTE-3",
+ "a real stored callsign always wins over derivation");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_StoredPlaceholder_PdnNodeCallsign_Derives()
+ {
+ // A pdn host whose DAPPS db predates a callsign config (or was
+ // reset to the placeholder) picks up the derived identity on
+ // the next start.
+ using (var c = DbInfo.GetConnection())
+ {
+ c.CreateTable();
+ c.Insert(new DbSystemOption { Option = "Callsign", Value = "N0CALL" });
+ }
+ Environment.SetEnvironmentVariable("PDN_NODE_CALLSIGN", "M9YYY");
+
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var conn = DbInfo.GetConnection();
+ conn.Find("Callsign")!.Value.Should().Be("M9YYY-7");
+ }
+
+ [Fact]
+ public void EnsureSchemaAndSeed_NoPdnEnv_PlaceholderUnchanged()
+ {
+ // Standalone install: no PDN_NODE_CALLSIGN, no DAPPS_CALLSIGN -
+ // the placeholder stays and the daemon boots into the existing
+ // setup-required flow (configure via /Setup // /Config).
+ DbStartup.EnsureSchemaAndSeed();
+
+ using var c = DbInfo.GetConnection();
+ c.Find("Callsign")!.Value.Should().Be("N0CALL");
}
[Fact]
diff --git a/src/dapps/dapps.core/Pages/Settings.cshtml b/src/dapps/dapps.core/Pages/Settings.cshtml
index 5ac510a..11527e0 100644
--- a/src/dapps/dapps.core/Pages/Settings.cshtml
+++ b/src/dapps/dapps.core/Pages/Settings.cshtml
@@ -19,6 +19,18 @@
font-family: var(--font-mono);
letter-spacing: 0.18em;
}
+
+ /* Deployment-managed (env-set) fields: badge next to the label,
+ control rendered read-only. See the env-managed script below. */
+ .env-badge {
+ display: inline-block; margin-left: 0.4rem;
+ padding: 0.05rem 0.4rem; border-radius: 999px;
+ font-size: 0.68rem; font-weight: 600;
+ text-transform: uppercase; letter-spacing: 0.05em;
+ background: color-mix(in srgb, var(--accent, #2563eb) 15%, transparent);
+ color: var(--accent, #2563eb);
+ cursor: help; vertical-align: middle;
+ }