diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..18fff08
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,386 @@
+root = true
+
+# All files
+[*]
+indent_style = space
+
+# Xaml files
+[*.xaml]
+indent_size = 2
+
+# MSBuild files
+[*.{csproj,targets,props}]
+indent_size = 2
+
+# Xml files
+[*.xml]
+indent_size = 2
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Coding Conventions ####
+[*.{cs,vb}]
+
+# Organize usings
+dotnet_separate_import_directive_groups = true
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:silent
+dotnet_style_qualification_for_field = false:silent
+dotnet_style_qualification_for_method = false:silent
+dotnet_style_qualification_for_property = false:silent
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:silent
+dotnet_style_predefined_type_for_member_access = true:silent
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+dotnet_style_readonly_field = true:warning
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all:suggestion
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+#### C# Coding Conventions ####
+[*.cs]
+
+# var preferences
+csharp_style_var_elsewhere = false:silent
+csharp_style_var_for_built_in_types = false:silent
+csharp_style_var_when_type_is_apparent = false:silent
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:silent
+csharp_style_expression_bodied_constructors = false:silent
+csharp_style_expression_bodied_indexers = true:silent
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:silent
+csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:silent
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:silent
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+csharp_prefer_static_local_function = true:warning
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
+
+# Code-block preferences
+csharp_prefer_braces = true:silent
+csharp_prefer_simple_using_statement = true:suggestion
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+csharp_preserve_single_line_statements = true
+csharp_style_namespace_declarations = block_scoped:silent
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_prefer_primary_constructors = false:suggestion
+
+#### Naming styles ####
+[*.{cs,vb}]
+
+# Naming rules
+
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.symbols = types_and_namespaces
+dotnet_naming_rule.types_and_namespaces_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.interfaces_should_be_ipascalcase.severity = suggestion
+dotnet_naming_rule.interfaces_should_be_ipascalcase.symbols = interfaces
+dotnet_naming_rule.interfaces_should_be_ipascalcase.style = ipascalcase
+
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.severity = suggestion
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.symbols = type_parameters
+dotnet_naming_rule.type_parameters_should_be_tpascalcase.style = tpascalcase
+
+dotnet_naming_rule.methods_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.methods_should_be_pascalcase.symbols = methods
+dotnet_naming_rule.methods_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.properties_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.properties_should_be_pascalcase.symbols = properties
+dotnet_naming_rule.properties_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.events_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.events_should_be_pascalcase.symbols = events
+dotnet_naming_rule.events_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_variables_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_variables_should_be_camelcase.symbols = local_variables
+dotnet_naming_rule.local_variables_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.local_constants_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.local_constants_should_be_camelcase.symbols = local_constants
+dotnet_naming_rule.local_constants_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.parameters_should_be_camelcase.severity = suggestion
+dotnet_naming_rule.parameters_should_be_camelcase.symbols = parameters
+dotnet_naming_rule.parameters_should_be_camelcase.style = camelcase
+
+dotnet_naming_rule.public_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_fields_should_be_pascalcase.symbols = public_fields
+dotnet_naming_rule.public_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_fields_should_be__camelcase.severity = suggestion
+dotnet_naming_rule.private_fields_should_be__camelcase.symbols = private_fields
+dotnet_naming_rule.private_fields_should_be__camelcase.style = _camelcase
+
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.severity = suggestion
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.symbols = private_static_fields
+dotnet_naming_rule.private_static_fields_should_be_s_camelcase.style = _camelcase
+
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.symbols = public_constant_fields
+dotnet_naming_rule.public_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.symbols = private_constant_fields
+dotnet_naming_rule.private_constant_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.symbols = public_static_readonly_fields
+dotnet_naming_rule.public_static_readonly_fields_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_fields_should_be_pascalcase.style = _camelcase
+
+dotnet_naming_rule.enums_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.enums_should_be_pascalcase.symbols = enums
+dotnet_naming_rule.enums_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.local_functions_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.local_functions_should_be_pascalcase.symbols = local_functions
+dotnet_naming_rule.local_functions_should_be_pascalcase.style = pascalcase
+
+dotnet_naming_rule.non_field_members_should_be_pascalcase.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascalcase.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase
+
+# Symbol specifications
+
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interfaces.required_modifiers =
+
+dotnet_naming_symbols.enums.applicable_kinds = enum
+dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.enums.required_modifiers =
+
+dotnet_naming_symbols.events.applicable_kinds = event
+dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.events.required_modifiers =
+
+dotnet_naming_symbols.methods.applicable_kinds = method
+dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.methods.required_modifiers =
+
+dotnet_naming_symbols.properties.applicable_kinds = property
+dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.properties.required_modifiers =
+
+dotnet_naming_symbols.public_fields.applicable_kinds = field
+dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_fields.required_modifiers =
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_fields.required_modifiers =
+
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum
+dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types_and_namespaces.required_modifiers =
+
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
+
+dotnet_naming_symbols.type_parameters.applicable_kinds = namespace
+dotnet_naming_symbols.type_parameters.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters.required_modifiers =
+
+dotnet_naming_symbols.private_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.local_variables.applicable_kinds = local
+dotnet_naming_symbols.local_variables.applicable_accessibilities = local
+dotnet_naming_symbols.local_variables.required_modifiers =
+
+dotnet_naming_symbols.local_constants.applicable_kinds = local
+dotnet_naming_symbols.local_constants.applicable_accessibilities = local
+dotnet_naming_symbols.local_constants.required_modifiers = const
+
+dotnet_naming_symbols.parameters.applicable_kinds = parameter
+dotnet_naming_symbols.parameters.applicable_accessibilities = *
+dotnet_naming_symbols.parameters.required_modifiers =
+
+dotnet_naming_symbols.public_constant_fields.applicable_kinds = field
+dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_constant_fields.required_modifiers = const
+
+dotnet_naming_symbols.public_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.public_static_readonly_fields.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readonly, static
+
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.pascalcase.required_prefix =
+dotnet_naming_style.pascalcase.required_suffix =
+dotnet_naming_style.pascalcase.word_separator =
+dotnet_naming_style.pascalcase.capitalization = pascal_case
+
+dotnet_naming_style.ipascalcase.required_prefix = I
+dotnet_naming_style.ipascalcase.required_suffix =
+dotnet_naming_style.ipascalcase.word_separator =
+dotnet_naming_style.ipascalcase.capitalization = pascal_case
+
+dotnet_naming_style.tpascalcase.required_prefix = T
+dotnet_naming_style.tpascalcase.required_suffix =
+dotnet_naming_style.tpascalcase.word_separator =
+dotnet_naming_style.tpascalcase.capitalization = pascal_case
+
+dotnet_naming_style._camelcase.required_prefix = _
+dotnet_naming_style._camelcase.required_suffix =
+dotnet_naming_style._camelcase.word_separator =
+dotnet_naming_style._camelcase.capitalization = camel_case
+
+dotnet_naming_style.camelcase.required_prefix =
+dotnet_naming_style.camelcase.required_suffix =
+dotnet_naming_style.camelcase.word_separator =
+dotnet_naming_style.camelcase.capitalization = camel_case
+
+dotnet_naming_style.s_camelcase.required_prefix = s_
+dotnet_naming_style.s_camelcase.required_suffix =
+dotnet_naming_style.s_camelcase.word_separator =
+dotnet_naming_style.s_camelcase.capitalization = camel_case
+
+###############################
+# Spell Check #
+###############################
+# https://learn.microsoft.com/visualstudio/ide/text-spell-checker
+spelling_exclusion_path = .\exclusions.dic
+spelling_checkable_types = strings,identifiers,comments
+# Update this to match your prefered language, you will need to ensure you have the language pack installed for the language
+spelling_languages = en-us
+# This is only the level that spelling errors occur in the Error List, it is not actually a compile-time warning/error
+spelling_error_severity = information
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..16d9f22
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "nuget" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
+
+ - package-ecosystem: "github-actions" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/build_app.yml b/.github/workflows/build_app.yml
new file mode 100644
index 0000000..9f22f52
--- /dev/null
+++ b/.github/workflows/build_app.yml
@@ -0,0 +1,88 @@
+name: Build LD_SysInfo
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+defaults:
+ run:
+ shell: pwsh
+
+env:
+ version: "1.0.${{ github.run_number }}"
+
+jobs:
+ build:
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up .NET Core
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.x'
+
+ - name: dotnet build
+ run: dotnet build --configuration Release -p:Version="${{ env.version }}"
+
+ - name: dotnet test
+ run: dotnet test --configuration Release --no-build --collect:"XPlat Code Coverage" --results-directory ${{github.workspace}}/.build/coverage
+
+ - name: ReportGenerator
+ uses: danielpalme/ReportGenerator-GitHub-Action@5.4.5
+ with:
+ reports: ${{github.workspace}}/.build/coverage/**/coverage.cobertura.xml
+ targetdir: ${{github.workspace}}/.build/coveragereport/
+ reporttypes: Html;MarkdownSummaryGithub
+ title: 'Code Coverage'
+
+ - name: Write PR Number
+ if: ${{ github.event_name == 'pull_request' }}
+ run: |
+ New-Item -Type File -Value "${{ github.event.number }}" -Force -Path "${{github.workspace}}/.build/coveragereport/PullRequestNumber"
+
+ - name: Upload Code Coverage Report
+ uses: actions/upload-artifact@v4
+ with:
+ name: CodeCoverage
+ path: ${{github.workspace}}/.build/coveragereport/
+ if-no-files-found: error
+
+ - name: dotnet publish
+ if: ${{ github.event_name != 'pull_request' }}
+ run: |
+ # If publishing single file, remove the --no-build below
+ dotnet publish --configuration Release --no-build -p:Version="${{ env.version }}" -p:PublishDir=${{github.workspace}}/.build/publish
+
+ - name: Upload artifact for deployment job
+ if: ${{ github.event_name != 'pull_request' }}
+ uses: actions/upload-artifact@v4
+ with:
+ name: app
+ path: ${{github.workspace}}/.build/publish
+
+ # This will automatically mark PRs from dependabot with auto merge.
+ # This allows them to automatically complete if they pass the rest of the CI
+ auto-merge:
+ if: ${{ github.event_name == 'pull_request' }}
+ runs-on: ubuntu-latest
+
+ permissions:
+ pull-requests: write
+ contents: write
+
+ steps:
+ - uses: fastify/github-action-merge-dependabot@v3.11.1
+ with:
+ # For GitHub Auto Merge to work it must be enabled on the repo.
+ # This can be done with the script here:
+ # https://github.com/Keboo/DotnetTemplates/blob/main/SetupRepo.ps1
+ # See documentation here:
+ # https://docs.github.com/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request
+ use-github-auto-merge: true
\ No newline at end of file
diff --git a/.github/workflows/code_coverage_comment.yml b/.github/workflows/code_coverage_comment.yml
new file mode 100644
index 0000000..6e16948
--- /dev/null
+++ b/.github/workflows/code_coverage_comment.yml
@@ -0,0 +1,42 @@
+name: Code Coverage PR Comment
+
+on:
+ workflow_run:
+ workflows: [Build LD_SysInfo]
+ types:
+ - completed
+
+defaults:
+ run:
+ shell: pwsh
+
+# This is required by marocchino/sticky-pull-request-comment
+# You must enable read/write permissions to allow workflows to create comments on the PR.
+# On your repo navigate to: Settings > Actions > General > Workflow permissions, and make sure to enable read and write permissions.
+# https://github.com/marocchino/sticky-pull-request-comment#error-resource-not-accessible-by-integration
+permissions:
+ pull-requests: write
+
+jobs:
+ post-code-coverage:
+ runs-on: ubuntu-latest
+ if: >
+ github.event.workflow_run.event == 'pull_request' &&
+ github.event.workflow_run.conclusion == 'success'
+ steps:
+ - name: Download artifacts
+ run: gh run download ${{ github.event.workflow_run.id }} -n CodeCoverage --repo ${{ github.repository }}
+ env:
+ GH_TOKEN: ${{ github.token }}
+
+ - name: 'Get PR Number'
+ id: get-pr-number
+ run: |
+ $pr_number = (cat PullRequestNumber)
+ "pr_number=$pr_number" >> $env:GITHUB_OUTPUT
+
+ - uses: marocchino/sticky-pull-request-comment@v2
+ with:
+ recreate: true
+ number: ${{ steps.get-pr-number.outputs.pr_number }}
+ path: SummaryGithub.md
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9491a2f..c6da057 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,7 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
-## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
@@ -29,7 +29,6 @@ x86/
bld/
[Bb]in/
[Oo]bj/
-[Oo]ut/
[Ll]og/
[Ll]ogs/
@@ -58,11 +57,14 @@ dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
-# .NET Core
+# .NET
project.lock.json
project.fragment.lock.json
artifacts/
+# Tye
+.tye/
+
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
@@ -91,6 +93,7 @@ StyleCopReport.xml
*.tmp_proj
*_wpftmp.csproj
*.log
+*.tlog
*.vspscc
*.vssscc
.builds
@@ -240,6 +243,8 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
+PublishDir/
+
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
@@ -294,6 +299,17 @@ node_modules/
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
@@ -350,6 +366,9 @@ ASALocalRun/
# Local History for Visual Studio
.localhistory/
+# Visual Studio History (VSHistory) files
+.vshistory/
+
# BeatPulse healthcheck temp database
healthchecksdb
@@ -360,4 +379,104 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
\ No newline at end of file
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Ignore live unit testing config
+*.lutconfig
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..2abd3c6
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,32 @@
+
+
+
+
+
+ enable
+ enable
+ 12
+ true
+
+
+
+
+
+
+ 1ef91ee6-7b55-474e-ab2b-2d164b723a56
+
+
+
\ No newline at end of file
diff --git a/Directory.Build.targets b/Directory.Build.targets
new file mode 100644
index 0000000..06dc368
--- /dev/null
+++ b/Directory.Build.targets
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..4bcc052
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,45 @@
+
+
+
+
+
+ true
+
+ true
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
\ No newline at end of file
diff --git a/LD-SysInfo/App.xaml b/LD-SysInfo/App.xaml
new file mode 100644
index 0000000..744ef1b
--- /dev/null
+++ b/LD-SysInfo/App.xaml
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LD-SysInfo/App.xaml.cs b/LD-SysInfo/App.xaml.cs
new file mode 100644
index 0000000..af3dd9e
--- /dev/null
+++ b/LD-SysInfo/App.xaml.cs
@@ -0,0 +1,56 @@
+using CommunityToolkit.Mvvm.Messaging;
+using MaterialDesignThemes.Wpf;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using System.Windows;
+using System.Windows.Threading;
+
+namespace LD_SysInfo;
+
+///
+/// Interaction logic for App.xaml
+///
+public partial class App : Application
+{
+ [STAThread]
+ private static void Main(string[] args)
+ {
+ MainAsync(args).GetAwaiter().GetResult();
+ }
+
+ private static async Task MainAsync(string[] args)
+ {
+ using IHost host = CreateHostBuilder(args).Build();
+ await host.StartAsync().ConfigureAwait(true);
+
+ App app = new();
+ app.InitializeComponent();
+ app.MainWindow = host.Services.GetRequiredService();
+ app.MainWindow.Visibility = Visibility.Visible;
+ app.Run();
+
+ await host.StopAsync().ConfigureAwait(true);
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureAppConfiguration((hostBuilderContext, configurationBuilder)
+ => configurationBuilder.AddUserSecrets(typeof(App).Assembly))
+ .ConfigureServices((hostContext, services) =>
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+
+ services.AddSingleton();
+ services.AddSingleton(provider => provider.GetRequiredService());
+
+ services.AddSingleton(_ => Current.Dispatcher);
+
+ services.AddTransient(provider =>
+ {
+ Dispatcher dispatcher = provider.GetRequiredService();
+ return new SnackbarMessageQueue(TimeSpan.FromSeconds(3.0), dispatcher);
+ });
+ });
+}
diff --git a/LD-SysInfo/AssemblyInfo.cs b/LD-SysInfo/AssemblyInfo.cs
new file mode 100644
index 0000000..8b5504e
--- /dev/null
+++ b/LD-SysInfo/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+using System.Windows;
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
+ //(used if a resource is not found in the page,
+ // or application resource dictionaries)
+ ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
+ //(used if a resource is not found in the page,
+ // app, or any theme specific resource dictionaries)
+)]
diff --git a/LD-SysInfo/Assets/LDShortcut.ico b/LD-SysInfo/Assets/LDShortcut.ico
new file mode 100644
index 0000000..6dbef03
Binary files /dev/null and b/LD-SysInfo/Assets/LDShortcut.ico differ
diff --git a/LD-SysInfo/Assets/trayicon.ico b/LD-SysInfo/Assets/trayicon.ico
new file mode 100644
index 0000000..0761cff
Binary files /dev/null and b/LD-SysInfo/Assets/trayicon.ico differ
diff --git a/LD-SysInfo/Assets/windowsdesktop-runtime-8.0.13-win-x64.exe b/LD-SysInfo/Assets/windowsdesktop-runtime-8.0.13-win-x64.exe
new file mode 100644
index 0000000..31f478e
Binary files /dev/null and b/LD-SysInfo/Assets/windowsdesktop-runtime-8.0.13-win-x64.exe differ
diff --git a/LD-SysInfo/DLShortcut.ico b/LD-SysInfo/DLShortcut.ico
new file mode 100644
index 0000000..b877981
Binary files /dev/null and b/LD-SysInfo/DLShortcut.ico differ
diff --git a/LD-SysInfo/EncryptionHelper.cs b/LD-SysInfo/EncryptionHelper.cs
new file mode 100644
index 0000000..d1dfd3f
--- /dev/null
+++ b/LD-SysInfo/EncryptionHelper.cs
@@ -0,0 +1,35 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+public class EncryptionHelper
+{
+ private static readonly byte[] Key = Encoding.UTF8.GetBytes("HWJGbwmF2pWdXySDExMNEbJSrXn0YCBF"); // 32 bytes for AES-256
+ private static readonly byte[] IV = Encoding.UTF8.GetBytes("VWYRtYCfch0sKs6k"); // 16 bytes
+
+ public static string EncryptData(string plainText)
+ {
+ using (Aes aes = Aes.Create())
+ {
+ aes.Key = Key;
+ aes.IV = IV;
+ aes.Mode = CipherMode.CBC;
+ aes.Padding = PaddingMode.PKCS7;
+
+ using (MemoryStream ms = new MemoryStream())
+ using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
+ {
+ using (StreamWriter sw = new StreamWriter(cs, new UTF8Encoding(false))) // 🔹 Disable BOM
+ {
+ sw.Write(plainText);
+ }
+ byte[] encryptedBytes = ms.ToArray();
+ string encryptedBase64 = Convert.ToBase64String(encryptedBytes);
+
+ Console.WriteLine("🔹 [DEBUG] Encrypted Data (Base64): " + encryptedBase64); // ✅ Debugging
+ return encryptedBase64;
+ }
+ }
+ }
+}
diff --git a/LD-SysInfo/LD_SysInfo.csproj b/LD-SysInfo/LD_SysInfo.csproj
new file mode 100644
index 0000000..0d31a2e
--- /dev/null
+++ b/LD-SysInfo/LD_SysInfo.csproj
@@ -0,0 +1,63 @@
+
+
+
+ PSG-Oversight
+
+ WinExe
+ net8.0-windows
+
+ true
+ LD_SysInfo.App
+ Assets\LDShortcut.ico
+ C:\Users\Sonder\source\repos\psg-oversight-app\BuildDir\bin\Debug\net8.0-windows\
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
diff --git a/LD-SysInfo/MainWindow.xaml b/LD-SysInfo/MainWindow.xaml
new file mode 100644
index 0000000..0cd71e3
--- /dev/null
+++ b/LD-SysInfo/MainWindow.xaml
@@ -0,0 +1,409 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LD-SysInfo/MainWindow.xaml.cs b/LD-SysInfo/MainWindow.xaml.cs
new file mode 100644
index 0000000..024213e
--- /dev/null
+++ b/LD-SysInfo/MainWindow.xaml.cs
@@ -0,0 +1,626 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using Microsoft.Win32;
+using System.IO;
+using System.Windows.Threading;
+using System.Windows.Forms;
+using Hardcodet.Wpf.TaskbarNotification;
+using LD_SysInfo.Services;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Windows.Media.Animation;
+using LD_SysInfo.Models;
+using MaterialDesignColors;
+using MaterialDesignThemes.Wpf;
+using System.Windows.Input;
+using Application = System.Windows.Application;
+using System.Windows.Media;
+
+
+
+namespace LD_SysInfo
+
+{
+ public partial class MainWindow : Window
+ {
+
+ private static readonly HttpClient httpClient = new HttpClient();
+ private static AppConfig _config;
+ private static string jwtToken = null; // 🔹 Store the JWT token globally in the app
+ private readonly DispatcherTimer messageClearTimer;
+ private readonly DispatcherTimer postTimer;
+ private readonly DispatcherTimer keepAliveTimer;
+ private readonly Uri LightThemeUri = new Uri("pack://application:,,,/Themes/LightTheme.xaml", UriKind.Absolute);
+ private readonly Uri DarkThemeUri = new Uri("pack://application:,,,/Themes/DarkTheme.xaml", UriKind.Absolute);
+ private bool isDarkTheme = false;
+
+ private static readonly string ConfigPath = Path.Combine(AppContext.BaseDirectory, "config.json");
+
+
+ public MainWindow()
+ {
+ // ⚠️ Temporary SSL Certificate Bypass - For Testing Only
+ System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
+
+ InitializeComponent();
+ LoadConfig();
+ DisplaySystemInfo();
+ AutoLogin();
+
+ // 🔍 Perform initial connectivity check
+ Task.Run(async () => await InitialCheckConnectivity());
+
+ // Initialize the system tray icon event
+ TrayIcon.TrayMouseDoubleClick += TrayIcon_DoubleClick;
+
+ // 🔥 Initialize the DispatcherTimer for fading out messages (statically set to 5 seconds)
+ messageClearTimer = new DispatcherTimer();
+ messageClearTimer.Interval = TimeSpan.FromSeconds(5); // Set to 5 seconds (static)
+ messageClearTimer.Tick += MessageClearTimer_Tick;
+
+ // 🔥 Initialize the DispatcherTimer for periodic POSTs using _config.PollingInterval
+ postTimer = new DispatcherTimer();
+ postTimer.Interval = TimeSpan.FromSeconds(_config.SystemInfoInterval);
+ postTimer.Tick += PostTimer_Tick;
+ postTimer.Start();
+
+ // 🔁 Initialize the KeepAlive timer using KeepAlivePeriod from config
+ keepAliveTimer = new DispatcherTimer();
+ keepAliveTimer.Interval = TimeSpan.FromSeconds(_config.KeepAlivePeriod); // Set from config
+ keepAliveTimer.Tick += KeepAliveTimer_Tick;
+ keepAliveTimer.Start();
+
+
+
+
+ }
+
+
+ private void LoadConfig()
+ {
+ try
+ {
+ //System.Windows.MessageBox.Show($"DEBUG: AppContext.BaseDirectory = {AppContext.BaseDirectory}");
+ //System.Windows.MessageBox.Show($"DEBUG: ConfigPath = {ConfigPath}");
+
+ if (!File.Exists((string?)ConfigPath))
+ {
+ System.Windows.MessageBox.Show("❌ config.json not found at resolved path!");
+ }
+
+ _config = ConfigManager.LoadConfig(ConfigPath);
+
+
+ if (_config == null || string.IsNullOrWhiteSpace(_config.ClientIdentifier))
+ {
+ throw new Exception("❌ Invalid or missing ClientIdentifier in config.json.");
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Windows.MessageBox.Show($"❌ Error loading config: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ _config = new AppConfig(); // Prevent application from crashing
+ }
+ }
+
+
+ private async Task InitialCheckConnectivity()
+ {
+ var apiClient = new ApiClient(_config);
+ bool isConnected = await apiClient.CheckConnectivity();
+
+ // You must update the UI from the UI thread
+ Dispatcher.Invoke(() =>
+ {
+ if (isConnected)
+ {
+ ConnectionStatusTextBlock.Text = "Connected to Server";
+ ConnectionStatusTextBlock.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Green);
+ }
+ else
+ {
+ ConnectionStatusTextBlock.Text = "No Connection";
+ ConnectionStatusTextBlock.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
+ }
+ });
+ }
+
+ private async void AutoLogin()
+ {
+ if (_config == null || _config.Auth == null ||
+ string.IsNullOrEmpty(_config.Auth.Username) || string.IsNullOrEmpty(_config.Auth.Password))
+ {
+ UpdateStatusUI(false);
+ return;
+ }
+
+ var apiClient = new ApiClient(_config);
+ LoginResponse loginResponse = await apiClient.AuthenticateAsync(_config.Auth.Username, _config.Auth.Password);
+
+ if (loginResponse != null && !string.IsNullOrEmpty(loginResponse.Token))
+ {
+ ApiClient.SetJwtToken(loginResponse.Token);
+ UpdateStatusUI(true);
+ }
+ else
+ {
+ UpdateStatusUI(false);
+ }
+ }
+
+ private async void AuthenticateButton_Click(object sender, RoutedEventArgs e)
+ {
+ var apiClient = new ApiClient(_config);
+ LoginResponse loginResponse = await apiClient.AuthenticateAsync(txtUsername.Text, pwdPassword.Password);
+
+ if (loginResponse != null && !string.IsNullOrEmpty(loginResponse.Token))
+ {
+ ApiClient.SetJwtToken(loginResponse.Token);
+ UpdateStatusUI(true); // 🔥 Successfully authenticated
+ }
+ else
+ {
+ UpdateStatusUI(false);
+ }
+ }
+
+
+ private async void CheckConnectivityButton_Click(object sender, RoutedEventArgs e)
+ {
+ var apiClient = new ApiClient(_config);
+ bool isConnected = await apiClient.CheckConnectivity();
+
+ if (isConnected)
+ {
+ ConnectionStatusTextBlock.Text = "Connected to Server";
+ ConnectionStatusTextBlock.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Green);
+ }
+ else
+ {
+ ConnectionStatusTextBlock.Text = "No Connection";
+ ConnectionStatusTextBlock.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
+ }
+ }
+
+ private void GenerateLogsButton_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ string logDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LD_SysInfo", "Logs");
+ Directory.CreateDirectory(logDir); // Ensure directory exists
+
+ string logFile = Path.Combine(logDir, $"log_{DateTime.Now:yyyyMMdd_HHmmss}.txt");
+ File.WriteAllText(logFile, "=== LD SysInfo Diagnostic Log ===\n");
+
+ File.AppendAllText(logFile, $"Timestamp: {DateTime.Now}\n");
+ File.AppendAllText(logFile, $"Hostname: {Environment.MachineName}\n");
+ File.AppendAllText(logFile, $"OS: {Environment.OSVersion}\n");
+ File.AppendAllText(logFile, $"Server Status: {ConnectionStatusTextBlock.Text}\n");
+
+ System.Windows.MessageBox.Show($"Logs generated successfully!\nPath: {logFile}", "Log Generated", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ System.Windows.MessageBox.Show($"Failed to generate logs.\nError: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void DisplaySystemInfo()
+ {
+ var sysInfo = SystemInfo.GetSystemInfo();
+ OSNameTextBlock.Text = $"OS Name: {sysInfo.OSName}";
+ OSVersionTextBlock.Text = $"OS Version: {sysInfo.OSVersion}";
+ WindowsVersionTextBlock.Text = $"Windows Version: {sysInfo.WindowsVersion}";
+ WindowsBuildTextBlock.Text = $"Windows Build: {sysInfo.WindowsBuild}";
+ OSArchitectureTextBlock.Text = $"OS Architecture: {sysInfo.OSArchitecture}";
+ ProcessorNameTextBlock.Text = $"Processor Name: {sysInfo.ProcessorName}";
+ ProcessorArchitectureTextBlock.Text = $"Processor Architecture: {sysInfo.ProcessorArchitecture}";
+ HostnameTextBlock.Text = $"Hostname: {sysInfo.Hostname}";
+ }
+
+ private void LoadInstalledAppsButton_Click(object sender, RoutedEventArgs e)
+ {
+ var applications = SystemInfo.GetInstalledApplicationsFromRegistry();
+ InstalledAppsListBox.ItemsSource = applications;
+ }
+
+ private async void StoreSystemInfoButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (string.IsNullOrEmpty(ApiClient.GetJwtToken())) // Check token via ApiClient
+ {
+ System.Windows.MessageBox.Show("❌ Please log in first.", "Not Authenticated", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ try
+ {
+ var sysInfo = SystemInfo.GetSystemInfo();
+ var applications = SystemInfo.GetInstalledApplicationsFromRegistry();
+
+ var formattedApplications = new List