mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 20:53:06 +00:00
added mark as success
This commit is contained in:
+6
@@ -535,6 +535,12 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
if (response.Success)
|
||||
{
|
||||
_logger.Info($"Session registered successfully: {response.SessionId}");
|
||||
|
||||
if (!string.IsNullOrEmpty(response.AdminAction))
|
||||
{
|
||||
_logger.Warning($"Session already marked as {response.AdminAction} by administrator — will run cleanup after startup");
|
||||
_pendingAdminAction = response.AdminAction;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+24
@@ -285,6 +285,30 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(response.AdminAction) && !_enrollmentTerminalEventSeen)
|
||||
{
|
||||
var succeeded = string.Equals(response.AdminAction, "Succeeded", StringComparison.OrdinalIgnoreCase);
|
||||
_logger.Warning($"=== ADMIN OVERRIDE: Session marked as {response.AdminAction} by administrator. Initiating cleanup... ===");
|
||||
|
||||
EmitEvent(new EnrollmentEvent
|
||||
{
|
||||
SessionId = _configuration.SessionId,
|
||||
TenantId = _configuration.TenantId,
|
||||
EventType = succeeded ? "enrollment_complete" : "enrollment_failed",
|
||||
Severity = succeeded ? EventSeverity.Info : EventSeverity.Warning,
|
||||
Source = "AdminOverride",
|
||||
Phase = EnrollmentPhase.Complete,
|
||||
Message = $"Session {response.AdminAction.ToLower()} by administrator — cleanup initiated",
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "adminAction", response.AdminAction }
|
||||
}
|
||||
});
|
||||
// EmitEvent triggers HandleEnrollmentComplete via the normal terminal event path
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.Success)
|
||||
{
|
||||
_spool.RemoveEvents(events);
|
||||
|
||||
@@ -76,6 +76,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
private readonly DateTime _agentStartTimeUtc = DateTime.UtcNow;
|
||||
private bool _enrollmentTerminalEventSeen;
|
||||
|
||||
// Admin override detected during session registration (agent restart after admin action)
|
||||
private string _pendingAdminAction;
|
||||
|
||||
// Auth failure circuit breaker
|
||||
private int _consecutiveAuthFailures = 0;
|
||||
private DateTime? _firstAuthFailureTime = null;
|
||||
@@ -150,6 +153,34 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Core
|
||||
// Register session with backend
|
||||
RegisterSessionAsync().Wait();
|
||||
|
||||
// Check if admin already terminated this session before we restarted
|
||||
if (!string.IsNullOrEmpty(_pendingAdminAction))
|
||||
{
|
||||
var succeeded = string.Equals(_pendingAdminAction, "Succeeded", StringComparison.OrdinalIgnoreCase);
|
||||
_logger.Warning($"=== ADMIN OVERRIDE on startup: Session already marked as {_pendingAdminAction} by administrator — running cleanup only ===");
|
||||
|
||||
// Start spool watcher briefly so the terminal event gets spooled for upload
|
||||
_spool.StartWatching();
|
||||
|
||||
EmitEvent(new EnrollmentEvent
|
||||
{
|
||||
SessionId = _configuration.SessionId,
|
||||
TenantId = _configuration.TenantId,
|
||||
EventType = succeeded ? "enrollment_complete" : "enrollment_failed",
|
||||
Severity = succeeded ? EventSeverity.Info : EventSeverity.Warning,
|
||||
Source = "AdminOverride",
|
||||
Phase = EnrollmentPhase.Complete,
|
||||
Message = $"Session {_pendingAdminAction.ToLower()} by administrator (detected on restart) — cleanup initiated",
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Data = new Dictionary<string, object>
|
||||
{
|
||||
{ "adminAction", _pendingAdminAction }
|
||||
}
|
||||
});
|
||||
// EmitEvent triggers HandleEnrollmentComplete which handles cleanup + exit
|
||||
return;
|
||||
}
|
||||
|
||||
// Start FileSystemWatcher for efficient event detection
|
||||
_spool.StartWatching();
|
||||
_logger.Info("FileSystemWatcher started for efficient event upload");
|
||||
|
||||
@@ -1 +1 @@
|
||||
476
|
||||
480
|
||||
|
||||
@@ -136,7 +136,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new AgentConfigResponse
|
||||
{
|
||||
ConfigVersion = 17, // unrestricted mode for tenant-level guardrail bypass
|
||||
ConfigVersion = 18, // admin session override (mark-succeeded + agent cleanup signal via ingest response)
|
||||
UploadIntervalSeconds = 30,
|
||||
SelfDestructOnComplete = tenantConfig.SelfDestructOnComplete ?? true,
|
||||
KeepLogFile = tenantConfig.KeepLogFile ?? false,
|
||||
|
||||
+15
-1
@@ -242,6 +242,19 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
request, sessionPrefix, classification, updatedSession,
|
||||
statusTransitioned, whiteGloveStatusTransitioned, failureReason);
|
||||
|
||||
// Check if an admin has externally marked the session as terminal.
|
||||
// Only signal the agent when this batch did NOT contain the terminal event itself
|
||||
// (i.e., the agent didn't just report completion — the admin did it out-of-band).
|
||||
string? adminAction = null;
|
||||
if (updatedSession != null &&
|
||||
classification.CompletionEvent == null && classification.FailureEvent == null &&
|
||||
(updatedSession.Status == SessionStatus.Succeeded || updatedSession.Status == SessionStatus.Failed))
|
||||
{
|
||||
adminAction = updatedSession.Status.ToString();
|
||||
_logger.LogInformation("{SessionPrefix} Admin override detected — signaling agent: AdminAction={AdminAction}",
|
||||
sessionPrefix, adminAction);
|
||||
}
|
||||
|
||||
// Build HTTP response + SignalR messages
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new IngestEventsResponse
|
||||
@@ -250,7 +263,8 @@ namespace AutopilotMonitor.Functions.Functions.Ingest
|
||||
EventsReceived = request.Events.Count,
|
||||
EventsProcessed = processedCount,
|
||||
Message = $"Successfully stored {processedCount} of {request.Events.Count} events",
|
||||
ProcessedAt = DateTime.UtcNow
|
||||
ProcessedAt = DateTime.UtcNow,
|
||||
AdminAction = adminAction
|
||||
});
|
||||
|
||||
var signalRMessages = BuildSignalRMessages(request, updatedSession, processedCount, newRuleResults);
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
using System.Net;
|
||||
using AutopilotMonitor.Functions.Helpers;
|
||||
using AutopilotMonitor.Functions.Services;
|
||||
using AutopilotMonitor.Shared.Models;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Azure.Functions.Worker.Extensions.SignalRService;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AutopilotMonitor.Functions.Functions.Sessions
|
||||
{
|
||||
public class MarkSessionSucceededFunction
|
||||
{
|
||||
private readonly ILogger<MarkSessionSucceededFunction> _logger;
|
||||
private readonly TableStorageService _storageService;
|
||||
|
||||
public MarkSessionSucceededFunction(
|
||||
ILogger<MarkSessionSucceededFunction> logger,
|
||||
TableStorageService storageService)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
[Function("MarkSessionSucceeded")]
|
||||
public async Task<MarkSessionSucceededOutput> Run(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "sessions/{sessionId}/mark-succeeded")] HttpRequestData req,
|
||||
string sessionId)
|
||||
{
|
||||
_logger.LogInformation($"MarkSessionSucceeded function processing request for session {sessionId}");
|
||||
|
||||
try
|
||||
{
|
||||
// Authentication + TenantAdminOrGA authorization enforced by PolicyEnforcementMiddleware
|
||||
string tenantId = TenantHelper.GetTenantId(req);
|
||||
string userIdentifier = TenantHelper.GetUserIdentifier(req);
|
||||
|
||||
_logger.LogInformation($"Marking session {sessionId} as succeeded for tenant {tenantId} by user {userIdentifier}");
|
||||
|
||||
// Update session status to Succeeded with manual reason
|
||||
var success = await _storageService.UpdateSessionStatusAsync(
|
||||
tenantId,
|
||||
sessionId,
|
||||
SessionStatus.Succeeded,
|
||||
null, // Keep current phase
|
||||
"Manually marked as succeeded by administrator"
|
||||
);
|
||||
|
||||
if (success)
|
||||
{
|
||||
// Log audit entry with actual user identifier
|
||||
await _storageService.LogAuditEntryAsync(
|
||||
tenantId,
|
||||
"UPDATE",
|
||||
"Session",
|
||||
sessionId,
|
||||
userIdentifier,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "Action", "MarkAsSucceeded" },
|
||||
{ "Reason", "Manually marked as succeeded" }
|
||||
}
|
||||
);
|
||||
|
||||
// Retrieve updated session data to include in SignalR message
|
||||
var updatedSession = await _storageService.GetSessionAsync(tenantId, sessionId);
|
||||
|
||||
_logger.LogInformation($"Successfully marked session {sessionId} as succeeded");
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
success = true,
|
||||
message = $"Session {sessionId} marked as succeeded"
|
||||
});
|
||||
|
||||
// Send SignalR notification to update all clients in the tenant
|
||||
object? sessionDelta = updatedSession != null ? new {
|
||||
updatedSession.CurrentPhase,
|
||||
updatedSession.CurrentPhaseDetail,
|
||||
updatedSession.Status,
|
||||
updatedSession.FailureReason,
|
||||
updatedSession.EventCount,
|
||||
updatedSession.DurationSeconds,
|
||||
updatedSession.CompletedAt,
|
||||
updatedSession.DiagnosticsBlobName
|
||||
} : null;
|
||||
|
||||
var tenantMessage = new SignalRMessageAction("newevents")
|
||||
{
|
||||
GroupName = $"tenant-{tenantId}",
|
||||
Arguments = new object[] { new {
|
||||
sessionId = sessionId,
|
||||
tenantId = tenantId,
|
||||
eventCount = 0,
|
||||
sessionUpdate = sessionDelta
|
||||
} }
|
||||
};
|
||||
|
||||
return new MarkSessionSucceededOutput
|
||||
{
|
||||
HttpResponse = response,
|
||||
SignalRMessages = new[] { tenantMessage }
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Session {sessionId} not found");
|
||||
var response = req.CreateResponse(HttpStatusCode.NotFound);
|
||||
await response.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = $"Session {sessionId} not found"
|
||||
});
|
||||
return new MarkSessionSucceededOutput { HttpResponse = response };
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error marking session {sessionId} as succeeded");
|
||||
|
||||
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResponse.WriteAsJsonAsync(new
|
||||
{
|
||||
success = false,
|
||||
message = "Internal server error"
|
||||
});
|
||||
|
||||
return new MarkSessionSucceededOutput { HttpResponse = errorResponse };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MarkSessionSucceededOutput
|
||||
{
|
||||
[HttpResult]
|
||||
public HttpResponseData? HttpResponse { get; set; }
|
||||
|
||||
[SignalROutput(HubName = "autopilotmonitor")]
|
||||
public SignalRMessageAction[]? SignalRMessages { get; set; }
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -116,13 +116,23 @@ namespace AutopilotMonitor.Functions.Functions.Sessions
|
||||
// Retrieve the stored session to include full data in SignalR message
|
||||
var session = await _storageService.GetSessionAsync(registration.TenantId, registration.SessionId);
|
||||
|
||||
// Check if admin has already marked this session as terminal (e.g., agent restarted after admin override)
|
||||
string? adminAction = null;
|
||||
if (session != null && (session.Status == SessionStatus.Succeeded || session.Status == SessionStatus.Failed))
|
||||
{
|
||||
adminAction = session.Status.ToString();
|
||||
_logger.LogInformation("Session {SessionId} already in terminal state {Status} — signaling agent: AdminAction={AdminAction}",
|
||||
registration.SessionId, session.Status, adminAction);
|
||||
}
|
||||
|
||||
var response = req.CreateResponse(HttpStatusCode.OK);
|
||||
var responseData = new RegisterSessionResponse
|
||||
{
|
||||
SessionId = registration.SessionId,
|
||||
Success = true,
|
||||
Message = "Session registered successfully",
|
||||
RegisteredAt = DateTime.UtcNow
|
||||
RegisteredAt = DateTime.UtcNow,
|
||||
AdminAction = adminAction
|
||||
};
|
||||
|
||||
await response.WriteAsJsonAsync(responseData);
|
||||
|
||||
@@ -20,6 +20,12 @@ namespace AutopilotMonitor.Shared.Models
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
public DateTime RegisteredAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Non-null when the session was already marked as terminal by an admin before agent restart.
|
||||
/// Values: "Succeeded", "Failed". Agent should run cleanup instead of starting monitoring.
|
||||
/// </summary>
|
||||
public string AdminAction { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,6 +80,12 @@ namespace AutopilotMonitor.Shared.Models
|
||||
/// The agent should execute its self-destruct routine and exit.
|
||||
/// </summary>
|
||||
public bool DeviceKillSignal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Non-null when an admin has externally changed the session status.
|
||||
/// Values: "Succeeded", "Failed". Agent should treat as terminal signal and run cleanup.
|
||||
/// </summary>
|
||||
public string AdminAction { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { Session } from "@/types";
|
||||
|
||||
interface AdminOverrideModalProps {
|
||||
show: boolean;
|
||||
action: "failed" | "succeeded";
|
||||
session: Session | null;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function AdminOverrideModal({ show, action, session, onConfirm, onCancel }: AdminOverrideModalProps) {
|
||||
if (!show || !session) return null;
|
||||
|
||||
const isFailed = action === "failed";
|
||||
const colorClass = isFailed ? "red" : "green";
|
||||
const label = isFailed ? "Failed" : "Succeeded";
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onClick={onCancel}>
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className={`flex-shrink-0 w-12 h-12 bg-${colorClass}-100 rounded-full flex items-center justify-center`}>
|
||||
{isFailed ? (
|
||||
<svg className={`w-6 h-6 text-${colorClass}-600`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className={`w-6 h-6 text-${colorClass}-600`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="ml-4 text-lg font-semibold text-gray-900">Mark Session as {label}</h3>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
You are about to manually mark this session as <span className={`font-semibold text-${colorClass}-600`}>{label}</span>.
|
||||
</p>
|
||||
<p className="text-sm text-gray-700 mb-2">
|
||||
Session <span className="font-mono text-xs">{session.sessionId}</span> for device <span className="font-semibold">{session.deviceName || session.serialNumber}</span> will be marked as {action} with the reason "Manually marked as {action} by administrator".
|
||||
</p>
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-3 mt-3">
|
||||
<p className="text-sm text-amber-800 font-medium mb-1">Agent Cleanup Warning</p>
|
||||
<p className="text-xs text-amber-700">
|
||||
This action will trigger the agent to stop monitoring and perform cleanup on the device (final data upload, diagnostics collection, and potentially self-destruct based on tenant configuration). If the agent is currently idle, the cleanup will occur on the next device restart.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-3">
|
||||
This action cannot be undone. Do you want to continue?
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<button onClick={onCancel} className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className={`px-4 py-2 ${isFailed ? 'bg-red-600 hover:bg-red-700' : 'bg-green-600 hover:bg-green-700'} text-white rounded-md transition-colors`}
|
||||
>
|
||||
Mark as {label}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import SessionInfoCard from "./components/SessionInfoCard";
|
||||
import PhaseTimeline from "./components/PhaseTimeline";
|
||||
import EventTimeline from "./components/EventTimeline";
|
||||
import AnalysisResultsSection from "./components/AnalysisResultsSection";
|
||||
import MarkFailedModal from "./components/MarkFailedModal";
|
||||
import AdminOverrideModal from "./components/AdminOverrideModal";
|
||||
import ReportSessionModal from "./components/ReportSessionModal";
|
||||
import { usePageSections } from "../../../hooks/usePageSections";
|
||||
import { PageSectionItem } from "../../../contexts/SidebarContext";
|
||||
@@ -41,6 +41,7 @@ export default function SessionDetailPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [severityFilters, setSeverityFilters] = useState<Set<string>>(new Set(["Info", "Warning", "Error", "Critical"]));
|
||||
const [showMarkFailedConfirm, setShowMarkFailedConfirm] = useState(false);
|
||||
const [showMarkSucceededConfirm, setShowMarkSucceededConfirm] = useState(false);
|
||||
const [showReportModal, setShowReportModal] = useState(false);
|
||||
const [reportSubmitting, setReportSubmitting] = useState(false);
|
||||
const [showScriptOutput, setShowScriptOutput] = useState(true);
|
||||
@@ -380,8 +381,11 @@ export default function SessionDetailPage() {
|
||||
setShowMarkFailedConfirm(true);
|
||||
};
|
||||
|
||||
const markAsSucceeded = () => {
|
||||
setShowMarkSucceededConfirm(true);
|
||||
};
|
||||
|
||||
const confirmMarkFailed = async () => {
|
||||
// Use the session's tenant ID if available
|
||||
const effectiveTenantId = sessionTenantId || tenantId;
|
||||
|
||||
try {
|
||||
@@ -393,12 +397,8 @@ export default function SessionDetailPage() {
|
||||
|
||||
if (response.ok) {
|
||||
setShowMarkFailedConfirm(false);
|
||||
// Session will be updated via SignalR, but we can also update local state immediately
|
||||
if (session) {
|
||||
setSession({
|
||||
...session,
|
||||
status: 'Failed'
|
||||
});
|
||||
setSession({ ...session, status: 'Failed' });
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to mark session as failed');
|
||||
@@ -408,10 +408,37 @@ export default function SessionDetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const confirmMarkSucceeded = async () => {
|
||||
const effectiveTenantId = sessionTenantId || tenantId;
|
||||
|
||||
try {
|
||||
const response = await authenticatedFetch(
|
||||
`${API_BASE_URL}/api/sessions/${sessionId}/mark-succeeded?tenantId=${effectiveTenantId}`,
|
||||
getAccessToken,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
setShowMarkSucceededConfirm(false);
|
||||
if (session) {
|
||||
setSession({ ...session, status: 'Succeeded' });
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to mark session as succeeded');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error marking session as succeeded:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const cancelMarkFailed = () => {
|
||||
setShowMarkFailedConfirm(false);
|
||||
};
|
||||
|
||||
const cancelMarkSucceeded = () => {
|
||||
setShowMarkSucceededConfirm(false);
|
||||
};
|
||||
|
||||
const handleSubmitReport = async (
|
||||
comment: string, email: string,
|
||||
screenshotBase64: string | null, screenshotFileName: string | null,
|
||||
@@ -802,15 +829,26 @@ export default function SessionDetailPage() {
|
||||
</button>
|
||||
)}
|
||||
{adminMode && (session?.status === 'InProgress' || session?.status === 'Pending') && (
|
||||
<button
|
||||
onClick={markAsFailed}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
Mark as Failed
|
||||
</button>
|
||||
<>
|
||||
<button
|
||||
onClick={markAsSucceeded}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Mark as Succeeded
|
||||
</button>
|
||||
<button
|
||||
onClick={markAsFailed}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
Mark as Failed
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowReportModal(true)}
|
||||
@@ -958,13 +996,21 @@ export default function SessionDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mark as Failed Confirmation Modal */}
|
||||
<MarkFailedModal
|
||||
{/* Admin Override Confirmation Modals */}
|
||||
<AdminOverrideModal
|
||||
show={showMarkFailedConfirm}
|
||||
action="failed"
|
||||
session={session}
|
||||
onConfirm={confirmMarkFailed}
|
||||
onCancel={cancelMarkFailed}
|
||||
/>
|
||||
<AdminOverrideModal
|
||||
show={showMarkSucceededConfirm}
|
||||
action="succeeded"
|
||||
session={session}
|
||||
onConfirm={confirmMarkSucceeded}
|
||||
onCancel={cancelMarkSucceeded}
|
||||
/>
|
||||
|
||||
{/* Report Session Modal */}
|
||||
<ReportSessionModal
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user