diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs
index a014cd253..08cba57c2 100644
--- a/csharp/App/Backend/Controller.cs
+++ b/csharp/App/Backend/Controller.cs
@@ -821,6 +821,47 @@ public class Controller : ControllerBase
});
}
+ ///
+ /// Test endpoint for the full AI diagnostic flow (knowledge base + Mistral API).
+ /// No auth required. Remove before production.
+ /// Usage: GET /api/TestDiagnoseError?errorDescription=SomeAlarm
+ ///
+ [HttpGet(nameof(TestDiagnoseError))]
+ public async Task TestDiagnoseError(string errorDescription = "AbnormalGridVoltage")
+ {
+ // 1. Try knowledge base first
+ var kbResult = AlarmKnowledgeBase.TryGetDiagnosis(errorDescription);
+ if (kbResult is not null)
+ {
+ return Ok(new
+ {
+ Source = "KnowledgeBase",
+ Alarm = errorDescription,
+ MistralEnabled = DiagnosticService.IsEnabled,
+ kbResult.Explanation,
+ kbResult.Causes,
+ kbResult.NextSteps
+ });
+ }
+
+ // 2. If not in KB, try Mistral directly with a test prompt
+ if (!DiagnosticService.IsEnabled)
+ return Ok(new { Source = "None", Alarm = errorDescription, Message = "Not in knowledge base and Mistral API key not configured." });
+
+ var aiResult = await DiagnosticService.TestCallMistralAsync(errorDescription);
+ if (aiResult is null)
+ return Ok(new { Source = "MistralFailed", Alarm = errorDescription, Message = "Mistral API call failed or returned empty." });
+
+ return Ok(new
+ {
+ Source = "MistralAI",
+ Alarm = errorDescription,
+ aiResult.Explanation,
+ aiResult.Causes,
+ aiResult.NextSteps
+ });
+ }
+
[HttpPut(nameof(UpdateFolder))]
public ActionResult UpdateFolder([FromBody] Folder folder, Token authToken)
{
diff --git a/csharp/App/Backend/Services/DiagnosticService.cs b/csharp/App/Backend/Services/DiagnosticService.cs
index 005734475..4757c97d7 100644
--- a/csharp/App/Backend/Services/DiagnosticService.cs
+++ b/csharp/App/Backend/Services/DiagnosticService.cs
@@ -88,6 +88,28 @@ public static class DiagnosticService
return response;
}
+ // ── test helper (no DB dependency) ─────────────────────────────
+
+ ///
+ /// Calls Mistral directly with a generic prompt. For testing only - no DB lookup.
+ ///
+ public static async Task TestCallMistralAsync(string errorDescription)
+ {
+ if (!IsEnabled) return null;
+
+ // Check cache first
+ if (Cache.TryGetValue(errorDescription, out var cached))
+ return cached;
+
+ var prompt = BuildPrompt(errorDescription, "SodioHome", new List());
+ var response = await CallMistralAsync(prompt);
+
+ if (response is not null)
+ Cache.TryAdd(errorDescription, response);
+
+ return response;
+ }
+
// ── prompt ──────────────────────────────────────────────────────
private static string BuildPrompt(string errorDescription, string productName, List recentErrors)
@@ -102,9 +124,12 @@ These are lithium-ion BESS units with a BMS, PV inverter, and grid inverter.
Error: {errorDescription}
Other recent errors: {recentList}
-Explain in plain English for a homeowner. List likely causes and next steps.
+Explain for a non-technical homeowner. Keep it very short and simple:
+- explanation: 1 short sentence, no jargon
+- causes: 2-3 bullet points, plain language
+- nextSteps: 2-3 simple action items a homeowner can understand
Reply with ONLY valid JSON, no markdown:
-{{""explanation"":""2-3 sentences"",""causes"":[""...""],""nextSteps"":[""...""]}}
+{{""explanation"":""1 short sentence"",""causes"":[""...""],""nextSteps"":[""...""]}}
";
}
@@ -142,8 +167,18 @@ Reply with ONLY valid JSON, no markdown:
return null;
}
+ // strip markdown code fences if Mistral wraps the JSON in ```json ... ```
+ var json = content.Trim();
+ if (json.StartsWith("```"))
+ {
+ var firstNewline = json.IndexOf('\n');
+ if (firstNewline >= 0) json = json[(firstNewline + 1)..];
+ if (json.EndsWith("```")) json = json[..^3];
+ json = json.Trim();
+ }
+
// parse the JSON the model produced
- var diagnostic = JsonConvert.DeserializeObject(content);
+ var diagnostic = JsonConvert.DeserializeObject(json);
return diagnostic;
}
catch (FlurlHttpException httpEx)
diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx
index 205413cdc..6092a0670 100644
--- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx
+++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx
@@ -461,6 +461,7 @@ function Installation(props: singleInstallationProps) {
}
/>
diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/Log.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/Log.tsx
index fa2b9b539..3fc396d2f 100644
--- a/typescript/frontend-marios2/src/content/dashboards/Log/Log.tsx
+++ b/typescript/frontend-marios2/src/content/dashboards/Log/Log.tsx
@@ -27,6 +27,7 @@ import Checkbox from '@mui/material/Checkbox';
interface LogProps {
errorLoadingS3Data: boolean;
id: number;
+ status?: number;
}
function Log(props: LogProps) {
@@ -47,7 +48,7 @@ function Log(props: LogProps) {
const tokencontext = useContext(TokenContext);
const { removeToken } = tokencontext;
- const [diagnoses, setDiagnoses] = useState<{ description: string; response: DiagnosticResponse }[]>([]);
+ const [diagnoses, setDiagnoses] = useState<{ description: string; lastSeen: string; response: DiagnosticResponse }[]>([]);
const [diagnosisLoading, setDiagnosisLoading] = useState(false);
const [expandedDiagnoses, setExpandedDiagnoses] = useState>(new Set());
@@ -78,11 +79,25 @@ function Log(props: LogProps) {
}, [updateCount]);
// fetch AI diagnosis for the latest 3 unique errors/warnings
+ // only when installation status is red (2) or orange (1)
useEffect(() => {
- // combine errors and warnings, filter non-errors, sort by date+time descending, deduplicate
+ // skip diagnosis if status is not alarm (2) or warning (1)
+ if (props.status !== 1 && props.status !== 2) {
+ setDiagnoses([]);
+ return;
+ }
+
+ // filter to last 24 hours only
+ const now = new Date();
+ const cutoff = new Date(now.getTime() - 24 * 60 * 60 * 1000);
+
const ignore = new Set(['NoAlarm', '0', '']);
const all = [...errors, ...warnings]
- .filter(item => !ignore.has(item.description.trim()))
+ .filter(item => {
+ if (ignore.has(item.description.trim())) return false;
+ const itemDate = new Date(item.date + 'T' + item.time);
+ return itemDate >= cutoff;
+ })
.sort((a, b) => (b.date + ' ' + b.time).localeCompare(a.date + ' ' + a.time));
const seen = new Set();
@@ -112,16 +127,16 @@ function Log(props: LogProps) {
.get(`/DiagnoseError?installationId=${props.id}&errorDescription=${encodeURIComponent(target.description)}`)
.then((res: AxiosResponse) => {
if (res.status === 204 || !res.data || !res.data.explanation) return null;
- return { description: target.description, response: res.data };
+ return { description: target.description, lastSeen: target.date + ' ' + target.time, response: res.data };
})
.catch(() => null)
)
).then(results => {
- setDiagnoses(results.filter(r => r !== null) as { description: string; response: DiagnosticResponse }[]);
+ setDiagnoses(results.filter(r => r !== null) as { description: string; lastSeen: string; response: DiagnosticResponse }[]);
}).finally(() => {
setDiagnosisLoading(false);
});
- }, [errors, warnings]);
+ }, [errors, warnings, props.status]);
const handleErrorButtonPressed = () => {
setErrorButtonPressed(!errorButtonPressed);
@@ -476,10 +491,10 @@ function Log(props: LogProps) {
-
+ {diag.description}
- {diag.description}
+ Last seen: {diag.lastSeen}
diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx
index e8ac17615..51d202da0 100644
--- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx
+++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx
@@ -346,6 +346,7 @@ function SalidomoInstallation(props: singleInstallationProps) {
}
/>
diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx
index d8621e97d..6a24ec99b 100644
--- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx
+++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx
@@ -478,6 +478,7 @@ function SodioHomeInstallation(props: singleInstallationProps) {
}
/>