Building a “Show Power Inline” Importer for Greenview’s AP Analysis Dashboard
TL;DR: I added a full‑stack PoE import pipeline that parses Cisco show power inline output, stores normalized modules/ports in Prisma, and wires it to a new API route and UI tab. The change lets the dashboard calculate per‑switch power budgets and cross‑reference them with AP deployments and reboot events.
The Problem
The AP analysis dashboard needed to answer a simple question: How much PoE capacity do we have left on each switch after allocating power to the deployed Aruba APs?
- Engineers were manually copying the
show power inlineCLI output from each Catalyst switch, pasting it into a spreadsheet, and then eyeballing the numbers. - The UI had no way to ingest that raw text, so we couldn’t correlate the data with the AP inventory stored in our Prisma DB.
- When we tried to fetch the data directly from the switch via HTTPS from Vercel, we hit a 403 Forbidden because the switch expected a browser‑like User‑Agent header.
Result: a noisy, error‑prone workflow that didn’t scale beyond a handful of switches.
What I Tried First
My first attempt was to store the raw CLI dump as a string in the Switch model and parse it on the client side with a giant regex in the React component.
// src/features/ap-analysis/ApAnalysisView.tsx (initial hack)
const raw = switch.poeDump; // huge string
const matches = raw.matchAll(/(\S+)\s+(\d+)\s+(\d+)%/g);
Problems quickly surfaced:
- Performance – the browser had to download megabytes of text for each switch, freezing the UI.
- Security – exposing raw switch output in the API response leaked internal hostnames.
- Maintainability – the regex broke on the next IOS‑XE version change.
I also tried hitting the switch API from the Vercel edge runtime, but the request was rejected with:
GET https://10.0.1.12/api/show-power-inline 403
User-Agent: Vercel
Adding a custom User-Agent header solved the 403, but the code was still tangled with UI logic.
The Implementation
1. Extend the Prisma schema
I added a lightweight model to store only the parsed data. The raw dump stays out of the DB.
// prisma/schema.prisma
model PoeModule {
id String @id @default(uuid())
switchName String
modules PoePort[]
createdAt DateTime @default(now())
}
model PoePort {
id String @id @default(uuid())
moduleId String
port String
powerW Int
allocatedW Int?
module PoeModule @relation(fields: [moduleId], references: [id])
}
The comment added in the diff clarifies the intent:
/// Captura de `show power inline` de un switch (pegada desde la UI). Solo se
/// guardan los datos interpretados (módulo, puerto, potencia)
2. Pure parser in src/lib/poe-parse.ts
I wrote a pure TypeScript parser that can handle one or many concatenated switch outputs. It extracts the switch prompt (e.g., UHPCM-SW-01) and then parses each “module – port – power” line.
// src/lib/poe-parse.ts
/**
* Parser de la salida de `show power inline` (Cisco IOS/IOS-XE, pilas Catalyst).
* Acepta uno o varios switches pegados juntos: el nombre se toma del prompt
* («UHPCM-SW-01#»).
*/
export interface PoePort {
port: string;
powerW: number;
allocatedW?: number;
}
export interface PoeModule {
switchName: string;
ports: PoePort[];
}
/** Normaliza el número de puerto (e.g., Gi1/0/1 → Gi1/0/1) */
export function normalizePort(port: string): string {
return port.trim().replace(/\s+/g, " ");
}
/** Main parser */
export function parseShowPowerInline(raw: string): PoeModule[] {
const modules: PoeModule[] = [];
const sections = raw.split("\n\n"); // blank line separates switches
for (const sec of sections) {
const lines = sec.split("\n").filter(Boolean);
const prompt = lines[0].match(/^(\S+?)#/)?.[1];
if (!prompt) continue;
const ports: PoePort[] = [];
for (const line of lines.slice(1)) {
// Example line: "Gi1/0/1 15.4 30.0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"
const [port, power] = line.split(/\s+/);
if (!port || !power) continue;
ports.push({ port: normalizePort(port), powerW: Math.round(parseFloat(power) * 1000) });
}
modules.push({ switchName: prompt, ports });
}
return modules;
}
3. Service layer – src/services/poe.service.ts
The service isolates DB interaction and keeps the parser pure.
// src/services/poe.service.ts
import { prisma } from "@/lib/prisma";
import { parseShowPowerInline, PoeModule } from "@/lib/poe-parse";
export async function importPoe(raw: string): Promise<void> {
const modules = parseShowPowerInline(raw);
for (const mod of modules) {
const dbMod = await prisma.poeModule.create({
data: {
switchName: mod.switchName,
},
});
const ports = mod.ports.map(p => ({
port: p.port,
powerW: p.powerW,
moduleId: dbMod.id,
}));
await prisma.poePort.createMany({ data: ports });
}
}
/** Simple report for the UI – total budget vs allocated */
export async function getPoeReport(switchName: string) {
const mod = await prisma.poeModule.findUnique({
where: { switchName },
include: { modules: true },
});
if (!mod) return null;
const total = mod.modules.reduce((sum, p) => sum + p.powerW, 0);
const allocated = mod.modules.reduce((sum, p) => sum + (p.allocatedW ?? 0), 0);
return { total, allocated, free: total - allocated };
}
4. API route – src/app/api/poe/route.ts
A force‑dynamic route (required
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/greenview · 2026-10-17
#playadev #buildinpublic
Top comments (0)