diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..06558cd
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,117 @@
+---
+kind: pipeline
+type: docker
+name: build
+
+steps:
+ - name: dotnet-build
+ image: mcr.microsoft.com/dotnet/sdk:10.0
+ commands:
+ - dotnet restore LiteCharmsShopAdmin.slnx
+ - dotnet build LiteCharmsShopAdmin.slnx -c Release
+
+ - name: dotnet-test
+ image: mcr.microsoft.com/dotnet/sdk:10.0
+ commands:
+ - dotnet restore LiteCharmsShopAdmin.slnx
+ - dotnet test LiteCharmsShopAdmin.slnx -c Release --no-restore
+
+trigger:
+ event: [ pull_request ]
+
+---
+kind: pipeline
+type: docker
+name: package
+
+steps:
+ - name: docker-build
+ image: plugins/docker
+ settings:
+ registry: nexus.khongisa.co.za
+ repo: nexus.khongisa.co.za/litecharms-shopadmin-uat
+ tags: [ latest, "1.${DRONE_BUILD_NUMBER}" ]
+ custom_labels:
+ - org.opencontainers.image.source=https://gitea.khongisa.co.za/litecharms/litecharms-shopadmin-uat
+ - org.opencontainers.image.version=1.${DRONE_BUILD_NUMBER}
+ - org.opencontainers.image.revision=${DRONE_COMMIT_SHA}
+ username: { from_secret: docker_username }
+ password: { from_secret: docker_password }
+
+ - name: gitea-tag-release
+ image: alpine/git
+ environment:
+ GITEA_TOKEN: { from_secret: git_token }
+ GITEA_USER: { from_secret: git_username }
+ GITEA_PASS: { from_secret: git_password }
+ commands:
+ - echo "169.255.58.144 gitea.khongisa.co.za" >> /etc/hosts
+ - apk add --no-cache curl
+ - git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/shopadmin.git
+ - git tag 1.${DRONE_BUILD_NUMBER}
+ - git push origin 1.${DRONE_BUILD_NUMBER}
+ - |
+ curl -X POST "https://gitea.khongisa.co.za/api/v1/repos/litecharms/shopadmin/releases" \
+ -H "Authorization: token $${GITEA_TOKEN}" \
+ -H "Content-Type: application/json" \
+ -d "{
+ \"tag_name\": \"1.${DRONE_BUILD_NUMBER}\",
+ \"target_commitish\": \"${DRONE_COMMIT_SHA}\",
+ \"name\": \"Release 1.${DRONE_BUILD_NUMBER}\",
+ \"body\": \"### Artifacts\n* **Docker Image:** nexus.khongisa.co.za/shopadmin:1.${DRONE_BUILD_NUMBER}\n* **NuGet:** [View on Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
+ \"draft\": false,
+ \"prerelease\": false
+ }"
+
+depends_on:
+ - build
+
+trigger:
+ event: [ pull_request ]
+
+---
+kind: pipeline
+type: docker
+name: uat
+
+steps:
+ - name: deploy
+ image: bitnami/kubectl:latest
+ environment:
+ KUBE_CONFIG: { from_secret: kube_config }
+ commands:
+ - mkdir -p $HOME/.kube
+ - echo "$KUBE_CONFIG" > $HOME/.kube/config
+ - kubectl apply -f litecharms-shopadmin-uat.yml
+ - sleep 10
+ - kubectl rollout restart deployment/litecharms-shopadmin -n litecharms-shopadmin-uat
+
+depends_on:
+ - package
+
+trigger:
+ event: [ pull_request ]
+
+---
+kind: pipeline
+type: docker
+name: prod
+
+steps:
+ - name: deploy
+ image: bitnami/kubectl:latest
+ environment:
+ KUBE_CONFIG: { from_secret: kube_config }
+ commands:
+ - mkdir -p $HOME/.kube
+ - echo "$KUBE_CONFIG" > $HOME/.kube/config
+ - kubectl apply -f litecharms-shopadmin.yml
+ - sleep 10
+ - kubectl rollout restart deployment/litecharms-shopadmin -n litecharms-shopadmin
+
+depends_on:
+ - uat
+
+trigger:
+ event: [ promote ]
+ target: [ production ]
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..4a5bdbf
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,248 @@
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
+root = true
+
+# C# files
+[*.cs]
+
+#### Core EditorConfig Options ####
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+end_of_line = crlf
+insert_final_newline = false
+
+#### .NET Code Actions ####
+
+# Type members
+dotnet_hide_advanced_members = false
+dotnet_member_insertion_location = with_other_members_of_the_same_kind
+dotnet_property_generation_behavior = prefer_throwing_properties
+
+# Symbol search
+dotnet_search_reference_assemblies = true
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+dotnet_separate_import_directive_groups = false
+dotnet_sort_system_directives_first = false
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_prefer_system_hash_code = true
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_non_hidden_explicit_cast_in_source = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+dotnet_style_allow_statement_immediately_after_block_experimental = true
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false
+csharp_style_var_for_built_in_types = false
+csharp_style_var_when_type_is_apparent = false
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true
+csharp_style_expression_bodied_constructors = false
+csharp_style_expression_bodied_indexers = true
+csharp_style_expression_bodied_lambdas = true
+csharp_style_expression_bodied_local_functions = true
+csharp_style_expression_bodied_methods = false
+csharp_style_expression_bodied_operators = false
+csharp_style_expression_bodied_properties = true
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true
+csharp_style_pattern_matching_over_is_with_cast_check = true
+csharp_style_prefer_extended_property_pattern = true
+csharp_style_prefer_not_pattern = true
+csharp_style_prefer_pattern_matching = true
+csharp_style_prefer_switch_expression = true
+
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true
+
+# Modifier preferences
+csharp_prefer_static_anonymous_function = true
+csharp_prefer_static_local_function = true
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true
+csharp_style_prefer_readonly_struct_member = true
+
+# Code-block preferences
+csharp_prefer_braces = true
+csharp_prefer_simple_using_statement = true
+csharp_prefer_system_threading_lock = true
+csharp_style_namespace_declarations = block_scoped
+csharp_style_prefer_method_group_conversion = true
+csharp_style_prefer_primary_constructors = true
+csharp_style_prefer_simple_property_accessors = true
+csharp_style_prefer_top_level_statements = true
+
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true
+csharp_style_deconstructed_variable_declaration = true
+csharp_style_implicit_object_creation_when_type_is_apparent = true
+csharp_style_inlined_variable_declaration = true
+csharp_style_prefer_implicitly_typed_lambda_expression = true
+csharp_style_prefer_index_operator = true
+csharp_style_prefer_local_over_anonymous_function = true
+csharp_style_prefer_null_check_over_type_check = true
+csharp_style_prefer_range_operator = true
+csharp_style_prefer_tuple_swap = true
+csharp_style_prefer_unbound_generic_type_in_nameof = true
+csharp_style_prefer_utf8_string_literals = true
+csharp_style_throw_expression = true
+csharp_style_unused_value_assignment_preference = discard_variable
+csharp_style_unused_value_expression_statement_preference = discard_variable
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace
+
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
+csharp_style_allow_embedded_statements_on_same_line_experimental = true
+
+#### 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
+
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
+
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.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 =
+
+# Naming styles
+
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
diff --git a/.filenesting.json b/.filenesting.json
new file mode 100644
index 0000000..483aefc
--- /dev/null
+++ b/.filenesting.json
@@ -0,0 +1,14 @@
+{
+ "root": true,
+ "dependentFileProviders": {
+ "add": {
+ "addedExtension": {},
+ "extensionToExtension": {
+ "add": {
+ ".css": [ ".razor" ],
+ ".cs": [ ".razor" ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..adb766b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,20 @@
+# Stage 1: Build
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /app
+
+COPY ["nuget.config", "./"]
+COPY ["ShopAdmin/ShopAdmin.csproj", "Shop/"]
+RUN dotnet restore "ShopAdmin/ShopAdmin.csproj" --configfile nuget.config
+COPY . .
+RUN dotnet publish "ShopAdmin/ShopAdmin.csproj" -c Release -o /app/publish /p:UseAppHost=false
+
+# Stage 2: Final Image
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+WORKDIR /app
+
+EXPOSE 8080
+EXPOSE 8081
+
+COPY --from=build /app/publish .
+
+ENTRYPOINT ["dotnet", "ShopAdmin.dll"]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d3f8705
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,18 @@
+PROPRIETARY LICENSE
+
+Copyright (c) 2026 Lite Charms (PTY) Ltd. All rights reserved.
+
+This software and its associated documentation (the "Software") are the
+proprietary property of Lite Charms (PTY) Ltd.
+
+The Software is provided for internal use only. Unauthorized copying,
+distribution, modification, or use of this file via any medium is
+strictly prohibited.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/LiteCharms.snk b/LiteCharms.snk
new file mode 100644
index 0000000..598e6da
Binary files /dev/null and b/LiteCharms.snk differ
diff --git a/LiteCharmsShopAdmin.slnx b/LiteCharmsShopAdmin.slnx
index 4e2253d..3b3c6ff 100644
--- a/LiteCharmsShopAdmin.slnx
+++ b/LiteCharmsShopAdmin.slnx
@@ -1 +1,3 @@
-
+
+
+
diff --git a/README.md b/README.md
index 93354d9..6e62d9d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,24 @@
-# LiteCharmsShopAdmin
\ No newline at end of file
+# LiteCharms Shop Admin
+
+The primary internal back-office and operations management interface for the LiteCharms platform.
+
+## ⚙️ Core Functions
+* **Product Engine:** Configure software packages (REST, gRPC, Identity Server), define add-ons, and maintain localized price lists.
+* **Process Override:** Manually progress order states, trigger overrides, and control customer workflow states.
+* **Analytics & Reports:** Monitor sales metrics, conversion funnels, and track intent data from the shop analytics schema.
+
+## 🏗 Architecture
+* **Type:** Kubernetes Deployment (`litecharms-admin` namespace)
+* **Scale:** Standalone high-availability instance.
+* **Ingress:** Restricted access via corporate VPN / secure tunnel routing via Traefik/IngressRoute.
+
+## 🚀 CI/CD Workflow
+* **Trigger:** Pull Request to `master`.
+* **Build:** Compiles Blazor WebAssembly / Server .NET 10.0 source.
+* **Containerize:** Docker image built and pushed to private Nexus registry as `litecharms-admin:1.${DRONE_BUILD_NUMBER}`.
+* **Deploy:** * Updates internal validation environment via GitOps/`kubectl apply`.
+ * Production release handled via Drone **Promotion** target.
+
+## 🌐 Endpoints
+* **Internal Hub:** `https://shop-admin.khongisa.co.za` (Restricted Network Access)
+* **State Sync:** Directly interfaces with the core PostgreSQL instance and publishes MediatR commands to the isolated `litecharms-scheduler` process.
\ No newline at end of file
diff --git a/ShopAdmin/Components/App.razor b/ShopAdmin/Components/App.razor
new file mode 100644
index 0000000..bd68990
--- /dev/null
+++ b/ShopAdmin/Components/App.razor
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ShopAdmin/Components/Layout/MainLayout.razor b/ShopAdmin/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..2ebe789
--- /dev/null
+++ b/ShopAdmin/Components/Layout/MainLayout.razor
@@ -0,0 +1,19 @@
+@inherits LayoutComponentBase
+
+
+
+
+
+
+ @* @Body *@
+
+
+
+
+
+ An unhandled error has occurred.
+
Reload
+
🗙
+
diff --git a/ShopAdmin/Components/Layout/MainLayout.razor.css b/ShopAdmin/Components/Layout/MainLayout.razor.css
new file mode 100644
index 0000000..38d1f25
--- /dev/null
+++ b/ShopAdmin/Components/Layout/MainLayout.razor.css
@@ -0,0 +1,98 @@
+.page {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+}
+
+main {
+ flex: 1;
+}
+
+.sidebar {
+ background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
+}
+
+.top-row {
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #d6d5d5;
+ justify-content: flex-end;
+ height: 3.5rem;
+ display: flex;
+ align-items: center;
+}
+
+ .top-row ::deep a, .top-row ::deep .btn-link {
+ white-space: nowrap;
+ margin-left: 1.5rem;
+ text-decoration: none;
+ }
+
+ .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
+ text-decoration: underline;
+ }
+
+ .top-row ::deep a:first-child {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+@media (max-width: 640.98px) {
+ .top-row {
+ justify-content: space-between;
+ }
+
+ .top-row ::deep a, .top-row ::deep .btn-link {
+ margin-left: 0;
+ }
+}
+
+@media (min-width: 641px) {
+ .page {
+ flex-direction: row;
+ }
+
+ .sidebar {
+ width: 250px;
+ height: 100vh;
+ position: sticky;
+ top: 0;
+ }
+
+ .top-row {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+
+ .top-row.auth ::deep a:first-child {
+ flex: 1;
+ text-align: right;
+ width: 0;
+ }
+
+ .top-row, article {
+ padding-left: 2rem !important;
+ padding-right: 1.5rem !important;
+ }
+}
+
+#blazor-error-ui {
+ color-scheme: light only;
+ background: lightyellow;
+ bottom: 0;
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ display: none;
+ left: 0;
+ padding: 0.6rem 1.25rem 0.7rem 1.25rem;
+ position: fixed;
+ width: 100%;
+ z-index: 1000;
+}
+
+ #blazor-error-ui .dismiss {
+ cursor: pointer;
+ position: absolute;
+ right: 0.75rem;
+ top: 0.5rem;
+ }
diff --git a/ShopAdmin/Components/Layout/ReconnectModal.razor b/ShopAdmin/Components/Layout/ReconnectModal.razor
new file mode 100644
index 0000000..e740b0c
--- /dev/null
+++ b/ShopAdmin/Components/Layout/ReconnectModal.razor
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ Rejoining the server...
+
+
+ Rejoin failed... trying again in seconds.
+
+
+ Failed to rejoin. Please retry or reload the page.
+
+
+ Retry
+
+
+ The session has been paused by the server.
+
+
+ Failed to resume the session. Please retry or reload the page.
+
+
+ Resume
+
+
+
diff --git a/ShopAdmin/Components/Layout/ReconnectModal.razor.css b/ShopAdmin/Components/Layout/ReconnectModal.razor.css
new file mode 100644
index 0000000..3ad3773
--- /dev/null
+++ b/ShopAdmin/Components/Layout/ReconnectModal.razor.css
@@ -0,0 +1,157 @@
+.components-reconnect-first-attempt-visible,
+.components-reconnect-repeated-attempt-visible,
+.components-reconnect-failed-visible,
+.components-pause-visible,
+.components-resume-failed-visible,
+.components-rejoining-animation {
+ display: none;
+}
+
+#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
+#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
+#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
+#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
+#components-reconnect-modal.components-reconnect-retrying,
+#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
+#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
+#components-reconnect-modal.components-reconnect-failed,
+#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
+ display: block;
+}
+
+
+#components-reconnect-modal {
+ background-color: white;
+ width: 20rem;
+ margin: 20vh auto;
+ padding: 2rem;
+ border: 0;
+ border-radius: 0.5rem;
+ box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
+ opacity: 0;
+ transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
+ animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
+ &[open]
+
+{
+ animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
+ animation-fill-mode: both;
+}
+
+}
+
+#components-reconnect-modal::backdrop {
+ background-color: rgba(0, 0, 0, 0.4);
+ animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
+ opacity: 1;
+}
+
+@keyframes components-reconnect-modal-slideUp {
+ 0% {
+ transform: translateY(30px) scale(0.95);
+ }
+
+ 100% {
+ transform: translateY(0);
+ }
+}
+
+@keyframes components-reconnect-modal-fadeInOpacity {
+ 0% {
+ opacity: 0;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
+
+@keyframes components-reconnect-modal-fadeOutOpacity {
+ 0% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+.components-reconnect-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+}
+
+#components-reconnect-modal p {
+ margin: 0;
+ text-align: center;
+}
+
+#components-reconnect-modal button {
+ border: 0;
+ background-color: #6b9ed2;
+ color: white;
+ padding: 4px 24px;
+ border-radius: 4px;
+}
+
+ #components-reconnect-modal button:hover {
+ background-color: #3b6ea2;
+ }
+
+ #components-reconnect-modal button:active {
+ background-color: #6b9ed2;
+ }
+
+.components-rejoining-animation {
+ position: relative;
+ width: 80px;
+ height: 80px;
+}
+
+ .components-rejoining-animation div {
+ position: absolute;
+ border: 3px solid #0087ff;
+ opacity: 1;
+ border-radius: 50%;
+ animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
+ }
+
+ .components-rejoining-animation div:nth-child(2) {
+ animation-delay: -0.5s;
+ }
+
+@keyframes components-rejoining-animation {
+ 0% {
+ top: 40px;
+ left: 40px;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+
+ 4.9% {
+ top: 40px;
+ left: 40px;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ }
+
+ 5% {
+ top: 40px;
+ left: 40px;
+ width: 0;
+ height: 0;
+ opacity: 1;
+ }
+
+ 100% {
+ top: 0px;
+ left: 0px;
+ width: 80px;
+ height: 80px;
+ opacity: 0;
+ }
+}
diff --git a/ShopAdmin/Components/Layout/ReconnectModal.razor.js b/ShopAdmin/Components/Layout/ReconnectModal.razor.js
new file mode 100644
index 0000000..a44de78
--- /dev/null
+++ b/ShopAdmin/Components/Layout/ReconnectModal.razor.js
@@ -0,0 +1,63 @@
+// Set up event handlers
+const reconnectModal = document.getElementById("components-reconnect-modal");
+reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
+
+const retryButton = document.getElementById("components-reconnect-button");
+retryButton.addEventListener("click", retry);
+
+const resumeButton = document.getElementById("components-resume-button");
+resumeButton.addEventListener("click", resume);
+
+function handleReconnectStateChanged(event) {
+ if (event.detail.state === "show") {
+ reconnectModal.showModal();
+ } else if (event.detail.state === "hide") {
+ reconnectModal.close();
+ } else if (event.detail.state === "failed") {
+ document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
+ } else if (event.detail.state === "rejected") {
+ location.reload();
+ }
+}
+
+async function retry() {
+ document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
+
+ try {
+ // Reconnect will asynchronously return:
+ // - true to mean success
+ // - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
+ // - exception to mean we didn't reach the server (this can be sync or async)
+ const successful = await Blazor.reconnect();
+ if (!successful) {
+ // We have been able to reach the server, but the circuit is no longer available.
+ // We'll reload the page so the user can continue using the app as quickly as possible.
+ const resumeSuccessful = await Blazor.resumeCircuit();
+ if (!resumeSuccessful) {
+ location.reload();
+ } else {
+ reconnectModal.close();
+ }
+ }
+ } catch (err) {
+ // We got an exception, server is currently unavailable
+ document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
+ }
+}
+
+async function resume() {
+ try {
+ const successful = await Blazor.resumeCircuit();
+ if (!successful) {
+ location.reload();
+ }
+ } catch {
+ reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
+ }
+}
+
+async function retryWhenDocumentBecomesVisible() {
+ if (document.visibilityState === "visible") {
+ await retry();
+ }
+}
diff --git a/ShopAdmin/Components/Pages/Counter.razor b/ShopAdmin/Components/Pages/Counter.razor
new file mode 100644
index 0000000..1a4f8e7
--- /dev/null
+++ b/ShopAdmin/Components/Pages/Counter.razor
@@ -0,0 +1,19 @@
+@page "/counter"
+@rendermode InteractiveServer
+
+Counter
+
+Counter
+
+Current count: @currentCount
+
+Click me
+
+@code {
+ private int currentCount = 0;
+
+ private void IncrementCount()
+ {
+ currentCount++;
+ }
+}
diff --git a/ShopAdmin/Components/Pages/Error.razor b/ShopAdmin/Components/Pages/Error.razor
new file mode 100644
index 0000000..576cc2d
--- /dev/null
+++ b/ShopAdmin/Components/Pages/Error.razor
@@ -0,0 +1,36 @@
+@page "/Error"
+@using System.Diagnostics
+
+Error
+
+Error.
+An error occurred while processing your request.
+
+@if (ShowRequestId)
+{
+
+ Request ID: @RequestId
+
+}
+
+Development Mode
+
+ Swapping to Development environment will display more detailed information about the error that occurred.
+
+
+ The Development environment shouldn't be enabled for deployed applications.
+ It can result in displaying sensitive information from exceptions to end users.
+ For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
+ and restarting the app.
+
+
+@code{
+ [CascadingParameter]
+ private HttpContext? HttpContext { get; set; }
+
+ private string? RequestId { get; set; }
+ private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+
+ protected override void OnInitialized() =>
+ RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+}
diff --git a/ShopAdmin/Components/Pages/Home.razor b/ShopAdmin/Components/Pages/Home.razor
new file mode 100644
index 0000000..9001e0b
--- /dev/null
+++ b/ShopAdmin/Components/Pages/Home.razor
@@ -0,0 +1,7 @@
+@page "/"
+
+Home
+
+Hello, world!
+
+Welcome to your new app.
diff --git a/ShopAdmin/Components/Pages/NotFound.razor b/ShopAdmin/Components/Pages/NotFound.razor
new file mode 100644
index 0000000..917ada1
--- /dev/null
+++ b/ShopAdmin/Components/Pages/NotFound.razor
@@ -0,0 +1,5 @@
+@page "/not-found"
+@layout MainLayout
+
+Not Found
+Sorry, the content you are looking for does not exist.
\ No newline at end of file
diff --git a/ShopAdmin/Components/Pages/Weather.razor b/ShopAdmin/Components/Pages/Weather.razor
new file mode 100644
index 0000000..f437e5e
--- /dev/null
+++ b/ShopAdmin/Components/Pages/Weather.razor
@@ -0,0 +1,64 @@
+@page "/weather"
+@attribute [StreamRendering]
+
+Weather
+
+Weather
+
+This component demonstrates showing data.
+
+@if (forecasts == null)
+{
+ Loading...
+}
+else
+{
+
+
+
+ Date
+ Temp. (C)
+ Temp. (F)
+ Summary
+
+
+
+ @foreach (var forecast in forecasts)
+ {
+
+ @forecast.Date.ToShortDateString()
+ @forecast.TemperatureC
+ @forecast.TemperatureF
+ @forecast.Summary
+
+ }
+
+
+}
+
+@code {
+ private WeatherForecast[]? forecasts;
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Simulate asynchronous loading to demonstrate streaming rendering
+ await Task.Delay(500);
+
+ var startDate = DateOnly.FromDateTime(DateTime.Now);
+ var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
+ forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = startDate.AddDays(index),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = summaries[Random.Shared.Next(summaries.Length)]
+ }).ToArray();
+ }
+
+ private class WeatherForecast
+ {
+ public DateOnly Date { get; set; }
+ public int TemperatureC { get; set; }
+ public string? Summary { get; set; }
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+ }
+}
diff --git a/ShopAdmin/Components/Routes.razor b/ShopAdmin/Components/Routes.razor
new file mode 100644
index 0000000..105855d
--- /dev/null
+++ b/ShopAdmin/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/ShopAdmin/Components/_Imports.razor b/ShopAdmin/Components/_Imports.razor
new file mode 100644
index 0000000..5fd3442
--- /dev/null
+++ b/ShopAdmin/Components/_Imports.razor
@@ -0,0 +1,11 @@
+@using System.Net.Http
+@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using static Microsoft.AspNetCore.Components.Web.RenderMode
+@using Microsoft.AspNetCore.Components.Web.Virtualization
+@using Microsoft.JSInterop
+@using ShopAdmin
+@using ShopAdmin.Components
+@using ShopAdmin.Components.Layout
diff --git a/ShopAdmin/Program.cs b/ShopAdmin/Program.cs
new file mode 100644
index 0000000..821838b
--- /dev/null
+++ b/ShopAdmin/Program.cs
@@ -0,0 +1,64 @@
+using LiteCharms.Features.Extensions;
+using LiteCharms.Features.Mediator;
+using ShopAdmin.Components;
+using static LiteCharms.Features.Email.Extensions.Constants;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+
+builder.AddMonitoring();
+
+builder.Services.AddControllers();
+builder.Services.AddBlazoredToast();
+builder.Services.AddEndpointsApiExplorer();
+
+builder.Services.AddMediator();
+
+builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(TelemetryPipelineBehavior<,>));
+builder.Services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingPipelineBehavior<,>));
+
+builder.Services.AddSalesServiceBus();
+builder.Services.AddGeneralServiceBus();
+builder.Services.AddQuartzSchedulerClient(ShopSchedulerName, builder.Configuration);
+
+builder.Services.AddEmailServices(builder.Configuration);
+builder.Services.AddEmailServiceBus();
+
+builder.Services.AddShopServices();
+builder.Services.AddShopDatabase(builder.Configuration);
+
+builder.Services.AddPostgresHealtchCheck();
+builder.Services.AddQuartzHealtchCheck();
+builder.Services.AddHealthChecksSupport(builder.Configuration);
+
+var app = builder.Build();
+
+var schedulerFactory = app.Services.GetRequiredService();
+var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName);
+
+if (!scheduler!.IsStarted)
+ await scheduler.Start();
+
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ app.UseHsts();
+}
+
+app.UseHealthChecks("/health", new HealthCheckOptions
+{
+ ResponseWriter = HealthChecks.UI.Client.UIResponseWriter.WriteHealthCheckUIResponse
+});
+
+app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
+app.UseHttpsRedirection();
+
+app.UseAntiforgery();
+
+app.MapStaticAssets();
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
+
+app.Run();
\ No newline at end of file
diff --git a/ShopAdmin/Properties/launchSettings.json b/ShopAdmin/Properties/launchSettings.json
new file mode 100644
index 0000000..1826785
--- /dev/null
+++ b/ShopAdmin/Properties/launchSettings.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5052",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7247;http://localhost:5052",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+ }
diff --git a/ShopAdmin/ShopAdmin.csproj b/ShopAdmin/ShopAdmin.csproj
new file mode 100644
index 0000000..0eb2409
--- /dev/null
+++ b/ShopAdmin/ShopAdmin.csproj
@@ -0,0 +1,57 @@
+
+
+
+ net10.0
+ enable
+ enable
+ ..\LiteCharms.snk
+ true
+ b929455a-ec27-42f9-bd54-3d54059c01bb
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ShopAdmin/appsettings.json b/ShopAdmin/appsettings.json
new file mode 100644
index 0000000..417425d
--- /dev/null
+++ b/ShopAdmin/appsettings.json
@@ -0,0 +1,23 @@
+{
+ "Email": {
+ "Credentials": {
+ "Username": "shop@litecharms.co.za"
+ },
+ "Port": 465,
+ "Host": "mail.litecharms.co.za",
+ "UseSsl": true
+ },
+ "Monitoring": {
+ "ApiKey": "",
+ "Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
+ "ServiceName": "LiteCharms.ShopAdmin"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Microsoft.EntityFrameworkCore": "Error"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ShopAdmin/wwwroot/app.css b/ShopAdmin/wwwroot/app.css
new file mode 100644
index 0000000..73a69d6
--- /dev/null
+++ b/ShopAdmin/wwwroot/app.css
@@ -0,0 +1,60 @@
+html, body {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+}
+
+a, .btn-link {
+ color: #006bb7;
+}
+
+.btn-primary {
+ color: #fff;
+ background-color: #1b6ec2;
+ border-color: #1861ac;
+}
+
+.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
+ box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
+}
+
+.content {
+ padding-top: 1.1rem;
+}
+
+h1:focus {
+ outline: none;
+}
+
+.valid.modified:not([type=checkbox]) {
+ outline: 1px solid #26b050;
+}
+
+.invalid {
+ outline: 1px solid #e50000;
+}
+
+.validation-message {
+ color: #e50000;
+}
+
+.blazor-error-boundary {
+ background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+ padding: 1rem 1rem 1rem 3.7rem;
+ color: white;
+}
+
+ .blazor-error-boundary::after {
+ content: "An error has occurred."
+ }
+
+.darker-border-checkbox.form-check-input {
+ border-color: #929292;
+}
+
+.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
+ color: var(--bs-secondary-color);
+ text-align: end;
+}
+
+.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
+ text-align: start;
+}
\ No newline at end of file
diff --git a/ShopAdmin/wwwroot/favicon.png b/ShopAdmin/wwwroot/favicon.png
new file mode 100644
index 0000000..8422b59
Binary files /dev/null and b/ShopAdmin/wwwroot/favicon.png differ
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..9c3f829
Binary files /dev/null and b/icon.png differ
diff --git a/litecharms-shopadmin-uat.yml b/litecharms-shopadmin-uat.yml
new file mode 100644
index 0000000..bfb7a88
--- /dev/null
+++ b/litecharms-shopadmin-uat.yml
@@ -0,0 +1,160 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: litecharms-shopadmin-uat
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: shop-config
+ namespace: litecharms-shopadmin-uat
+data:
+ ASPNETCORE_ENVIRONMENT: "Development"
+ ASPNETCORE_URLS: "http://0.0.0.0:8080"
+ Monitoring__Address: "http://aspire-dashboard-service.aspire.svc.cluster.local:18889"
+ Monitoring__ServiceName: "LiteCharms.Shop.Uat"
+---
+apiVersion: v1
+kind: Secret
+metadata:
+ name: shop-secrets
+ namespace: litecharms-shopadmin-uat
+type: Opaque
+data:
+ connection-string: SG9zdD0xOTIuMTY4LjEuMTcwO0RhdGFiYXNlPXNob3AtZGV2O1VzZXJuYW1lPXNob3AtZGV2LXVzZXI7UGFzc3dvcmQ9a1ZWbW9XS0ozeHpnUVg7UGVyc2lzdCBTZWN1cml0eSBJbmZvPVRydWU=
+ connection-string-quartz: SG9zdD0xOTIuMTY4LjEuMTcwO0RhdGFiYXNlPXNjaGVkdWxlci1kZXY7VXNlcm5hbWU9c2NoZWR1bGVyLWRldi11c2VyO1Bhc3N3b3JkPWtWVm1vV0tKM3h6Z1FYO1BlcnNpc3QgU2VjdXJpdHkgSW5mbz1UcnVl
+ aspire-apikey: bWMzRzYzSzJqNVpPRXNpMEFqTW9qTFRYbTFLRVpGY3R6SUlqU3dEaVRHdXQ4cUdTa1B1V3d4R1AxUmJzY0pVbw==
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: shop-data-pvc
+ namespace: litecharms-shopadmin-uat
+spec:
+ accessModes: ["ReadWriteMany"]
+ storageClassName: nfs-storage
+ resources:
+ requests:
+ storage: 2Gi
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: litecharms-shopadmin
+ namespace: litecharms-shopadmin-uat
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: shop
+ template:
+ metadata:
+ labels:
+ app: shop
+ spec:
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: node-role.kubernetes.io/control-plane
+ operator: DoesNotExist
+ containers:
+ - name: shop
+ image: nexus.khongisa.co.za/litecharms-shopadmin:latest
+ imagePullPolicy: Always
+ resources:
+ limits:
+ memory: "512Mi"
+ cpu: "500m"
+ requests:
+ memory: "256Mi"
+ cpu: "100m"
+ ports:
+ - containerPort: 8080
+ envFrom:
+ - configMapRef:
+ name: shop-config
+ env:
+ - name: ConnectionStrings__PostgresScheduler
+ valueFrom:
+ secretKeyRef:
+ name: shop-secrets
+ key: connection-string-quartz
+ - name: ConnectionStrings__PostgresShop
+ valueFrom:
+ secretKeyRef:
+ name: shop-secrets
+ key: connection-string
+ - name: Monitoring__Address
+ valueFrom:
+ configMapKeyRef:
+ name: shop-config
+ key: Monitoring__Address
+ - name: Monitoring__ServiceName
+ valueFrom:
+ configMapKeyRef:
+ name: shop-config
+ key: Monitoring__ServiceName
+ - name: Monitoring__ApiKey
+ valueFrom:
+ secretKeyRef:
+ name: shop-secrets
+ key: aspire-apikey
+ volumeMounts:
+ - name: data
+ mountPath: /app/wwwroot/content
+ resources:
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 3
+ periodSeconds: 5
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: shop-data-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: shop-service
+ namespace: litecharms-shopadmin-uat
+spec:
+ type: ClusterIP
+ selector:
+ app: shop
+ ports:
+ - name: http
+ protocol: TCP
+ port: 80
+ targetPort: 8080
+---
+apiVersion: traefik.io/v1alpha1
+kind: IngressRoute
+metadata:
+ name: shop-web-secure
+ namespace: litecharms-shopadmin-uat
+spec:
+ entryPoints:
+ - websecure
+ routes:
+ - match: Host(`shop.uat.khongisa.co.za`)
+ kind: Rule
+ services:
+ - name: shop-service
+ port: 80
+ sticky:
+ cookie:
+ name: "lp-sticky-session"
+ httpOnly: true
+ secure: true
+ tls: {}
\ No newline at end of file
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..87c9c3d
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file