mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
Auto Nest's Stock Options gets a Load from ledger button that reads the nest
material's free sheets and drops from MaterialLedger's API-key-protected
GET /api/stock/opennest. Ledger rows are request-local: the engine sees them as
ordinary NestPlateStock rows with ledger:{n} ids and finite quantities, and they
are never written to Nest.PlateOptions.
- OpenNest.Data/MaterialLedger: settings file, read-only client (X-Api-Key,
5 s timeout, closed LedgerStockResult set) and LedgerStockMapper (bundles and
sheets one row each, same-size drops grouped with ordered item codes, unknown
shapes and too-small items skipped, inch/mm conversion).
- Options: MaterialLedger URL and API key (DPAPI, CurrentUser) with Test.
- Auto Nest grid: Use, Qty and Source columns for every row; PlateOption gains
request-local Quantity and Id, which NestStockBuilder passes to the engine.
Ledger rows score by area; hand-typed costs stay saved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.0 KiB
C#
106 lines
4.0 KiB
C#
using System.Text.Json;
|
|
|
|
namespace OpenNest.Data.MaterialLedger;
|
|
|
|
/// <summary>
|
|
/// The MaterialLedger address and API key for this PC, stored at
|
|
/// %APPDATA%\OpenNest\material-ledger.json. The key is written only in the protected form the
|
|
/// caller's protector produces (DPAPI on the desktop), never in plain text. Loading never
|
|
/// throws; a missing, corrupt or unreadable file means not configured.
|
|
/// </summary>
|
|
public sealed class MaterialLedgerSettings
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
PropertyNameCaseInsensitive = true,
|
|
};
|
|
|
|
/// <summary>Base URL of MaterialLedger, e.g. http://ledger:5000. Empty means not configured.</summary>
|
|
public string BaseUrl { get; set; } = "";
|
|
|
|
/// <summary>The plain API key in memory (ML key name <c>Api:Keys:OpenNest</c>).</summary>
|
|
public string ApiKey { get; set; } = "";
|
|
|
|
public bool IsConfigured => !string.IsNullOrWhiteSpace(BaseUrl);
|
|
|
|
/// <summary>%APPDATA%\OpenNest\material-ledger.json.</summary>
|
|
public static string DefaultPath => Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"OpenNest", "material-ledger.json");
|
|
|
|
/// <summary>
|
|
/// Reads the settings, unprotecting the stored key with <paramref name="unprotect"/>. A key
|
|
/// that cannot be unprotected (another user or PC) loads as empty so the ledger reports
|
|
/// unauthorized instead of failing to start.
|
|
/// </summary>
|
|
public static MaterialLedgerSettings Load(string path, Func<string, string> unprotect)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(unprotect);
|
|
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
|
return new();
|
|
|
|
try
|
|
{
|
|
var file = JsonSerializer.Deserialize<SettingsFile>(File.ReadAllText(path), JsonOptions);
|
|
if (file == null)
|
|
return new();
|
|
var settings = new MaterialLedgerSettings { BaseUrl = NormalizeUrl(file.BaseUrl) };
|
|
if (!string.IsNullOrEmpty(file.ProtectedApiKey))
|
|
{
|
|
try
|
|
{
|
|
settings.ApiKey = unprotect(file.ProtectedApiKey);
|
|
}
|
|
catch (Exception ex) when (ex is not OutOfMemoryException)
|
|
{
|
|
settings.ApiKey = "";
|
|
}
|
|
}
|
|
return settings;
|
|
}
|
|
catch (Exception ex) when (ex is JsonException or IOException or UnauthorizedAccessException)
|
|
{
|
|
return new();
|
|
}
|
|
}
|
|
|
|
/// <summary>Writes camelCase JSON with the key protected by <paramref name="protect"/>.</summary>
|
|
public void Save(string path, Func<string, string> protect)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(protect);
|
|
var json = JsonSerializer.Serialize(new SettingsFile
|
|
{
|
|
BaseUrl = NormalizeUrl(BaseUrl),
|
|
ProtectedApiKey = string.IsNullOrEmpty(ApiKey) ? "" : protect(ApiKey),
|
|
}, JsonOptions);
|
|
var directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
|
if (!string.IsNullOrEmpty(directory))
|
|
Directory.CreateDirectory(directory);
|
|
File.WriteAllText(path, json);
|
|
}
|
|
|
|
/// <summary>Validate and normalize a ledger base URL; blank is valid and means not configured.</summary>
|
|
public static bool TryNormalizeUrl(string? url, out string normalized)
|
|
{
|
|
normalized = NormalizeUrl(url);
|
|
if (normalized.Length == 0)
|
|
return true;
|
|
if (Uri.TryCreate(normalized, UriKind.Absolute, out var uri)
|
|
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
|
return true;
|
|
|
|
normalized = "";
|
|
return false;
|
|
}
|
|
|
|
private static string NormalizeUrl(string? url) => (url ?? "").Trim().TrimEnd('/');
|
|
|
|
private sealed class SettingsFile
|
|
{
|
|
public string? BaseUrl { get; set; }
|
|
public string? ProtectedApiKey { get; set; }
|
|
}
|
|
}
|