Admin Dashboard (Phenom Global Command Center)

Detailed documentation for the Phenom Global Command Center admin dashboard, featuring UAP monitoring, map views, and team management capabilities.

Overview

The Phenom Global Command Center is a comprehensive admin dashboard built to manage and monitor UAP (Unidentified Anomalous Phenomena) data globally. Located at admin_sandbox/phenom/ in the phenom-backend repository, this is a modern React application providing real-time visualization and management capabilities.

Current Version: v1.3.0 (April 2026)

Access

URL: https://nest.thephenom.app/ Authentication: AWS Cognito User Pool (phenom-dev-local, us-east-1_AkG9mnbjA). Email + password sign-in, with sign-up, OTP verification, forgot-password, and reset-password flows. Access gate: CF Access gates entry to N.E.S.T. domains at the network level. All API routes use Cognito Bearer tokens exclusively.

Technology Stack

Frontend Framework

  • React 19 - Latest React with concurrent features
  • TypeScript - Type-safe development
  • Vite - Lightning-fast build tool and dev server
  • TanStack Router - Type-safe routing with code-splitting

UI & Styling

  • Shadcn UI - High-quality component library
  • Tailwind CSS - Utility-first CSS framework
  • Radix UI - Accessible component primitives
  • Lucide React - Beautiful icon library
  • next-themes - Dark/light mode support

State & Data Management

  • Zustand — Lightweight state management (used for the Cognito authStore that holds the ID token)
  • TanStack React Query — Async state and caching
  • Axios / fetch — HTTP clients for the nest-api Worker
  • amazon-cognito-identity-js — Cognito SDK for sign-in, sign-up, OTP, forgot/reset password

Development Tools

  • ESLint - Code linting
  • cz-git - Conventional commits
  • PostCSS - CSS processing

Key Features

1. Dashboard Overview

The main dashboard provides:

  • System Status: Real-time health monitoring
  • Quick Stats: Key metrics and analytics
  • Recent Activity: Latest phenomenon events
  • Task Overview: Pending and completed tasks

2. Map View

Interactive Global Map featuring:

  • Real-time Aircraft Tracking: Integration with OpenSky Network API
  • Phenomenon Locations: Pinpointed UAP events on map
  • Geofencing: Define areas of interest
  • Layer Control: Toggle different data layers
  • Search & Filter: Find specific events or locations

Technical Details:

  • Provider: Google Maps API + CesiumJS 3D globe
  • Data Source: Hasura phenom and drops tables, fetched via GET /api/events on the nest-api Worker. ADSB aircraft data via OpenSky Network.
  • Proxy: Cloudflare Functions for OpenSky API; nest-api Worker for Hasura
  • Updates: PhenomFetcher polls /api/events every 60 seconds with Authorization: Bearer ${idToken}

3. Globe View

3D Earth Visualization with:

  • Interactive 3D Globe: Rotate and zoom
  • Phenomenon Markers: Global UAP event visualization
  • Location Data: Timestamp and coordinates
  • Visual Effects: Atmospheric rendering

4. User Management

Comprehensive User Administration:

  • User List: View all team members
  • CRUD Operations: Create, read, update, delete users
  • Role Assignment: Manage user permissions
  • Activity Tracking: Monitor user actions
  • Search & Filter: Quick user lookup

User Fields:

  • Name, Email, Phone
  • Role (admin, editor, viewer, etc.)
  • Status (active, inactive, suspended)
  • Created/Updated timestamps
  • Activity logs

5. Task Management

Built-in Task System:

  • Task Creation: Assign tasks to team members
  • Status Tracking: Todo, In Progress, Completed
  • Priority Levels: High, Medium, Low
  • Due Dates: Deadline management
  • Comments: Task discussion threads

6. Settings & Configuration

Application Settings:

Appearance:

  • Theme: Light, Dark, System
  • Font Size: Adjust text scaling
  • Color Scheme: Customize accent colors

Profile:

  • Personal Information: Name, email, bio
  • Avatar: Profile picture upload
  • Preferences: Language, timezone

Account:

  • Security: Password change, 2FA
  • Privacy: Data visibility settings
  • Notifications: Email and push preferences

Command Palette (keyboard shortcut):

  • Universal Search: Find users, tasks, phenomena
  • Quick Actions: Navigate anywhere instantly
  • Keyboard Navigation: Full keyboard support
  • Recent Searches: Quick access to history

Application Architecture

Directory Structure

admin_sandbox/phenom/
├── src/
│   ├── features/           # Feature modules
│   │   ├── auth/          # Authentication
│   │   ├── dashboard/     # Main dashboard
│   │   ├── settings/      # Settings pages
│   │   ├── tasks/         # Task management
│   │   └── users/         # User management
│   ├── components/
│   │   ├── layout/        # App layout components
│   │   ├── search/        # Global search
│   │   └── ui/            # Shadcn UI components
│   ├── lib/
│   │   └── utils.ts       # Utility functions
│   ├── stores/
│   │   └── auth-store.ts  # Zustand auth store
│   ├── routes/            # TanStack router config
│   └── main.tsx           # App entry point
├── public/                # Static assets
└── package.json           # Dependencies

Routing Structure

/                          # Dashboard home
├── /dashboard            # Main dashboard view
├── /users                # User management
│   ├── /users/new       # Create new user
│   └── /users/:id/edit  # Edit user
├── /tasks                # Task management
│   ├── /tasks/new       # Create task
│   └── /tasks/:id       # Task details
├── /settings             # Settings hub
│   ├── /settings/profile
│   ├── /settings/account
│   └── /settings/appearance
└── /auth                 # Authentication flows
    ├── /auth/login
    └── /auth/register

Data Source: Hasura via nest-api Worker

All dashboard data flows through the nest-api Worker to Hasura:

Browser → nest-api Worker → Hasura → PostgreSQL

The frontend never talks to Hasura directly for events — it always goes through the Worker so that:

  1. The /api/events response shape stays stable (the Worker normalizes Hasura columns into the legacy shape the dashboard already consumed)
  2. Drop media URLs can be presigned server-side using AWS credentials that are not exposed to the browser
  3. The Worker is the single place to add caching, rate limiting, or auth-driven filtering later

Request flow

  1. User signs in via Cognito (amazon-cognito-identity-js). The ID token is stored in the Zustand authStore.
  2. PhenomFetcher (and useNestUsers) calls fetch('/api/events', { headers: { Authorization: \Bearer ${idToken}` } })`.
  3. The nest-api Worker (admin_sandbox/nest-api/src/auth.ts) verifies the JWT signature against the Cognito JWKS endpoint and stores the raw token on the AuthUser.
  4. The Worker (admin_sandbox/nest-api/src/routes/events.ts) issues a single GraphQL query against Hasura, forwarding the same JWT as a Bearer token. Hasura validates the JWT against the same Cognito JWKS and applies row-level security.
  5. Phenom and drop rows are mapped into the response shape and returned.

Hasura tables consumed by /api/events

TableColumns read
phenomid, reported_at, lat, lng, calculated_location_address, reporter_name, reporter_email, video_url, sensors_data, airnav
dropsid, created_at, filename, file_type, file_size, file_hash, lat, lng, location_source, c2pa_status, c2pa_issuer, ai_detected, claim_generator, media_url, s3_key, sensors_data, planes

phenom.airnav is already pre-normalized to the dashboard’s expected shape by the migration script (hasura/scripts/migrate_firebase_to_postgres.py). drops.planes is still stored as raw OpenSky state[] arrays and the Worker normalizes it at read time.

Response shape

{
  events: Array<
    | {
        id: string
        source: 'phenom'
        timestamp: string  // ISO from phenom.reported_at
        owner: { name: string; email?: string }
        calculatedLocation: { lat: number; lng: number }
        calculatedLocationAddress: string | null
        video: string | null  // phenom.video_url
        sensorsData: object | null
        airnav: Plane[]
      }
    | {
        id: string
        source: 'drop'
        timestamp: string  // ISO from drops.created_at
        filename: string | null
        fileType: string | null
        fileSize: number | null
        fileHash: string | null
        calculatedLocation: { lat: number; lng: number }
        locationSource: string | null
        c2paStatus: string | null
        c2paIssuer: string | null
        aiDetected: boolean | null
        claimGenerator: string | null
        mediaUrl: string | null
        sensorsData: object | null
        airnav: Plane[]  // normalized from drops.planes
        owner: { name: 'Drop Submission' }
      }
  >,
  fetchedAt: string  // ISO
}

The frontend useNestUsers hook derives a user list and per-user upload counts from this same response.

OpenSky Network Integration

Purpose

Real-time aircraft tracking to correlate UAP sightings with known aircraft positions.

Implementation

Cloudflare Function Proxy (functions/opensky/[[path]].js):

// Proxies requests to OpenSky API with:
- Token caching (memory + Cloudflare KV)
- CORS handling
- Automatic token refresh
- Request forwarding

API Client:

  • Client ID: jonathan_at_phenom-api-client
  • Endpoint: https://opensky-network.org/api/
  • Features: Live aircraft positions, flight data, historical tracking

Usage in Dashboard

// Fetch aircraft data via proxy
const response = await fetch('/opensky/states/all')
const aircraftData = await response.json()

// Display on map
aircraftData.states.forEach(aircraft => {
  // Plot aircraft position on map
  addAircraftMarker(aircraft)
})

Authentication Flow

Cognito Authentication

The dashboard authenticates users directly against AWS Cognito using amazon-cognito-identity-js. There is no longer a Firebase or Cloudflare Access step in the user-facing path.

  1. User visits nest.thephenom.app. The /_authenticated/route.tsx route guard calls useAuthStore.restoreSession() which checks for an existing Cognito session in localStorage (the SDK persists refresh tokens there).
  2. If no valid session, the route redirects to /sign-in.
  3. The user enters email + password. useAuthStore.login() calls authenticateUser() in src/lib/cognito.ts, which uses Cognito’s USER_SRP_AUTH flow.
  4. On success, the ID token JWT is stored in the Zustand authStore along with the decoded email.
  5. Protected routes render. Any API call to /api/events (and future Cognito-migrated routes) includes Authorization: Bearer ${idToken}.

Auxiliary flows

FlowRouteCognito API
Sign up/sign-upuserPool.signUp(email, password, ...)
OTP verification/otp?email=...user.confirmRegistration(code, true, ...)
Forgot password/forgot-passworduser.forgotPassword(...)
Reset password/reset-password?email=...user.confirmPassword(code, newPassword, ...)
Sign outsidebar dropdownuser.signOut() then navigate to /sign-in

Implementation lives under admin_sandbox/phenom/src/features/auth/. The Cognito SDK requires the global polyfill in Vite — vite.config.ts sets define: { global: 'globalThis' }.

Session Management

Auth Store (Zustand) — src/stores/authStore.ts:

interface AuthState {
  user: { email: string } | null
  idToken: string | null
  isAuthenticated: boolean
  isLoading: boolean
  login: (email: string, password: string) => Promise<void>
  logout: () => void
  restoreSession: () => Promise<void>
}

Token storage:

  • ID token: in-memory, in the Zustand store. Re-derived on page load via restoreSession().
  • Refresh token: persisted to localStorage by amazon-cognito-identity-js (standard SDK behavior).
  • Expiration: ID tokens last 1 hour; the SDK transparently refreshes using the refresh token.

When the dashboard receives a 401 from any nest-api endpoint, the global axios error handler in src/main.tsx calls useAuthStore.getState().logout() and navigates back to /sign-in.

Development Setup

Prerequisites

Node.js 18+
npm or pnpm

Installation

cd admin_sandbox/phenom
npm install

Environment Variables

Copy .env.example to .env and fill in the values:

# Cognito User Pool
VITE_COGNITO_USER_POOL_ID=us-east-1_AkG9mnbjA
VITE_COGNITO_CLIENT_ID=2eq1vf0nvl5o3rha2vshm8j0mn

These values are not secrets — Cognito Pool ID and Client ID are visible in any browser request to Cognito. They are committed to .env.example so onboarding requires no out-of-band credential exchange.

Switching pools: For staging set VITE_COGNITO_USER_POOL_ID=us-east-1_n8gO6SbP6 and use the matching client ID from phenom-staging (e.g., phenom-dev-hasura-client).

The nest-api Worker has its own configuration in admin_sandbox/nest-api/wrangler.toml:

[vars]
HASURA_GRAPHQL_URL = "https://api-staging.thephenom.app/v1/graphql"
COGNITO_REGION = "us-east-1"
COGNITO_USER_POOL_ID = "us-east-1_AkG9mnbjA"

Worker secrets (set via wrangler secret put): GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY.

Development Server

npm run dev
# Opens at http://localhost:5173

Build for Production

npm run build
# Output: dist/

Deployment

Build Configuration

Netlify (netlify.toml):

[build]
  command = "npm run build"
  publish = "dist"
  functions = "functions"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Cloudflare Functions

Functions deployed alongside static assets:

  • Automatic deployment with Netlify/Cloudflare Pages
  • Cloudflare KV for token caching
  • Worker bindings configured in dashboard

Security Considerations

Access Control

  • Zero Trust: Cloudflare Access at perimeter
  • Cognito Auth: Application-level authentication via AWS Cognito
  • Hasura row-level security: Permissions evaluated against Cognito JWT claims
  • Role-Based: Granular permissions per feature

Data Security

  • Encryption: TLS/SSL in transit
  • Hasura row-level security: Permissions evaluated against the Cognito JWT claims at query time
  • No service account in request path: The Worker forwards the user’s JWT rather than holding admin credentials. A compromised Worker secret cannot read the database directly.
  • API tokens: OpenSky token cached in Cloudflare KV; AWS S3 signing keys held as Worker secrets, never exposed to the browser

Credential Management

The Cognito Pool ID and Client ID are not secrets — they live in .env.example and are committed to the repo. Provisioning a new developer requires only:

  1. cp admin_sandbox/phenom/.env.example admin_sandbox/phenom/.env
  2. Get a Cognito user added to the phenom-dev-local pool by an existing admin
  3. Sign in via npm run dev

The Worker’s secrets (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY) are managed via wrangler secret put.

Known Limitations

  • OAuth Incomplete: Phenomenon deletion disabled
  • Local URLs: Development configured for localhost
  • Token Refresh: Manual intervention may be needed

Performance Optimizations

Code Splitting

  • Route-based: Lazy loading for each page
  • Component-level: Dynamic imports where needed
  • Bundle Size: Optimized with Vite

Caching Strategy

  • OpenSky Data: Cached in Cloudflare KV (configurable TTL)
  • Static Assets: CDN caching via Cloudflare

Monitoring

  • React Query DevTools: Debug data fetching
  • Error Reporting: Client-side error logs

Troubleshooting

Common Issues

“Authentication Failed”

  • Verify GitHub organization membership
  • Check INT team membership in phenom-earth org
  • Clear browser cookies and try again

“Map Not Loading”

  • Check Google Maps API key validity
  • Verify API quotas not exceeded
  • Check browser console for errors

“Aircraft Data Not Updating”

  • Verify OpenSky API credentials
  • Check Cloudflare Function logs
  • Confirm KV namespace binding

“401 Unauthorized from /api/events”

  • Confirm the user is signed in to Cognito (useAuthStore.getState().idToken should be a non-null string in DevTools)
  • Confirm the request actually carries the Authorization: Bearer ... header (Network tab)
  • Check the Worker logs for [events] Error: — Hasura JWT validation failures surface here

“global is not defined” in console after a new install

  • The Cognito SDK requires a global polyfill. Confirm vite.config.ts has define: { global: 'globalThis' } and restart the dev server.

User Content Curation

GitHub Issues: #55 (lists + sharing), #56 (mobile)

Overview

Authenticated users can create named curated lists of phenom/drop content and share them with other team members.

Data Layer

  • Storage: Hasura/PostgreSQL (tables: lists, list_items, list_shares, item_shares)
  • API: nest-api Worker REST endpoints (/api/lists/*, /api/shares/*)
  • Auth: Cognito Bearer token → forwarded to Hasura for row-level security

User Features

  • Create lists: Named collections (e.g., “East Coast Events”, “High Confidence”)
  • Add items: From either phenom sightings or drop submissions
  • Share lists: With other team members (read or write permission)
  • Share items: Quick “send this to someone” with optional note
  • Notifications: Badge on profile avatar for unseen shares

Profile Hub

The lower-left profile dropdown shows:

  • User identity (from Cloudflare Access GitHub OAuth)
  • My Lists with item counts
  • Shared With Me (items and lists from other users)
  • Create New List button

Mobile Responsive Map

On screens below 768px, the event sidebar is replaced with a draggable bottom sheet:

  • Collapsed: handle bar showing event count
  • Peek: 2-3 items visible, map mostly visible
  • Expanded: full scrollable list
  • Item tap: centers map on marker

Future Enhancements

Planned Features:

  • Enhanced OAuth implementation for secure deletions
  • Advanced data analytics dashboard
  • Multi-language support
  • Webhook integrations
  • Email notifications for shared content (Brevo)

Support & Resources

Documentation:

Internal Resources:

For technical support, contact the development team or file an issue in the GitHub repository.