DEV Community

Roberto Luna
Roberto Luna

Posted on

Building a “Show Power Inline” Importer for Greenview’s AP Analysis Dashboard

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 inline CLI 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);
Enter fullscreen mode Exit fullscreen mode

Problems quickly surfaced:

  1. Performance – the browser had to download megabytes of text for each switch, freezing the UI.
  2. Security – exposing raw switch output in the API response leaked internal hostnames.
  3. 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
Enter fullscreen mode Exit fullscreen mode

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])
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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)