Configure desktop host and deployment settings

Adds desktop hosting/browser launch configuration, app settings examples, project dependency/config updates, and ignores generated/local files.
main
SYED MUSTUFA AHMED NAQVI 2026-05-15 11:37:30 +05:00
parent fd1655947e
commit 267c4c8881
8 changed files with 184 additions and 3 deletions

10
.gitignore vendored
View File

@ -9,6 +9,11 @@
bin/ bin/
obj/ obj/
# Publish artifacts
publish/
artifacts/
artifacts/**
# .NET Core # .NET Core
*.runtimeconfig.dev.json *.runtimeconfig.dev.json
@ -45,4 +50,7 @@ node_modules/
*.temp *.temp
# Environment variables # Environment variables
.env .env
# Local overrides (connection strings, machine-specific paths). Copy from appsettings.Example.json.
appsettings.Local.json

View File

@ -6,6 +6,10 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>AVSCartonShipmentVerifier</AssemblyName> <AssemblyName>AVSCartonShipmentVerifier</AssemblyName>
<RootNamespace>AVSCartonShipmentVerifier</RootNamespace> <RootNamespace>AVSCartonShipmentVerifier</RootNamespace>
<UserSecretsId>AVSCartonShipmentVerifier-d4f8e9b2-3c1a-4f6e-9b7d-2a8e5c1f9d3b</UserSecretsId>
<!-- Release / publish: hide console on Windows when double-clicking the exe. Debug: keep console for logs. -->
<OutputType Condition="'$(Configuration)' == 'Release'">WinExe</OutputType>
<OutputType Condition="'$(Configuration)' != 'Release'">Exe</OutputType>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -17,4 +21,9 @@
<None Update="Resources\Sound\alert.wav" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> <None Update="Resources\Sound\alert.wav" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
<!-- Default SDK Content does not copy wwwroot to bin; static assets use a runtime manifest. Copy wwwroot into output so WebRootPath exists when running the built exe (silences StaticFileMiddleware warning; xcopy-friendly). -->
<ItemGroup>
<Content Update="wwwroot\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,16 @@
namespace AVSCartonShipmentVerifier.Configuration;
public sealed class DesktopHostOptions
{
public const string SectionName = "DesktopHost";
/// <summary>
/// When true (default on Windows), opens the default browser after the server starts.
/// </summary>
public bool LaunchBrowser { get; init; } = true;
/// <summary>
/// Optional URL to open. If empty, the first bound HTTP address from Kestrel is used.
/// </summary>
public string? BrowserUrl { get; init; }
}

View File

@ -0,0 +1,88 @@
using System.Diagnostics;
using AVSCartonShipmentVerifier.Configuration;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace AVSCartonShipmentVerifier.Hosting;
/// <summary>
/// Opens the default browser once the web server has started (Windows desktop-style launch).
/// </summary>
internal sealed class LaunchBrowserHostedService : IHostedService
{
private readonly IHostApplicationLifetime _lifetime;
private readonly IOptions<DesktopHostOptions> _options;
private readonly IServer _server;
private readonly ILogger<LaunchBrowserHostedService> _logger;
public LaunchBrowserHostedService(
IServer server,
IHostApplicationLifetime lifetime,
IOptions<DesktopHostOptions> options,
ILogger<LaunchBrowserHostedService> logger)
{
_server = server;
_lifetime = lifetime;
_options = options;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
var opts = _options.Value;
if (!opts.LaunchBrowser)
return Task.CompletedTask;
_lifetime.ApplicationStarted.Register(OpenBrowser);
return Task.CompletedTask;
}
private void OpenBrowser()
{
try
{
var url = ResolveUrl();
if (string.IsNullOrWhiteSpace(url))
{
_logger.LogWarning("Could not determine URL to open in the browser.");
return;
}
if (OperatingSystem.IsWindows())
{
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", url);
}
else
{
Process.Start(new ProcessStartInfo("xdg-open", url) { UseShellExecute = true });
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to launch the default browser.");
}
}
private string? ResolveUrl()
{
var configured = _options.Value.BrowserUrl;
if (!string.IsNullOrWhiteSpace(configured))
return configured;
var addresses = _server.Features.Get<IServerAddressesFeature>()?.Addresses;
if (addresses is null || addresses.Count == 0)
return null;
return addresses.FirstOrDefault(static a => a.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
?? addresses.FirstOrDefault(static a => a.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
?? addresses.FirstOrDefault();
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@ -1,5 +1,6 @@
using AVSCartonShipmentVerifier.Configuration; using AVSCartonShipmentVerifier.Configuration;
using AVSCartonShipmentVerifier.Data; using AVSCartonShipmentVerifier.Data;
using AVSCartonShipmentVerifier.Hosting;
using AVSCartonShipmentVerifier.Repositories; using AVSCartonShipmentVerifier.Repositories;
using AVSCartonShipmentVerifier.ScanHistory; using AVSCartonShipmentVerifier.ScanHistory;
using AVSCartonShipmentVerifier.Services; using AVSCartonShipmentVerifier.Services;
@ -8,6 +9,8 @@ using Microsoft.Extensions.FileProviders;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
builder.Services builder.Services
.AddOptions<CartonVerificationOptions>() .AddOptions<CartonVerificationOptions>()
.Bind(builder.Configuration.GetSection(CartonVerificationOptions.SectionName)) .Bind(builder.Configuration.GetSection(CartonVerificationOptions.SectionName))
@ -20,6 +23,12 @@ builder.Services
.ValidateDataAnnotations() .ValidateDataAnnotations()
.ValidateOnStart(); .ValidateOnStart();
builder.Services
.AddOptions<DesktopHostOptions>()
.Bind(builder.Configuration.GetSection(DesktopHostOptions.SectionName));
builder.Services.AddHostedService<LaunchBrowserHostedService>();
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ScanHistoryDbContext>((serviceProvider, options) => builder.Services.AddDbContext<ScanHistoryDbContext>((serviceProvider, options) =>
@ -40,6 +49,24 @@ using (var scope = app.Services.CreateScope())
{ {
var db = scope.ServiceProvider.GetRequiredService<ScanHistoryDbContext>(); var db = scope.ServiceProvider.GetRequiredService<ScanHistoryDbContext>();
db.Database.EnsureCreated(); db.Database.EnsureCreated();
// EnsureCreated does not add new tables to an existing SQLite file; create rejected-scan storage explicitly.
db.Database.ExecuteSqlRaw(
"""
CREATE TABLE IF NOT EXISTS rejected_scan_entries (
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
RawInputValue TEXT NOT NULL,
ModelNumber TEXT NOT NULL,
UniqueNumber TEXT NOT NULL,
RejectionReason TEXT NOT NULL,
ProcessedAtUtc TEXT NOT NULL
);
""");
db.Database.ExecuteSqlRaw(
"""
CREATE INDEX IF NOT EXISTS IX_rejected_scan_entries_ProcessedAtUtc
ON rejected_scan_entries (ProcessedAtUtc);
""");
} }
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())

View File

@ -1,4 +1,10 @@
{ {
"ConnectionStrings": {
"VerificationDatabase": "Server=192.168.90.147;Port=3306;Database=UIND;User ID=utopia;Password=Utopia01;SslMode=None;Allow User Variables=True;"
},
"DesktopHost": {
"LaunchBrowser": false
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Debug",

18
appsettings.Example.json Normal file
View File

@ -0,0 +1,18 @@
{
"ConnectionStrings": {
"VerificationDatabase": ""
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
}
}
},
"DesktopHost": {
"LaunchBrowser": true,
"BrowserUrl": "http://localhost:5000"
}
}

View File

@ -1,6 +1,15 @@
{ {
"ConnectionStrings": { "ConnectionStrings": {},
"VerificationDatabase": "" "Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
}
}
},
"DesktopHost": {
"LaunchBrowser": true,
"BrowserUrl": ""
}, },
"CartonVerification": { "CartonVerification": {
"TableName": "carton_verification_log", "TableName": "carton_verification_log",