new ui + search clients
build-and-push / build (push) Failing after 34s

This commit is contained in:
2026-08-01 13:34:07 +05:00
parent 2957347583
commit 4a7de76155
2 changed files with 42 additions and 5 deletions
+7 -1
View File
@@ -219,7 +219,7 @@ per-resource filters:
| Group check | `/radgroupcheck` | GET, POST, PUT, DELETE | `groupname`, `attribute` | | Group check | `/radgroupcheck` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
| Group reply | `/radgroupreply` | GET, POST, PUT, DELETE | `groupname`, `attribute` | | Group reply | `/radgroupreply` | GET, POST, PUT, DELETE | `groupname`, `attribute` |
| **VLANs** | `/vlan` | see below | — | | **VLANs** | `/vlan` | see below | — |
| **Clients** | `/client` | see below | | | **Clients** | `/client` | see below | `search`, `status` |
| **Devices** | `/device` | GET (list), PUT (alias) | — | | **Devices** | `/device` | GET (list), PUT (alias) | — |
| User↔group | `/radusergroup` | GET, POST, PUT, DELETE | `username`, `groupname` | | User↔group | `/radusergroup` | GET, POST, PUT, DELETE | `username`, `groupname` |
| Accounting | `/radacct` | GET (read-only) | `username`, `nasipaddress`, `active` | | Accounting | `/radacct` | GET (read-only) | `username`, `nasipaddress`, `active` |
@@ -263,6 +263,7 @@ at client creation.
| Action | Request | | Action | Request |
|------------------|-----------------------------------------------------| |------------------|-----------------------------------------------------|
| List clients | `GET /client/` → `{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}` | | List clients | `GET /client/` → `{total,limit,offset,items:[{mac_address,group,status,name,phone,alias}]}` |
| Search / filter | `GET /client/?search=<term>&status=<new\|paid\|unpaid>` |
| Get one client | `GET /client/{mac_address}` | | Get one client | `GET /client/{mac_address}` |
| Add a client | `POST /client/add` body `{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}` | | Add a client | `POST /client/add` body `{"mac_address":"14-99-3E-74-CB-7F","group":"staff","name":"Ali Hassan","phone":"7712345","alias":"Living Room TV"}` |
| Edit a client | `POST /client/edit` body `{"mac_address":"...", group?, status?, name?, phone?, alias?}` | | Edit a client | `POST /client/edit` body `{"mac_address":"...", group?, status?, name?, phone?, alias?}` |
@@ -283,6 +284,11 @@ response.
`unpaid`. In the DB the group is stored in the `radusergroup.groupname` column, `unpaid`. In the DB the group is stored in the `radusergroup.groupname` column,
`status` in `customers.status`, and `name`/`phone`/`alias` in `radadmin_clients`. `status` in `customers.status`, and `name`/`phone`/`alias` in `radadmin_clients`.
- Adding a client whose MAC already exists → `409`. - Adding a client whose MAC already exists → `409`.
- **List** accepts two optional filters (both applied before pagination, so
`total` reflects the filtered set): `search` is a case-insensitive substring
matched across `mac_address`, `name`, `phone`, and `alias`; `status` is an exact
match on `new` / `paid` / `unpaid`. Combine them freely, e.g.
`GET /client/?search=ali&status=unpaid`.
- **Import** validates each row with the same rules as `add`; with `dry_run:true` - **Import** validates each row with the same rules as `add`; with `dry_run:true`
nothing is written and it returns `{total, valid, created:0, dry_run, errors:[{row, nothing is written and it returns `{total, valid, created:0, dry_run, errors:[{row,
mac, detail}]}` for a preview. With `dry_run:false` the valid rows are inserted mac, detail}]}` for a preview. With `dry_run:false` the valid rows are inserted
+35 -4
View File
@@ -11,7 +11,7 @@ This router hides that fan-out behind mac_address + group + status.
""" """
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy import delete, func, select, update from sqlalchemy import delete, func, or_, select, update
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
@@ -55,8 +55,18 @@ def _stage_client(db: Session, cli: ClientCreate) -> None:
@router.get("/", response_model=Page[ClientOut]) @router.get("/", response_model=Page[ClientOut])
def list_clients(page: PageParams = Depends(), db: Session = Depends(get_db)): def list_clients(
"""List clients — MAC, group and status, joined from customers + radusergroup.""" page: PageParams = Depends(),
search: str | None = None,
status: str | None = None,
db: Session = Depends(get_db),
):
"""List clients — MAC, group and status, joined from customers + radusergroup.
Optional filters:
- ``search`` case-insensitive substring match across MAC, name, phone, alias.
- ``status`` exact match on billing status (new/paid/unpaid).
"""
base = ( base = (
select( select(
Customer.mac_address, RadUserGroup.groupname, Customer.status, Customer.mac_address, RadUserGroup.groupname, Customer.status,
@@ -65,7 +75,28 @@ def list_clients(page: PageParams = Depends(), db: Session = Depends(get_db)):
.outerjoin(RadUserGroup, RadUserGroup.username == Customer.username) .outerjoin(RadUserGroup, RadUserGroup.username == Customer.username)
.outerjoin(RadadminClient, RadadminClient.mac_address == Customer.username) .outerjoin(RadadminClient, RadadminClient.mac_address == Customer.username)
) )
total = db.execute(select(func.count()).select_from(Customer)).scalar_one() # Count over the same joins so filtered totals drive pagination correctly.
count_stmt = (
select(func.count())
.select_from(Customer)
.outerjoin(RadadminClient, RadadminClient.mac_address == Customer.username)
)
if search:
term = f"%{search.strip()}%"
cond = or_(
Customer.mac_address.ilike(term),
RadadminClient.name.ilike(term),
RadadminClient.phone.ilike(term),
RadadminClient.alias.ilike(term),
)
base = base.where(cond)
count_stmt = count_stmt.where(cond)
if status:
base = base.where(Customer.status == status)
count_stmt = count_stmt.where(Customer.status == status)
total = db.execute(count_stmt).scalar_one()
rows = db.execute(base.order_by(Customer.id.desc()).limit(page.limit).offset(page.offset)).all() rows = db.execute(base.order_by(Customer.id.desc()).limit(page.limit).offset(page.offset)).all()
items = [ items = [
ClientOut(mac_address=mac, group=gn, status=st, name=nm, phone=ph, alias=al) ClientOut(mac_address=mac, group=gn, status=st, name=nm, phone=ph, alias=al)