From 7b394d64462be7e9a2f534da74bd653cad570b86 Mon Sep 17 00:00:00 2001 From: Niranjan Date: Tue, 7 Apr 2026 13:30:25 +0530 Subject: [PATCH] new changes --- .../api/__pycache__/dashboard.cpython-314.pyc | Bin 3010 -> 4227 bytes .../api/__pycache__/firewall.cpython-314.pyc | Bin 6617 -> 8053 bytes YakPanel-server/backend/app/api/dashboard.py | 30 +++++++++- YakPanel-server/backend/app/api/firewall.py | 32 ++++++++++ .../__pycache__/site_service.cpython-314.pyc | Bin 50615 -> 52463 bytes .../backend/app/services/site_service.py | 28 +++++++++ YakPanel-server/docs/FEATURE-PARITY.md | 4 +- YakPanel-server/frontend/src/api/client.ts | 13 ++++ .../frontend/src/pages/DashboardPage.tsx | 41 ++++++++++++- .../frontend/src/pages/FirewallPage.tsx | 56 +++++++++++++++++- 10 files changed, 197 insertions(+), 7 deletions(-) diff --git a/YakPanel-server/backend/app/api/__pycache__/dashboard.cpython-314.pyc b/YakPanel-server/backend/app/api/__pycache__/dashboard.cpython-314.pyc index b2764e052c55bed77cce1189c07848caf4177c30..b6b030e41836800e019173fca17df5faf3efbce6 100644 GIT binary patch literal 4227 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 diff --git a/YakPanel-server/backend/app/api/__pycache__/firewall.cpython-314.pyc b/YakPanel-server/backend/app/api/__pycache__/firewall.cpython-314.pyc index f03629d7ed6a78e8cceff3632cc02faea58c41f5..292c2d3fed1be50450d415ad0a6ad3935b34ff3e 100644 GIT binary patch delta 2558 zcma)8U2NM_6!wkn{5nqBv`N!;jr&8^tQ{>|M_W3|&lu~(H`Nbp1_OHiIDFNnQWDiTO&XcI!>F;%NIvyc#i7ha%f{D=o8&UJnY6+&Fe=ic*k z&-sq;xyQbD_}6j1kN0~i7|Wk-DL)2o@GXuf9XERKIu0X~N6{ObP39On%0^#oo;qTt zLm~5R;M#y2wYl%m<4jG3+RSCNzpXt&iTfgy8dT8`aSX%g5Ief5M9hpk-a$9lZ#fi% zLgp;i7>QF^1qRXu47$(?YEWnH#lV}X>uL!7F zK->@yjh~6s9ZkxAp&Lo|?VfWx{Gq;=}GZ69|aZS5K-^c_2alr!a0IK6O66pb2Yyb})(U)|KjY-$P z;9v*n0Y^7=gjP_ZsqoZ=XD#G5g}j!ho~6aw{-DkW2{d0;GlZ(Q1dBZ&BNkZ6S!SlV&zO)lVUWDRv# zsai&jyP}#Wom`l>AQXklurHq#sTrnb(wUd#tYins>-acWlRKT8p|MqEZDb=pwsfl8 z(X}-BjVo04wXIw$Zu$0?qx;^PEc;`tBU}Eyak$?wAtmEsd6@%&aedHpPG3AAko zI!l4h6}-*&Zt%UE{DEzLaDyM*_|aJT*Nh1=qeiwSynyp*R2Y)dT%%3O0P(p%<2 z%NO5GEvHr)FbI2h{65aPL-|yUnVSyG5uS9F|LS@pX408JUF)wp*U$`Uf%tE&ne6phF8LbV9z({^gDC6=V!1Ue z$v8E$h@T^@OV6a$G}bK|?jZg*FRTp=PngT@Sd?^l%U?fwRe@SP53=F)uiRbeX}fnr z;Yy3uO`uB9l~2O|%ITQd?;V1i(%zp?a4#D33KuI9)Nl=m&Z1R0i=kmwQrnY0WF*0r z%xLrTieV?4VDJTEOs)`y21r>9&a1LcJjCO1p;Fj^dUhFTHt_DQfcj~8^f!R4Kj08L zZs!SIK$TO9wCgWHTeOlj{@?M5<}>^Nq-yXVqT!3+8b1#tiC+L(;6(5(NU{p8MM`+^ zDWcy!M0R0L#xj1+JQsYZ?=tA1cgzrzdTMxzuqhxqsUwmeJ;i5uYLCEFpW&nlwWE7!c^Gxg2yCJVGSoq%7toP08X*uuS;&By6$vfm%-Z zY0$8aMPl#eavC;LIjk8PZZ-SD-K};R^Yw#<5b7c>fN^?#E_?!^n7I~NL!6mziKgsZ z0ykFZHCf{cJ|D#e_?xXXX1m`-A-vkBu vlwJ!Ry*TnOY|1g1?GPNFVB4LA66jVa^M#4oCf_N~&G~4b|SGL_|c5B58@!;8z zXE=HBqVZrPX}p=J2Ty7|7)^MZ_#cQD51MG4*~*7#3{Cp$&di%%=QlHNzm9DTgnPqP z6(#t&{raty(N@Dvf$wa!=e?K+)C$I$*noIm6A^uEL~XfHKX=l%ZlKkQRx$RDjfUo< zT_t*}umfvMFDQz|CeKa}FXr=ka2#)O*;vXN8E(f@PI~dYu{1|UU)pdz4m-BX;VRlm zBeldc=AuXkUuaEO88j2Lh*5RAp$#8hhA;s!^49tw*3_QzjxAfvXba;FXvxY{4uEI8 zL=L%Sz)U^}fJVnu1f4-E{`hR2MYeW|5j2T6n$9{3-?Vj>Y$WflLeSJTS<0{RR1O7% zC|rpk%N#H{B$CU}PiCo`GHe4}sTj5m|6)<$FeUd1E!^HtdSul_3wR#SCv400UtkLO&P+mrY+A`tp{P44i|;tlMF)6<}NXGcXuE38!fB1Wu|}|*u)tO zcjDK*iBMQRz}Sqhvy_q(mbt^HD^jsC-u~uzOgxPoL8+UOd#rKf=e4jdj1k*a+{-|GZ;TwIMJZ9?p{iY zBm8gUro_!g+PFuJKLRWw9=0^&0?Ja)N^>XYiI*+0mEH9t+x4n>YV1KHo3$-&N;T!= zGM-)wf69Vm^!GGT7$6uV*faHV^hwor@_0)^$cNWYsdsJf1S!kkQTc%23ZWy bd&B*S@I_&aZIrZq66a!*;z{grkWzjDJ}E8` diff --git a/YakPanel-server/backend/app/api/dashboard.py b/YakPanel-server/backend/app/api/dashboard.py index 9b9cea72..33675569 100644 --- a/YakPanel-server/backend/app/api/dashboard.py +++ b/YakPanel-server/backend/app/api/dashboard.py @@ -1,4 +1,6 @@ """YakPanel - Dashboard API""" +import os + from fastapi import APIRouter, Depends from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func @@ -10,6 +12,24 @@ from app.models.user import User router = APIRouter(prefix="/dashboard", tags=["dashboard"]) +def _root_inode_usage() -> dict: + """Inode use on filesystem root (Linux). Returns percent or null if unavailable.""" + try: + sv = os.statvfs("/") + except (AttributeError, OSError): + return {"percent": None, "used": None, "total": None} + total = int(sv.f_files) + free = int(sv.f_ffree) + if total <= 0: + return {"percent": None, "used": None, "total": None} + used = max(0, total - free) + return { + "percent": round(100.0 * used / total, 1), + "used": used, + "total": total, + } + + @router.get("/stats") async def get_stats( current_user: User = Depends(get_current_user), @@ -18,10 +38,10 @@ async def get_stats( """Get dashboard statistics""" import psutil - from app.services.site_service import get_site_count + from app.services.site_service import get_site_count, ssl_alert_summary from app.models.ftp import Ftp from app.models.database import Database - from sqlalchemy import select, func + site_count = await get_site_count(db) ftp_result = await db.execute(select(func.count()).select_from(Ftp)) ftp_count = ftp_result.scalar() or 0 @@ -32,11 +52,14 @@ async def get_stats( cpu_percent = psutil.cpu_percent(interval=1) memory = psutil.virtual_memory() disk = psutil.disk_usage("/") + inodes = _root_inode_usage() + ssl_alerts = await ssl_alert_summary(db) return { "site_count": site_count, "ftp_count": ftp_count, "database_count": database_count, + "ssl_alerts": ssl_alerts, "system": { "cpu_percent": cpu_percent, "memory_percent": memory.percent, @@ -45,5 +68,8 @@ async def get_stats( "disk_percent": disk.percent, "disk_used_gb": round(disk.used / 1024 / 1024 / 1024, 2), "disk_total_gb": round(disk.total / 1024 / 1024 / 1024, 2), + "inode_percent": inodes["percent"], + "inode_used": inodes["used"], + "inode_total": inodes["total"], }, } diff --git a/YakPanel-server/backend/app/api/firewall.py b/YakPanel-server/backend/app/api/firewall.py index 36135669..2e6628f7 100644 --- a/YakPanel-server/backend/app/api/firewall.py +++ b/YakPanel-server/backend/app/api/firewall.py @@ -20,6 +20,38 @@ class CreateFirewallRuleRequest(BaseModel): ps: str = "" +@router.get("/status") +async def firewall_backend_status(current_user: User = Depends(get_current_user)): + """UFW and firewalld presence/state for the Security UI (read-only).""" + ufw_out, _ = exec_shell_sync("ufw status 2>/dev/null", timeout=5) + ufw_text = (ufw_out or "").strip() + ufw_detected = bool(ufw_text) and "Status:" in ufw_text + ufw_active: bool | None = None + if ufw_detected: + if "Status: active" in ufw_text: + ufw_active = True + elif "Status: inactive" in ufw_text: + ufw_active = False + + fw_state_out, _ = exec_shell_sync("firewall-cmd --state 2>/dev/null", timeout=5) + fw_line = (fw_state_out or "").strip().lower() + firewalld_running = fw_line == "running" + firewalld_detected = fw_line in ("running", "not running") + + return { + "ufw": { + "detected": ufw_detected, + "active": ufw_active, + "summary_line": ufw_text.split("\n")[0] if ufw_text else "", + }, + "firewalld": { + "detected": firewalld_detected, + "running": firewalld_running, + "state": fw_line or None, + }, + } + + @router.get("/list") async def firewall_list( current_user: User = Depends(get_current_user), diff --git a/YakPanel-server/backend/app/services/__pycache__/site_service.cpython-314.pyc b/YakPanel-server/backend/app/services/__pycache__/site_service.cpython-314.pyc index 95af3f2f5c9983d041c2cd286f3a144e7d3f3c68..67f5c97b8e0b9dfca60e6328db1ef908d1d78e2e 100644 GIT binary patch delta 1509 zcma)6ZA=_R7@pa^-TUH}JGpXPIb@OYaZveap+YI2^*BfgEnD51aL__QIC324>|I)^ zk(1VP)R02Pq{ol8VEob8CPLO&O@M?zTiYL%1QO4x(fGr^1xC~V`G{)**x<+ z@4WB4@4UOSH$Pw>PO?J2U@`zQ#=lte-aj)TqzHQ$06IVzNC#~oSUy|Q248+v%CZ3| z+Xj4Q9}MP6E-8oT+_=t{oJ8eO72GY=;{})s-S`2lc9j$R{%w1S)F4$5bs!#7Dpe9y zwY|1aswTW%OscyoQX=SY$ysir|1t7%K3jn+o!8M~+d@p3Q>jOGjpA)aNhXud^4ygcmYQl>?#9LCx{=?GA z2@mlA{3nN&=IGJT9K^#$JT(75)6nxWwE&<8F9L~`SxGBv3YkoxCHprGYeCb)5R*=N z6VQ1_$OF>^>dvBCo$kR0an8;sAPL=VrPfn-;ES!K{I-vbPkPD(2)+HMhYWTULfEU5Tpi<8b~Vus--fELLG&J z6tr~VPYQs_$CO>Bu$O|1fYXS6r<{Z2{va3i+D=Jxpe_pa)aIt}h{|CKHAE`(>&zCP z4n*1w9#|B`cf7+#4t-n+_4jw6x6ujW93uBq@nXwZGKPcs`kjujJC>RjZoox@o}sLw zRcrBd>lC~se<)A4M)WJz;vW-JzD=-xFJw)5SB0Vo8#4-#(_@v<#x-MBEFtM)^JsI- zoE(K$`AI%zwnxjSS}q@mnNy=juO6E`Hr2YCR=lp&ID}20F(s}WfM6L<8EcF7ePhav zC0H*W9zFateCYY{X;0!e(twb(o()XNcAMZLgl<-|$kbVhSW1R&o?o(MQ(kFx>-c3$Z5|P~VJ%ChI|d%oNyoWOxPQ@x zzdU~)PN+2_cQ}sz4JOotv6nRPhFUmr*914!tt;;`+yGg)p}sS{N6T1Qb>>q?yAyB-bF2LTQIAWvtL74t$Nkl zIw<3!xpLTx`{n{9?7O+wcrOV+eYkYK5)NWzzRc*OhJFfu{Q10xdymj+)wZyUWA~%8 zYSmH;%%Ei>S|XxFA(|9XKa@f`!fzP@ZXj1dcYfbt8zzkHV?Kf*_4_|h%$Z>^ts A-2eap delta 412 zcmXYtJxD@P6vyv*Pb-t8O{IcL)SlK-RzCaCzUMBDB{uY7N##|s$sC%D%D6!`wp6r4 z-9c%HKr5`EhNc1`Xm4%oJn3>d{Ql?s;2!SJv7FzLO>R>|J;5_5-ox4CfyrWWNQBIj zG@&DA68Bzt=Adho+RcPI%p}qnfw+r~Q77h(|Lmp;CYP4tophYvLK95z5Ads#RKln6?}ZC=^khlE($d(`VgKuALP4%>B*6?3_A z1AGeSX-d*ZaF7)j=iRl^uOup$PN{}r3)y(x0w4=TgmkdZGx-T%{4Ku>QLYwx^qVNj zB3iIZU=v3-4|U+f*ETY4fu<+=am2Z{WAjwZkUSnA6uV`hJf0t&B zDMXu0NbFAHfy02 int: return result.scalar() or 0 +async def ssl_alert_summary(db: AsyncSession) -> dict: + """Sites with LE certs expiring soon or expired (for dashboard banners).""" + result = await db.execute(select(Site).order_by(Site.id)) + sites = result.scalars().all() + expired: list[dict] = [] + expiring: list[dict] = [] + for s in sites: + domain_result = await db.execute(select(Domain).where(Domain.pid == s.id).order_by(Domain.id)) + domain_rows = domain_result.scalars().all() + hostnames = [d.name for d in domain_rows] + if not hostnames: + continue + ssl = _best_ssl_for_hostnames(hostnames) + if ssl["status"] == "expired": + expired.append({ + "site": s.name, + "primary": hostnames[0], + "days_left": ssl.get("days_left"), + }) + elif ssl["status"] == "expiring": + expiring.append({ + "site": s.name, + "primary": hostnames[0], + "days_left": ssl.get("days_left"), + }) + return {"expired": expired, "expiring": expiring} + + async def get_site_with_domains(db: AsyncSession, site_id: int) -> dict | None: """Get site with domain list for editing.""" result = await db.execute(select(Site).where(Site.id == site_id)) diff --git a/YakPanel-server/docs/FEATURE-PARITY.md b/YakPanel-server/docs/FEATURE-PARITY.md index d4cc4d05..0d3311f8 100644 --- a/YakPanel-server/docs/FEATURE-PARITY.md +++ b/YakPanel-server/docs/FEATURE-PARITY.md @@ -17,7 +17,9 @@ Internal checklist against common hosting-panel capabilities used as a product r | FTP logs (tail) | Done | `GET /ftp/logs` | | Cron job templates (YakPanel JSON) | Done | `GET /crontab/templates`, `data/cron_templates.json` | | Backup plans + optional S3 upload | Done | `backup.py`, `BackupPlan` S3 fields, `boto3` optional | -| Database / FTP / firewall / monitor | Partial (pre-existing) | respective `api/*.py` | +| Dashboard SSL expiry / inode alerts | Done | `GET /dashboard/stats` โ†’ `ssl_alerts`, `system.inode_*` | +| Firewall UFW + firewalld status in UI | Done | `GET /firewall/status`, `FirewallPage.tsx` | +| Database / FTP / firewall rules engine | Partial (pre-existing) | respective `api/*.py` | | Mail server | Not planned | โ€” | | WordPress one-click | Not planned | plugin later | diff --git a/YakPanel-server/frontend/src/api/client.ts b/YakPanel-server/frontend/src/api/client.ts index 08ca6e83..2f43f5b3 100644 --- a/YakPanel-server/frontend/src/api/client.ts +++ b/YakPanel-server/frontend/src/api/client.ts @@ -523,11 +523,14 @@ export async function updateDatabasePassword(dbId: number, password: string) { }) } +export type SslAlertSite = { site: string; primary: string; days_left: number | null | undefined } + export async function getDashboardStats() { return apiRequest<{ site_count: number ftp_count: number database_count: number + ssl_alerts: { expired: SslAlertSite[]; expiring: SslAlertSite[] } system: { cpu_percent: number memory_percent: number @@ -536,10 +539,20 @@ export async function getDashboardStats() { disk_percent: number disk_used_gb: number disk_total_gb: number + inode_percent: number | null + inode_used: number | null + inode_total: number | null } }>('/dashboard/stats') } +export async function getFirewallBackendStatus() { + return apiRequest<{ + ufw: { detected: boolean; active: boolean | null; summary_line: string } + firewalld: { detected: boolean; running: boolean; state: string | null } + }>('/firewall/status') +} + export async function applyCrontab() { return apiRequest<{ status: boolean; msg: string; count: number }>('/crontab/apply', { method: 'POST' }) } diff --git a/YakPanel-server/frontend/src/pages/DashboardPage.tsx b/YakPanel-server/frontend/src/pages/DashboardPage.tsx index 5cb066ec..51ef8db8 100644 --- a/YakPanel-server/frontend/src/pages/DashboardPage.tsx +++ b/YakPanel-server/frontend/src/pages/DashboardPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' import { getDashboardStats } from '../api/client' import { PageHeader, AdminCard, AdminAlert } from '../components/admin' @@ -6,6 +7,7 @@ interface Stats { site_count: number ftp_count: number database_count: number + ssl_alerts?: { expired: { site: string; primary: string; days_left?: number | null }[]; expiring: { site: string; primary: string; days_left?: number | null }[] } system: { cpu_percent: number memory_percent: number @@ -14,6 +16,9 @@ interface Stats { disk_percent: number disk_used_gb: number disk_total_gb: number + inode_percent?: number | null + inode_used?: number | null + inode_total?: number | null } } @@ -47,9 +52,39 @@ export function DashboardPage() { ) } + const ssl = stats.ssl_alerts || { expired: [], expiring: [] } + const inodePct = stats.system.inode_percent + const inodeWarn = typeof inodePct === 'number' && inodePct >= 85 + return ( <> + + {ssl.expired.length > 0 ? ( + + Expired certificates:{' '} + {ssl.expired.map((s) => `${s.site} (${s.primary})`).join('; ')}.{' '} + + Renew in Domains & SSL + + + ) : null} + {ssl.expiring.length > 0 ? ( + + Certificates expiring within 14 days:{' '} + {ssl.expiring.map((s) => `${s.site} (${s.days_left ?? '?'}d)`).join('; ')}.{' '} + + Open Domains & SSL + + + ) : null} + {inodeWarn ? ( + + Root filesystem inode usage is high ({inodePct}%). Large numbers of small files can exhaust inodes before + disk space โ€” consider cleanup or archiving. + + ) : null} +
@@ -78,7 +113,11 @@ export function DashboardPage() { iconClass="ti ti-database-export" title="Disk" value={`${stats.system.disk_percent}%`} - subtitle={`${stats.system.disk_used_gb} / ${stats.system.disk_total_gb} GB`} + subtitle={ + typeof stats.system.inode_percent === 'number' + ? `${stats.system.disk_used_gb} / ${stats.system.disk_total_gb} GB ยท inodes ${stats.system.inode_percent}%` + : `${stats.system.disk_used_gb} / ${stats.system.disk_total_gb} GB` + } />
diff --git a/YakPanel-server/frontend/src/pages/FirewallPage.tsx b/YakPanel-server/frontend/src/pages/FirewallPage.tsx index 76f2a61a..10966469 100644 --- a/YakPanel-server/frontend/src/pages/FirewallPage.tsx +++ b/YakPanel-server/frontend/src/pages/FirewallPage.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import Modal from 'react-bootstrap/Modal' -import { apiRequest, applyFirewallRules } from '../api/client' +import { apiRequest, applyFirewallRules, getFirewallBackendStatus } from '../api/client' import { PageHeader, AdminButton, AdminAlert, AdminTable, EmptyState } from '../components/admin' interface FirewallRule { @@ -11,6 +11,11 @@ interface FirewallRule { ps: string } +interface FirewallBackendStatus { + ufw: { detected: boolean; active: boolean | null; summary_line: string } + firewalld: { detected: boolean; running: boolean; state: string | null } +} + export function FirewallPage() { const [rules, setRules] = useState([]) const [loading, setLoading] = useState(true) @@ -19,11 +24,18 @@ export function FirewallPage() { const [creating, setCreating] = useState(false) const [creatingError, setCreatingError] = useState('') const [applying, setApplying] = useState(false) + const [backend, setBackend] = useState(null) const loadRules = () => { setLoading(true) - apiRequest('/firewall/list') - .then(setRules) + Promise.all([ + apiRequest('/firewall/list'), + getFirewallBackendStatus().catch(() => null), + ]) + .then(([list, st]) => { + setRules(list) + setBackend(st) + }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)) } @@ -107,6 +119,44 @@ export function FirewallPage() { {error ? {error} : null} + {backend ? ( +
+
+
Host firewall (live)
+
+
+ UFW + {!backend.ufw.detected ? ( + Not detected + ) : backend.ufw.active === true ? ( + Active + ) : backend.ufw.active === false ? ( + Inactive + ) : ( + Unknown + )} + {backend.ufw.summary_line ? ( + {backend.ufw.summary_line} + ) : null} +
+
+ firewalld + {!backend.firewalld.detected ? ( + Not detected + ) : backend.firewalld.running ? ( + Running + ) : ( + Not running + )} +
+
+

+ Panel "Apply to UFW" runs ufw only. If you use firewalld, sync rules there separately. +

+
+
+ ) : null} +
Rules are stored in the panel. Click "Apply to UFW" to run ufw allow/deny for each rule.