littleshop/LittleShop/Areas/Admin/Views/BotActivity/LiveView.cshtml
SysAdmin a1af91807e
All checks were successful
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Successful in 1m1s
feat: Add TeleBot session tracking to LittleShop and fix live activity feed ordering
- Add LittleShopSessionId and MessageCount properties to UserSession model
- Integrate SessionManager with BotManagerService for remote session tracking
- Wire up SessionManager.SetBotManagerService() at startup in Program.cs
- Create remote sessions via BotManagerService.StartSessionAsync() when users connect
- Update remote sessions periodically (every 10 messages) via UpdateSessionAsync()
- Fix live activity feed to show newest records at top by reversing array iteration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-25 20:03:08 +00:00

363 lines
14 KiB
Plaintext

@{
ViewData["Title"] = "Live Bot Activity";
Layout = "_Layout";
}
<style>
.activity-feed {
max-height: 600px;
overflow-y: auto;
}
.activity-item {
transition: all 0.3s ease;
border-left: 3px solid transparent;
}
.activity-item:hover {
background-color: rgba(0, 123, 255, 0.05);
border-left-color: #007bff;
}
.activity-item.new {
animation: slideIn 0.5s ease;
background-color: rgba(40, 167, 69, 0.1);
}
@@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.user-bubble {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 0.85rem;
}
.stats-card {
transition: transform 0.2s;
}
.stats-card:hover {
transform: translateY(-5px);
}
.activity-type-badge {
font-size: 0.75rem;
padding: 2px 8px;
}
.pulse {
animation: pulse 2s infinite;
}
@@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
}
}
</style>
<div class="container-fluid">
<div class="row mb-4">
<div class="col">
<h1><i class="fas fa-satellite-dish"></i> Live Bot Activity</h1>
<p class="text-muted">Real-time view of customer interactions across all bots</p>
</div>
<div class="col-auto">
<div class="d-flex align-items-center">
<span class="badge bg-success pulse me-2">LIVE</span>
<small class="text-muted">Updates every 5 seconds</small>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card stats-card border-primary">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted">Active Users</h6>
<h2 class="mb-0" id="activeUsersCount">0</h2>
</div>
<div class="text-primary">
<i class="fas fa-users fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card border-success">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted">Today's Activities</h6>
<h2 class="mb-0" id="todayActivitiesCount">0</h2>
</div>
<div class="text-success">
<i class="fas fa-chart-line fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card border-info">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted">Unique Visitors</h6>
<h2 class="mb-0" id="uniqueVisitorsCount">0</h2>
</div>
<div class="text-info">
<i class="fas fa-fingerprint fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stats-card border-warning">
<div class="card-body">
<div class="d-flex justify-content-between">
<div>
<h6 class="text-muted">Cart Actions</h6>
<h2 class="mb-0" id="cartActionsCount">0</h2>
</div>
<div class="text-warning">
<i class="fas fa-shopping-cart fa-2x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Active Users Panel -->
<div class="col-md-4">
<div class="card">
<div class="card-header bg-primary text-white">
<i class="fas fa-user-clock"></i> Active Users (Last 5 min)
</div>
<div class="card-body p-0" style="max-height: 500px; overflow-y: auto;">
<div class="list-group list-group-flush" id="activeUsersList">
<!-- Active users will be populated here -->
</div>
</div>
</div>
<!-- Popular Products -->
<div class="card mt-3">
<div class="card-header bg-success text-white">
<i class="fas fa-fire"></i> Trending Products (7 days)
</div>
<div class="card-body p-0">
<div class="list-group list-group-flush" id="popularProductsList">
<!-- Popular products will be populated here -->
</div>
</div>
</div>
</div>
<!-- Activity Feed -->
<div class="col-md-8">
<div class="card">
<div class="card-header bg-dark text-white">
<i class="fas fa-stream"></i> Live Activity Feed
</div>
<div class="card-body activity-feed p-0" id="activityFeed">
<!-- Activities will be populated here -->
</div>
</div>
</div>
</div>
</div>
@section Scripts {
<script>
let lastActivityId = '';
function getActivityIcon(type) {
const icons = {
'Browse': 'fa-eye',
'ViewProduct': 'fa-search',
'AddToCart': 'fa-cart-plus',
'RemoveFromCart': 'fa-cart-arrow-down',
'Checkout': 'fa-cash-register',
'Search': 'fa-search',
'Register': 'fa-user-plus',
'Login': 'fa-sign-in-alt'
};
return icons[type] || 'fa-circle';
}
function getActivityColor(type) {
const colors = {
'Browse': 'info',
'ViewProduct': 'primary',
'AddToCart': 'success',
'RemoveFromCart': 'warning',
'Checkout': 'danger',
'Search': 'secondary'
};
return colors[type] || 'secondary';
}
function updateActivities() {
// Get recent activities
$.get('@Url.Action("GetRecentActivities")', { count: 30 }, function(activities) {
const feed = $('#activityFeed');
// Reverse so oldest is prepended first, newest ends up at top
activities.slice().reverse().forEach(function(activity) {
const existingItem = $(`#activity-${activity.id}`);
if (existingItem.length === 0) {
const isNew = lastActivityId && activity.id !== lastActivityId;
const html = `
<div id="activity-${activity.id}" class="activity-item ${isNew ? 'new' : ''} p-3 border-bottom">
<div class="d-flex justify-content-between align-items-start">
<div class="d-flex">
<div class="me-3 text-${getActivityColor(activity.activityType)}">
<i class="fas ${getActivityIcon(activity.activityType)} fa-lg"></i>
</div>
<div>
<div class="mb-1">
<span class="user-bubble">${activity.userDisplayName}</span>
<span class="activity-type-badge badge bg-${getActivityColor(activity.activityType)} ms-2">
${activity.activityType}
</span>
</div>
<div class="text-dark">
${activity.activityDescription}
</div>
${activity.productName ? `
<div class="text-muted small mt-1">
<i class="fas fa-box"></i> ${activity.productName}
${activity.value ? ` - £${activity.value.toFixed(2)}` : ''}
</div>
` : ''}
</div>
</div>
<div class="text-muted small">
${activity.timeAgo}
</div>
</div>
</div>
`;
feed.prepend(html);
// Limit feed to 30 items
while (feed.children().length > 30) {
feed.children().last().remove();
}
}
});
if (activities.length > 0) {
lastActivityId = activities[0].id;
}
});
}
function updateActiveUsers() {
$.get('@Url.Action("GetActiveUsers")', function(users) {
const list = $('#activeUsersList');
list.empty();
$('#activeUsersCount').text(users.length);
users.forEach(function(user) {
const html = `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">${user.userName}</h6>
<small class="text-muted">${user.lastAction}</small>
</div>
<div class="text-end">
<span class="badge bg-primary">${user.activityCount} actions</span>
${user.totalValue > 0 ? `<br><small class="text-success">£${user.totalValue.toFixed(2)}</small>` : ''}
</div>
</div>
</div>
`;
list.append(html);
});
if (users.length === 0) {
list.append('<div class="list-group-item text-muted text-center">No active users</div>');
}
});
}
function updateStatistics() {
$.get('@Url.Action("GetStatistics")', function(stats) {
$('#todayActivitiesCount').text(stats.todayActivities);
$('#uniqueVisitorsCount').text(stats.uniqueUsersToday);
// Update popular products
const productsList = $('#popularProductsList');
productsList.empty();
stats.popularProducts.forEach(function(product) {
const html = `
<div class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">${product.productName}</h6>
</div>
<div>
<span class="badge bg-info">${product.viewCount} views</span>
<span class="badge bg-success">${product.addToCartCount} carts</span>
</div>
</div>
</div>
`;
productsList.append(html);
});
// Calculate cart actions
let cartActions = 0;
$('#activityFeed .activity-item').each(function() {
const text = $(this).text();
if (text.includes('AddToCart') || text.includes('Checkout')) {
cartActions++;
}
});
$('#cartActionsCount').text(cartActions);
});
}
// Initial load and periodic updates
$(document).ready(function() {
updateActivities();
updateActiveUsers();
updateStatistics();
// Update every 5 seconds
setInterval(updateActivities, 5000);
setInterval(updateActiveUsers, 10000);
setInterval(updateStatistics, 15000);
});
</script>
}