Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions NeoHub/NeoHub/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
<MudNavLink Href="panel-config" Icon="@Icons.Material.Filled.Build">Panel Config</MudNavLink>
</div>

<div class="nav-item px-3">
<MudNavLink Href="access-codes" Icon="@Icons.Material.Filled.Badge">Access Codes</MudNavLink>
</div>

<div class="nav-item px-3">
<MudNavLink Href="settings" Icon="@Icons.Material.Filled.Settings">Settings</MudNavLink>
</div>
Expand All @@ -35,8 +39,8 @@
</div>
</nav>
<div class="nav-version" onclick="event.stopPropagation()">
<div>GitHub @GithubShortHash Build @BuildInfo.BuildNumber</div>
<div>@BuildInfo.BuildTimeUtc.ToString("yyyy-MM-dd HH:mm") UTC @BuildAge</div>
<div>GitHub @GithubShortHash · Build @BuildInfo.BuildNumber</div>
<div>@BuildInfo.BuildTimeUtc.ToString("yyyy-MM-dd HH:mm") UTC · @BuildAge</div>
</div>
</div>

Expand All @@ -57,5 +61,3 @@
}
}
}


179 changes: 179 additions & 0 deletions NeoHub/NeoHub/Components/Pages/AccessCodes.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
@page "/access-codes"
@using NeoHub.Services
@using NeoHub.Services.Models
@using DSC.TLink.ITv2.MediatR
@inject IPanelAccessCodeService AccessCodeService
@inject IPanelStateService PanelState
@inject IITv2SessionManager SessionManager
@inject ISessionMonitor SessionMonitor
@inject ISnackbar Snackbar
@implements IDisposable

<PageTitle>Access Codes</PageTitle>

<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-4">
<MudText Typo="Typo.h4" GutterBottom="true">Access Codes</MudText>

@if (_sessionId is null)
{
<MudAlert Severity="Severity.Warning">No active panel session.</MudAlert>
}
else
{
<MudPaper Class="pa-4 mb-3">
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle1">Session: @_sessionId</MudText>
<MudSpacer />
<MudSwitch T="bool" Label="Show full codes" @bind-Value="_revealCodes" />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
OnClick="ReadAccessCodesAsync"
Disabled="@_reading"
StartIcon="@Icons.Material.Filled.Download">
@(_reading ? "Reading..." : "Read Access Codes")
</MudButton>
</MudStack>

@if (_lastResult is not null)
{
<MudAlert Severity="@(_lastResult.Success ? Severity.Success : Severity.Error)" Dense="true" Class="mt-3">
@if (_lastResult.Success)
{
<span>Read completed: @_lastResult.ReadCount OK, @_lastResult.FailedCount failed.</span>
}
else
{
<span>Read failed: @_lastResult.ErrorMessage</span>
}
</MudAlert>
}
</MudPaper>

@if (_session?.AccessCodes.Any() == true)
{
<MudPaper Class="pa-2">
<MudSimpleTable Dense="true" Hover="true" FixedHeader="true" Style="max-height: 70vh;">
<thead>
<tr>
<th>#</th>
<th>Label</th>
<th>Code</th>
<th>Length</th>
<th>Proximity Tag</th>
<th>Partitions</th>
<th>Updated</th>
</tr>
</thead>
<tbody>
@foreach (var item in _session.AccessCodes.Values.OrderBy(x => x.UserIndex))
{
<tr style="@(item.IsActive ? "" : "opacity: 0.5;")">
<td>@item.UserIndex</td>
<td>@(string.IsNullOrWhiteSpace(item.Label) ? "-" : item.Label)</td>
<td style="font-family: monospace;">@FormatCode(item)</td>
<td>@(item.CodeLength?.ToString() ?? "-")</td>
<td>@(item.HasProximityTag ? "Yes" : "-")</td>
<td>@(item.Partitions.Count == 0 ? "-" : string.Join(",", item.Partitions))</td>
<td>@item.LastUpdated.ToLocalTime().ToString("HH:mm:ss")</td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info">No access codes loaded yet.</MudAlert>
}
}
</MudContainer>

@code {
private string? _sessionId;
private SessionState? _session;
private bool _reading;
private bool _revealCodes;
private AccessCodeReadResult? _lastResult;
private bool _disposed;

protected override void OnInitialized()
{
RefreshSession();
SessionMonitor.SessionsChanged += OnSessionsChanged;
PanelState.SessionStateChanged += OnSessionStateChanged;
}

private void OnSessionsChanged()
{
InvokeAsync(() =>
{
if (_disposed) return;
RefreshSession();
StateHasChanged();
});
}

private void OnSessionStateChanged(object? sender, SessionStateChangedEventArgs e)
{
if (_sessionId == null || !string.Equals(_sessionId, e.SessionId, StringComparison.OrdinalIgnoreCase))
return;

InvokeAsync(() =>
{
if (_disposed) return;
_session = PanelState.GetSession(_sessionId);
StateHasChanged();
});
}

private void RefreshSession()
{
var active = SessionManager.GetActiveSessions().ToList();
if (_sessionId == null || !active.Contains(_sessionId))
_sessionId = active.FirstOrDefault();

_session = _sessionId is null ? null : PanelState.GetSession(_sessionId);
}

private async Task ReadAccessCodesAsync()
{
if (_sessionId is null)
return;

_reading = true;
_lastResult = null;
StateHasChanged();

try
{
_lastResult = await AccessCodeService.ReadAllAsync(_sessionId, CancellationToken.None);
_session = PanelState.GetSession(_sessionId);

if (_lastResult.Success)
Snackbar.Add("Access codes read finished", Severity.Success);
else
Snackbar.Add($"Access code read failed: {_lastResult.ErrorMessage}", Severity.Error);
}
finally
{
_reading = false;
StateHasChanged();
}
}

private string FormatCode(AccessCodeState code)
{
if (string.IsNullOrEmpty(code.CodeValue))
return "-";
if (_revealCodes)
return code.CodeValue;
return new string('*', Math.Max(4, code.CodeValue.Length));
}

public void Dispose()
{
_disposed = true;
SessionMonitor.SessionsChanged -= OnSessionsChanged;
PanelState.SessionStateChanged -= OnSessionStateChanged;
}
}
1 change: 1 addition & 0 deletions NeoHub/NeoHub/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public static void Main(string[] args)
// Application services
builder.Services.AddSingleton<IPanelStateService, PanelStateService>();
builder.Services.AddSingleton<IPanelCommandService, PanelCommandService>();
builder.Services.AddSingleton<IPanelAccessCodeService, PanelAccessCodeService>();
builder.Services.AddSingleton<IPanelConfigurationService, PanelConfigurationService>();
builder.Services.AddSingleton<IPanelConfigFileService, PanelConfigFileService>();
builder.Services.AddSingleton<ISessionMonitor, SessionMonitor>();
Expand Down
8 changes: 8 additions & 0 deletions NeoHub/NeoHub/Services/AccessCodeReadResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace NeoHub.Services
{
public record AccessCodeReadResult(bool Success, string? ErrorMessage = null)
{
public int ReadCount { get; init; }
public int FailedCount { get; init; }
}
}
5 changes: 3 additions & 2 deletions NeoHub/NeoHub/Services/Handlers/PanelConfigurationHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ public async Task Handle(SessionConnectedNotification notification, Cancellation
}

_logger.LogInformation(
"Panel capabilities: {MaxZones} zones, {MaxPartitions} partitions",
capabilities.MaxZones, capabilities.MaxPartitions);
"Panel capabilities: {MaxZones} zones, {MaxPartitions} partitions, {MaxUsers} users",
capabilities.MaxZones, capabilities.MaxPartitions, capabilities.MaxUsers);

_panelState.UpdateSession(sessionId, s =>
{
s.MaxZones = capabilities.MaxZones;
s.MaxPartitions = capabilities.MaxPartitions;
s.MaxUsers = capabilities.MaxUsers;
});

var connectionSettings = _connectionSettings.CurrentValue.Connections
Expand Down
7 changes: 7 additions & 0 deletions NeoHub/NeoHub/Services/IPanelAccessCodeService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace NeoHub.Services
{
public interface IPanelAccessCodeService
{
Task<AccessCodeReadResult> ReadAllAsync(string sessionId, CancellationToken ct);
}
}
22 changes: 22 additions & 0 deletions NeoHub/NeoHub/Services/Models/AccessCodeState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace NeoHub.Services.Models
{
/// <summary>
/// In-memory representation of a panel access code slot.
/// Contains sensitive data (full code value) and must not be persisted.
/// </summary>
public class AccessCodeState
{
public int UserIndex { get; set; }
public string? Label { get; set; }
public string? CodeValue { get; set; }
public int? CodeLength { get; set; }
public bool IsActive { get; set; }
public bool HasProximityTag { get; set; }
public List<byte> Partitions { get; set; } = new();
public byte[] RawAccessCode { get; set; } = Array.Empty<byte>();
public byte[] RawAttributes { get; set; } = Array.Empty<byte>();
public byte[] RawPartitionAssignments { get; set; } = Array.Empty<byte>();
public byte[] RawConfiguration { get; set; } = Array.Empty<byte>();
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
}
}
5 changes: 4 additions & 1 deletion NeoHub/NeoHub/Services/Models/SessionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public class SessionState
public ConnectionPhase ConnectionPhase { get; set; }
public int MaxZones { get; set; }
public int MaxPartitions { get; set; }
public int MaxUsers { get; set; }
public Dictionary<byte, PartitionState> Partitions { get; } = new();
public Dictionary<byte, ZoneState> Zones { get; } = new();
public Dictionary<int, AccessCodeState> AccessCodes { get; } = new();
public DateTime? AccessCodesLastReadAt { get; set; }

/// <summary>
/// Installer configuration data read via SectionRead.
Expand Down Expand Up @@ -69,4 +72,4 @@ public class SessionState

public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
}
}
}
Loading