mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
added skill and docs rules update
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
# Session Debug Skill
|
||||
|
||||
Du analysierst eine Autopilot-Monitor Session anhand ihrer SessionId. Du holst dir selbststaendig alle noetigen Daten aus Azure Table Storage via REST API und korrelierst sie mit dem Code.
|
||||
|
||||
## Input
|
||||
|
||||
Der User gibt eine SessionId (GUID) an, optional gefolgt von einer Frage.
|
||||
Beispiel: `/session-debug abc12345-1234-1234-1234-123456789abc warum ist die Session failed?`
|
||||
|
||||
Die gesamte Eingabe wird als `$ARGUMENTS` uebergeben. Die SessionId ist das erste Wort (GUID-Format), alles danach ist die Frage des Users.
|
||||
|
||||
## Hilfsskript
|
||||
|
||||
Nutze das Helper-Script `.claude/scripts/query-table.sh` fuer alle Table-Abfragen:
|
||||
|
||||
```bash
|
||||
.claude/scripts/query-table.sh <TableName> "<OData-Filter>" [select_fields]
|
||||
```
|
||||
|
||||
Das Script:
|
||||
- Parst den Connection String aus `$AUTOPILOT_MONITOR_TABLE_CS`
|
||||
- Baut die REST API URL zusammen
|
||||
- Gibt JSON zurueck
|
||||
|
||||
## Schritt 1: Session-Metadaten holen
|
||||
|
||||
Zuerst die Session aus der **Sessions** Table holen um die TenantId zu bekommen:
|
||||
|
||||
```bash
|
||||
.claude/scripts/query-table.sh Sessions "RowKey eq '${SESSION_ID}'"
|
||||
```
|
||||
|
||||
Aus dem Ergebnis:
|
||||
- **PartitionKey** = TenantId
|
||||
- **RowKey** = SessionId
|
||||
- Wichtige Felder: `Status`, `CurrentPhase`, `EnrollmentType`, `StartedAt`, `CompletedAt`, `FailureReason`, `EventCount`, `DurationSeconds`, `AgentVersion`, `IsPreProvisioned`, `IsHybridJoin`, `IsUserDriven`, `OsBuild`, `SerialNumber`, `DeviceName`, `Manufacturer`, `Model`
|
||||
|
||||
Fasse die Session-Metadaten kurz zusammen bevor du weiter machst.
|
||||
|
||||
## Schritt 2: Events holen
|
||||
|
||||
Mit TenantId und SessionId die Events aus der **Events** Table holen:
|
||||
|
||||
```bash
|
||||
.claude/scripts/query-table.sh Events "PartitionKey eq '${TENANT_ID}_${SESSION_ID}'"
|
||||
```
|
||||
|
||||
Events-Struktur:
|
||||
- **PartitionKey**: `{TenantId}_{SessionId}`
|
||||
- **RowKey**: `{Timestamp:yyyyMMddHHmmssfff}_{Sequence:D10}` (chronologisch sortiert)
|
||||
- **EventType**: z.B. `phase_transition`, `app_install_started`, `app_install_completed`, `error_detected`, `esp_page_change`, `hello_screen_detected`, `desktop_arrival`, `enrollment_complete`, `enrollment_failed`, `performance_snapshot`, `agent_metrics_snapshot`, `collector_started`, `collector_stopped`, `completion_check`, etc.
|
||||
- **Severity**: Trace=-1, Debug=0, Info=1, Warning=2, Error=3, Critical=4
|
||||
- **Source**: Agent, IME, Registry, WMI, Network
|
||||
- **Phase**: 0=Unknown, 1=DevicePreparation, 2=DeviceSetup, 3=AccountSetup, 4=DeviceESP, 5=UserESP, 6=Complete, 7=PreProvisioning
|
||||
- **Message**: Menschenlesbarer Text
|
||||
- **DataJson**: Strukturierte Metadaten als JSON String (oft die wichtigsten Details!)
|
||||
- **Sequence**: Reihenfolge bei gleichem Timestamp
|
||||
|
||||
## Schritt 3: Bei Bedarf weitere Tables abfragen
|
||||
|
||||
Nur abfragen wenn fuer die Frage relevant:
|
||||
|
||||
### RuleResults (automatische Analyse-Ergebnisse)
|
||||
```bash
|
||||
.claude/scripts/query-table.sh RuleResults "PartitionKey eq '${TENANT_ID}_${SESSION_ID}'"
|
||||
```
|
||||
|
||||
### AppInstallSummaries (App-Installationen)
|
||||
```bash
|
||||
.claude/scripts/query-table.sh AppInstallSummaries "PartitionKey eq '${TENANT_ID}'" "PartitionKey,RowKey,AppName,Status,DurationSeconds,StartedAt,CompletedAt"
|
||||
```
|
||||
Dann filtern: nur Eintraege deren RowKey mit der SessionId beginnt.
|
||||
|
||||
### TenantConfiguration
|
||||
```bash
|
||||
.claude/scripts/query-table.sh TenantConfiguration "PartitionKey eq '${TENANT_ID}' and RowKey eq 'config'"
|
||||
```
|
||||
|
||||
## Schritt 4: Analyse
|
||||
|
||||
Nachdem du die Daten hast:
|
||||
|
||||
1. **Chronologie aufbauen**: Events nach Timestamp sortiert durchgehen
|
||||
2. **Phasen-Uebergaenge**: phase_transition Events zeigen den Ablauf
|
||||
3. **Fehler identifizieren**: Events mit Severity >= 2 (Warning) hervorheben
|
||||
4. **DataJson parsen**: Oft stecken die Details in DataJson - immer reinschauen bei relevanten Events
|
||||
5. **Code-Korrelation**: Wenn der User eine Frage zum Verhalten hat, lies den relevanten Code im Repo und erklaere anhand der Events was passiert ist
|
||||
6. **Completion-Logik**: Bei Fragen zur Session-Completion, schau in:
|
||||
- `src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker*.cs`
|
||||
- Die 3 Completion-Pfade: IME pattern, ESP+Hello composite, Desktop+Hello (no-ESP)
|
||||
|
||||
## Wichtige Code-Pfade fuer Event-Analyse
|
||||
|
||||
- **Event-Ingestion**: `src/Backend/AutopilotMonitor.Functions/Functions/IngestEventsFunction.cs`
|
||||
- **Session-Updates**: `src/Backend/AutopilotMonitor.Functions/Services/TableStorageService.Sessions.cs`
|
||||
- **Enrollment Tracker**: `src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker*.cs`
|
||||
- **ESP/Hello Tracker**: `src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker*.cs`
|
||||
- **Desktop Arrival**: `src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DesktopArrivalDetector.cs`
|
||||
- **Collectors**: `src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/`
|
||||
- **Web Session View**: `src/Web/autopilot-monitor-web/app/sessions/[sessionId]/`
|
||||
- **Shared Models/Enums**: `src/Shared/AutopilotMonitor.Shared/`
|
||||
|
||||
## Ausgabe-Format
|
||||
|
||||
1. **Session-Uebersicht**: Status, Typ, Dauer, Geraet (kompakt, tabellarisch)
|
||||
2. **Event-Timeline**: Wichtigste Events chronologisch mit Timestamp, EventType, Message (nicht alle - nur relevante, keine performance_snapshot/agent_metrics_snapshot)
|
||||
3. **Auffaelligkeiten**: Errors, Warnings, ungewoehnliche Muster
|
||||
4. **Antwort auf User-Frage**: Falls der User eine spezifische Frage hat, diese gezielt beantworten mit Verweis auf Events UND Code
|
||||
|
||||
## Hinweise
|
||||
|
||||
- Die Env-Variable `AUTOPILOT_MONITOR_TABLE_CS` enthaelt den Connection String mit SAS Token
|
||||
- Bei vielen Events (>100) kann die Ausgabe gross werden - fokussiere auf relevante Events, filtere performance_snapshot und agent_metrics_snapshot raus wenn sie nicht relevant sind
|
||||
- DataJson Felder enthalten oft die wichtigsten Details - IMMER reinschauen bei relevanten Events
|
||||
- Wenn ein Event unklar ist, schau im Code nach wo dieser EventType erzeugt wird (grep nach dem EventType-String)
|
||||
- Phase-Enum: Unknown=0, DevicePreparation=1, DeviceSetup=2, AccountSetup=3, DeviceESP=4, UserESP=5, Complete=6, PreProvisioning=7
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
# Helper script to query Azure Table Storage via REST API
|
||||
# Usage: query-table.sh <table_name> <odata_filter> [select_fields]
|
||||
#
|
||||
# Requires AUTOPILOT_MONITOR_TABLE_CS environment variable with format:
|
||||
# TableEndpoint=https://....table.core.windows.net/;SharedAccessSignature=sv=...
|
||||
|
||||
TABLE_NAME="$1"
|
||||
FILTER="$2"
|
||||
SELECT="$3"
|
||||
|
||||
if [ -z "$TABLE_NAME" ] || [ -z "$FILTER" ]; then
|
||||
echo "Usage: query-table.sh <table_name> <odata_filter> [select_fields]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$AUTOPILOT_MONITOR_TABLE_CS" ]; then
|
||||
echo "ERROR: AUTOPILOT_MONITOR_TABLE_CS environment variable not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Parse connection string
|
||||
ENDPOINT=$(echo "$AUTOPILOT_MONITOR_TABLE_CS" | sed -n 's/.*TableEndpoint=\([^;]*\).*/\1/p' | sed 's:/*$::')
|
||||
SAS=$(echo "$AUTOPILOT_MONITOR_TABLE_CS" | sed -n 's/.*SharedAccessSignature=\(.*\)/\1/p')
|
||||
|
||||
if [ -z "$ENDPOINT" ] || [ -z "$SAS" ]; then
|
||||
echo "ERROR: Could not parse connection string (need TableEndpoint and SharedAccessSignature)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# URL-encode the OData filter for use in query string
|
||||
ENCODED_FILTER=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$FILTER', safe=''))" 2>/dev/null)
|
||||
if [ -z "$ENCODED_FILTER" ]; then
|
||||
# Fallback: manual encoding of common OData chars
|
||||
ENCODED_FILTER=$(echo "$FILTER" | sed "s/ /%20/g; s/'/%27/g")
|
||||
fi
|
||||
|
||||
# Build full URL with query params
|
||||
QUERY_URL="${ENDPOINT}/${TABLE_NAME}()?${SAS}&\$filter=${ENCODED_FILTER}"
|
||||
if [ -n "$SELECT" ]; then
|
||||
ENCODED_SELECT=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$SELECT', safe=''))" 2>/dev/null)
|
||||
if [ -z "$ENCODED_SELECT" ]; then
|
||||
ENCODED_SELECT="$SELECT"
|
||||
fi
|
||||
QUERY_URL="${QUERY_URL}&\$select=${ENCODED_SELECT}"
|
||||
fi
|
||||
|
||||
# Execute query
|
||||
curl -s -H "Accept: application/json;odata=nometadata" \
|
||||
-H "x-ms-version: 2024-11-04" \
|
||||
--globoff \
|
||||
"$QUERY_URL"
|
||||
@@ -57,15 +57,21 @@ export function SectionAnalyzeRules() {
|
||||
</div>
|
||||
<div className="px-4 py-4 space-y-2 text-sm text-gray-700">
|
||||
<p>Inspects a field inside the <code className="bg-gray-100 px-1 rounded">data</code> payload of a specific event type. Use this to match on values like error codes or app names.</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs">
|
||||
<div className="p-3 bg-amber-50 border border-amber-200 rounded text-xs text-amber-900">
|
||||
<strong>Important:</strong> The available data fields depend on <strong>which collector type</strong> produced the event.
|
||||
Structured collectors (Registry, WMI, JSON, XML) expose individual named fields.
|
||||
<strong>Command</strong> collectors produce only raw text — use <code className="bg-amber-100 px-1 rounded">dataField: "output"</code> with <code className="bg-amber-100 px-1 rounded">contains</code> or <code className="bg-amber-100 px-1 rounded">regex</code> to search the command output.
|
||||
See the <em>Data fields by collector type</em> table in the Gather Rules section for a full reference.
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-xs mt-2">
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">Operators</p>
|
||||
<ul className="mt-1 space-y-0.5 text-gray-500">
|
||||
<li><code className="bg-gray-100 px-1 rounded">equals</code> — exact match</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">contains</code> — substring match</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">regex</code> — regular expression match</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">contains</code> / <code className="bg-gray-100 px-1 rounded">not_contains</code> — substring match</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">regex</code> / <code className="bg-gray-100 px-1 rounded">not_regex</code> — regular expression match</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">gt / lt / gte / lte</code> — numeric comparisons</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">exists</code> — field is present with any value</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">exists</code> / <code className="bg-gray-100 px-1 rounded">not_exists</code> — field presence check</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@@ -75,6 +81,8 @@ export function SectionAnalyzeRules() {
|
||||
<li><code className="bg-gray-100 px-1 rounded">appName</code> — application name</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">exitCode</code> — process exit code</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">phase</code> — enrollment phase name</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">output</code> — command stdout (Command collector)</li>
|
||||
<li><code className="bg-gray-100 px-1 rounded">error_output</code> — command stderr (Command collector)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,6 +90,11 @@ export function SectionAnalyzeRules() {
|
||||
<p className="font-semibold text-green-900 mb-1">Example — detect error code 0x80070002</p>
|
||||
<p>Source: <strong>event_data</strong> · Event Type: <strong>app_install_failed</strong> · Data Field: <strong>errorCode</strong> · Operator: <strong>equals</strong> · Value: <strong>0x80070002</strong></p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-50 border border-green-200 rounded-lg text-xs">
|
||||
<p className="font-semibold text-green-900 mb-1">Example — check command output for a certificate subject</p>
|
||||
<p>Source: <strong>event_data</strong> · Event Type: <strong>gather_machine_cert_store</strong> · Data Field: <strong>output</strong> · Operator: <strong>not_contains</strong> · Value: <strong>CN=MyCertificate</strong></p>
|
||||
<p className="text-green-700 mt-1">Confidence Factor: <code className="bg-green-100 px-1 rounded">gather_machine_cert_store.error_output exists</code> → +10 (command had errors)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,6 +286,35 @@ export function SectionAnalyzeRules() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Example 5 */}
|
||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-2.5 bg-indigo-50 border-b border-indigo-200">
|
||||
<p className="font-semibold text-sm text-indigo-900">Example 5 — Analyze Command Output (Gather Rule)</p>
|
||||
<p className="text-xs text-indigo-700 mt-0.5">Check if a certificate subject is missing from certutil output</p>
|
||||
</div>
|
||||
<div className="px-4 py-4 text-xs space-y-2">
|
||||
<div className="p-2 bg-gray-100 rounded text-gray-600">
|
||||
<strong>Gather Rule:</strong> <code>certutil -store My</code> → outputs as event type <code>gather_machine_cert_store</code>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div><p className="text-gray-500">Category</p><p className="font-medium">identity</p></div>
|
||||
<div><p className="text-gray-500">Severity</p><p className="font-medium">warning</p></div>
|
||||
<div><p className="text-gray-500">Base Confidence</p><p className="font-medium">50</p></div>
|
||||
<div><p className="text-gray-500">Threshold</p><p className="font-medium">40</p></div>
|
||||
</div>
|
||||
<div className="bg-gray-50 border border-gray-200 rounded px-3 py-2 font-mono space-y-1">
|
||||
<p className="text-gray-400">{`// Condition 1 — certutil data was gathered`}</p>
|
||||
<p>{`source: "event_type" eventType: "gather_machine_cert_store" operator: "exists"`}</p>
|
||||
<p className="text-gray-400 pt-1">{`// Condition 2 — certificate subject not found in command output`}</p>
|
||||
<p>{`source: "event_data" eventType: "gather_machine_cert_store" dataField: "output" operator: "not_contains" value: "CN=MyCert"`}</p>
|
||||
<p className="text-gray-400 pt-1">{`// Confidence Factor (+20 if command produced no output at all)`}</p>
|
||||
<p>{`signal: "no_output" condition: "gather_machine_cert_store.output not_exists" weight: 20`}</p>
|
||||
<p className="text-gray-400">{`// Confidence Factor (+10 if command had errors)`}</p>
|
||||
<p>{`signal: "cmd_error" condition: "gather_machine_cert_store.error_output exists" weight: 10`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* ── JSON editing tip ── */}
|
||||
|
||||
@@ -224,6 +224,17 @@ export function SectionGatherRules() {
|
||||
</div>
|
||||
</GuardToggle>
|
||||
</div>
|
||||
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-xs text-amber-900">
|
||||
<p className="font-semibold mb-1">Data fields produced</p>
|
||||
<p className="mb-2">Unlike structured collectors (JSON, XML, WMI) where individual fields are extracted, a command produces <strong>raw text output</strong>. The event data contains:</p>
|
||||
<ul className="list-disc ml-5 space-y-0.5">
|
||||
<li><code className="bg-amber-100 px-1 rounded">output</code> — Standard output (stdout) of the command, max 32 KB</li>
|
||||
<li><code className="bg-amber-100 px-1 rounded">error_output</code> — Standard error (stderr), max 8 KB</li>
|
||||
<li><code className="bg-amber-100 px-1 rounded">exit_code</code> — Process exit code (0 = success)</li>
|
||||
<li><code className="bg-amber-100 px-1 rounded">command</code> — The command string that was executed</li>
|
||||
</ul>
|
||||
<p className="mt-2">In Analyze Rules, use <code className="bg-amber-100 px-1 rounded">dataField: "output"</code> with operators like <code className="bg-amber-100 px-1 rounded">contains</code>, <code className="bg-amber-100 px-1 rounded">not_contains</code>, or <code className="bg-amber-100 px-1 rounded">regex</code> to inspect the command output.</p>
|
||||
</div>
|
||||
<div className="p-3 bg-green-50 border border-green-200 rounded-lg">
|
||||
<p className="font-semibold text-green-900 text-xs mb-1">Example — TPM at Enrollment Complete</p>
|
||||
<div className="mt-1 text-xs space-y-0.5">
|
||||
@@ -407,8 +418,33 @@ export function SectionGatherRules() {
|
||||
<p className="text-gray-700 text-sm">
|
||||
Each gather rule execution emits an event with the configured <strong>Output Event Type</strong> and <strong>Severity</strong>.
|
||||
The collected data is stored in the event's <code className="bg-gray-100 px-1 rounded">data</code> field as key-value pairs.
|
||||
These events appear in the session timeline.
|
||||
These events appear in the session timeline and can be inspected by <strong>Analyze Rules</strong> using the <code className="bg-gray-100 px-1 rounded">event_data</code> condition source.
|
||||
</p>
|
||||
|
||||
<div className="p-4 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-900">
|
||||
<p className="font-semibold mb-2">Data fields by collector type</p>
|
||||
<p className="text-xs mb-3">Each collector type produces different data fields. When writing Analyze Rules, you reference these fields via <code className="bg-amber-100 px-1 rounded">dataField</code> in <code className="bg-amber-100 px-1 rounded">event_data</code> conditions.</p>
|
||||
<div className="border border-amber-200 rounded-lg overflow-hidden">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-amber-100/50 border-b border-amber-200">
|
||||
<tr>
|
||||
<th className="px-3 py-1.5 text-left font-semibold">Collector</th>
|
||||
<th className="px-3 py-1.5 text-left font-semibold">Data Fields</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-amber-100">
|
||||
<tr><td className="px-3 py-1.5 font-medium">Registry</td><td className="px-3 py-1.5">Value names as keys (e.g., <code className="bg-amber-100 px-1 rounded">DisplayName</code>), or <code className="bg-amber-100 px-1 rounded">subkey_count</code> + <code className="bg-amber-100 px-1 rounded">subkey_0</code>, <code className="bg-amber-100 px-1 rounded">subkey_1</code>, …</td></tr>
|
||||
<tr><td className="px-3 py-1.5 font-medium">Event Log</td><td className="px-3 py-1.5">Event properties as key-value pairs (varies by event)</td></tr>
|
||||
<tr><td className="px-3 py-1.5 font-medium">WMI Query</td><td className="px-3 py-1.5">WMI object properties as keys (e.g., <code className="bg-amber-100 px-1 rounded">Name</code>, <code className="bg-amber-100 px-1 rounded">Status</code>)</td></tr>
|
||||
<tr><td className="px-3 py-1.5 font-medium">File</td><td className="px-3 py-1.5"><code className="bg-amber-100 px-1 rounded">exists</code>, <code className="bg-amber-100 px-1 rounded">path</code>, <code className="bg-amber-100 px-1 rounded">size_bytes</code>, <code className="bg-amber-100 px-1 rounded">content</code> (if readContent is true)</td></tr>
|
||||
<tr className="bg-amber-100/30"><td className="px-3 py-1.5 font-medium">Command</td><td className="px-3 py-1.5"><code className="bg-amber-100 px-1 rounded">output</code> (stdout), <code className="bg-amber-100 px-1 rounded">error_output</code> (stderr), <code className="bg-amber-100 px-1 rounded">exit_code</code>, <code className="bg-amber-100 px-1 rounded">command</code> — raw text only, no parsed fields</td></tr>
|
||||
<tr><td className="px-3 py-1.5 font-medium">Log Parser</td><td className="px-3 py-1.5">Named capture groups from regex (e.g., <code className="bg-amber-100 px-1 rounded">appName</code>, <code className="bg-amber-100 px-1 rounded">action</code>)</td></tr>
|
||||
<tr><td className="px-3 py-1.5 font-medium">JSON</td><td className="px-3 py-1.5">Matched JSON values as structured key-value pairs</td></tr>
|
||||
<tr><td className="px-3 py-1.5 font-medium">XML</td><td className="px-3 py-1.5">Matched XML elements/attributes as key-value pairs</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user