diff --git a/.dockerignore b/.dockerignore index 6e9e9362..3dc17691 100644 --- a/.dockerignore +++ b/.dockerignore @@ -332,3 +332,6 @@ # performance testing sandbox **/sandbox + +# Vite +# **/build/ huh, also ignored when building inside dockerfile, thought it was only during COPY... \ No newline at end of file diff --git a/Common.Web/Common.Web.csproj b/Common.Web/Common.Web.csproj index 5292d0b3..4f0b06a8 100644 --- a/Common.Web/Common.Web.csproj +++ b/Common.Web/Common.Web.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable @@ -9,8 +9,7 @@ - - + diff --git a/Common.Web/Extensions.cs b/Common.Web/Extensions.cs new file mode 100644 index 00000000..b690fad5 --- /dev/null +++ b/Common.Web/Extensions.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Hosting; + +namespace Common.Web +{ + public static class Extensions + { + public static bool HasEnvironmentPart(this IHostEnvironment hostEnvironment, string environmentPart) + { + ArgumentNullException.ThrowIfNull(hostEnvironment); + return hostEnvironment.EnvironmentName.Split('.') + .Any(part => string.Equals(environmentPart, part, StringComparison.OrdinalIgnoreCase)); + } + public static bool HasDevelopmentEnvironment(this IHostEnvironment hostEnvironment) + => hostEnvironment.HasEnvironmentPart(Environments.Development); + } +} diff --git a/Common.Web/ServiceConfiguration.cs b/Common.Web/ServiceConfiguration.cs index 332e0570..af186f43 100644 --- a/Common.Web/ServiceConfiguration.cs +++ b/Common.Web/ServiceConfiguration.cs @@ -10,7 +10,7 @@ namespace Common.Web { public static class ServiceConfiguration { - public static void ConfigureProcessingPipelineServices(IServiceCollection services, IEnumerable pluginModules) + public static void ConfigureProcessingPipelineServices(IServiceCollection services, IConfiguration config, IEnumerable pluginModules) { services.AddSingleton(); //(sp => new TableClientFactory("vektor") services.AddSingleton(); @@ -18,7 +18,7 @@ public static void ConfigureProcessingPipelineServices(IServiceCollection servic services.AddSingleton(); foreach (var plugin in pluginModules) - plugin.ConfigureServices(services); + plugin.ConfigureServices(services, config); } public static void ConfigurePlugins(IApplicationBuilder app, IEnumerable pluginModules) @@ -36,7 +36,8 @@ public static void ConfigureApplicationInsights(IApplicationBuilder app, IConfig if (aiConn == "SECRET" || aiConn == string.Empty) { if (isDevelopment == false) - throw new ArgumentException("InstrumentationKey not set"); + Console.WriteLine($"Warning: InstrumentationKey not set ({aiConn})"); + //throw new ArgumentException("InstrumentationKey not set"); } else { diff --git a/Common.Web/TypedConfiguration.cs b/Common.Web/TypedConfiguration.cs index 96a932b8..3954851a 100644 --- a/Common.Web/TypedConfiguration.cs +++ b/Common.Web/TypedConfiguration.cs @@ -21,55 +21,101 @@ public class TypedConfiguration // https://referbruv.com/blog/posts/working-with-options-pattern-in-aspnet-core-the-complete-guide var appSettings = new T(); - config.GetSection(sectionKey).Bind(appSettings); - services.AddSingleton(appSettings.GetType(), appSettings!); + //config.GetSection(sectionKey).Bind(appSettings); + //services.AddSingleton(appSettings.GetType(), appSettings!); - RecurseBind(appSettings, services, config); + //RecurseBind(appSettings, services, config); + XBind(appSettings, services, config.GetSection(typeof(T).Name)); + // https://kaylumah.nl/2021/11/29/validated-strongly-typed-ioptions.html + // If we want to inject IOptions instead of just Type, this is needed: https://stackoverflow.com/a/61157181 services.ConfigureOptions(instance) + //services.Configure(config.GetSection("AceKnowledge")); - // https://kaylumah.nl/2021/11/29/validated-strongly-typed-ioptions.html - // If we want to inject IOptions instead of just Type, this is needed: https://stackoverflow.com/a/61157181 services.ConfigureOptions(instance) - //services.Configure(config.GetSection("AceKnowledge")); - - return appSettings; + return appSettings; } - private static void RecurseBind(object appSettings, IServiceCollection services, IConfiguration config) + private static void XBind(object setting, IServiceCollection services, IConfiguration config) + { + config.Bind(setting); + Rec(setting); + void Rec(object s) + { + services.AddSingleton(s.GetType(), s); + var props = s.GetType() + .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + foreach (var item in props) + { + if (item.PropertyType == typeof(string) || item.PropertyType.IsPrimitive) + { } + else if (item.PropertyType.IsAssignableTo(typeof(System.Collections.IEnumerable)) + && item.PropertyType.IsGenericType) + { } + else + { + var v = item.GetValue(s); + if (v != null) + Rec(v); + } + } + } + } + + private static void RecurseBind(object appSettings, IServiceCollection services, IConfiguration config) { - var props = appSettings.GetType() - .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) - .Where(o => !o.PropertyType.IsSealed); // TODO: (low) better check than IsSealed (also unit test) + var props = appSettings.GetType() + .GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public) + ; //.Where(o => !o.PropertyType.IsSealed); // TODO: (low) better check than IsSealed (also unit test) - foreach (var prop in props) - { - var instance = prop.GetValue(appSettings); - //config.GetSection(prop.Name).Bind(instance); - services.AddSingleton(instance!.GetType(), instance!); + foreach (var prop in props) + { + var instance = prop.GetValue(appSettings); + if (instance == null) + { + var isNullable = Nullable.GetUnderlyingType(prop.PropertyType) != null; + if (isNullable) + continue; + if (prop.GetCustomAttributes(typeof(System.Runtime.CompilerServices.NullableAttribute), true).Any()) + continue; + throw new Exception($"Null value for non-nullable property '{prop.Name}'"); + } + //config.GetSection(prop.Name).Bind(instance); - RecurseBind(instance, services, config); + if (instance is System.Collections.IList lst) + { + foreach (var item in lst) + { + services.AddSingleton(item.GetType(), item); + //RecurseBind(item, services, config); + } + } + else + { + services.AddSingleton(instance!.GetType(), instance!); + RecurseBind(instance, services, config); + } - //var asOptions = Microsoft.Extensions.Options.Options.Create(instance); - //services.ConfigureOptions(instance); + //var asOptions = Microsoft.Extensions.Options.Options.Create(instance); + //services.ConfigureOptions(instance); - // Execute validation (if available) - var validatorType = instance.GetType().Assembly.GetTypes() - .Where(t => - { - var validatorInterface = t.GetInterfaces().SingleOrDefault(o => - o.IsGenericType && o.GetGenericTypeDefinition() == typeof(Microsoft.Extensions.Options.IValidateOptions<>)); - return validatorInterface != null && validatorInterface.GenericTypeArguments.Single() == instance.GetType(); - } - ).FirstOrDefault(); - if (validatorType != null) - { - var validator = Activator.CreateInstance(validatorType); - var m = validatorType.GetMethod("Validate"); - var result = (Microsoft.Extensions.Options.ValidateOptionsResult?)m?.Invoke(validator, new object[] { "", instance }); - if (result!.Failed) - { - throw new Exception($"{validatorType.Name}: {result.FailureMessage}"); - } - } - } - } + // Execute validation (if available) + var validatorType = instance.GetType().Assembly.GetTypes() + .Where(t => + { + var validatorInterface = t.GetInterfaces().SingleOrDefault(o => + o.IsGenericType && o.GetGenericTypeDefinition() == typeof(Microsoft.Extensions.Options.IValidateOptions<>)); + return validatorInterface != null && validatorInterface.GenericTypeArguments.Single() == instance.GetType(); + } + ).FirstOrDefault(); + if (validatorType != null) + { + var validator = Activator.CreateInstance(validatorType); + var m = validatorType.GetMethod("Validate"); + var result = (Microsoft.Extensions.Options.ValidateOptionsResult?)m?.Invoke(validator, new object[] { "", instance }); + if (result!.Failed) + { + throw new Exception($"{validatorType.Name}: {result.FailureMessage}"); + } + } + } + } } } diff --git a/Common/Common.csproj b/Common/Common.csproj index d8110dae..80381397 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -1,6 +1,6 @@  - net9.0 + net10.0 enable enable diff --git a/Directory.Packages.props b/Directory.Packages.props index 6598017c..0ce35a65 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,62 +5,66 @@ $(NoWarn);NU1507 - + - - - - - - + + + + + + + - - - - - + + + + + - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - + - - + + - + - - - + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a2094c76..251ef8de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ # Build the runtime image +# podman build -t trainingapi . -f Dockerfile FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base WORKDIR /app # Expose the port the app runs on @@ -33,7 +34,7 @@ WORKDIR /app COPY --from=publish /app/publish . ENV ASPNETCORE_HTTP_PORTS=80 # Define the startup command -ENTRYPOINT ["dotnet", "TrainingApi.dll"] +ENTRYPOINT ["dotnet", "TrainingApi.dll", "--environment=Docker"] # FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build diff --git a/Dockerfile.web b/Dockerfile.web index dfda06a2..bebce751 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -1,11 +1,18 @@ FROM node:24-alpine +# podman build -t adminapp . -f Dockerfile.web +# podman start adminapp -p 5171:5171 --name adminapp-1 + # WORKDIR ProblemSource/AdminApp WORKDIR /app # nothing copied? COPY ProblemSource/AdminApp/* / # works, but too much copied? -COPY ProblemSource/AdminApp/** / +# COPY ProblemSource/AdminApp/** / +# RUN rm -rf ./ProblemSource/AdminApp/build/_app/ + +COPY ProblemSource/AdminApp/package.json . + # COPY ./ProblemSource/AdminApp/** ./ # COPY ./ProblemSource/AdminApp/package.json ./ # COPY ./ProblemSource/AdminApp/src/** ./src/ @@ -13,14 +20,41 @@ COPY ProblemSource/AdminApp/** / # RUN find -maxdepth 2 -ls # no output.. +RUN echo "First copy result" RUN ls -ltR RUN npm install +RUN npm install -g http-server +# apk add curl + +# RUN rm -rf ./ProblemSource/AdminApp/build/_app/ +# COPY . . +# COPY ProblemSource/AdminApp/ . +# COPY ProblemSource/AdminApp/ ProblemSource/AdminApp/ +# RUN rm -rf ./ProblemSource/AdminApp/build/_app/ + +COPY ProblemSource/AdminApp/ . -COPY . . +RUN echo "Second copy result" +RUN ls -ltR + +EXPOSE 5171 +ENV PORT=5171 +# ENV PUBLIC_LOCAL_SERVER_PATH=https://kistudysync.azurewebsites.net + +# EXPOSE 80 +# ENV PORT=80 RUN npm run build -EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file +# https://stackoverflow.com/questions/61106423/how-to-put-a-svelte-app-in-a-docker-container +CMD [ "http-server", "build", "--proxy", "http://localhost:5171?"] +# CMD [ "http-server", "ProblemSource/AdminApp/build", "--proxy", "http://localhost:5171?"] +# CMD [ "http-server", "ProblemSource/AdminApp/build", "--proxy", "http://localhost:5171/index.html?"] +# CMD [ "http-server", "ProblemSource/AdminApp/build" ] + +# CMD ["npm", "run", "start"] +# CMD ["npm", "run", "dev", "--", "--host"] + +# podman build -t adminapp . -f Dockerfile.web \ No newline at end of file diff --git a/EmailServices/EmailServices.csproj b/EmailServices/EmailServices.csproj index c37e7ac5..6a3c0401 100644 --- a/EmailServices/EmailServices.csproj +++ b/EmailServices/EmailServices.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable diff --git a/ML.AzureFunction/ML.AzureFunction.csproj b/ML.AzureFunction/ML.AzureFunction.csproj index 287e855c..32549c50 100644 --- a/ML.AzureFunction/ML.AzureFunction.csproj +++ b/ML.AzureFunction/ML.AzureFunction.csproj @@ -20,7 +20,7 @@ - + diff --git a/ML.Helpers/ML.Helpers.csproj b/ML.Helpers/ML.Helpers.csproj index fa7a2386..0fa851b0 100644 --- a/ML.Helpers/ML.Helpers.csproj +++ b/ML.Helpers/ML.Helpers.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable diff --git a/MLTools/ML.Dynamic.csproj b/MLTools/ML.Dynamic.csproj index 9d558492..46153d1c 100644 --- a/MLTools/ML.Dynamic.csproj +++ b/MLTools/ML.Dynamic.csproj @@ -1,11 +1,12 @@  - net9.0 + net10.0 enable enable + diff --git a/MLTools/MLDynamicPredict.cs b/MLTools/MLDynamicPredict.cs index 8e1a24c6..4b3cd8ca 100644 --- a/MLTools/MLDynamicPredict.cs +++ b/MLTools/MLDynamicPredict.cs @@ -26,7 +26,7 @@ public MLDynamicPredict(DataViewSchema schema, ITransformer model, ColumnInfo co var ctx = new MLContext(seed: 0); var model = ctx.Model.Load(modelPath, out DataViewSchema schema); - var type = MLDynamicPredict.CreateType(schema); + var type = CreateType(schema); var instance = DynamicTypeFactory.CreateInstance(type, values); var predictor = new MLDynamicPredict(schema, model, colInfo); diff --git a/Organization.Tests/Organization.Tests.csproj b/Organization.Tests/Organization.Tests.csproj index c8bf9b41..9fda1c88 100644 --- a/Organization.Tests/Organization.Tests.csproj +++ b/Organization.Tests/Organization.Tests.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable false diff --git a/Organization/Organization.csproj b/Organization/Organization.csproj index bcbf88ca..b46c4338 100644 --- a/Organization/Organization.csproj +++ b/Organization/Organization.csproj @@ -1,6 +1,6 @@ - + - net9.0 + net10.0 enable enable diff --git a/PluginModuleBase/IPluginModule.cs b/PluginModuleBase/IPluginModule.cs index 50de9654..92d0c01a 100644 --- a/PluginModuleBase/IPluginModule.cs +++ b/PluginModuleBase/IPluginModule.cs @@ -1,11 +1,12 @@ using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace PluginModuleBase { public interface IPluginModule { - void ConfigureServices(IServiceCollection services); - void Configure(IApplicationBuilder app); + void ConfigureServices(IServiceCollection services, IConfiguration config); + void Configure(IApplicationBuilder app); } } diff --git a/PluginModuleBase/PluginModuleBase.csproj b/PluginModuleBase/PluginModuleBase.csproj index 0c569973..4a238f73 100644 --- a/PluginModuleBase/PluginModuleBase.csproj +++ b/PluginModuleBase/PluginModuleBase.csproj @@ -1,12 +1,12 @@  - net9.0 + net10.0 enable enable + - \ No newline at end of file diff --git a/ProblemSource/AdminApp/.env b/ProblemSource/AdminApp/.env index 556a9d7c..29a9bc6b 100644 --- a/ProblemSource/AdminApp/.env +++ b/ProblemSource/AdminApp/.env @@ -1,3 +1,8 @@ VITE_HTTPS=true # Public -PUBLIC_LOCAL_SERVER_PATH="https://kistudysync.azurewebsites.net" +PUBLIC_LOCAL_SERVER_PATH=http://localhost:9090 +#PUBLIC_LOCAL_SERVER_PATH="https://kistudysync.azurewebsites.net" +PUBLIC_HTTPS=true +PUBLIC_PORT=5171 +PUBLIC_VITE_TEST1=abc +VITE_PUBLIC_TEST1=cde \ No newline at end of file diff --git a/ProblemSource/AdminApp/src/apiFacade.ts b/ProblemSource/AdminApp/src/apiFacade.ts index 8d478816..ca809561 100644 --- a/ProblemSource/AdminApp/src/apiFacade.ts +++ b/ProblemSource/AdminApp/src/apiFacade.ts @@ -10,7 +10,7 @@ export class ApiFacade { impersonateUser: string | null = null; constructor(baseUrl: string) { - // console.log("baseUrl", baseUrl); + // console.log("api baseUrl", baseUrl); const http = { fetch: (r: Request, init?: RequestInit) => { init = init || {}; diff --git a/ProblemSource/AdminApp/src/globalStore.ts b/ProblemSource/AdminApp/src/globalStore.ts index 6eb07bec..e56582b4 100644 --- a/ProblemSource/AdminApp/src/globalStore.ts +++ b/ProblemSource/AdminApp/src/globalStore.ts @@ -4,7 +4,7 @@ import type { LoginCredentials } from './apiClient'; import { ApiFacade } from './apiFacade'; import type { CurrentUserInfo } from './currentUserInfo'; import { Assistant } from './services/assistant'; -import { resolveLocalServerBaseUrl, Startup } from './startup'; +import { resolveLocalServerBaseUrl } from './startup'; import { SeverityLevel, type NotificationItem } from './types'; import type { TrainingUpdateMessage } from './types.js'; import { Realtime } from './services/realtime'; @@ -62,7 +62,7 @@ export const realtimeTrainingListener = (() => { signal.trigger(null); try { - await realtime.connect(Startup.resolveLocalServerBaseUrl(window.location)); + await realtime.connect(resolveLocalServerBaseUrl(window.location)); } catch (err) { console.error('error connecting', err); } diff --git a/ProblemSource/AdminApp/src/routes/teacher/+page.svelte b/ProblemSource/AdminApp/src/routes/teacher/+page.svelte index 7a754014..fe7c00d1 100644 --- a/ProblemSource/AdminApp/src/routes/teacher/+page.svelte +++ b/ProblemSource/AdminApp/src/routes/teacher/+page.svelte @@ -70,7 +70,10 @@ groups = Object.entries(groupsData).map((o) => ({ group: o[0], summaries: o[1] })); } + let lastClick = 0; async function onSelectGroup(groupId: string) { + if (Date.now() - lastClick < 50) return; + lastClick = Date.now(); detailedTrainingsData = await apiFacade.trainings.getSummaries(groupId); // RealtimelineTools.testData(detailedTrainingsData.map(o => o.id)).forEach(o => rtlTools.append(o));; getRealtimeData(); @@ -159,7 +162,7 @@ tabs={groups.map((g) => { return { id: g.group }; })} - on:selected={(e) => onSelectGroup(e.detail)} + on:selected={(e) => { console.log(e); onSelectGroup(e.detail); }} >