Compare commits

...

5 Commits

Author SHA1 Message Date
Oliver Kieselbach 8608ee6272 feat(agent): netsh WiFi fallback labels for all enrollment-geography languages
Derived the language set from actual enrollment geography
(get_geographic_metrics, 365d, 10k sessions across ~50 countries) and
documented the country->language mapping on the key arrays. Newly
covered: Finnish, Hungarian, Lithuanian, Latvian, Romanian, Croatian/
Bosnian, Slovak, Indonesian/Malay, Vietnamese, Thai, Arabic, Bulgarian
radio-type variant and Swedish "Radiotyp". New fi/id parser tests; the
unknown-language tests now use Greek (outside the geography set).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:18:34 +02:00
Oliver Kieselbach f762b932a2 feat(agent): CJK + Russian labels in netsh WiFi fallback parser
Extend the localized label sets with the remaining languages seen in
enrollments: Russian, Japanese, Chinese (Simplified + Traditional,
incl. the 信道/通道 channel variants) and Korean (incl. the two radio-
type spellings). SSID stays untranslated everywhere. New zh-CN and ko
parser tests; suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:14:07 +02:00
Oliver Kieselbach 173bfc40a9 feat(agent): multi-language label support in netsh WiFi fallback
The netsh fallback previously matched English labels only, silently
dropping fields on localized OS images. The parser now matches the
label variants of the languages commonly seen in enrollments
(EN/DE/FR/ES/PT/IT/NL/PL/CS/TR; Scandinavian locales reuse the EN/DE
spellings) and is split into a pure Parse(output) function covered by
unit tests (EN/DE/ES samples, "94 %" spacing, AP-BSSID collision,
unknown-language degradation - SSID itself is untranslated in all
locales). Unknown labels still fall through harmlessly; the native
WLAN API remains the primary path.

V2.Core suite 2095/2095 green; fallback live-verified against netsh
on dev hardware (identical values to the API path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 14:08:46 +02:00
Oliver Kieselbach 609d9c47af fix(agent): netsh fallback + step diagnostics for WiFi info collection
Field regression on 2.0.1383 (session f7c56065, HP ProBook 450 G7,
build 26200.8037): the native WLAN API path returned nothing in the
OOBE/SYSTEM context and the null path was silent, so wifi_signal_info
disappeared entirely - while netsh-based 2.0.1380/1382 sessions on the
same build kept reporting SSID/signal the same morning. Suspected (not
yet proven) cause: Wi-Fi location gating of WlanQueryInterface on
24H2+; inbox netsh is unaffected.

- WifiInfoReader: new TryGetCurrentConnection overload reporting WHY
  the read yielded nothing (native rc per step, interface state);
  callers log it at Debug so the agent log explains missing WiFi info.
- NetshWifiFallback (new): parses netsh wlan show interfaces ONLY when
  the WLAN API yields nothing on a WiFi-connected device; SystemPaths.
  Netsh restored for it.
- WifiConnectionInfo.SignalPercent is now int? (netsh fallback cannot
  always parse localized "Signal"); event payload emits the field only
  when present - same shape as before.

V2.Core suite 2085/2085 green; interactive positive path re-verified
(SSID/94%/802.11ax/ch116, empty diagnostics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 13:55:46 +02:00
Oliver Kieselbach 5496d575c1 style: two-line Started column (date over time) in session list
Narrows the column so more columns fit on one page; matches the
two-line style of Device/Model/Status cells.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 13:37:01 +02:00
8 changed files with 510 additions and 22 deletions
@@ -0,0 +1,209 @@
#nullable enable
using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop;
using Xunit;
namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Interop
{
/// <summary>
/// Tests for the pure netsh-output parser of the WiFi fallback path. netsh labels follow
/// the OS UI language — the parser must extract SSID/signal/radio/channel from the label
/// variants of the languages commonly seen in enrollments, and degrade gracefully (fields
/// absent, never throw) on unrecognized ones.
/// </summary>
public sealed class NetshWifiFallbackTests
{
[Fact]
public void Parse_english_output_extracts_all_fields()
{
const string output = @"
There is 1 interface on the system:
Name : Wi-Fi
Description : Intel(R) Wi-Fi 6 AX201 160MHz
GUID : 754b6c07-5ae9-49d5-ba6c-4fbd6e38bbf6
State : connected
SSID : contoso-corp
AP BSSID : 3c:37:12:53:c8:44
Band : 5 GHz
Channel : 116
Radio type : 802.11ax
Signal : 94%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("contoso-corp", r!.Ssid);
Assert.Equal(94, r.SignalPercent);
Assert.Equal("802.11ax", r.RadioType);
Assert.Equal(116, r.Channel);
}
[Fact]
public void Parse_german_output_extracts_all_fields()
{
const string output = @"
Es ist 1 Schnittstelle auf dem System vorhanden:
Name : WLAN
Beschreibung : Intel(R) Wi-Fi 6 AX201 160MHz
Status : Verbunden
SSID : fabrikam-wlan
Kanal : 44
Funktyp : 802.11ac
Signal : 87%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("fabrikam-wlan", r!.Ssid);
Assert.Equal(87, r.SignalPercent);
Assert.Equal("802.11ac", r.RadioType);
Assert.Equal(44, r.Channel);
}
[Fact]
public void Parse_spanish_output_extracts_signal_and_channel()
{
const string output = @"
Nombre : Wi-Fi
SSID : contoso-es
Canal : 6
Tipo de radio : 802.11n
Señal : 72%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("contoso-es", r!.Ssid);
Assert.Equal(72, r.SignalPercent);
Assert.Equal("802.11n", r.RadioType);
Assert.Equal(6, r.Channel);
}
[Fact]
public void Parse_chinese_simplified_output_extracts_all_fields()
{
const string output = @"
名称 : WLAN
SSID : contoso-cn
信道 : 149
无线电类型 : 802.11ac
信号 : 68%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("contoso-cn", r!.Ssid);
Assert.Equal(68, r.SignalPercent);
Assert.Equal("802.11ac", r.RadioType);
Assert.Equal(149, r.Channel);
}
[Fact]
public void Parse_korean_output_extracts_all_fields()
{
const string output = @"
이름 : Wi-Fi
SSID : contoso-kr
채널 : 36
라디오 종류 : 802.11ax
신호 : 91%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("contoso-kr", r!.Ssid);
Assert.Equal(91, r.SignalPercent);
Assert.Equal("802.11ax", r.RadioType);
Assert.Equal(36, r.Channel);
}
[Fact]
public void Parse_signal_with_space_before_percent_is_parsed()
{
var r = NetshWifiFallback.Parse(" Signal : 81 %\r\n SSID : x\r\n");
Assert.NotNull(r);
Assert.Equal(81, r!.SignalPercent);
}
[Fact]
public void Parse_ap_bssid_does_not_overwrite_ssid()
{
const string output = @"
SSID : contoso-corp
AP BSSID : aa:bb:cc:dd:ee:ff
";
var r = NetshWifiFallback.Parse(output);
Assert.Equal("contoso-corp", r!.Ssid);
}
[Fact]
public void Parse_finnish_output_extracts_all_fields()
{
const string output = @"
Nimi : Wi-Fi
SSID : contoso-fi
Kanava : 100
Radiotyyppi : 802.11ax
Signaali : 79%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("contoso-fi", r!.Ssid);
Assert.Equal(79, r.SignalPercent);
Assert.Equal("802.11ax", r.RadioType);
Assert.Equal(100, r.Channel);
}
[Fact]
public void Parse_indonesian_output_extracts_all_fields()
{
const string output = @"
Nama : Wi-Fi
SSID : contoso-id
Saluran : 11
Jenis radio : 802.11n
Sinyal : 64%
";
var r = NetshWifiFallback.Parse(output);
Assert.NotNull(r);
Assert.Equal("contoso-id", r!.Ssid);
Assert.Equal(64, r.SignalPercent);
Assert.Equal("802.11n", r.RadioType);
Assert.Equal(11, r.Channel);
}
[Fact]
public void Parse_unknown_language_without_ssid_returns_null()
{
// Greek is not in the enrollment-geography label set -> nothing to report.
var r = NetshWifiFallback.Parse(" Κατάσταση : συνδεδεμένο\r\n Σήμα : 90%\r\n");
Assert.Null(r);
}
[Fact]
public void Parse_unknown_language_still_yields_ssid()
{
// SSID is untranslated in all locales — partial result beats none.
var r = NetshWifiFallback.Parse(" SSID : contoso-gr\r\n Σήμα : 90%\r\n");
Assert.NotNull(r);
Assert.Equal("contoso-gr", r!.Ssid);
Assert.Null(r.SignalPercent);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("Der WLAN-AutoKonfig-Dienst (wlansvc) wird nicht ausgeführt.")]
public void Parse_empty_or_error_output_returns_null(string? output)
{
Assert.Null(NetshWifiFallback.Parse(output));
}
}
}
@@ -22,7 +22,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Monitoring.Interop
if (result != null)
{
Assert.InRange(result.SignalPercent, 0, 100);
Assert.True(result.SignalPercent.HasValue, "native API path must always carry signal quality");
Assert.InRange(result.SignalPercent.Value, 0, 100);
if (result.Channel.HasValue)
Assert.True(result.Channel.Value > 0, "channel must be positive when present");
}
@@ -0,0 +1,221 @@
using System;
using System.Diagnostics;
using AutopilotMonitor.Agent.V2.Core.Security;
namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
{
/// <summary>
/// Fallback WiFi reader via <c>netsh wlan show interfaces</c> output parsing, used ONLY
/// when <see cref="WifiInfoReader"/> (native WLAN API) yields nothing on a WiFi-connected
/// device. Field evidence 2026-07-28: the WLAN API can return nothing in the OOBE/SYSTEM
/// context on builds where inbox netsh still reports the connection — until that is fully
/// root-caused (suspect: Wi-Fi location gating, 24H2+), netsh stays as the safety net.
/// <para>
/// netsh labels follow the OS UI language, so the parser matches the label variants of
/// every language implied by the enrollment geography seen to date (see the comment on
/// the key arrays for the country→language derivation). Unknown-language labels fall
/// through harmlessly — the affected field is simply absent, same as before. "SSID"
/// itself is untranslated in all known locales.
/// </para>
/// </summary>
internal static class NetshWifiFallback
{
private const int NetshTimeoutMs = 5000;
// Label variants per field, by OS UI language. Matched case-insensitively against the
// full text left of the colon, so acronym-suffixed keys ("AP BSSID") never collide.
// Language set derived from enrollment geography (get_geographic_metrics, 365d):
// US/GB/AU/CA/IE/SG/IN/ZA/NG/PH en · DE/AT/CH de · BE/NL nl · BE/FR/CH fr · DK da ·
// SE sv · PL pl · TR tr · CN zh-CN · HK zh-TW · ES/MX/PE/PY es · CZ cs · SK sk ·
// IT/CH it · ID id · MY ms · FI fi · LT lt · LV lv · HU hu · BR/PT pt · JP ja ·
// KR ko · TH th · VN vi · RO/MD ro · BG bg · BA/HR hr · AE/SA/EG/JO ar ·
// MN/GE/AZ usually en/ru images → ru.
private static readonly string[] SignalKeys =
{
"Signal", // en, de, fr, sv, da, nb, hr
"Señal", // es
"Sinal", // pt
"Segnale", // it
"Signaal", // nl
"Sygnał", // pl
"Signál", // cs, sk
"Sinyal", // tr, id
"Isyarat", // ms
"Signaali", // fi
"Signalas", // lt
"Signāls", // lv
"Jel", // hu
"Semnal", // ro
"Tín hiệu", // vi
"สัญญาณ", // th
"الإشارة", // ar
"إشارة", // ar (variant without article)
"Сигнал", // ru, bg
"シグナル", // ja
"信号", // zh-CN
"訊號", // zh-TW
"신호", // ko
};
private static readonly string[] RadioTypeKeys =
{
"Radio type", // en
"Funktyp", // de
"Type de radio", // fr
"Tipo de radio", // es
"Tipo de rádio", // pt
"Tipo di radio", // it
"Radiotype", // nl, da
"Radiotyp", // sv
"Typ radia", // pl
"Typ rádia", // cs, sk
"Radyo türü", // tr
"Jenis radio", // id, ms
"Radiotyyppi", // fi
"Radijo tipas", // lt
"Radio tips", // lv
"Rádió típusa", // hu
"Tip radio", // ro
"Vrsta radija", // hr, bs
"Loại radio", // vi
"ชนิดวิทยุ", // th
"نوع الراديو", // ar
"Тип радио", // ru
"Тип на радиото", // bg
"無線の種類", // ja
"无线电类型", // zh-CN
"無線電類型", // zh-TW
"라디오 종류", // ko
"무선 종류", // ko (variant)
};
private static readonly string[] ChannelKeys =
{
"Channel", // en
"Kanal", // de, sv, da, nb, tr, hr
"Canal", // fr, es, pt, ro
"Canale", // it
"Kanaal", // nl
"Kanał", // pl
"Kanál", // cs, sk
"Saluran", // id, ms
"Kanava", // fi
"Kanalas", // lt
"Kanāls", // lv
"Csatorna", // hu
"Kênh", // vi
"ช่องสัญญาณ", // th
"ช่อง", // th (variant)
"القناة", // ar
"قناة", // ar (variant without article)
"Канал", // ru, bg
"チャネル", // ja
"信道", // zh-CN
"通道", // zh-TW / zh-CN (variant)
"채널", // ko
};
/// <summary>
/// Runs <c>netsh wlan show interfaces</c> and parses its output. Returns a
/// <see cref="WifiConnectionInfo"/> with the fields that could be parsed, or null when
/// nothing was found (no WLAN, netsh failure, timeout, or unrecognized localized
/// labels). Never throws.
/// </summary>
internal static WifiConnectionInfo TryRead()
{
try
{
var psi = new ProcessStartInfo
{
FileName = SystemPaths.Netsh,
Arguments = "wlan show interfaces",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
string output;
using (var process = Process.Start(psi))
{
output = process.StandardOutput.ReadToEnd();
if (!process.WaitForExit(NetshTimeoutMs))
{
try { process.Kill(); } catch { }
return null;
}
}
return Parse(output);
}
catch
{
return null;
}
}
/// <summary>
/// Pure parser over the netsh output text. Internal for direct unit-testing via
/// InternalsVisibleTo (TryRead itself spawns a process and stays untested).
/// </summary>
internal static WifiConnectionInfo Parse(string output)
{
if (string.IsNullOrEmpty(output))
return null;
string ssid = null, radioType = null;
int? signal = null, channel = null;
foreach (var line in output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries))
{
var trimmed = line.Trim();
var colonIndex = trimmed.IndexOf(':');
if (colonIndex < 0) continue;
var key = trimmed.Substring(0, colonIndex).Trim();
var value = trimmed.Substring(colonIndex + 1).Trim();
if (key.Equals("SSID", StringComparison.OrdinalIgnoreCase))
{
ssid = value;
}
else if (MatchesAny(key, SignalKeys))
{
// Value formats seen: "94%", "94% " (trailing space), "94 %".
if (int.TryParse(value.TrimEnd('%', ' '), out var sig))
signal = sig;
}
else if (MatchesAny(key, RadioTypeKeys))
{
radioType = value;
}
else if (MatchesAny(key, ChannelKeys))
{
if (int.TryParse(value, out var ch))
channel = ch;
}
}
if (ssid == null && signal == null && radioType == null && channel == null)
return null;
return new WifiConnectionInfo
{
Ssid = ssid,
SignalPercent = signal,
RadioType = radioType,
Channel = channel,
};
}
private static bool MatchesAny(string key, string[] candidates)
{
foreach (var candidate in candidates)
{
if (key.Equals(candidate, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
}
}
@@ -13,8 +13,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
/// <summary>SSID of the connected network; null when the AP broadcasts an empty SSID.</summary>
public string Ssid { get; internal set; }
/// <summary>Signal quality 0–100 (same semantics as the netsh "Signal" percentage).</summary>
public int SignalPercent { get; internal set; }
/// <summary>
/// Signal quality 0–100 (same semantics as the netsh "Signal" percentage). Always set
/// on the native WLAN API path; null only when the netsh fallback could not parse it.
/// </summary>
public int? SignalPercent { get; internal set; }
/// <summary>Human-readable PHY generation ("802.11ax" …); null for exotic/unknown PHYs.</summary>
public string RadioType { get; internal set; }
@@ -37,20 +40,45 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
/// wins. Returns null when no interface has an active connection.
/// </summary>
public static WifiConnectionInfo TryGetCurrentConnection(Guid? preferredInterfaceId = null)
=> TryGetCurrentConnection(preferredInterfaceId, out _);
/// <summary>
/// Same as <see cref="TryGetCurrentConnection(Guid?)"/> but reports WHY the read
/// yielded nothing via <paramref name="diagnostics"/> (native return codes per step).
/// The WLAN API can fail in ways that are invisible from the outside (e.g. per-process
/// access gating on newer Windows builds) — callers log this at Debug so field issues
/// are explainable from the agent log instead of a silent missing event.
/// </summary>
public static WifiConnectionInfo TryGetCurrentConnection(Guid? preferredInterfaceId, out string diagnostics)
{
var diag = new StringBuilder();
var clientHandle = IntPtr.Zero;
try
{
if (WlanOpenHandle(WLAN_CLIENT_VERSION, IntPtr.Zero, out _, out clientHandle) != ERROR_SUCCESS)
var rc = WlanOpenHandle(WLAN_CLIENT_VERSION, IntPtr.Zero, out _, out clientHandle);
if (rc != ERROR_SUCCESS)
{
diagnostics = $"WlanOpenHandle rc={rc}";
return null;
}
var interfaceList = IntPtr.Zero;
try
{
if (WlanEnumInterfaces(clientHandle, IntPtr.Zero, out interfaceList) != ERROR_SUCCESS)
rc = WlanEnumInterfaces(clientHandle, IntPtr.Zero, out interfaceList);
if (rc != ERROR_SUCCESS)
{
diagnostics = $"WlanEnumInterfaces rc={rc}";
return null;
}
var count = (uint)Marshal.ReadInt32(interfaceList);
if (count == 0)
{
diagnostics = "no WLAN interfaces";
return null;
}
var infoSize = Marshal.SizeOf(typeof(WLAN_INTERFACE_INFO));
// List header: DWORD dwNumberOfItems + DWORD dwIndex, then the info array.
var firstEntry = new IntPtr(interfaceList.ToInt64() + 8);
@@ -61,17 +89,21 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
var entryPtr = new IntPtr(firstEntry.ToInt64() + i * infoSize);
var info = (WLAN_INTERFACE_INFO)Marshal.PtrToStructure(entryPtr, typeof(WLAN_INTERFACE_INFO));
var connection = QueryCurrentConnection(clientHandle, info.InterfaceGuid);
var connection = QueryCurrentConnection(clientHandle, info.InterfaceGuid, diag);
if (connection == null)
continue;
if (preferredInterfaceId.HasValue && info.InterfaceGuid == preferredInterfaceId.Value)
{
diagnostics = null;
return connection;
}
if (fallback == null)
fallback = connection;
}
diagnostics = fallback == null ? diag.ToString() : null;
return fallback;
}
finally
@@ -80,8 +112,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
WlanFreeMemory(interfaceList);
}
}
catch
catch (Exception ex)
{
diagnostics = $"exception {ex.GetType().Name}: {ex.Message}";
return null;
}
finally
@@ -91,21 +124,28 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Interop
}
}
private static WifiConnectionInfo QueryCurrentConnection(IntPtr clientHandle, Guid interfaceGuid)
private static WifiConnectionInfo QueryCurrentConnection(IntPtr clientHandle, Guid interfaceGuid, StringBuilder diag)
{
var dataPtr = IntPtr.Zero;
try
{
if (WlanQueryInterface(clientHandle, ref interfaceGuid,
var rc = WlanQueryInterface(clientHandle, ref interfaceGuid,
WLAN_INTF_OPCODE.wlan_intf_opcode_current_connection,
IntPtr.Zero, out _, out dataPtr, IntPtr.Zero) != ERROR_SUCCESS)
IntPtr.Zero, out _, out dataPtr, IntPtr.Zero);
if (rc != ERROR_SUCCESS)
{
diag.Append($"[{interfaceGuid:D}] query(current_connection) rc={rc}; ");
return null;
}
var attributes = (WLAN_CONNECTION_ATTRIBUTES)Marshal.PtrToStructure(
dataPtr, typeof(WLAN_CONNECTION_ATTRIBUTES));
if (attributes.isState != WLAN_INTERFACE_STATE.wlan_interface_state_connected)
{
diag.Append($"[{interfaceGuid:D}] state={attributes.isState}; ");
return null;
}
var assoc = attributes.wlanAssociationAttributes;
return new WifiConnectionInfo
@@ -909,24 +909,28 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo
{
try
{
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid);
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid, out var wifiDiag);
if (wifi == null)
return;
var data = new Dictionary<string, object>
{
["wifiSignalPercent"] = wifi.SignalPercent
};
_logger.Debug($"EnrollmentTracker: native WLAN API returned no WiFi info ({wifiDiag ?? "no connected WLAN interface"}) — trying netsh fallback");
wifi = NetshWifiFallback.TryRead();
if (wifi == null)
return;
}
var data = new Dictionary<string, object>();
if (wifi.Ssid != null)
data["wifiSsid"] = wifi.Ssid;
if (wifi.SignalPercent.HasValue)
data["wifiSignalPercent"] = wifi.SignalPercent.Value;
if (wifi.RadioType != null)
data["wifiRadioType"] = wifi.RadioType;
if (wifi.Channel.HasValue)
data["wifiChannel"] = wifi.Channel.Value;
var message = wifi.Ssid != null
? $"WiFi: {wifi.Ssid}, Signal: {wifi.SignalPercent}%"
: $"WiFi signal info, Signal: {wifi.SignalPercent}%";
var message = wifi.Ssid != null ? $"WiFi: {wifi.Ssid}" : "WiFi signal info";
if (wifi.SignalPercent.HasValue)
message += $", Signal: {wifi.SignalPercent.Value}%";
if (wifi.RadioType != null)
message += $" ({wifi.RadioType})";
@@ -398,7 +398,13 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic
{
try
{
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid);
var wifi = WifiInfoReader.TryGetCurrentConnection(interfaceGuid, out var wifiDiag);
if (wifi == null)
{
_logger.Debug($"NetworkChangeDetector: native WLAN API returned no WiFi info ({wifiDiag ?? "no connected WLAN interface"}) — trying netsh fallback");
wifi = NetshWifiFallback.TryRead();
}
if (wifi != null)
return (wifi.Ssid, wifi.SignalPercent, wifi.RadioType);
}
@@ -35,6 +35,9 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
/// <summary>C:\Windows\System32\tzutil.exe</summary>
public static readonly string TzUtil = Path.Combine(System32, "tzutil.exe");
/// <summary>C:\Windows\System32\netsh.exe</summary>
public static readonly string Netsh = Path.Combine(System32, "netsh.exe");
/// <summary>C:\Windows\System32\schtasks.exe</summary>
public static readonly string Schtasks = Path.Combine(System32, "schtasks.exe");
@@ -885,12 +885,16 @@ function SessionCell({
</td>
);
case "started":
case "started": {
// Two-line date/time keeps the column narrow so more columns fit on one page.
const started = new Date(session.startedAt);
return (
<td className="px-3 py-4 whitespace-nowrap text-sm text-gray-500">
{new Date(session.startedAt).toLocaleString()}
<div>{started.toLocaleDateString()}</div>
<div className="text-xs text-gray-400">{started.toLocaleTimeString()}</div>
</td>
);
}
case "country":
return (