Skip to main content
Back to Projects
Ops Panel logo

Ops Panel

Internal System

Ops Panel architecture diagram Ops Panel 24 projenin durumu tek tabloda — tarama panelin dışında koşar Proje adları bu şemada maskelidir. Panel teşhis eder; düzeltmeyi ilgili repoda ben yaparım. ZAMANLAYICI GitHub Actions toplu tarama burada koşar sıklık sınırı ve zaman aşımı yok servis hesabı anahtarları Actions secret'larında Tarayıcı script domain ve SSL bitişi dağıtım durumu arama ve ölçüm bağlantıları 24 proje × birden çok çağrı Neon Postgres 9 tablo · 7 migration envanter · tarama · bulgu · öneri · görev bulgunun ne zamandan beri açık olduğu geçmişten okunur Panel — yalnız okur ve gösterir 11 sayfa: envanter, taramalar, bulgular, öneriler, görevler, yönetim proje-01 proje-02 proje-03 … proje-24 Panel kod değiştirmez, repo okumaz, sayfa yüklenirken canlı veri çekmez. Panel tarafındaki secret Yalnız tek proje için "şimdi tara". Toplu taramanın anahtarları buraya hiç girmez. Sızma yüzeyi ikiye bölünmüş olur. Neden iki ayrı secret kümesi Barındırma planı zamanlayıcıyı günde bir çalışmaya ve fonksiyonu on saniyeye sınırlıyor; 24 projelik toplu tarama bu sürede bitmiyor. Tarama dışarı taşınınca geniş yetkili anahtarlar da dışarıda kaldı: panelin ortamında yalnız tek proje sorgulayacak kadar yetki var. Bedeli, aynı anahtarın iki yerde yönetilmesi ve birinin unutulabilmesi. Ekranda görünen alan adları, trafik sayıları, kayıt firması ve DNS haritası bu görselde bilerek yer almıyor.
The status of 24 projects collected in one table. The bulk scan runs outside the panel, inside GitHub Actions; broadly scoped service-account keys live only in Actions secrets. Results are written to nine tables on Neon Postgres, and the panel's 11 pages only read that data — it changes no code, reads no repository and fetches nothing live on page load. The panel's own secret carries permission for a single-project 'scan now' and nothing more.

An internal control panel that watches the 24 projects I run from a single table. Domain and certificate expiry, deployment status and gaps in search and analytics wiring are collected in one place. The panel does not change code; it diagnoses, and I apply the fix in the project's own repository.

Technologies Used

  • Next.js 16
  • TypeScript
  • Drizzle ORM
  • Neon Postgres
  • Auth.js
  • GitHub Actions
  • Web Push (VAPID)

Key Features

  • The state of 24 projects in one table — inventory, scans, findings, recommendations and tasks
  • The bulk scan runs outside the panel, as a scheduled job
  • The panel does not change code, does not read repositories, does not fetch live data on page load
  • How long a finding has been open is read from history
  • Broadly-scoped keys never enter the panel's environment
  • Every finding comes with a copyable fix command

Development Time

1.5 months

Problem

When you run twenty-four separate projects and domains, what breaks is not the code but the visibility. A certificate is expiring, a deployment failed and stayed failed, analytics were never installed on one site — none of these show up as an error; one day somebody just happens to notice. The real part of the problem is not detection but duration: without knowing how long a gap has been there, you cannot decide which one to fix first. The second issue is that the monitoring tool is itself a source of risk: a set of keys that can reach twenty-four projects puts the question of where those keys live at the centre of the design.

Users

One user: me. That is a constraint which shaped the panel — no multi-user authorisation, invitation flow or role management was written. But there are two distinct moments of use: the morning look (what broke today) and the weekly look (which gap has been open the longest). The panel is divided along those two.

My Role

Single developer. The constraint analysis itself, data model, scanner script, panel interface, notification layer and deployment.

Architecture

The system is split in two, and that split is the centre of the design. The bulk scan does not run in the panel's environment but outside it, as a scheduled job: it queries twenty-four projects in turn and writes the result directly into Neon Postgres. The panel only reads. No request goes to an external service while a page loads; everything on screen is a scan result written earlier. The data model rests on five concepts: the project in the inventory, a scan run and an individual scan, the finding a scan produces, the recommendation derived from a finding, and the task a decision turns into. Because changes are written to a separate log, when a finding was opened and how long it has been standing can be read from history. When the panel shows a finding it offers a copyable command alongside it; the command runs in my terminal, not in the panel.

Key Decisions

  1. The bulk scan does not run in the panel's environment

    Two hard constraints drove this. First, the scheduler: on the hosting plan in use, a scheduled job runs at most once a day and the trigger moment is uncertain. Second, duration: on the same plan the function timeout is ten seconds, and a scan across twenty-four projects does not finish in that time. Moving the scan outside removed both — no frequency limit, no timeout.

    Trade-offThe system now lives in two environments and both configurations have to be correct together. A failure on the scan side does not appear as an error in the panel; the data simply is not refreshed, and noticing that requires looking at the scan runs themselves.

  2. Broadly-scoped keys never enter the panel's environment

    The split came out of the scheduler constraint, but its real value is security: the key set that reaches twenty-four projects lives where the scan runs, and the panel's environment holds only enough authority to query a single project. Compromising the panel does not mean access to the whole portfolio.

    Trade-offThe same key is managed in two places, and when one is rotated the other can be forgotten. There is no automated check catching this; the forgotten side only surfaces when the related feature stops working.

  3. The panel diagnoses; it does not change code

    The panel is a deployed application; it cannot reach a local filesystem and cannot read repositories. Rather than trying to work around that constraint, the scope was drawn to match it: the panel shows the problem and its context, puts a copyable command next to it, and I apply the fix in my own terminal. A tool that writes automatic fixes would have to work with the same assumptions across twenty-four different codebases.

    Trade-offThe fix step is not automated; the panel can say something is broken but only sees it fixed on the next scan. In between, the same finding remains on screen.

  4. Masked project names instead of real domains on screen

    The panel's inventory holds domain names, registrar and DNS details, traffic figures and contact records. Those belong to customers; an image shown in a portfolio cannot carry that information. In this case study the projects appear with numbered labels.

    Trade-offMasking weakens the sense of real use: the screenshot explains what the system does but does not show what a user sees. That cost is paid deliberately.

Stack Rationale

Next.js and Drizzle on the panel side, a plain Node script on the scan side — both share the same schema, because the single source of truth is the database. Neon is on the same serverless model as the deployment and is reachable from the externally-running scan job as well; no second copy of the inventory is kept. Using a CI pipeline as the scheduler is not a preference but a constraint: it was the only option without frequency and duration limits. Because there is a single user, no elaborate authorisation model was built. Web Push exists to shorten the time it takes to notice a finding — an alert lost in an inbox is not an alert.

Security

The panel is session-protected and route protection runs in the Node runtime. Sensitive inventory fields — registrar, DNS configuration, contact records — are visible only inside the panel and never enter any external output, screenshot or this case study. Broadly-scoped service account keys are not present in the panel's environment; it holds only enough authority to query a single project. Service account permissions are granted manually for each property — this cannot be automated, which also prevents permissions from quietly widening.

Performance

This panel is a closed system; because there is no date-stamped Lighthouse output, no page-speed score is written here. The structural measure is this: no request goes to an external service while a page loads, and every value on screen is read from the database. Because a scan across twenty-four projects did not fit in a ten-second function timeout, it was moved outside the panel; the only scan path left in the panel is the "scan now" endpoint, which runs for a single project and is measured in seconds.

Results

Active development
2026-07 → 2026-08 · 47 commits
TypeScript source files
87
Test files
11
Schema
9 tables · 7 migrations — project, scan, scan run, finding, recommendation, task, change log
Monitoring coverage
24 projects in a single inventory; no second copy of the inventory is kept
Routes
11 pages
Architectural guarantee
The panel does not write: it does not change code, read repositories, or fetch live data on page load
Architectural guarantee
The broadly-scoped keys of the bulk scan are not present in the panel's environment

What I Would Do Differently

I originally kept the inventory as YAML files in the repository and had written that I would not move it into a database; then I moved it and deleted the YAML side entirely. What I consider right is not the move but the deletion: when two copies of the same information exist, which one is correct eventually becomes an argument. The same lesson came out of Bergaz Operations — there, because four separate systems keep their own records, the same person exists as two entries in two places and matching is done by hand. The second point is the scan side: today I would have the scan write its own health into the database too. Right now the panel does not distinguish between "there is data" and "the data is fresh"; a failed scan run quietly leaves old data looking correct.

Internal system

Let's find where to start

Tell me what you want to build; I'll tell you up front how long it takes and what the sensible first step is.