Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

107 Commits

Folders and files

Repository files navigation

NewnopDesk

License: MIT

A multi-tenant client issue portal for software agencies — purpose-built around Newnop's operating model of 50+ client products across engineering centers in Korea, Sri Lanka, and India.

Inspired by ServiceNow and Jira Service Management, NewnopDesk gives each client a dedicated workspace to raise and track issues, while routing tickets to the right internal engineering team and giving leadership cross-portfolio visibility.

An AI assistant helps staff with the routine work: it suggests triage for new issues, tracks client sentiment, finds similar past issues with a suggested fix, and summarises long threads. Staff approve every suggestion; the AI never changes an issue itself, and client sentiment never changes priority.


Screenshots

Sign In Register
Create Issue Dashboard
Issue List Issue Detail

Live Demo

Role Email Password
Admin admin@newnop.com Demo@2026
Engineer (Sri Lanka) ravindu@newnop.com Demo@2026
Client (Apartment LK) feedback@apartment-lk.com Demo@2026

Documentation

Document Description
AI Features What the AI does, architecture, design decisions, safety, eval results
Deployment Guide Complete AWS setup, CI/CD, environment variables, runbook
Backend README API reference, data model, AI job queue, local development
Frontend README Component structure, state management, AI panels, local development
AI Service README Python service: API contract, code layout, tests
Evaluations Labelled eval data, method and full results

Architecture

flowchart LR
    U[Browser] --> CF[CloudFront]
    CF -->|static files| S3[("S3: React app")]
    CF -->|/api| N

    subgraph EC2["EC2 t3.micro"]
        N["Nginx → Express API<br/>+ AI job worker (PM2)"]
        A["AI service<br/>FastAPI (Docker)"]
    end

    N -->|"127.0.0.1, service token"| A
    N --> DB[("RDS PostgreSQL 17<br/>app data + pgvector")]
    A -->|"own schema and role"| DB
    A --> G[Gemini API]
    N -->|presigned URLs| S3F[("S3: attachments")]
Loading

The Express API owns all data and every access decision. AI work is queued in PostgreSQL in the same transaction as the issue or comment and processed in the background, so the app never waits for the AI and keeps working if it's down. The AI service only sees the text it's sent, and its database role can't read application tables. Details in docs/ai.md.


AI Features

All AI panels are for staff only. Clients never see AI output.

Feature What staff get
Triage suggestion Impact, urgency, category and owning team for each new issue, with reasons and the resulting priority. Apply, edit or reject.
Client mood Sentiment, frustration and escalation risk on every client message, with the quoted evidence. High-risk issues are flagged on the dashboard.
Similar issues and suggested fix Resolved issues from the same company, and a fix built from their resolution notes that cites its sources.
Thread summary Key points citing the comments they came from, and open questions (admins).
Client health Weekly sentiment trend per client company (admins).

AI in action

Real output from gemini-3.5-flash-lite on the seeded demo data, not mock-ups.

Issue page with the AI triage suggestion A client's new issue. The triage suggestion sits beside it with a reason for every field and the priority it would produce. The issue itself is unchanged until an engineer applies it.

Triage suggestion card

Triage suggestion. Impact, urgency, category and team, each with a one-sentence reason. Staff can change any field before applying; the model and prompt version are shown and stored.

Similar resolved issues and suggested fix

Similar issues and a suggested fix. Found by local embeddings and pgvector within the same company only. The fix is built from that ticket's resolution notes and cites it; citing anything not retrieved is rejected.

Thread summary with cited key points

Thread summary. A nine-comment thread condensed for an admin. Every key point cites the comments it came from (#3, #5) or the description, and citations are checked against the thread.

Client mood card

Client mood. Sentiment and escalation risk with the client's exact words as evidence; the quote must appear verbatim in their message or the output is rejected. Internal only, and it never changes priority.

High escalation risk on the dashboard

High escalation risk on the dashboard. Open issues whose latest client message signals escalation, so someone can reach out before the client does.

Client health page

Client health. Weekly frustration per client, dated by when each message was written. Davinci Law calmed down after an incident was handled; the trend shows it.

Measured, not assumed (full results), on labelled data with the real model:

Result Baseline
Team routing 91% 49% routing by product
Priority 79% (99% within one level) 63% using the client's own impact and urgency
Sentiment agreement 87.5%, Cohen's κ 0.79 55% always guessing the most common label
Similar-issue recall@5 100%, 0 cross-company results –
Prompt-injection attacks blocked 24 of 24 –

Tech Stack

Frontend: React 19 · TypeScript · Vite · Tailwind CSS · shadcn/ui · TanStack Query · Zustand · React Hook Form · Zod

Backend: Node.js · Express · TypeScript · Prisma · PostgreSQL 17

AI service: Python 3.12 · FastAPI · Pydantic · Google Gemini (gemini-3.5-flash-lite) · fastembed (local embeddings) · pgvector · uv · pytest

Infrastructure: AWS EC2 · RDS for PostgreSQL · S3 · CloudFront · Nginx · PM2 · Docker · GitHub Container Registry

CI/CD: GitHub Actions — tests must pass before every deploy; the AI service ships as an image built in CI


Quick Start (Local Development)

Prerequisites

  • Node.js 24 LTS
  • Docker (runs PostgreSQL locally and powers the integration tests)
  • Git

Backend

docker compose up -d db       # PostgreSQL 17 + pgvector on localhost:5432
cd backend
cp .env.example .env          # DATABASE_URL already matches the local database
npm install
npm run keys:generate         # generates RSA keys for JWT signing
npx prisma migrate dev
npx tsx prisma/seed.ts
npm run dev

API runs at http://localhost:4000

Frontend

cd frontend
npm install
npm run dev

App runs at http://localhost:5173 — API calls proxy to localhost:4000 via Vite config.

AI service (optional)

The app works without it. To turn the AI features on:

cd ai-service
cp .env.example .env          # set GEMINI_API_KEY and AI_SERVICE_TOKEN (or AI_PROVIDER=fake for no key)
uv sync
uv run ai-db bootstrap --admin-url postgresql://newnopdesk:newnopdesk_local@127.0.0.1:5432/newnopdesk
uv run ai-db migrate
uv run ai-serve               # http://127.0.0.1:8000

Then set AI_ENABLED=true and the same AI_SERVICE_TOKEN in backend/.env, restart the backend, and fill the AI data for the seeded issues with npm run ai:reindex and npm run ai:backfill. See the AI service README.


Repository Structure

IssueTracker/
├── .github/
│   └── workflows/
│       ├── ci.yml                # Backend typecheck + integration tests
│       ├── ci-ai-service.yml     # AI service lint, types, tests, image build
│       ├── deploy-frontend.yml   # S3 + CloudFront deployment
│       ├── deploy-backend.yml    # Runs ci.yml, then deploys to EC2 via SSH
│       └── deploy-ai-service.yml # Runs ci-ai-service.yml, pushes the image to GHCR, deploys it
├── ai-service/
│   ├── src/ai_service/           # FastAPI app, prompts, LLM providers, retrieval, safety
│   ├── migrations/               # Alembic migrations for the `ai` schema
│   ├── evals/                    # labelled eval data, runner and committed results
│   ├── tests/
│   └── Dockerfile
├── deploy/
│   └── compose.prod.yml          # AI service container on the EC2
├── backend/
│   ├── src/
│   │   ├── features/             # auth, issues, products, users, companies, ai
│   │   ├── middleware/           # authenticate, role guards
│   │   ├── lib/                  # prisma client, S3 helpers
│   │   └── config/               # env validation
│   ├── prisma/
│   │   ├── schema.prisma
│   │   ├── migrations/
│   │   └── seed.ts
│   ├── keys/                     # RSA keys (gitignored, generated locally)
│   ├── ecosystem.config.js       # PM2 process config
│   └── prisma.config.ts
├── frontend/
│   └── src/
│       ├── api/                  # Axios call functions, one file per resource
│       ├── components/
│       │   ├── home/             # home page sections (KPIs, engineer stats, my-work)
│       │   ├── issue/            # issue detail sub-components (header, sidebar, form)
│       │   ├── issues/           # shared issue widgets (timeline, comments, attachments)
│       │   ├── layout/           # AppShell, Header, Sidebar
│       │   └── ui/               # shadcn/ui primitives + custom badges
│       ├── hooks/                # TanStack Query wrappers + utility hooks
│       ├── lib/                  # api-client, format, schemas, theme, utils
│       ├── pages/                # one file per route
│       ├── router/               # createBrowserRouter + route guards
│       ├── store/                # Zustand stores (auth, tabs)
│       └── types/                # TypeScript interfaces mirroring API responses
└── docs/
    ├── ai.md                     # AI features: design, safety, evals
    └── deployment.md             # full AWS deployment guide

CI/CD

Every push to main triggers automatic deployment:

  • Changes to frontend/** → Vite build → S3 sync → CloudFront cache invalidation
  • Changes to backend/** → integration tests → TypeScript build verified → SSH to EC2 → git pull → rebuild → migrate → pm2 restart
  • Changes to ai-service/** → lint, types, tests → Docker image built and pushed to GitHub Container Registry → SSH to EC2 → migrate → start the new container, rolling back automatically if it isn't healthy

The two EC2 deployments never run at the same time, so a backend build and an image pull don't compete for the instance's 1 GB of memory.

See the Deployment Guide for full pipeline documentation.


Design Decisions

Why a relational database: The data is highly relational — companies own products, products have issues, issues have comments and activity logs. Foreign keys and joins are the natural fit, which ruled out a document store like MongoDB.

Why PostgreSQL (migrated from MySQL 8): The project started on MySQL. The planned AI features need vector similarity search, which RDS MySQL can't do, and running a second database just for vectors would mean two engines, two backups, and keeping them in sync on a 1 GB server. PostgreSQL with the pgvector extension handles both the application data and the vectors in one database, and its role and schema permissions let the AI service be restricted to its own tables. The migration was low-risk: there was no production data to move (the demo database is rebuilt from the seed), only two migrations to regenerate, and the one raw SQL query was already portable. The single behaviour difference was that PostgreSQL's LIKE is case-sensitive where MySQL's default collation was not; search now uses ILIKE, and a test pins it. RDS connections use verified TLS (sslmode=verify-full with Amazon's CA bundle).

Why search uses ILIKE: Search is a case-insensitive substring match across title, description, and ticket number. At this data size that's fast and simple. PostgreSQL full-text search (a tsvector column with a GIN index) is the upgrade path if the data grows.

Why multi-tenant isolation at the API layer: Every issue query goes through one function, buildTenantWhere, which adds the caller's company or product scope to the Prisma query. That's simple and testable, and integration tests probe every issue route with users from another company to prove nothing leaks. PostgreSQL also supports row-level security, which would add a second, database-enforced layer. It isn't used yet: with a connection pool, it needs the tenant ID set at the start of every transaction, which is a larger change for this stage of the project.

Why JWT in localStorage: For this assignment, localStorage simplifies the auth flow. In production, httpOnly cookies with SameSite=Strict would prevent XSS token theft. This trade-off is documented and the fix is a one-line change to the cookie configuration.

Why priority is computed, not selected: The ITIL Impact × Urgency matrix ensures consistency — no engineer can mark a low-impact, low-urgency issue as Critical. Priority is derived from the two independent inputs and stored for indexed queries.

Why client sentiment never changes priority: Tone is not impact. A calm report that every tenant is locked out is critical; a furious complaint about a typo is not. Letting anger raise priority would push polite clients and second-language writers down the queue, teach clients to write angrily, and let a wrong or manipulated AI reading reorder the engineering backlog. So sentiment is stored separately, no priority code reads it, and a test proves the issue is untouched by even the angriest reading. Frustration goes where it's useful: high-risk issues are flagged on the dashboard and client trends on the client-health page, for a human to respond to.

Why the AI suggests but never decides: The AI has no write path to issues. Staff apply, edit or reject each suggestion, which keeps accountability with people, limits what a prompt injection can do to "a suggestion someone has to approve", and records every outcome so the acceptance rate can be measured per model and prompt version.

Why a separate Python service for AI: Python has the best tooling for strict output validation (Pydantic), LLM SDKs and embeddings. A separate service with its own restricted database role can't read other tenants' data, can fail without taking the app down, and deploys independently as a Docker image. The cost is one local network hop.

Why pgvector instead of a vector database: Similar-issue search needs vector similarity. Keeping vectors in the same PostgreSQL, in their own schema and role, means no extra server, backup or sync job on a 1 GB instance, and lets database permissions enforce that the AI service can't touch application tables. At this scale PostgreSQL does an exact search in under a millisecond. Full reasoning in docs/ai.md.

Why the AI service runs in Docker but the backend on PM2: The AI service has heavy native dependencies (ONNX runtime, an embedding model) that are easiest to ship as a tested image built once in CI; the server just pulls and runs it, with a memory limit so it can't starve the backend. The backend was already running reliably on PM2, and moving it brought no benefit worth the change.


Acknowledgements

Visual design references Newnop's brand palette as published on newnop.com. Layout and UX patterns are inspired by Linear and Jira Service Management.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages