Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ public async Task<SpeechVoice[]> GetVoicesAsync(

return [];
}

/// <inheritdoc/>
public object GetService(Type serviceType, object serviceKey = null)
{
Expand Down
87 changes: 13 additions & 74 deletions src/Startup/CrestApps.Core.Mvc.Web/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,42 +74,6 @@
// Early startup marker — writes immediately to confirm the process launched.
var crashLogDir = Path.Combine(builder.Environment.ContentRootPath, "App_Data", "logs");
Directory.CreateDirectory(crashLogDir);
File.WriteAllText(
Path.Combine(crashLogDir, "startup-marker.txt"),
$"Process started at {DateTime.UtcNow:O}, PID={Environment.ProcessId}{Environment.NewLine}");

AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
var message = $"[{DateTime.UtcNow:O}] Unhandled exception (IsTerminating={e.IsTerminating}):{Environment.NewLine}{e.ExceptionObject}{Environment.NewLine}";

try
{
File.AppendAllText(Path.Combine(crashLogDir, "crash.log"), message);
}
catch
{
// Best-effort — the process is already dying.
}

Console.Error.Write(message);
};

TaskScheduler.UnobservedTaskException += (_, e) =>
{
var message = $"[{DateTime.UtcNow:O}] Unobserved task exception:{Environment.NewLine}{e.Exception}{Environment.NewLine}";

try
{
File.AppendAllText(Path.Combine(crashLogDir, "crash.log"), message);
}
catch
{
// Best-effort.
}

Console.Error.Write(message);
e.SetObserved();
};

// Prevent background/hosted-service exceptions from tearing down the host.
// The default BackgroundServiceExceptionBehavior.StopHost silently kills the
Expand All @@ -120,6 +84,7 @@
{
options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
});

// =============================================================================
// 2. LOGGING
// =============================================================================
Expand All @@ -131,6 +96,7 @@
builder.WebHost.UseNLog();
var appDataPath = Path.Combine(builder.Environment.ContentRootPath, "App_Data");
Directory.CreateDirectory(appDataPath);

// =============================================================================
// 3. APPLICATION CONFIGURATION
// =============================================================================
Expand Down Expand Up @@ -164,6 +130,7 @@
builder.Services.AddSingleton<IConfigureOptions<AIDataSourceOptions>, SiteSettingsConfigureAIDataSourceOptions>();
builder.Services.AddSingleton<IConfigureOptions<ChatInteractionMemoryOptions>, SiteSettingsConfigureChatInteractionMemoryOptions>();
builder.Services.AddSingleton<IConfigureOptions<DefaultAIDeploymentSettings>, SiteSettingsConfigureDefaultDeploymentOptions>();

// =============================================================================
// 4. ASP.NET CORE MVC SETUP
// =============================================================================
Expand All @@ -173,7 +140,9 @@
builder.Services.AddLocalization();
builder.Services.AddControllersWithViews()
.AddCrestAppsStoreCommitterFilter();

builder.Services.AddHttpContextAccessor();

// =============================================================================
// 5. AUTHENTICATION & AUTHORIZATION
// =============================================================================
Expand All @@ -185,8 +154,10 @@
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
});

builder.Services.AddAuthorizationBuilder()
.AddPolicy("Admin", policy => policy.RequireRole("Administrator"));

// =============================================================================
// 6. CRESTAPPS FOUNDATION + AI SERVICES
// =============================================================================
Expand Down Expand Up @@ -367,24 +338,11 @@

var app = builder.Build();

try
{
// YesSql schema initialization — creates tables on first run.
await app.Services.InitializeYesSqlSchemaAsync();

// Seed sample articles on first run.
await app.Services.SeedArticlesAsync();
}
catch (Exception ex)
{
var msg = $"[{DateTime.UtcNow:O}] Startup initialization failed:{Environment.NewLine}{ex}{Environment.NewLine}";
// YesSql schema initialization — creates tables on first run.
await app.Services.InitializeYesSqlSchemaAsync();

try { File.AppendAllText(Path.Combine(crashLogDir, "crash.log"), msg); } catch { }

Console.Error.Write(msg);

throw;
}
// Seed sample articles on first run.
await app.Services.SeedArticlesAsync();

// =============================================================================
// 14. MIDDLEWARE PIPELINE
Expand Down Expand Up @@ -440,6 +398,7 @@
await next();
});
});

app.MapHub<AIChatHub>("/hubs/ai-chat");
app.MapHub<ChatInteractionHub>("/hubs/chat-interaction");
app.MapMcp("mcp");
Expand All @@ -453,24 +412,4 @@
app.MapControllerRoute(name: "areas", pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");

try
{
await app.RunAsync();
}
catch (Exception ex)
{
var crashMessage = $"[{DateTime.UtcNow:O}] Host terminated unexpectedly:{Environment.NewLine}{ex}{Environment.NewLine}";

try
{
File.AppendAllText(Path.Combine(crashLogDir, "crash.log"), crashMessage);
}
catch
{
// Best-effort.
}

Console.Error.Write(crashMessage);

throw;
}
await app.RunAsync();
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ namespace CrestApps.Core.Mvc.Web.Services;

internal static class YesSqlServiceCollectionExtensions
{
private static readonly (string LegacyValue, string CurrentValue)[] _legacyDocumentTypeReplacements = [("CrestApps.AI.", "CrestApps.Core.AI."), ("CrestApps.Infrastructure.", "CrestApps.Core.Infrastructure."), ("CrestApps.Mvc.Web", "CrestApps.Core.Mvc.Web"),];

/// <summary>
/// Registers YesSql with SQLite, all index providers, and the catalog/manager
/// services that the MVC sample application needs. Call this from Program.cs to
Expand Down Expand Up @@ -137,7 +135,6 @@ public static async Task InitializeYesSqlSchemaAsync(this IServiceProvider servi
await ConfigureSqliteConnectionAsync(connection);
await using var transaction = await connection.BeginTransactionAsync();
var schemaBuilder = new SchemaBuilder(store.Configuration, transaction);
await NormalizeLegacyDocumentTypeNamesAsync(store, connection, transaction, logger);
await TryCreateTableAsync(() => schemaBuilder.CreateAIProfileIndexSchemaAsync(storeOptions));
await TryCreateTableAsync(() => schemaBuilder.CreateAIProviderConnectionIndexSchemaAsync(storeOptions));
await TryCreateTableAsync(() => schemaBuilder.CreateA2AConnectionIndexSchemaAsync(storeOptions));
Expand Down Expand Up @@ -176,30 +173,6 @@ private static async Task ConfigureSqliteConnectionAsync(DbConnection connection
await command.ExecuteNonQueryAsync();
}

private static async Task NormalizeLegacyDocumentTypeNamesAsync(IStore store, DbConnection connection, DbTransaction transaction, ILogger logger)
{
var dialect = store.Configuration.SqlDialect;
var documentTableName = store.Configuration.TableNameConvention.GetDocumentTable(string.Empty);
var table = $"{store.Configuration.TablePrefix}{documentTableName}";
var quotedTableName = dialect.QuoteForTableName(table, store.Configuration.Schema);
var quotedTypeColumnName = dialect.QuoteForColumnName(nameof(Document.Type));
foreach (var (legacyValue, currentValue) in _legacyDocumentTypeReplacements)
{
await using var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = $"""
UPDATE {quotedTableName}
SET {quotedTypeColumnName} = REPLACE({quotedTypeColumnName}, '{legacyValue}', '{currentValue}')
WHERE {quotedTypeColumnName} LIKE '%{legacyValue}%'
""";
var updated = await command.ExecuteNonQueryAsync();
if (updated > 0 && logger.IsEnabled(LogLevel.Information))
{
logger.LogInformation("Updated {Count} stored YesSql document type names in {TableName} from '{LegacyValue}' to '{CurrentValue}'.", updated, table, legacyValue, currentValue);
}
}
}

private static async Task EnsureAIDocumentIndexExtensionColumnAsync(IStore store, DbConnection connection, DbTransaction transaction, SchemaBuilder schemaBuilder, YesSqlStoreOptions storeOptions, ILogger logger)
{
var tableName = await FindIndexTableNameAsync(connection, transaction, store.Configuration.TablePrefix, nameof(AIDocumentIndex));
Expand Down
Loading