Skip to content

Commit 782b779

Browse files
committed
Add server power state detection, resource bars, activity logging, search/filter, and live resources tab
1 parent c24b0c3 commit 782b779

7 files changed

Lines changed: 908 additions & 41 deletions

File tree

config/migrate.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const tables = {
1212
{ name: 'first_name', def: 'VARCHAR(255)' },
1313
{ name: 'last_name', def: 'VARCHAR(255)' },
1414
{ name: 'password_set', def: 'TINYINT(1) NOT NULL DEFAULT 0' },
15+
{ name: 'ptero_client_api_key', def: 'VARCHAR(255) DEFAULT NULL' },
1516
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
1617
],
1718
},
@@ -33,6 +34,16 @@ const tables = {
3334
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
3435
],
3536
},
37+
activity_log: {
38+
columns: [
39+
{ name: 'id', def: 'INT AUTO_INCREMENT PRIMARY KEY' },
40+
{ name: 'user_id', def: 'INT NOT NULL' },
41+
{ name: 'action', def: 'VARCHAR(50) NOT NULL' },
42+
{ name: 'details', def: 'VARCHAR(255) DEFAULT \'\'' },
43+
{ name: 'server_id', def: 'INT DEFAULT NULL' },
44+
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },
45+
],
46+
},
3647

3748
};
3849

@@ -51,7 +62,8 @@ const constraints = [
5162
{ table: 'user_ips', sql: 'ALTER TABLE user_ips ADD INDEX idx_ip (ip_address)', name: 'idx_ip' },
5263
{ table: 'user_ips', sql: 'ALTER TABLE user_ips ADD INDEX idx_user (user_id)', name: 'idx_user' },
5364
{ table: 'user_ips', sql: 'ALTER TABLE user_ips ADD CONSTRAINT fk_user_ips_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE', name: 'fk_user_ips_user' },
54-
65+
{ table: 'activity_log', sql: 'ALTER TABLE activity_log ADD INDEX idx_activity_user (user_id)', name: 'idx_activity_user' },
66+
{ table: 'activity_log', sql: 'ALTER TABLE activity_log ADD INDEX idx_activity_created (created_at)', name: 'idx_activity_created' },
5567
];
5668

5769
export async function migrate() {

public/css/style.css

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,31 @@ cap-widget {
731731
letter-spacing: 0.05em;
732732
}
733733

734+
.power-state-dot {
735+
display: inline-flex;
736+
align-items: center;
737+
gap: 4px;
738+
font-size: 0.7rem;
739+
font-weight: 500;
740+
margin-left: 6px;
741+
}
742+
.power-state-dot::before {
743+
content: '';
744+
width: 7px;
745+
height: 7px;
746+
border-radius: 50%;
747+
flex-shrink: 0;
748+
}
749+
.power-state-dot.running::before { background: #10b981; }
750+
.power-state-dot.offline::before { background: #6b7280; }
751+
.power-state-dot.starting::before { background: #f59e0b; animation: pulse-dot 1s ease-in-out infinite; }
752+
.power-state-dot.stopping::before { background: #f59e0b; animation: pulse-dot 1s ease-in-out infinite; }
753+
754+
@keyframes pulse-dot {
755+
0%, 100% { opacity: 1; }
756+
50% { opacity: 0.4; }
757+
}
758+
734759
.status-active {
735760
background: rgba(16, 185, 129, 0.12);
736761
color: var(--accent-green);
@@ -1448,4 +1473,312 @@ tbody tr:hover {
14481473
to { transform: scale(0.9); opacity: 0; }
14491474
}
14501475

1476+
/* ===== ACTIVITY TIMELINE ===== */
1477+
.activity-list {
1478+
display: flex;
1479+
flex-direction: column;
1480+
gap: 0;
1481+
}
1482+
1483+
.activity-item {
1484+
display: flex;
1485+
align-items: flex-start;
1486+
gap: 14px;
1487+
padding: 14px 0;
1488+
border-bottom: 1px solid rgba(255,255,255,0.04);
1489+
position: relative;
1490+
}
1491+
1492+
.activity-item:last-child {
1493+
border-bottom: none;
1494+
}
1495+
1496+
.activity-item::before {
1497+
content: '';
1498+
position: absolute;
1499+
left: 15px;
1500+
top: 42px;
1501+
bottom: 0;
1502+
width: 1px;
1503+
background: rgba(255,255,255,0.06);
1504+
}
1505+
1506+
.activity-item:last-child::before {
1507+
display: none;
1508+
}
1509+
1510+
.activity-icon {
1511+
width: 32px;
1512+
height: 32px;
1513+
border-radius: 50%;
1514+
display: flex;
1515+
align-items: center;
1516+
justify-content: center;
1517+
flex-shrink: 0;
1518+
font-size: 0.8rem;
1519+
position: relative;
1520+
z-index: 1;
1521+
}
1522+
1523+
.activity-icon-server_created { background: rgba(16,185,129,0.15); color: var(--accent-green); }
1524+
.activity-icon-server_renewed { background: rgba(6,182,212,0.15); color: var(--accent-cyan); }
1525+
.activity-icon-server_renamed { background: rgba(238,129,50,0.15); color: var(--accent-1); }
1526+
.activity-icon-server_reinstalled { background: rgba(245,158,11,0.15); color: var(--accent-orange); }
1527+
.activity-icon-server_deleted { background: rgba(239,68,68,0.15); color: var(--accent-red); }
1528+
.activity-icon-account_registered { background: rgba(168,85,247,0.15); color: #a855f7; }
1529+
.activity-icon-password_changed { background: rgba(59,130,246,0.15); color: #3b82f6; }
1530+
.activity-icon-email_changed { background: rgba(59,130,246,0.15); color: #3b82f6; }
1531+
.activity-icon-account_deleted { background: rgba(239,68,68,0.15); color: var(--accent-red); }
1532+
.activity-icon-api_key_updated { background: rgba(236,201,75,0.15); color: #ecc94b; }
1533+
1534+
.activity-content {
1535+
flex: 1;
1536+
min-width: 0;
1537+
}
1538+
1539+
.activity-action {
1540+
font-size: 0.88rem;
1541+
font-weight: 600;
1542+
color: var(--text-primary);
1543+
}
1544+
1545+
.activity-details {
1546+
font-size: 0.78rem;
1547+
color: var(--text-muted);
1548+
margin-top: 2px;
1549+
}
1550+
1551+
.activity-time {
1552+
font-size: 0.72rem;
1553+
color: var(--text-muted);
1554+
white-space: nowrap;
1555+
padding-top: 2px;
1556+
}
1557+
1558+
.activity-empty {
1559+
text-align: center;
1560+
padding: 32px 16px;
1561+
color: var(--text-muted);
1562+
font-size: 0.88rem;
1563+
}
1564+
1565+
/* ===== SEARCH & FILTERS ===== */
1566+
.servers-toolbar {
1567+
display: flex;
1568+
gap: 12px;
1569+
margin-bottom: 20px;
1570+
flex-wrap: wrap;
1571+
align-items: center;
1572+
}
1573+
1574+
.search-wrapper {
1575+
position: relative;
1576+
flex: 1;
1577+
min-width: 200px;
1578+
}
1579+
1580+
.search-wrapper svg {
1581+
position: absolute;
1582+
left: 14px;
1583+
top: 50%;
1584+
transform: translateY(-50%);
1585+
color: var(--text-muted);
1586+
pointer-events: none;
1587+
}
1588+
1589+
.search-wrapper input {
1590+
width: 100%;
1591+
padding: 10px 14px 10px 40px;
1592+
background: var(--bg-secondary);
1593+
border: 1px solid var(--border);
1594+
border-radius: var(--radius-sm);
1595+
color: var(--text-primary);
1596+
font-family: 'Schibsted Grotesk', sans-serif;
1597+
font-size: 0.9rem;
1598+
outline: none;
1599+
transition: all var(--transition);
1600+
}
1601+
1602+
.search-wrapper input:focus {
1603+
border-color: var(--accent-1);
1604+
box-shadow: 0 0 0 3px rgba(238,129,50,0.15);
1605+
}
1606+
1607+
.search-wrapper input::placeholder {
1608+
color: var(--text-muted);
1609+
}
1610+
1611+
.filter-group {
1612+
display: flex;
1613+
gap: 8px;
1614+
flex-wrap: wrap;
1615+
}
1616+
1617+
.filter-btn {
1618+
padding: 8px 16px;
1619+
background: var(--bg-secondary);
1620+
border: 1px solid var(--border);
1621+
border-radius: 99px;
1622+
color: var(--text-secondary);
1623+
font-family: 'Schibsted Grotesk', sans-serif;
1624+
font-size: 0.8rem;
1625+
font-weight: 500;
1626+
cursor: pointer;
1627+
transition: all var(--transition);
1628+
white-space: nowrap;
1629+
}
1630+
1631+
.filter-btn:hover {
1632+
border-color: var(--border-hover);
1633+
color: var(--text-primary);
1634+
}
1635+
1636+
.filter-btn.active {
1637+
background: rgba(238,129,50,0.12);
1638+
border-color: var(--accent-1);
1639+
color: var(--accent-1);
1640+
}
1641+
1642+
/* ===== RESOURCE BARS ===== */
1643+
.resource-bars {
1644+
display: flex;
1645+
flex-direction: column;
1646+
gap: 6px;
1647+
margin-top: 8px;
1648+
}
1649+
1650+
.resource-bar-row {
1651+
display: flex;
1652+
align-items: center;
1653+
gap: 8px;
1654+
}
1655+
1656+
.resource-bar-icon {
1657+
width: 16px;
1658+
height: 16px;
1659+
flex-shrink: 0;
1660+
color: var(--text-muted);
1661+
}
1662+
1663+
.resource-bar-track {
1664+
flex: 1;
1665+
height: 6px;
1666+
background: rgba(255,255,255,0.06);
1667+
border-radius: 99px;
1668+
overflow: hidden;
1669+
position: relative;
1670+
}
1671+
1672+
.resource-bar-fill {
1673+
height: 100%;
1674+
border-radius: 99px;
1675+
background: linear-gradient(90deg, var(--accent-1), var(--accent-2));
1676+
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
1677+
width: 100%;
1678+
position: relative;
1679+
}
1680+
1681+
.resource-bar-fill.memory { background: linear-gradient(90deg, #ee8132, #f59e0b); }
1682+
.resource-bar-fill.cpu { background: linear-gradient(90deg, #06b6d4, #3b82f6); }
1683+
.resource-bar-fill.disk { background: linear-gradient(90deg, #059669, #10b981); }
1684+
1685+
.resource-bar-fill.usage-low { background: linear-gradient(90deg, #059669, #10b981); }
1686+
.resource-bar-fill.usage-mid { background: linear-gradient(90deg, #f59e0b, #ee8132); }
1687+
.resource-bar-fill.usage-high { background: linear-gradient(90deg, #ef4444, #dc2626); }
1688+
1689+
.resource-bar-label {
1690+
font-size: 0.68rem;
1691+
color: var(--text-muted);
1692+
font-family: 'JetBrains Mono', monospace;
1693+
white-space: nowrap;
1694+
min-width: 75px;
1695+
text-align: right;
1696+
}
1697+
1698+
.resource-bar-value {
1699+
font-size: 0.68rem;
1700+
color: var(--text-secondary);
1701+
font-family: 'JetBrains Mono', monospace;
1702+
white-space: nowrap;
1703+
min-width: 32px;
1704+
}
1705+
1706+
/* Resource gauges for server detail */
1707+
.resource-gauges {
1708+
display: grid;
1709+
grid-template-columns: repeat(3, 1fr);
1710+
gap: 20px;
1711+
margin-top: 8px;
1712+
}
1713+
1714+
.resource-gauge {
1715+
text-align: center;
1716+
padding: 20px 12px;
1717+
background: rgba(255,255,255,0.02);
1718+
border: 1px solid var(--border);
1719+
border-radius: var(--radius-md);
1720+
position: relative;
1721+
overflow: hidden;
1722+
}
1723+
1724+
.resource-gauge-value {
1725+
font-size: 1.8rem;
1726+
font-weight: 800;
1727+
letter-spacing: -0.03em;
1728+
margin-bottom: 4px;
1729+
}
1730+
1731+
.resource-gauge-label {
1732+
font-size: 0.75rem;
1733+
color: var(--text-muted);
1734+
text-transform: uppercase;
1735+
letter-spacing: 0.06em;
1736+
font-weight: 600;
1737+
}
1738+
1739+
.resource-gauge-bar {
1740+
margin-top: 12px;
1741+
height: 4px;
1742+
background: rgba(255,255,255,0.06);
1743+
border-radius: 99px;
1744+
overflow: hidden;
1745+
}
1746+
1747+
.resource-gauge-fill {
1748+
height: 100%;
1749+
border-radius: 99px;
1750+
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
1751+
}
1752+
1753+
.resource-gauge-sub {
1754+
font-size: 0.68rem;
1755+
color: var(--text-muted);
1756+
margin-top: 6px;
1757+
font-family: 'JetBrains Mono', monospace;
1758+
}
1759+
1760+
/* Account API key section */
1761+
.api-key-input-group {
1762+
display: flex;
1763+
gap: 8px;
1764+
align-items: stretch;
1765+
}
1766+
1767+
.api-key-input-group input {
1768+
flex: 1;
1769+
}
1770+
1771+
@media (max-width: 768px) {
1772+
.servers-toolbar {
1773+
flex-direction: column;
1774+
align-items: stretch;
1775+
}
1776+
.filter-group {
1777+
justify-content: center;
1778+
}
1779+
.resource-gauges {
1780+
grid-template-columns: 1fr;
1781+
}
1782+
}
1783+
14511784

0 commit comments

Comments
 (0)