Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
{% extends "admin/base.html" %} | |
{% block admin_content %} | |
<div class="admin-header"> | |
<div class="admin-title">Campaign #{{ campaign.id }} Details</div> | |
<a href="{{ url_for('admin.campaigns') }}" class="btn-secondary">Back to Campaigns</a> | |
</div> | |
<!-- Campaign Overview --> | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Campaign Overview</div> | |
<div class="campaign-status"> | |
<span class="status-badge status-{{ campaign.status }}"> | |
{{ campaign.status.replace('_', ' ').title() }} | |
</span> | |
</div> | |
</div> | |
<div class="campaign-details"> | |
<div class="detail-grid"> | |
<div class="detail-item"> | |
<div class="detail-label">Target Model:</div> | |
<div class="detail-value"> | |
<strong>{{ campaign.model.name }}</strong> | |
<span class="model-type-badge model-type-{{ campaign.model_type }}"> | |
{{ campaign.model_type.upper() }} | |
</span> | |
</div> | |
</div> | |
<div class="detail-item"> | |
<div class="detail-label">Detected At:</div> | |
<div class="detail-value">{{ campaign.detected_at.strftime('%Y-%m-%d %H:%M:%S') }}</div> | |
</div> | |
<div class="detail-item"> | |
<div class="detail-label">Detection Window:</div> | |
<div class="detail-value">{{ campaign.time_window_hours }} hours</div> | |
</div> | |
<div class="detail-item"> | |
<div class="detail-label">Total Votes:</div> | |
<div class="detail-value">{{ campaign.vote_count }}</div> | |
</div> | |
<div class="detail-item"> | |
<div class="detail-label">Users Involved:</div> | |
<div class="detail-value">{{ campaign.user_count }}</div> | |
</div> | |
<div class="detail-item"> | |
<div class="detail-label">Confidence Score:</div> | |
<div class="detail-value"> | |
<div class="confidence-bar"> | |
<div class="confidence-fill" style="width: {{ (campaign.confidence_score * 100)|round }}%"></div> | |
<span class="confidence-text">{{ (campaign.confidence_score * 100)|round }}%</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
{% if campaign.resolved_at %} | |
<div class="resolution-info"> | |
<h4>Resolution Information</h4> | |
<div class="detail-grid"> | |
<div class="detail-item"> | |
<div class="detail-label">Resolved By:</div> | |
<div class="detail-value">{{ campaign.resolver.username if campaign.resolver else 'System' }}</div> | |
</div> | |
<div class="detail-item"> | |
<div class="detail-label">Resolved At:</div> | |
<div class="detail-value">{{ campaign.resolved_at.strftime('%Y-%m-%d %H:%M:%S') }}</div> | |
</div> | |
</div> | |
{% if campaign.admin_notes %} | |
<div class="admin-notes"> | |
<div class="detail-label">Admin Notes:</div> | |
<div class="detail-value">{{ campaign.admin_notes }}</div> | |
</div> | |
{% endif %} | |
</div> | |
{% endif %} | |
</div> | |
</div> | |
<!-- Campaign Participants --> | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Campaign Participants ({{ participants|length }})</div> | |
</div> | |
{% if participants %} | |
<div class="table-responsive"> | |
<table class="admin-table"> | |
<thead> | |
<tr> | |
<th>User</th> | |
<th>Votes in Campaign</th> | |
<th>First Vote</th> | |
<th>Last Vote</th> | |
<th>Suspicion Level</th> | |
<th>Account Age</th> | |
<th>Current Status</th> | |
<th>Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for participant, user in participants %} | |
<tr> | |
<td> | |
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"> | |
{{ user.username }} | |
</a> | |
</td> | |
<td>{{ participant.votes_in_campaign }}</td> | |
<td>{{ participant.first_vote_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td>{{ participant.last_vote_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td> | |
<span class="suspicion-badge suspicion-{{ participant.suspicion_level }}"> | |
{{ participant.suspicion_level.title() }} | |
</span> | |
</td> | |
<td> | |
{% if user.join_date %} | |
{{ ((campaign.detected_at - user.join_date).days) }} days | |
{% else %} | |
Unknown | |
{% endif %} | |
</td> | |
<td> | |
<div class="user-status" data-user-id="{{ user.id }}"> | |
Checking... | |
</div> | |
</td> | |
<td> | |
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}" class="action-btn"> | |
View User | |
</a> | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<p>No participants found.</p> | |
{% endif %} | |
</div> | |
<!-- Related Timeouts --> | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Related Timeouts ({{ related_timeouts|length }})</div> | |
</div> | |
{% if related_timeouts %} | |
<div class="table-responsive"> | |
<table class="admin-table"> | |
<thead> | |
<tr> | |
<th>User</th> | |
<th>Reason</th> | |
<th>Created</th> | |
<th>Expires</th> | |
<th>Status</th> | |
<th>Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for timeout in related_timeouts %} | |
<tr> | |
<td> | |
<a href="{{ url_for('admin.user_detail', user_id=timeout.user.id) }}"> | |
{{ timeout.user.username }} | |
</a> | |
</td> | |
<td class="text-truncate" title="{{ timeout.reason }}">{{ timeout.reason }}</td> | |
<td>{{ timeout.created_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td>{{ timeout.expires_at.strftime('%Y-%m-%d %H:%M') }}</td> | |
<td> | |
{% if timeout.is_currently_active() %} | |
<span class="status-badge status-active">Active</span> | |
{% else %} | |
<span class="status-badge status-expired">Expired</span> | |
{% endif %} | |
</td> | |
<td> | |
<a href="{{ url_for('admin.timeouts') }}" class="action-btn"> | |
Manage | |
</a> | |
</td> | |
</tr> | |
{% endfor %} | |
</tbody> | |
</table> | |
</div> | |
{% else %} | |
<p>No related timeouts.</p> | |
{% endif %} | |
</div> | |
<!-- Resolution Actions --> | |
{% if campaign.status == 'active' %} | |
<div class="admin-card"> | |
<div class="admin-card-header"> | |
<div class="admin-card-title">Resolve Campaign</div> | |
</div> | |
<form method="POST" action="{{ url_for('admin.resolve_campaign_route', campaign_id=campaign.id) }}" class="admin-form"> | |
<div class="form-group"> | |
<label for="status">Resolution Status</label> | |
<select id="status" name="status" class="form-control" required> | |
<option value="">Select resolution...</option> | |
<option value="resolved">Resolved - Legitimate coordinated campaign</option> | |
<option value="false_positive">False Positive - Not a real campaign</option> | |
</select> | |
</div> | |
<div class="form-group"> | |
<label for="admin_notes">Admin Notes</label> | |
<textarea id="admin_notes" name="admin_notes" class="form-control" rows="3" | |
placeholder="Add notes about the resolution decision..."></textarea> | |
</div> | |
<button type="submit" class="btn-primary">Resolve Campaign</button> | |
</form> | |
</div> | |
{% endif %} | |
<style> | |
.campaign-details { | |
background-color: var(--light-gray); | |
padding: 20px; | |
border-radius: var(--radius); | |
border: 1px solid var(--border-color); | |
} | |
.detail-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 16px; | |
margin-bottom: 20px; | |
} | |
.detail-item { | |
display: flex; | |
flex-direction: column; | |
gap: 4px; | |
} | |
.detail-label { | |
font-weight: 500; | |
color: #666; | |
font-size: 14px; | |
} | |
.detail-value { | |
font-size: 16px; | |
display: flex; | |
align-items: center; | |
gap: 8px; | |
} | |
.campaign-status { | |
display: flex; | |
align-items: center; | |
} | |
.model-type-badge { | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 11px; | |
font-weight: 500; | |
color: white; | |
text-transform: uppercase; | |
letter-spacing: 0.5px; | |
} | |
.model-type-tts { | |
background-color: #007bff; | |
} | |
.model-type-conversational { | |
background-color: #28a745; | |
} | |
.confidence-bar { | |
position: relative; | |
width: 120px; | |
height: 24px; | |
background-color: #e9ecef; | |
border-radius: 12px; | |
overflow: hidden; | |
} | |
.confidence-fill { | |
height: 100%; | |
background: linear-gradient(90deg, #dc3545 0%, #ffc107 50%, #28a745 100%); | |
transition: width 0.3s ease; | |
} | |
.confidence-text { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
font-size: 12px; | |
font-weight: 500; | |
color: #333; | |
text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); | |
} | |
.resolution-info { | |
border-top: 1px solid var(--border-color); | |
padding-top: 20px; | |
margin-top: 20px; | |
} | |
.resolution-info h4 { | |
margin: 0 0 16px 0; | |
color: var(--primary-color); | |
} | |
.admin-notes { | |
margin-top: 16px; | |
} | |
.suspicion-badge { | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
font-weight: 500; | |
color: white; | |
} | |
.suspicion-low { | |
background-color: #28a745; | |
} | |
.suspicion-medium { | |
background-color: #ffc107; | |
color: black; | |
} | |
.suspicion-high { | |
background-color: #dc3545; | |
} | |
.status-badge { | |
padding: 4px 8px; | |
border-radius: 4px; | |
font-size: 12px; | |
font-weight: 500; | |
color: white; | |
} | |
.status-active { | |
background-color: #dc3545; | |
} | |
.status-resolved { | |
background-color: #28a745; | |
} | |
.status-false_positive { | |
background-color: #ffc107; | |
color: black; | |
} | |
.status-expired { | |
background-color: #6c757d; | |
} | |
.user-status { | |
font-size: 12px; | |
} | |
.user-status.timed-out { | |
color: #dc3545; | |
font-weight: 500; | |
} | |
.user-status.active { | |
color: #28a745; | |
} | |
@media (max-width: 768px) { | |
.detail-grid { | |
grid-template-columns: 1fr; | |
} | |
.confidence-bar { | |
width: 100px; | |
} | |
.admin-header { | |
flex-direction: column; | |
gap: 12px; | |
align-items: flex-start; | |
} | |
} | |
</style> | |
<script> | |
document.addEventListener('DOMContentLoaded', function() { | |
// Check user timeout status for each participant | |
const userStatusElements = document.querySelectorAll('.user-status'); | |
userStatusElements.forEach(async (element) => { | |
const userId = element.dataset.userId; | |
try { | |
// This would need to be implemented as an API endpoint | |
// For now, we'll just show a placeholder | |
element.textContent = 'Active'; | |
element.className = 'user-status active'; | |
} catch (error) { | |
element.textContent = 'Unknown'; | |
element.className = 'user-status'; | |
} | |
}); | |
}); | |
</script> | |
{% endblock %} |