A futuristic engineering operations dashboard. Pulls projects, GitHub repositories, issues, pull requests, deployments, monitoring, alerts, and infrastructure health into one place.
Source of truth for product scope: nexus_control_center_roadmap.md.
Visual target: specs/visual-reference.md.
- Backend β Laravel 13 (PHP 8.4+), Eloquent, Horizon, Reverb (websockets), domain-driven layout under
app/Domain/{BoundedContext}/{Actions|Jobs|Queries|Services|...}. - Frontend β Vue 3 + Inertia v2 + TypeScript + Tailwind CSS. Single-page navigation; no API client layer (Inertia handles it).
- Tests β PHPUnit feature tests with
RefreshDatabase,Http::fake(),Queue::fake(). CI runs Pint +php artisan test+npm run build. - Infrastructure β SQLite for dev, MySQL/PostgreSQL for prod. Redis for queues + cache. GitHub App for OAuth + webhooks.
Status legend: β¬ not started Β· π‘ in progress Β· π’ done Β· π΄ blocked
| # | Phase | Status | Notes |
|---|---|---|---|
| 0 | Foundation (auth, layout, static overview) | π’ | 9/9 specs done. |
| 1 | Projects & Repositories | π’ | 3/3 specs done. |
| 2 | GitHub Integration MVP | π’ | 4/4 specs done β connection, repository import + sync, issues sync, PRs + unified Work Items page. |
| 3 | GitHub Webhooks & Activity Feed | π’ | 3/3 specs done (017β019). Phase complete. |
| 4 | Deployments & CI/CD | π’ | 3/3 specs done (020β022) β workflow runs storage + sync, cross-repo timeline UI with realtime, Overview success-rate widget. |
| 5 | Website Monitoring | π’ | 3/3 specs done (023β025) β monitor MVP + manual probe, scheduled checks + uptime calc + activity events, Overview uptime KPI + Reverb live updates + perf chart. |
| 6 | Docker Host Agent MVP | β¬ | β |
| 7 | Alerts Engine | β¬ | β |
| 8 | Analytics & Health Scores | β¬ | β |
| 9 | Polish & Production Readiness | β¬ | β |
| 10 | Future Innovation | β¬ | β |
Detailed per-spec status lives in specs/README.md. Each spec is one GitHub issue + one branch + one PR β see .claude/skills/nexus-spec-workflow for the workflow.
After Phase 2:
- Sign in, register, verify email.
- Create projects with color/icon. Manually link a GitHub repository or import via the connected GitHub account.
- Imported repos auto-sync metadata (description, branches, language, stars/forks, last push) and mirror their issues + pull requests into local tables.
- Per-Repository tabs for Overview / Issues / Pull Requests with state badges, branch names, comment counts, and external links to GitHub.
- A unified
/work-itemsqueue across all your imported repos, filterable by kind / state / repository / free-text search. - Settings page surfaces the GitHub connection (encrypted token storage, scope display, Reconnect CTA when expired) and a per-user "N repositories linked, last sync β¦" indicator.
- Manual "Run sync" buttons everywhere a sync job exists.
- Controller flash messages (
->with('status'|'error', β¦)) render as a dismissable top banner inAppLayout, so failed actions (OAuth callbacks, sync triggers) surface to the user instead of failing silently. Silent OAuth callback branches alsoLog::warningfor postmortem.
After Phase 3 (complete):
- Spec 017 (done) β GitHub webhook ingestion endpoint at
POST /webhooks/github. VerifiesX-Hub-Signature-256(HMAC-SHA-256, timing-safe), stores deliveries idempotently, dispatches an async job, routes to per-event handlers.issuesandpull_requestevents update the local mirrors and createactivity_eventsrows. - Spec 018 (done) β Activity Feed UI.
RecentActivityForUserQuerypowers a sharedactivity.recentInertia prop registered inHandleInertiaRequests::share(), so every authenticated page lights up the right rail with the latest events without per-controller plumbing. New/activitypage (linked from the sidebar between Alerts and Settings) shows up to 100 events. - Spec 019 (done) β Real-time broadcasting via Reverb.
CreateActivityEventActiondispatchesActivityEventCreated(aShouldBroadcastNowevent on a privateusers.{id}.activitychannel) every time a row lands. Echo + Pusher are wired inbootstrap.ts; theuseActivityFeedcomposable (resources/js/lib/) seeds from the shared prop and prepends broadcast events into the rail and the/activitypage in real time. Three new webhook handlers (workflow_run,push,release) extend the spec-017 ingestion: workflow runs surface asworkflow.{succeeded,failed}, releases asrelease.published, and pushes silently updaterepositories.last_pushed_at(no activity row β too noisy). When the websocket isn't connected the rail and the activity page show a small "Live updates offline" pill; page-load reads still surface the latest data.
After Phase 4 (complete):
- Spec 020 (done) β Workflow runs storage + sync.
workflow_runstable with FK torepositoriesand the same six-column sync pattern as issues / PRs.SyncRepositoryWorkflowRunsActionupserts on(repository_id, github_id); the matchingJobchains offSyncGitHubRepositoryJobso importing a repo backfills its run history. Per-repo Workflow Runs tab on the show page lists status, conclusion, branch, run number, actor, started-at; rows link out to the GitHub Actions run. Spec 019'sWorkflowRunWebhookHandlernow upserts into the new table for everyworkflow_rundelivery (queued / in_progress / completed) so the timeline reflects in-flight states. - Spec 021 (done) β Deployment timeline UI. New
/deploymentspage renders the cross-repo workflow runs as a chronological timeline. URL-bound filters (project / repository / status / conclusion / branch) survive reload; the repository dropdown narrows client-side based on the selected project. Per-run detail drawer (Teleportoverlay, slide-from-right, Esc / backdrop / close-button dismiss with focus restoration) shows head SHA, duration, actor, conclusion, and a CTA out to GitHub. Real-time updates via a newWorkflowRunUpsertedevent broadcast on a privateusers.{id}.deploymentschannel β fires from the webhook handler upsert path (not from bulk REST sync, which would flood the channel). SidebarDeploymentsentry replaces the Phase 4 placeholder;Pipelinesstays disabled as a future filter view. - Spec 022 (done) β Overview success-rate widget.
GetOverviewDashboardQuery::deploymentsKpi()aggregatesworkflow_runsover the last 24h (vs the prior 24h) for the headline numbers plus a 12-day daily completed-run sparkline. Returnssuccessful_24h(primary value),success_rate_24h(integer percent or null when no completed runs landed),change_percent(capped[-100, +999]),sparkline, and a status enum. Window keys onrun_completed_at(notrun_started_at) so long-running jobs land in the bucket where they actually completed. The Overview's Deployments KPI card secondary line now reads92% success(orβ% successfor empty windows) instead of the static "Successful" placeholder.
After Phase 5 (complete):
- Spec 023 (done) β Website monitor MVP.
websites+website_checkstables withWebsiteStatus(pending|up|down|slow|error) andWebsiteCheckStatusenums.RunWebsiteProbeActionis a pure HTTP probe (no DB writes) classifying the response into up / slow (>3000ms hard threshold) / down / error;RecordWebsiteCheckActionpersists the check + updatesWebsite.{status,last_checked_at,last_success_at,last_failure_at}. CRUD pages live under/monitoring/websites/*; per-site show page hosts a manual "Probe now" button (sync request β instant feedback β€timeout_ms). SidebarMonitoringentry replaces the Phase 5 placeholder. - Spec 024 (done) β Scheduled checks + uptime calc + activity events.
DispatchDueWebsiteChecksJobruns every minute (Schedule::job(...)->everyMinute()->withoutOverlapping()inroutes/console.php), filters due websites in PHP (cross-DB compat), and dispatchesRunWebsiteCheckJobper row. The per-website job reuses the spec-023 actions so manual probes and scheduled probes never drift.RecordWebsiteCheckActiondetects healthyβfailed category transitions and emitswebsite.down/website.upactivity events on swings (steady-state runs stay silent). Spec 019'sActivityEventCreated::broadcastOn()was extended to resolve recipient channels forsource: monitoringrows viametadata.website_id β website β project β owner_user_idso monitoring incidents broadcast in realtime.GetWebsitePerformanceSummaryQueryreturns count-based uptime % over 24h / 7d / 30d windows + last-incident timestamp; the show page renders a 4-tile uptime stats strip.RecentActivityForUserQueryextended to surface monitoring events alongside repo events on the right rail. - Spec 025 (done) β Overview uptime KPI + Reverb live updates + perf chart.
GetMonitoringUptimeKpiQueryaggregateswebsite_checksvolume-weighted across all the user's monitors over 24h (vs prior 24h) plus a 12-day daily sparkline; replaces the long-standingMOCK_KPIS['uptime']on Overview. Empty 24h window β null overall + muted status; status thresholds at 99 / 95. NewWebsiteCheckRecordedevent (ShouldBroadcastNow, pre-resolved owner id,users.{id}.monitoringchannel, light-weight{check_id, website_id}pulse) fires fromRecordWebsiteCheckActionon every persisted check (steady-state runs included; transition events stay separate). The website show page subscribes via Echo, filters client-side bywebsite_id, and partial-reloadswebsite + summary + checkson each pulse. Response-timeSparklineof the last 50response_time_msvalues renders in the recent-checks card with leading-null skip + carry-forward fill;<2 data pointsrenders a "not enough data" placeholder.
# 1. Install backend + frontend deps
composer install
npm install
# 2. Set up the env + key + db + storage
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate --seed
php artisan storage:link
# 3. Run the dev stack (Laravel + Vite + queue worker + scheduler)
composer devcomposer dev runs Laravel, Vite, the queue worker, and the scheduler in parallel via concurrently. For real-time/websocket testing, use composer dev:horizon (also runs Reverb + Horizon). The scheduler tick is what drives spec 024's website-monitor probes β without composer dev (or a real cron in prod), DispatchDueWebsiteChecksJob never fires and monitors stay on whatever state the last manual "Probe now" left them in.
Phase 2's repository sync and Phase 3's webhooks both need a GitHub-side app registered against your account/org. Without GITHUB_CLIENT_ID set, the "Connect GitHub" button on /settings redirects to GitHub with an empty client_id and GitHub itself returns a 404 β that's the canonical "I tried to connect and got a 404" symptom.
Two options β either works for spec 013's OAuth flow:
- GitHub OAuth App (simpler β fine until you start spec 017's webhooks): https://github.com/settings/developers β "New OAuth App".
- GitHub App (use this if you also want Phase 3 webhooks; one app covers both flows): https://github.com/settings/apps β "New GitHub App".
Settings either way:
| Field | Value |
|---|---|
| Application / GitHub App name | Nexus Control Center (dev) (any name; per-developer) |
| Homepage URL | http://localhost:8000 |
| Authorization callback URL | http://localhost:8000/integrations/github/callback |
| Webhook URL (GitHub App only, optional pre-Phase-3) | https://<ngrok>.ngrok.io/webhooks/github |
| Webhook secret (GitHub App only) | a random string β must match GITHUB_WEBHOOK_SECRET in .env |
| Permissions (GitHub App only) | Repository: read for Metadata, Issues, Pull requests. Account: read for Email addresses (optional). |
GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GITHUB_OAUTH_REDIRECT_URI=http://localhost:8000/integrations/github/callback
GITHUB_WEBHOOK_SECRET= # set this when you start spec 017
If you previously had Laravel running, restart it so config picks up the new env:
php artisan config:clear
php artisan serveGitHub validates the OAuth callback host exactly. If your GITHUB_OAUTH_REDIRECT_URI says localhost but you browse via http://127.0.0.1:8000, the OAuth handshake will fail. Pick one and stick with it across the env value, the GitHub App's callback URL, and the URL you use in your browser.
Spec 017's webhook ingestion needs GitHub to be able to reach your dev server. Point an ngrok (or Cloudflare Tunnel) tunnel at port 8000 and set the GitHub App's Webhook URL to https://<your-tunnel>/webhooks/github. The X-Hub-Signature-256 header is verified against GITHUB_WEBHOOK_SECRET β if that doesn't match the value configured on the App, every delivery 401's and never lands in the database.
Sometimes you want to browse the running app from a public URL β to demo the dashboard, hit it from another device, or run an OAuth callback that GitHub can reach. composer run dev boots two long-running HTTP servers β Laravel on :8000 and Vite on :5173 β and the browser needs to talk to both. Tunneling only port 8000 will load the HTML but every Vite asset (@vite/client, app.ts, β¦) will 404 / CORS-fail because the page tries to fetch them from localhost:5173.
Set up two cloudflared tunnels and point Vite at its public URL:
# Terminal 1 β Laravel
cloudflared tunnel --url http://localhost:8000
# β https://<random>.trycloudflare.com (call this URL_A)
# Terminal 2 β Vite
cloudflared tunnel --url http://localhost:5173
# β https://<random>.trycloudflare.com (call this URL_B)Then update .env:
APP_URL=https://URL_A.trycloudflare.com
VITE_DEV_SERVER_URL=https://URL_B.trycloudflare.comβ¦and restart composer run dev. Vite reads VITE_DEV_SERVER_URL once at boot; vite.config.js flips into tunnel mode when it's set β binds 0.0.0.0, locks port 5173, allows cross-origin requests, and emits asset URLs at the public host so the browser can fetch them through the tunnel. You'll see a [vite] tunnel mode active β origin=β¦ line in the composer run dev output (prefixed vite:); if it's missing, Vite didn't pick up VITE_DEV_SERVER_URL and you'll see CORS / 403 errors in the browser.
Caveats with quick tunnels (cloudflared tunnel --url ...):
- The tunnel URL changes on every run. Re-edit
.envand restartcomposer run deveach time. - The OAuth callback URL configured in your GitHub App must match the new
APP_URL. Update the GitHub App settings every time the Laravel tunnel URL changes β or use a named tunnel bound to a stable hostname so the URL stays put. - HMR over
wss://*.trycloudflare.comcan drop intermittently β when it does, asset URLs are absolute so a manual refresh still serves the latest code. Named tunnels are more stable. - If port 5173 is already in use, set
VITE_DEV_SERVER_PORT=5174(or any free port) in.envand point your secondcloudflaredat the same port. - The default Laravel
APP_URL-derivedSESSION_DOMAIN/SANCTUM_STATEFUL_DOMAINSwon't include your tunnel hostname. If session-based requests start 419'ing through the tunnel, setSESSION_DOMAIN=(blank) and add the tunnel hostname toSANCTUM_STATEFUL_DOMAINS. - TLS terminates at the tunnel β
php artisan serveonly sees plain HTTP locally. Two things tame this:AppServiceProvider::boot()callsURL::forceScheme('https')wheneverAPP_URLstarts withhttps://so generated links don't trigger Mixed Content blocks. If you ever swap to a non-HTTPS tunnel, changeAPP_URLtohttp://...to disable the override.bootstrap/app.phptrusts loopback proxies (127.0.0.1,::1), so cloudflared'sX-Forwarded-Proto: httpsis honored. Without this, signed URLs (email verification, password reset) verify the signature against anhttp://URL while it was signed againsthttps://, and every click 403's "Invalid signature." Loopback-only trust is safe in prod too β anything that can connect from loopback already has direct app access.
- Reverb (websockets, port 8080) is wired by spec 019 β set up a third tunnel when you want the activity feed to update without page refresh:
Then in
cloudflared tunnel --url http://localhost:8080 # β https://<random>.trycloudflare.com (call this URL_C).envβ only theVITE_REVERB_*(browser-side) keys change; leave the server-sideREVERB_*keys on their localhost defaults because the Laravel process still talks to Reverb on the same machine:Restart# Browser β Reverb (over the tunnel) VITE_REVERB_HOST=URL_C.trycloudflare.com VITE_REVERB_SCHEME=https VITE_REVERB_PORT=443
composer run devand make surephp artisan reverb:startis running too βcomposer devdoesn't start it, so usecomposer dev:horizonor run it in another terminal. The[vite] tunnel mode activebanner prints the resolved env at boot. To verify it's wired end-to-end, openURL_A/activityand trigger a webhook event (or push a commit to a synced repo) β the row should prepend instantly. If it doesn't, the rail and/activitypage show a "Live updates offline" pill and page-load reads still surface the latest events. Named tunnels make this triple substantially less painful.
Local-only dev (no tunnel) is unaffected β leave VITE_DEV_SERVER_URL empty and Vite behaves exactly as before.
php artisan test # PHP feature + unit tests
./vendor/bin/pint # PHP formatter (CI gate)
npm run build # vue-tsc + Vite production build (CI gate)CI workflow: .github/workflows/ci.yml.
- Branches:
spec/NNN-<slug>for spec branches,chore/<slug>/fix/<slug>for everything else. Direct push tomainis blocked by branch protection. - One spec β one issue β one branch β one PR. Tasks within a spec live as checklists in the spec markdown, not as separate issues.
- Squash-merge into
main. Each spec lands as one commit. - Bookkeeping (flipping spec status to
done, updating phase trackers) ships as a small follow-upchore:PR after the feature merges.
Full workflow: .claude/skills/nexus-spec-workflow/SKILL.md.
app/
Domain/ β bounded contexts (Activity, Dashboard, GitHub, Monitoring, Repositories, β¦)
{Context}/
Actions/ β invokable use-cases
Jobs/ β ShouldQueue background work
Services/ β external API wrappers
WebhookHandlers/β per-event handlers (spec 017+)
Probes/ β value objects for probe results (Monitoring)
Queries/ β read-side query classes
Exceptions/
Enums/ β string-backed PHP enums (status, severity, β¦)
Events/ β broadcast events (ActivityEventCreated, WorkflowRunUpserted, WebsiteCheckRecorded)
Http/Controllers/
Http/Controllers/Monitoring/
Http/Controllers/Webhooks/
Models/
Policies/ β Gate policies (Project, Repository, Website)
resources/js/
Pages/ β Inertia page components
Activity/
Deployments/ β cross-repo timeline + drawer (spec 021)
Monitoring/Websites/β monitor CRUD + show (specs 023β025)
Projects/
Repositories/
WorkItems/
Overview.vue Β· Settings/Index.vue Β· Welcome.vue
Components/
Activity/ β ActivityFeed, ActivityFeedItem, ActivityHeatmap
Dashboard/ β KpiCard, Sparkline, StatusBadge
Sidebar/
lib/
useActivityFeed.ts β Echo composable for the right rail (spec 019)
workflowRunStyles.ts β shared tone/label maps for workflow runs
websiteStyles.ts β shared tone maps for website status
Layouts/AppLayout.vue β primary chrome (sidebar + topbar + right rail)
specs/
README.md β phase tracker + per-spec links
phase-N-<slug>/ β one folder per phase
README.md β phase summary + task list
NNN-<slug>.md β individual spec