Backport PR #1439: packageManifest CWS endpoint + v1/v2 config merge fix

Backports PepperDash/Essentials#1439 (merged to main at 6ceddcf5) onto
dev/v3-routing (v3.0.0-dev-v3-routing.40):

- New GET /packageManifest CWS route reporting Essentials/plugin/touchpanel
  UI package version info (GetPackageManifestRequestHandler).
- EssentialsConfig.Versions now round-trips via [JsonProperty("versions")].
- ConfigReader now detects v1 vs v2 config by presence of system/template
  nodes (previously any versions node caused the v1->v2 merge to be
  skipped), and re-applies the versions node after merge since
  PortalConfigReader.MergeConfigs does not carry it forward.
- Mobile control: track and validate UI client app versions
  (ConnectedClientVersionInfo, MobileControlSystemController changes merged
  cleanly from the PR).
- Directory.Build.props: adds PackageId AssemblyMetadata item (kept this
  branch's 3.0.0-local version).

Conflict resolution notes:
- This branch had already introduced its own AppVersion type (Version +
  RepoUrl only) for TouchpanelWrapperApp/UserInterfaces. Replaced it with
  the PR's unified NugetVersion (adds PackageId/RepoUrl/Name, all
  NullValueHandling.Ignore) to match main's schema, since AppVersion had
  no other references in the codebase.
- ConfigReader.LoadConfig on this branch had already fixed a double
  Stream.ReadToEnd() bug (reading fileContents once); kept that fix and
  layered the PR's v1/v2 detection fix + versions-node preservation on
  top of it, plus this branch's null-ConfigObject-after-merge guard.
- EssentialsWebApi.SetupRoutes: inserted the new packageManifest route
  into this branch's existing (longer) route list, which already
  includes routing-specific endpoints not present on main.

Builds clean (dotnet build on the full solution, 0 errors).

Not pushed - pending local hardware/config testing before push.
This commit is contained in:
jkdevito 2026-07-08 22:49:29 -05:00
parent 73bc50ef0d
commit f0025ce56c
7 changed files with 467 additions and 33 deletions

View file

@ -58,11 +58,32 @@ namespace PepperDash.Essentials
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
new Dictionary<string, IMobileControlMessenger>();
private readonly Dictionary<string, ConnectedClientVersionInfo> _connectedClientVersions =
new Dictionary<string, ConnectedClientVersionInfo>(StringComparer.InvariantCultureIgnoreCase);
private readonly object _connectedClientVersionsLock = new object();
/// <summary>
/// Get the custom messengers
/// </summary>
public ReadOnlyDictionary<string, IMobileControlMessenger> Messengers => new ReadOnlyDictionary<string, IMobileControlMessenger>(_messengers);
/// <summary>
/// Gets the most recently reported UI app version for each connected client, keyed by clientId
/// </summary>
public ReadOnlyDictionary<string, ConnectedClientVersionInfo> ConnectedClientVersions
{
get
{
lock (_connectedClientVersionsLock)
{
return new ReadOnlyDictionary<string, ConnectedClientVersionInfo>(
_connectedClientVersions.ToDictionary(kv => kv.Key, kv => kv.Value.Clone())
);
}
}
}
/// <summary>
/// Get the default messengers
/// </summary>
@ -1006,6 +1027,28 @@ namespace PepperDash.Essentials
" Not Enabled in Config.\r\n"
);
}
var connectedClientVersions = ConnectedClientVersions;
if (connectedClientVersions.Count == 0)
{
CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions: None reported yet\r\n");
}
else
{
CrestronConsole.ConsoleCommandResponse("\r\nUI Client App Versions:\r\n");
foreach (var kv in connectedClientVersions)
{
var v = kv.Value;
var match = string.IsNullOrEmpty(v.ExpectedAppVersion) || string.Equals(v.ExpectedAppVersion, v.AppVersion, StringComparison.OrdinalIgnoreCase);
CrestronConsole.ConsoleCommandResponse(
$" Client: {v.ClientId} Touchpanel: {v.TouchpanelKey} Room: {v.RoomKey}\r\n" +
$" Reported: {v.AppVersion} Expected: {(string.IsNullOrEmpty(v.ExpectedAppVersion) ? "(not configured)" : v.ExpectedAppVersion)} Match: {(match ? "Yes" : "NO - MISMATCH")}\r\n" +
$" Last Seen (UTC): {v.LastSeen:yyyy-MM-dd HH:mm:ss}\r\n"
);
}
}
}
/// <summary>
@ -1402,6 +1445,8 @@ namespace PepperDash.Essentials
var roomKey = content["roomKey"].Value<string>();
var touchpanelKey = content.SelectToken("touchpanelKey");
TrackClientAppVersion(clientId, roomKey, touchpanelKey?.Value<string>(), content.SelectToken("appVersion")?.Value<string>());
if (_roomCombiner == null)
{
var message = new MobileControlMessage
@ -1473,6 +1518,50 @@ namespace PepperDash.Essentials
SendTouchpanelKey(clientId, touchpanelKey);
}
/// <summary>
/// Records the app version reported by a connecting UI client (e.g. the mobile control React app's
/// build-time APP_VERSION) and compares it against the configured versions.touchpanelWrapperApp version.
/// </summary>
private void TrackClientAppVersion(string clientId, string roomKey, string touchpanelKey, string appVersion)
{
if (string.IsNullOrEmpty(appVersion))
{
return;
}
var expectedVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version;
var info = new ConnectedClientVersionInfo
{
ClientId = clientId,
RoomKey = roomKey,
TouchpanelKey = touchpanelKey,
AppVersion = appVersion,
ExpectedAppVersion = expectedVersion,
LastSeen = DateTime.UtcNow
};
lock (_connectedClientVersionsLock)
{
_connectedClientVersions[clientId] = info;
}
if (!string.IsNullOrEmpty(expectedVersion) && !string.Equals(expectedVersion, appVersion, StringComparison.OrdinalIgnoreCase))
{
this.LogWarning(
"Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}, which does not match configured versions.touchpanelWrapperApp version {expectedVersion}",
clientId, touchpanelKey, appVersion, expectedVersion
);
}
else
{
this.LogVerbose(
"Client {clientId} (touchpanel {touchpanelKey}) reported UI app version {appVersion}",
clientId, touchpanelKey, appVersion
);
}
}
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
{
if (touchpanelKeyToken == null)