diff --git a/.github/workflows/validate-bicep-params.yml b/.github/workflows/validate-bicep-params.yml
index a483c8473..31846fcd5 100644
--- a/.github/workflows/validate-bicep-params.yml
+++ b/.github/workflows/validate-bicep-params.yml
@@ -34,9 +34,16 @@ jobs:
- name: Validate infra/ parameters
id: validate_infra
continue-on-error: true
+ env:
+ ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
set +e
- python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color --json-output infra_results.json 2>&1 | tee infra_output.txt
+ RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
+ python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color \
+ --json-output infra_results.json \
+ --html-output email_body.html \
+ --accelerator-name "${ACCELERATOR_NAME}" \
+ --run-url "${RUN_URL}" 2>&1 | tee infra_output.txt
EXIT_CODE=${PIPESTATUS[0]}
set -e
echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY"
@@ -61,24 +68,25 @@ jobs:
name: bicep-validation-results
path: |
infra_results.json
+ email_body.html
retention-days: 30
- name: Send schedule notification on failure
if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure'
env:
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_RUN_ID: ${{ github.run_id }}
ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
- RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- INFRA_OUTPUT=$(sed 's/&/\&/g; s/\</g; s/>/\>/g' infra_output.txt)
+ if [ -f email_body.html ]; then
+ EMAIL_BODY=$(cat email_body.html)
+ else
+ EMAIL_BODY="
Bicep parameter validation failed but no HTML report was generated. Check the workflow run for details.
"
+ fi
jq -n \
--arg name "${ACCELERATOR_NAME}" \
- --arg infra "$INFRA_OUTPUT" \
- --arg url "$RUN_URL" \
- '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: ("Dear Team,
The scheduled Bicep Parameter Validation for " + $name + " has detected parameter mapping errors.
infra/ Results:
" + $infra + "
Run URL: " + $url + "
Please fix the parameter mapping issues at your earliest convenience.
Best regards,
Your Automation Team
")}' \
+ --arg body "$EMAIL_BODY" \
+ '{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \
| curl -X POST "${LOGICAPP_URL}" \
-H "Content-Type: application/json" \
-d @- || echo "Failed to send notification"
@@ -87,18 +95,18 @@ jobs:
if: github.event_name == 'schedule' && steps.result.outputs.status == 'success'
env:
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
- GITHUB_REPOSITORY: ${{ github.repository }}
- GITHUB_RUN_ID: ${{ github.run_id }}
ACCELERATOR_NAME: ${{ env.accelerator_name }}
run: |
- RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- INFRA_OUTPUT=$(sed 's/&/\&/g; s/\</g; s/>/\>/g' infra_output.txt)
+ if [ -f email_body.html ]; then
+ EMAIL_BODY=$(cat email_body.html)
+ else
+ EMAIL_BODY="Bicep parameter validation passed. Check the workflow run for details.
"
+ fi
jq -n \
--arg name "${ACCELERATOR_NAME}" \
- --arg infra "$INFRA_OUTPUT" \
- --arg url "$RUN_URL" \
- '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: ("Dear Team,
The scheduled Bicep Parameter Validation for " + $name + " has completed successfully. All parameter mappings are valid.
infra/ Results:
" + $infra + "
Run URL: " + $url + "
Best regards,
Your Automation Team
")}' \
+ --arg body "$EMAIL_BODY" \
+ '{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \
| curl -X POST "${LOGICAPP_URL}" \
-H "Content-Type: application/json" \
-d @- || echo "Failed to send notification"
diff --git a/README.md b/README.md
index 078ed55d0..3ed312b4e 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ To deploy this solution accelerator, ensure you have access to an [Azure subscri
Check the [Azure Products by Region](https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/table) page and select a **region** where the following services are available: Azure OpenAI Service, Azure AI Search, and Azure Semantic Search.
-Here are some example regions where the services are available: East US, East US2, Japan East, UK South, Sweden Central.
+Here are some example regions where the services are available: Australia East, East US2, France Central, Japan East, Norway East, Sweden Central, UK South, West US.
Pricing varies per region and usage, so it isn't possible to predict exact costs for your usage. The majority of the Azure resources used in this infrastructure are on usage-based pricing tiers. However, Azure Container Registry has a fixed cost per registry per day.
diff --git a/docs/CustomizingAzdParameters.md b/docs/CustomizingAzdParameters.md
index c2c052c47..71ce95f29 100644
--- a/docs/CustomizingAzdParameters.md
+++ b/docs/CustomizingAzdParameters.md
@@ -2,7 +2,7 @@
By default this template will use the environment name as the prefix to prevent naming collisions within Azure. The parameters below show the default values. You only need to run the statements below if you need to change the values.
-> To override any of the parameters, run `azd env set ` before running `azd up`. On the first azd command, it will prompt you for the environment name. Be sure to choose 3-20 characters alphanumeric unique name.
+> To override any of the parameters, run `azd env set ` before running `azd up`. On the first azd command, it will prompt you for the environment name. Be sure to choose 3-16 characters alphanumeric unique name.
## Parameters
@@ -23,7 +23,7 @@ By default this template will use the environment name as the prefix to prevent
| `AZURE_ENV_REASONING_MODEL_NAME` | string | `o4-mini` | Specifies the name of the reasoning GPT model to be deployed. |
| `AZURE_ENV_REASONING_MODEL_VERSION` | string | `2025-04-16` | Version of the reasoning GPT model to be used for deployment. |
| `AZURE_ENV_REASONING_MODEL_CAPACITY` | int | `50` | Sets the reasoning GPT model capacity. |
-| `AZURE_ENV_IMAGETAG` | string | `latest_v3` | Docker image tag used for container deployments. |
+| `AZURE_ENV_IMAGE_TAG` | string | `latest_v4` | Docker image tag used for container deployments. |
| `AZURE_ENV_ENABLE_TELEMETRY` | bool | `true` | Enables telemetry for monitoring and diagnostics. |
| `AZURE_EXISTING_AIPROJECT_RESOURCE_ID` | string | `` | Set this if you want to reuse an AI Foundry Project instead of creating a new one. |
| `AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID` | string | Guide to get your [Existing Workspace ID](re-use-log-analytics.md) | Set this if you want to reuse an existing Log Analytics Workspace instead of creating a new one. |
diff --git a/docs/azure_app_service_auth_setup.md b/docs/azure_app_service_auth_setup.md
index 5b7fc1adc..248ec2fc4 100644
--- a/docs/azure_app_service_auth_setup.md
+++ b/docs/azure_app_service_auth_setup.md
@@ -26,7 +26,7 @@ This document provides step-by-step instructions to configure Azure App Registra

-5. Accept the default values and click on `Add` button to go back to the previous page with the idenity provider added.
+5. Accept the default values and click on `Add` button to go back to the previous page with the identity provider added.

diff --git a/docs/create_new_app_registration.md b/docs/create_new_app_registration.md
index 28edbf452..ce60435cf 100644
--- a/docs/create_new_app_registration.md
+++ b/docs/create_new_app_registration.md
@@ -20,7 +20,7 @@

-6. Click on `+ Add a platform`.
+6. Click on `+ Add redirect URI`.

diff --git a/docs/images/azure-app-service-auth-setup/AddDetails.png b/docs/images/azure-app-service-auth-setup/AddDetails.png
index f36b596f2..f5946c6db 100644
Binary files a/docs/images/azure-app-service-auth-setup/AddDetails.png and b/docs/images/azure-app-service-auth-setup/AddDetails.png differ
diff --git a/docs/images/azure-app-service-auth-setup/AddPlatform.png b/docs/images/azure-app-service-auth-setup/AddPlatform.png
index 6c74919b4..2424f2a8f 100644
Binary files a/docs/images/azure-app-service-auth-setup/AddPlatform.png and b/docs/images/azure-app-service-auth-setup/AddPlatform.png differ
diff --git a/docs/images/azure-app-service-auth-setup/Web.png b/docs/images/azure-app-service-auth-setup/Web.png
index 35f846453..d997cbd3a 100644
Binary files a/docs/images/azure-app-service-auth-setup/Web.png and b/docs/images/azure-app-service-auth-setup/Web.png differ
diff --git a/infra/main.parameters.json b/infra/main.parameters.json
index 313b53e58..7aa975400 100644
--- a/infra/main.parameters.json
+++ b/infra/main.parameters.json
@@ -48,13 +48,13 @@
"value": "${AZURE_ENV_REASONING_MODEL_CAPACITY}"
},
"backendContainerImageTag": {
- "value": "${AZURE_ENV_IMAGE_TAG}"
+ "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}"
},
"frontendContainerImageTag": {
- "value": "${AZURE_ENV_IMAGE_TAG}"
+ "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}"
},
"MCPContainerImageTag": {
- "value": "${AZURE_ENV_IMAGE_TAG}"
+ "value": "${AZURE_ENV_IMAGE_TAG=latest_v4}"
},
"enableTelemetry": {
"value": "${AZURE_ENV_ENABLE_TELEMETRY}"
diff --git a/infra/scripts/validate_bicep_params.py b/infra/scripts/validate_bicep_params.py
index 1e87e6b15..95289548e 100644
--- a/infra/scripts/validate_bicep_params.py
+++ b/infra/scripts/validate_bicep_params.py
@@ -29,6 +29,7 @@
from __future__ import annotations
import argparse
+import html
import json
import re
import sys
@@ -341,6 +342,241 @@ def print_report(results: list[ValidationResult], *, use_color: bool = True) ->
print(f"{c['ERROR']}Parameter mapping issues detected!{c['RESET']}")
+# ---------------------------------------------------------------------------
+# HTML email report
+# ---------------------------------------------------------------------------
+
+def _html_escape(text: str) -> str:
+ """Escape HTML special characters."""
+ return html.escape(text, quote=True)
+
+
+def generate_html_report(
+ results: list[ValidationResult],
+ *,
+ accelerator_name: str = "",
+ run_url: str = "",
+ scan_dir: str = "",
+) -> str:
+ """Build a structured HTML email body from validation results."""
+ total_errors = sum(
+ 1 for r in results for i in r.issues if i.severity == "ERROR"
+ )
+ total_warnings = sum(
+ 1 for r in results for i in r.issues if i.severity == "WARNING"
+ )
+ has_errors = total_errors > 0
+ overall_status = "Issues Detected" if has_errors else "Passed"
+ status_color = "#D32F2F" if has_errors else "#2E7D32"
+ status_bg = "#FFEBEE" if has_errors else "#E8F5E9"
+ status_icon = "❌" if has_errors else "✅"
+
+ parts: list[str] = []
+
+ # --- Document wrapper (Outlook-compatible, no gradient/border-radius/box-shadow) ---
+ parts.append(
+ ''
+ ''
+ ''
+ ''
+ ''
+ )
+
+ # --- Header banner (solid color, Outlook-safe) ---
+ parts.append(
+ f''
+ f''
+ f'Bicep Parameter Validation Report'
+ f''
+ f'{_html_escape(accelerator_name) if accelerator_name else "Accelerator"}'
+ f' — Automated Check '
+ f' | '
+ )
+
+ # --- Summary card ---
+ parts.append(
+ f''
+ f''
+ f'| '
+ f''
+ f'{status_icon} Overall Status: {overall_status}'
+ f' | '
+ f''
+ f''
+ )
+ # Accelerator name pill
+ if accelerator_name:
+ parts.append(
+ f''
+ f'Accelerator '
+ f'{_html_escape(accelerator_name)}'
+ f' | '
+ )
+ # Scan directory pill
+ if scan_dir:
+ parts.append(
+ f''
+ f'Scan Directory '
+ f'{_html_escape(scan_dir)}/'
+ f' | '
+ )
+ # Error count pill
+ err_pill_color = "#D32F2F" if total_errors > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Errors '
+ f''
+ f'{total_errors} | '
+ )
+ # Warning count pill
+ warn_pill_color = "#F57C00" if total_warnings > 0 else "#2E7D32"
+ parts.append(
+ f''
+ f'Warnings '
+ f''
+ f'{total_warnings} | '
+ )
+ parts.append("
| | ")
+
+ # --- Per-pair detail sections ---
+ parts.append('')
+ for r in results:
+ errors = [i for i in r.issues if i.severity == "ERROR"]
+ warnings = [i for i in r.issues if i.severity == "WARNING"]
+
+ if not r.issues:
+ badge = (
+ 'PASS'
+ )
+ elif errors:
+ badge = (
+ 'FAIL'
+ )
+ else:
+ badge = (
+ 'WARN'
+ )
+
+ parts.append(
+ f''
+ f'| '
+ f'{badge} '
+ f''
+ f'{_html_escape(r.pair)}'
+ f''
+ f'{len(errors)} error(s), {len(warnings)} warning(s)'
+ f' | '
+ )
+
+ if r.issues:
+ # --- Errors section ---
+ if errors:
+ parts.append(
+ '| '
+ ''
+ '● Errors | '
+ ''
+ ''
+ ''
+ '| Parameter | '
+ 'Details | '
+ )
+ for idx, issue in enumerate(errors):
+ bg = "#ffffff" if idx % 2 == 0 else "#fff5f5"
+ parts.append(
+ f''
+ f'| '
+ f'{_html_escape(issue.param_name)} | '
+ f'{_html_escape(issue.message)} | '
+ f' '
+ )
+ parts.append(" | ")
+
+ # --- Warnings section ---
+ if warnings:
+ parts.append(
+ '| '
+ ''
+ '● Warnings | '
+ ''
+ ''
+ ''
+ '| Parameter | '
+ 'Details | '
+ )
+ for idx, issue in enumerate(warnings):
+ bg = "#ffffff" if idx % 2 == 0 else "#fffaf0"
+ parts.append(
+ f''
+ f'| '
+ f'{_html_escape(issue.param_name)} | '
+ f'{_html_escape(issue.message)} | '
+ f' '
+ )
+ parts.append(" | ")
+ else:
+ parts.append(
+ '| All parameters validated successfully.'
+ ' | '
+ )
+
+ parts.append(" ")
+
+ parts.append(" | ")
+
+ # --- Footer with run URL ---
+ footer_parts: list[str] = []
+ if run_url:
+ footer_parts.append(
+ f'View Workflow Run'
+ )
+ if has_errors:
+ footer_parts.append(
+ ''
+ 'Please fix the parameter mapping issues at your earliest convenience. '
+ )
+ footer_parts.append(
+ ''
+ 'Best regards, Your Automation Team '
+ )
+ parts.append(
+ f'| '
+ f'{"".join(footer_parts)} | '
+ )
+
+ # --- Close wrapper ---
+ parts.append(" |
")
+ return "".join(parts)
+
+
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
@@ -379,6 +615,23 @@ def main() -> int:
type=Path,
help="Write results as JSON to the given file path.",
)
+ parser.add_argument(
+ "--html-output",
+ type=Path,
+ help="Write a structured HTML email report to the given file path.",
+ )
+ parser.add_argument(
+ "--accelerator-name",
+ type=str,
+ default="",
+ help="Accelerator display name for the HTML report header.",
+ )
+ parser.add_argument(
+ "--run-url",
+ type=str,
+ default="",
+ help="Workflow run URL to include in the HTML report footer.",
+ )
args = parser.parse_args()
results: list[ValidationResult] = []
@@ -415,6 +668,19 @@ def main() -> int:
)
print(f"\nJSON report written to {args.json_output}")
+ # Optional HTML email report
+ if args.html_output:
+ scan_dir = str(args.dir) if args.dir else ""
+ html_report = generate_html_report(
+ results,
+ accelerator_name=args.accelerator_name,
+ run_url=args.run_url,
+ scan_dir=scan_dir,
+ )
+ args.html_output.parent.mkdir(parents=True, exist_ok=True)
+ args.html_output.write_text(html_report, encoding="utf-8")
+ print(f"HTML report written to {args.html_output}")
+
has_errors = any(r.has_errors for r in results)
return 1 if args.strict and has_errors else 0
diff --git a/src/App/src/App.tsx b/src/App/src/App.tsx
index a4fc17c6d..59a2b0b4e 100644
--- a/src/App/src/App.tsx
+++ b/src/App/src/App.tsx
@@ -1,11 +1,19 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import './App.css';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { HomePage, PlanPage } from './pages';
import { useWebSocket } from './hooks/useWebSocket';
+import { useAppDispatch } from './store/hooks';
+import { hydrateCurrentUser } from './store/slices/appSlice';
+import { getUserInfoGlobal } from './api/config';
function App() {
useWebSocket();
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ dispatch(hydrateCurrentUser(getUserInfoGlobal()));
+ }, [dispatch]);
return (
diff --git a/src/App/src/commonComponents/components/Panels/PanelFooter.tsx b/src/App/src/commonComponents/components/Panels/PanelFooter.tsx
index d0ae9619f..ed0aca9c3 100644
--- a/src/App/src/commonComponents/components/Panels/PanelFooter.tsx
+++ b/src/App/src/commonComponents/components/Panels/PanelFooter.tsx
@@ -4,7 +4,7 @@ const PanelFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return (
diff --git a/src/App/src/commonComponents/components/Panels/PanelLeft.tsx b/src/App/src/commonComponents/components/Panels/PanelLeft.tsx
index 19c124eae..1baceafa7 100644
--- a/src/App/src/commonComponents/components/Panels/PanelLeft.tsx
+++ b/src/App/src/commonComponents/components/Panels/PanelLeft.tsx
@@ -98,7 +98,7 @@ const PanelLeft: React.FC
= ({
{content}
- {footer && {footer}
}
+ {footer && {footer}
}
{panelResize && (
{
+ if (!name) return 'U';
+ const cleanName = name.replace(/\s*\([^)]*\)/g, '').trim();
+ if (!cleanName) return 'U';
+ const parts = cleanName.split(/\s+/).filter(Boolean);
+ if (parts.length >= 2) {
+ return (parts[0][0] + parts[1][0]).toUpperCase();
+ }
+ return cleanName.charAt(0).toUpperCase();
+};
+
+interface LoginButtonProps {
+ showName?: boolean;
+}
+
+const LoginButton: React.FC
= ({ showName = false }) => {
+ const styles = useStyles();
+ const userName = useAppSelector(state => state.app.userName);
+ const userId = useAppSelector(state => state.app.userId);
+ const userEmail = useAppSelector(state => state.app.userEmail);
+ const isAuthenticated = Boolean(userId && userId !== 'anonymous');
+
+ const logout = useCallback(() => {
+ const logoutUrl = '/.auth/logout?post_logout_redirect_uri=' + encodeURIComponent('/.auth/login/aad');
+ window.location.href = logoutUrl;
+ }, []);
+
+ const displayName = isAuthenticated ? userName || userId || 'User' : 'Guest';
+
+ if (!isAuthenticated) {
+ return (
+
+
+ {showName &&
Guest}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+export default LoginButton;
diff --git a/src/App/src/components/content/PlanPanelLeft.tsx b/src/App/src/components/content/PlanPanelLeft.tsx
index d9bfe023c..f1fb34b49 100644
--- a/src/App/src/components/content/PlanPanelLeft.tsx
+++ b/src/App/src/components/content/PlanPanelLeft.tsx
@@ -1,6 +1,7 @@
import React from "react";
import PanelLeft from "@/commonComponents/components/Panels/PanelLeft";
import PanelLeftToolbar from "@/commonComponents/components/Panels/PanelLeftToolbar";
+import PanelFooter from "@/commonComponents/components/Panels/PanelFooter";
import {
Body1Strong,
Toast,
@@ -16,14 +17,12 @@ import {
import TaskList from "./TaskList";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
-import { Plan, PlanPanelLefProps, Task, UserInfo } from "@/models";
+import { Plan, PlanPanelLefProps, Task } from "@/models";
import { apiService } from "@/api";
import { TaskService } from "@/store";
import ContosoLogo from "../../commonComponents/imports/ContosoLogo";
import "../../styles/PlanPanelLeft.css";
-import PanelFooter from "@/commonComponents/components/Panels/PanelFooter";
-import PanelUserCard from "../../commonComponents/components/Panels/UserCard";
-import { getUserInfoGlobal } from "@/api/config";
+import LoginButton from "../auth/LoginButton";
import TeamSelector from "../common/TeamSelector";
import { TeamConfig } from "../../models/Team";
import TeamSelected from "../common/TeamSelected";
@@ -47,9 +46,6 @@ const PlanPanelLeft: React.FC = ({
const [plans, setPlans] = useState(null);
const [plansLoading, setPlansLoading] = useState(false);
const [plansError, setPlansError] = useState(null);
- const [userInfo, setUserInfo] = useState(
- getUserInfoGlobal()
- );
// Use parent's selected team if provided, otherwise use local state
const [localSelectedTeam, setLocalSelectedTeam] = useState(null);
@@ -86,15 +82,14 @@ const PlanPanelLeft: React.FC = ({
useEffect(() => {
loadPlansData();
- setUserInfo(getUserInfoGlobal());
- }, [loadPlansData, setUserInfo]);
+ }, [loadPlansData]);
useEffect(() => {
if (reloadTasks) {
loadPlansData(true); // Force refresh when reloadTasks is true
}
- }, [loadPlansData, setUserInfo, reloadTasks]);
+ }, [loadPlansData, reloadTasks]);
useEffect(() => {
if (plans) {
const { completed } =
@@ -250,12 +245,7 @@ const PlanPanelLeft: React.FC = ({
diff --git a/src/App/src/store/slices/appSlice.ts b/src/App/src/store/slices/appSlice.ts
index 6fdebc915..7c5b4a352 100644
--- a/src/App/src/store/slices/appSlice.ts
+++ b/src/App/src/store/slices/appSlice.ts
@@ -1,8 +1,9 @@
/**
- * App Slice — global application state: config, theme, WebSocket connection.
+ * App Slice — global application state: config, theme, WebSocket connection, auth.
*/
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '../store';
+import { UserInfo } from '../../models';
export interface AppState {
/** Has the runtime config been loaded from /config? */
@@ -11,12 +12,21 @@ export interface AppState {
isDarkMode: boolean;
/** Is the global WebSocket connected? */
wsConnected: boolean;
+ /** Current user ID from EasyAuth */
+ userId: string;
+ /** Current user display name */
+ userName: string;
+ /** Current user email */
+ userEmail: string;
}
const initialState: AppState = {
configLoaded: false,
isDarkMode: window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false,
wsConnected: false,
+ userId: '',
+ userName: '',
+ userEmail: '',
};
const appSlice = createSlice({
@@ -32,6 +42,30 @@ const appSlice = createSlice({
setWsConnected(state, action: PayloadAction) {
state.wsConnected = action.payload;
},
+ hydrateCurrentUser(state, action: PayloadAction) {
+ const userInfo = action.payload;
+ if (!userInfo || !userInfo.user_id) {
+ state.userId = 'anonymous';
+ state.userName = '';
+ state.userEmail = '';
+ return;
+ }
+ state.userId = userInfo.user_id || 'anonymous';
+ state.userName = userInfo.user_first_last_name || '';
+ // Extract email from claims
+ const userClaims = userInfo.user_claims || [];
+ let emailVal = userInfo.user_email || '';
+ for (const claim of userClaims) {
+ if (claim.typ === 'preferred_username' ||
+ claim.typ === 'email' ||
+ claim.typ === 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress' ||
+ claim.typ === 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn') {
+ emailVal = claim.val;
+ break;
+ }
+ }
+ state.userEmail = emailVal;
+ },
},
});
@@ -39,6 +73,7 @@ export const {
setConfigLoaded,
setIsDarkMode,
setWsConnected,
+ hydrateCurrentUser,
} = appSlice.actions;
/* ── Granular Selectors ───────────────────────────────────────── */
diff --git a/src/App/src/styles/PlanPanelLeft.css b/src/App/src/styles/PlanPanelLeft.css
index ad8d3ea6f..c621f2d25 100644
--- a/src/App/src/styles/PlanPanelLeft.css
+++ b/src/App/src/styles/PlanPanelLeft.css
@@ -56,6 +56,8 @@ color: #2F2F4A; */
flex-direction: column;
gap: 12px;
width: 100%;
+ align-items: flex-start;
+ padding-left: 8px;
}
/* TASKLIST */