Skip to content

Commit 93d237c

Browse files
committed
Add /log page with paginated activity log and truncate overview activity to 4 items with blur overlay
1 parent 988490c commit 93d237c

4 files changed

Lines changed: 144 additions & 16 deletions

File tree

public/css/style.css

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,48 @@ tbody tr:hover {
15591559
font-size: 0.88rem;
15601560
}
15611561

1562+
.activity-list-truncated {
1563+
max-height: 340px;
1564+
overflow: hidden;
1565+
position: relative;
1566+
}
1567+
1568+
.activity-list-truncated::after {
1569+
content: '';
1570+
position: absolute;
1571+
bottom: 0;
1572+
left: 0;
1573+
right: 0;
1574+
height: 100px;
1575+
background: linear-gradient(transparent, var(--bg-card));
1576+
pointer-events: none;
1577+
}
1578+
1579+
.activity-more-overlay {
1580+
text-align: center;
1581+
padding: 12px 0 4px;
1582+
}
1583+
1584+
/* ===== LOG PAGE ===== */
1585+
.log-pagination {
1586+
display: flex;
1587+
align-items: center;
1588+
justify-content: center;
1589+
gap: 12px;
1590+
padding: 16px 0;
1591+
}
1592+
1593+
.log-pagination-info {
1594+
font-size: 0.82rem;
1595+
color: var(--text-muted);
1596+
}
1597+
1598+
.log-pagination .btn-ghost[disabled] {
1599+
opacity: 0.4;
1600+
cursor: not-allowed;
1601+
pointer-events: none;
1602+
}
1603+
15621604
/* ===== SEARCH & FILTERS ===== */
15631605
.servers-toolbar {
15641606
display: flex;

public/js/app.js

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,7 @@ async function renderDashboard() {
428428
<div class="page" id="page-pterodactyl"></div>
429429
<div class="page" id="page-account"></div>
430430
<div class="page" id="page-server-detail"></div>
431+
<div class="page" id="page-log"></div>
431432
</main>
432433
</div>
433434
@@ -499,6 +500,8 @@ function navigateTo(page) {
499500
else if (param === 'links') renderAccountLinks();
500501
else if (param === 'dangerous') renderDangerous();
501502
else renderAccount();
503+
} else if (basePage === 'log') {
504+
renderLog();
502505
}
503506
}
504507

@@ -539,6 +542,8 @@ window.addEventListener('popstate', () => {
539542
else if (param === 'links') renderAccountLinks();
540543
else if (param === 'dangerous') renderDangerous();
541544
else renderAccount();
545+
} else if (basePage === 'log') {
546+
renderLog();
542547
}
543548
}
544549
});
@@ -697,6 +702,63 @@ function getActionLabel(action) {
697702
return labels[action] || action;
698703
}
699704

705+
async function renderLog(pageNum) {
706+
const el = $('#page-log');
707+
pageNum = pageNum || 1;
708+
const limit = 50;
709+
const offset = (pageNum - 1) * limit;
710+
711+
el.innerHTML = html`
712+
<div class="page-header">
713+
<h1 class="page-title">Activity Log</h1>
714+
<p class="page-subtitle">All account activity</p>
715+
</div>
716+
<div class="card" style="margin-bottom:20px">
717+
<div id="log-list">
718+
<div style="text-align:center;padding:24px;color:var(--text-secondary)"><span class="spinner"></span> Loading...</div>
719+
</div>
720+
</div>
721+
`;
722+
723+
try {
724+
const data = await api(`/activity?limit=${limit}&offset=${offset}`);
725+
const list = $('#log-list');
726+
727+
if (data.activities.length === 0) {
728+
list.innerHTML = '<div class="activity-empty">No activity found.</div>';
729+
return;
730+
}
731+
732+
const pageInfo = data.totalPages > 1 ? html`
733+
<div class="log-pagination">
734+
<button class="btn btn-ghost btn-sm" onclick="renderLog(${pageNum - 1})" ${pageNum <= 1 ? 'disabled' : ''}>Previous</button>
735+
<span class="log-pagination-info">Page ${data.page} of ${data.totalPages} (${data.total} total)</span>
736+
<button class="btn btn-ghost btn-sm" onclick="renderLog(${pageNum + 1})" ${pageNum >= data.totalPages ? 'disabled' : ''}>Next</button>
737+
</div>
738+
` : '';
739+
740+
list.innerHTML = html`
741+
${pageInfo}
742+
<div class="activity-list">
743+
${data.activities.map(a => html`
744+
<div class="activity-item">
745+
<div class="activity-icon activity-icon-${a.action}">${activityIcons[a.action] || ''}</div>
746+
<div class="activity-content">
747+
<div class="activity-action">${getActionLabel(a.action)}</div>
748+
<div class="activity-details">${a.details || ''}</div>
749+
</div>
750+
<div class="activity-time">${formatRelativeTime(a.created_at)}</div>
751+
</div>
752+
`).join('')}
753+
</div>
754+
${pageInfo}
755+
`;
756+
} catch (err) {
757+
const list = $('#log-list');
758+
if (list) list.innerHTML = '<div class="activity-empty">Could not load activity log.</div>';
759+
}
760+
}
761+
700762
async function renderActivity() {
701763
const el = $('#page-overview');
702764
let activitySection = $('#activity-section');
@@ -718,24 +780,36 @@ async function renderActivity() {
718780
}
719781

720782
try {
721-
const data = await api('/activity');
783+
const data = await api('/activity?limit=5');
722784
const list = $('#activity-list');
723785

724786
if (data.activities.length === 0) {
725787
list.innerHTML = '<div class="activity-empty">No activity yet. Create a server to get started.</div>';
726788
return;
727789
}
728790

729-
list.innerHTML = data.activities.slice(0, 10).map(a => html`
730-
<div class="activity-item">
731-
<div class="activity-icon activity-icon-${a.action}">${activityIcons[a.action] || ''}</div>
732-
<div class="activity-content">
733-
<div class="activity-action">${getActionLabel(a.action)}</div>
734-
<div class="activity-details">${a.details || ''}</div>
735-
</div>
736-
<div class="activity-time">${formatRelativeTime(a.created_at)}</div>
791+
const showCount = Math.min(4, data.activities.length);
792+
const hasMore = data.total > 4;
793+
794+
list.innerHTML = html`
795+
<div class="activity-list${hasMore ? ' activity-list-truncated' : ''}">
796+
${data.activities.slice(0, showCount).map(a => html`
797+
<div class="activity-item">
798+
<div class="activity-icon activity-icon-${a.action}">${activityIcons[a.action] || ''}</div>
799+
<div class="activity-content">
800+
<div class="activity-action">${getActionLabel(a.action)}</div>
801+
<div class="activity-details">${a.details || ''}</div>
802+
</div>
803+
<div class="activity-time">${formatRelativeTime(a.created_at)}</div>
804+
</div>
805+
`).join('')}
737806
</div>
738-
`).join('');
807+
${hasMore ? html`
808+
<div class="activity-more-overlay">
809+
<a class="btn btn-primary btn-sm" onclick="navigateTo('log')" style="margin-top:12px;width:auto">View all logs</a>
810+
</div>
811+
` : ''}
812+
`;
739813
} catch (err) {
740814
const list = $('#activity-list');
741815
if (list) list.innerHTML = '<div class="activity-empty">Could not load activity.</div>';

server.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,16 @@ app.get('/api/activity', async (req, res) => {
104104
const decoded = jwt.verify(token, process.env.JWT_SECRET);
105105
const userId = decoded.userId;
106106

107-
const activities = await getRecentActivity(userId);
108-
res.json({ activities });
107+
const limit = Math.min(parseInt(req.query.limit) || 20, 50);
108+
const offset = parseInt(req.query.offset) || 0;
109+
const result = await getRecentActivity(userId, limit, offset);
110+
res.json({
111+
activities: result.activities,
112+
total: result.total,
113+
page: Math.floor(offset / limit) + 1,
114+
totalPages: Math.ceil(result.total / limit),
115+
limit,
116+
});
109117
} catch (err) {
110118
if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
111119
return res.status(401).json({ error: 'Invalid or expired token' });

services/activity.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ export async function logActivity(userId, action, details = '', serverId = null)
1111
}
1212
}
1313

14-
export async function getRecentActivity(userId, limit = 20) {
14+
export async function getRecentActivity(userId, limit = 20, offset = 0) {
15+
const [countResult] = await query(
16+
'SELECT COUNT(*) as total FROM activity_log WHERE user_id = ?',
17+
[userId]
18+
);
1519
const rows = await query(
16-
'SELECT * FROM activity_log WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
17-
[userId, limit]
20+
'SELECT * FROM activity_log WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?',
21+
[userId, limit, offset]
1822
);
19-
return rows;
23+
return { activities: rows, total: countResult.total };
2024
}

0 commit comments

Comments
 (0)