Javis OS
Javis OS는 기업가를 돕는 에이전시 AI로, 업무를 지원하고 함께 성장합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Smart IT Helpdesk 🚀
Enterprise IT Service Management & AI-Powered Triage Platform
An enterprise-grade IT Support Ticket Management System built with Next.js 16 (App Router), TypeScript, Prisma ORM + PostgreSQL, Tailwind CSS, and Google Gemini (gemini-2.5-flash by default) for ticket triage suggestions, ticket summaries for IT staff, and on-demand translation.
Live demo: https://smart-helpdesk-tan.vercel.app (one-click demo logins for each role)README 전문 보기 ↗An enterprise-grade IT Support Ticket Management System built with Next.js 16 (App Router), TypeScript, Prisma ORM + PostgreSQL, Tailwind CSS, and Google Gemini (gemini-2.5-flash by default) for ticket triage suggestions, ticket summaries for IT staff, and on-demand translation.
Live demo: https://smart-helpdesk-tan.vercel.app (one-click demo logins for each role)
Built with AI coding tools: Antigravity IDE with Claude 3.5 Sonnet and Gemini for the first version, then Claude Code for the review and rework. AI-USAGE.md explains what was delegated, what I decided, and how the generated code was validated.
Smart IT Helpdesk bridges the gap between end-user frustration and IT service resolution. Traditional ticketing systems suffer from vague problem descriptions, misrouted tickets, and manual triage delays.
This platform solves these challenges through:
OPEN → ASSIGNED → IN_PROGRESS → RESOLVED → CLOSED) with immutable historical audit logs.| Layer | Technology | Details / Rationale |
|---|---|---|
| Framework | Next.js 16.3+ (App Router) | React Server Components (RSC), Turbopack, and Server Actions |
| Language | TypeScript 5 | Strict typing throughout models, APIs, and UI components |
| Database & ORM | Prisma ORM 7 (node-postgres adapter) + PostgreSQL (Neon) | Versioned migrations (prisma/migrations), check constraints on roles, statuses and ratings; CLI settings in prisma.config.ts |
| Authentication | jose (JWT) + bcryptjs | Stateless encrypted HTTP-only session cookies with 12-round salt hashing |
| AI Engine | Google Gemini via @google/genai (gemini-2.5-flash, falls back to gemini-2.5-flash-lite) | Schema-validated JSON output for triage and summaries; translation |
| Styling & Design | Tailwind CSS + CSS Design Tokens | Clean typography, dark/light adaptive surfaces, and zero-border minimalism |
| Motion & Charts | Framer Motion & Recharts | Micro-animations, interactive layout transitions, and queue distribution graphs |
| Internationalization | Custom Context Engine + Cookies | Instant zero-reload locale toggling, bidirectional layout (rtl/ltr) |
| File Storage | Vercel Blob (private) or local disk | Attachments are never public; /api/files/[id] checks who may see them |
| Testing & CI | Vitest + GitHub Actions | Integration tests against a real PostgreSQL test database; lint, types, tests and build on every push |
The system uses a relational database schema (PostgreSQL) designed for auditability and operational integrity. It matches prisma/schema.prisma and the SQL in prisma/migrations/.
erDiagram
User ||--o{ Ticket : "creates (createdTickets)"
User |o--o{ Ticket : "assigned to (assignedTickets)"
User ||--o{ Comment : "authors"
User ||--o{ TicketHistory : "triggers"
Ticket ||--o{ Comment : "contains"
Ticket ||--o{ TicketHistory : "audits"
Ticket |o--o{ Attachment : "has files"
User ||--o{ Attachment : "uploads"
User {
String id PK "cuid()"
String name "Full Name"
String email UK "Corporate Email"
String password "Bcrypt Hashed"
String role "EMPLOYEE | IT_SUPPORT | ADMIN"
String accountStatus "PENDING | APPROVED | REJECTED | SUSPENDED | INVITED"
String skills "JSON Array: ['Hardware', 'Network']"
Boolean isAvailable "Active Assignment Status"
Int sessionVersion "Bumped to sign out every device"
DateTime lastLoginAt "Nullable"
DateTime createdAt "Timestamp"
DateTime updatedAt "Timestamp"
}
Ticket {
String id PK "cuid()"
Int ticketNumber UK "Auto-incrementing Identifier (#101...)"
String title "Incident Subject"
String description "Incident Narrative"
String category "Hardware | Network | Software | Printer..."
String priority "LOW | MEDIUM | HIGH | CRITICAL"
String status "OPEN | ASSIGNED | IN_PROGRESS | RESOLVED | CLOSED"
DateTime slaDeadline "SLA Target Timestamp"
Boolean slaBreached "Breach Status Flag"
Int csatRating "1 to 5 Star Rating"
String csatFeedback "User Closure Feedback"
DateTime resolvedAt "Resolution Timestamp"
DateTime closedAt "Closure Timestamp"
String assetId "Optional Corporate Asset Tag"
String createdById FK "User.id"
String assignedToId FK "User.id (Nullable)"
DateTime createdAt "Timestamp"
DateTime updatedAt "Timestamp"
}
Comment {
String id PK "cuid()"
String content "Message Text"
Boolean isInternal "Private IT Agent Note"
String ticketId FK "Ticket.id"
String authorId FK "User.id"
DateTime createdAt "Timestamp"
}
Attachment {
String id PK "cuid(); served at /api/files/id"
String storageKey UK "Random key in private storage"
String name "Original file name"
Int size "Bytes"
String type "Derived from the extension"
String uploadedById FK "User.id"
String ticketId FK "Ticket.id (null until the ticket is created)"
DateTime createdAt "Timestamp"
}
TicketHistory {
String id PK "cuid()"
String action "English sentence (logs, legacy rows)"
String event "Event type, e.g. status_changed"
String meta "JSON event data, e.g. from/to"
String overrideReason "Reserved, not written by the app yet"
String ticketId FK "Ticket.id"
String userId FK "User.id"
DateTime createdAt "Timestamp"
}
AppSettings {
String id PK "'singleton'"
String appName "Enterprise Portal Title"
String supportEmail "Support Desk Contact"
String defaultPriority "Default Ticket Urgency"
Boolean autoAssignmentEnabled "Skill Routing Switch"
Int slaCriticalHours "SLA Target for Critical"
Int slaHighHours "SLA Target for High"
Int slaMediumHours "SLA Target for Medium"
Int slaLowHours "SLA Target for Low"
String businessHoursStart "e.g. 09:00"
String businessHoursEnd "e.g. 17:00"
String workDays "JSON Array of Active Days"
Boolean pauseSlaOnWeekends "SLA clock counts business days only"
Boolean enableAiTriage "AI Integration Flag"
Boolean fallbackHeuristicsEnabled "Labelled keyword rules when AI is down"
String autoApproveDomain "Employees with this email domain are approved on sign-up"
Boolean maintenanceMode "Only admins can sign in"
Boolean notifyNewUser "Reserved (no email yet)"
Boolean notifyCriticalTicket "Reserved (no email yet)"
String categoriesList "JSON Array of Active Categories"
String cannedResponses "JSON Array of Macros"
DateTime updatedAt "Timestamp"
}
RateLimit {
String key PK "e.g. login:account:[email protected]"
Int count "Attempts in the current window"
DateTime windowStart "Start of the fixed window"
}
AppSettings is a single row (id = 'singleton') and RateLimit stands alone; neither has relations.
Everything about a ticket hangs off the ticket. The app never deletes tickets, but the foreign keys cascade so a ticket removed from the database takes its comments, history and attachments with it:
flowchart TD
U[User] -->|creates| T[Ticket]
U -.->|is assigned| T
T -->|cascade| C[Comment<br/>public or internal note]
T -->|cascade| H[TicketHistory<br/>typed audit events]
T -->|cascade| A[Attachment<br/>file in private storage]
U -->|uploads| A
Attachment exists before its ticket (ticketId is null between upload and ticket creation), and is linked only if the requester uploaded it.createdById), an agent's queue (assignedToId, status), overdue tickets (status, slaDeadline), and a ticket's comments and history in time order.Roles, statuses and priorities are String columns, validated in the application and by PostgreSQL CHECK constraints in the migrations:
| Column | Allowed values |
|---|---|
User.role | EMPLOYEE, IT_SUPPORT, ADMIN |
User.accountStatus | PENDING, APPROVED, REJECTED, SUSPENDED, INVITED |
Ticket.status | OPEN, ASSIGNED, IN_PROGRESS, RESOLVED, CLOSED |
Ticket.priority, AppSettings.defaultPriority | LOW, MEDIUM, HIGH, CRITICAL |
Ticket.csatRating | NULL or 1–5 |
Attachment.size | 0 or more |
Why not Prisma enums? A CHECK gives the same guarantee (the database refuses any other value), but:
ON_HOLD status) is a one-line constraint change, and needs no regenerated types across the code;ALTER TABLE ... DROP CONSTRAINT ...), whereas converting a populated column to an enum type and back is a riskier migration.Prisma does not model CHECK constraints, so they live only in the SQL migrations (20261002000000_init, 20261004120000_check_default_priority).
The platform includes 4 pre-configured corporate accounts representing all administrative, operational, and end-user personas. When DEMO_MODE=true, the /login page also offers them as 1-click demo logins (off by default, because it puts the passwords in the page).
| Persona | Name | Password | Role | Permissions & Domain Focus | |
|---|---|---|---|---|---|
| System Admin | System Admin | [email protected] | admin123 | ADMIN | Complete platform governance: user account approvals, global SLA policies, canned macros, and enterprise-wide ticket oversight. |
| IT Support (Infra) | Bob Williams | [email protected] | support123 | IT_SUPPORT | Operational triage specialist. Specialized skills in Network, Printer, Email & Communication, and Access & Permissions. |
| IT Support (Systems) | Mike Davis | [email protected] | support123 | IT_SUPPORT | Systems and workstations specialist. Specialized skills in Hardware and Software diagnostics. |
| Corporate Employee | Alice Johnson | [email protected] | employee123 | EMPLOYEE | Standard enterprise end-user. Restricted to creating tickets, viewing own history, submitting CSAT surveys, and utilizing AI self-help triage. |
Permission matrix (enforced in the Server Actions and API routes; the UI only hides what the server would refuse):
| Action | Employee | IT Support | Admin |
|---|---|---|---|
| Create a ticket (with AI triage suggestions) | ✅ | ✅ | ✅ |
| See tickets | Own only | All | All |
| See and write internal notes | ❌ | ✅ | ✅ |
| Reply on a ticket | Own only | ✅ | ✅ |
| AI summary of a ticket | ❌ | ✅ | ✅ |
| Translate a ticket or comment | ✅ | ✅ | ✅ |
| Claim an unassigned ticket / take over a colleague's (logged) | ❌ | ✅ | ❌ |
| Assign a ticket to an agent | ❌ | Unassigned or own tickets | Any unfinished ticket |
| Move a ticket to its next status | ❌ | Own or unassigned tickets | ❌ (oversight only) |
| Confirm a resolution, reopen, rate (CSAT) | Requester only | ❌ | ❌ |
| Set own availability for auto-assignment | ❌ | ✅ | ❌ |
| Approve, suspend, invite users; change roles and skills | ❌ | ❌ | ✅ |
| Settings (SLA, categories, AI, maintenance mode) | ❌ | ❌ | ✅ |
Admins deliberately don't move tickets through statuses: the agent doing the work does, and the admin reassigns when needed.
Tickets advance strictly through a verified linear progression (lib/ticket-status.ts):
stateDiagram-v2
[*] --> OPEN: created, no agent available
[*] --> ASSIGNED: created, auto-assigned by skill
OPEN --> ASSIGNED: agent claims, or agent/admin assigns
ASSIGNED --> IN_PROGRESS: assigned agent starts work
IN_PROGRESS --> RESOLVED: assigned agent resolves
RESOLVED --> CLOSED: requester confirms, or agent closes
RESOLVED --> IN_PROGRESS: requester reopens (with a reason)
CLOSED --> IN_PROGRESS: requester reopens (with a reason)
ASSIGNED --> OPEN: agent suspended or demoted
IN_PROGRESS --> OPEN: agent suspended or demoted
CLOSED --> [*]
RESOLVED → OPEN) or skip states (e.g., OPEN → RESOLVED); the server rejects it.IN_PROGRESS (or OPEN if unassigned), gets a fresh SLA deadline, and the reopen is logged.OPEN and unassigned, and each move is logged.Account lifecycle (User.accountStatus):
stateDiagram-v2
[*] --> APPROVED: employee signs up with the company domain
[*] --> PENDING: any other sign-up
[*] --> INVITED: admin invites (one-time password)
INVITED --> APPROVED: user sets their own password
PENDING --> APPROVED: admin approves
PENDING --> REJECTED: admin rejects
APPROVED --> SUSPENDED: admin suspends
SUSPENDED --> APPROVED: admin restores
REJECTED --> APPROVED: admin approves
These are the usual paths; on the Users page an admin can set any account to PENDING, APPROVED, REJECTED or SUSPENDED (except their own). Only APPROVED users can work in the app; an INVITED user can sign in only to change their password. A status change takes effect on the user's next request, because every session is checked against the database.
TicketHistory as a typed event (lib/history.ts) with the actor and time, so the timeline is shown in the viewer's language.canvas-confetti./tickets/new): Gemini returns a category (one of the admin's categories), a priority and 2–3 self-help steps, in the user's language. The answer is constrained by a JSON schema and validated on the server. The employee clicks Use this suggestion or Dismiss; nothing is applied automatically./tickets/[id]): On request, Gemini summarises the description and the whole discussion and proposes a next step, which can be inserted into the reply.ar) and English (en) dictionary translation without path alterations (/ar/tickets vs /en/tickets).dir="rtl" / dir="ltr" adjustment paired with localized typography (Cairo for Arabic, Inter for English).مفتوحة / Open), categories (طابعات / Printer), priorities, and relative timestamps (منذ 5 دقائق / 5 minutes ago) adapt dynamically to the active session language.TicketDrawer.tsx): Allows IT agents to review diagnostics, assign tickets, and post comments directly from the main list without losing page context.lib/session-check.ts): suspending, rejecting or deleting a user, changing their role, or turning on maintenance mode takes effect on their next click. The cookie is removed and the login page says why. Changing a password signs out the user's other devices (User.sessionVersion).lib/rate-limit.ts, stored in the database so they hold across restarts and servers):
nosniff, referrer and permissions policies, HSTS.lib/errors.ts) and shown in the user's language.lib/monitoring.ts). A crashed page shows a translated error screen with a reference number.Copy .env.example to .env; every variable is explained there.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | yes | PostgreSQL connection used by the app (on Neon: the pooled string) |
DIRECT_URL | yes | Direct connection used by migrations (on Neon: the unpooled string; locally the same as DATABASE_URL) |
SESSION_SECRET | yes | Signs the session cookie; at least 32 random characters |
GEMINI_API_KEY | no | Google Gemini for triage suggestions, summaries and translation |
GEMINI_MODEL / GEMINI_FALLBACK_MODEL | no | Override the models (defaults gemini-2.5-flash / gemini-2.5-flash-lite) |
BLOB_READ_WRITE_TOKEN | on Vercel | Private Vercel Blob store for attachments (set by Vercel when the store is connected) |
STORAGE_DIR | no | Local folder for attachments when there is no Blob token (default .data/uploads) |
DEMO_MODE | no | true shows 1-click logins for the seeded accounts. Never with real users |
SENTRY_DSN / NEXT_PUBLIC_SENTRY_DSN | no | Error monitoring with Sentry (server / browser). Without them nothing is reported |
SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT | no | Let the build upload source maps, so Sentry shows readable stack traces |
TEST_DATABASE_URL | for tests | Separate database for npm test, wiped on every run; its name must end in _test |
Without a Gemini key the app still works: triage suggestions come from labelled keyword rules (if enabled in Settings), and summaries/translation report that AI is not configured. Google's free tier allows only a few requests per minute per model, which is why a fallback model is used.
helpdesk_test for the tests.# 1. Install dependencies (also generates the Prisma client)
npm install
# 2. Configure: database URLs, SESSION_SECRET, and GEMINI_API_KEY if you have one
cp .env.example .env
# 3. Create the tables
npm run db:deploy
# 4. Demo data: users, categories, tickets #101-#108 (deletes existing users and tickets)
npm run db:seed
# 5. Start the app
npm run dev
Open http://localhost:3000.
Changing the schema: edit prisma/schema.prisma, then npm run db:migrate -- --name what_changed creates and applies a migration. Commit it; deployments apply it with prisma migrate deploy.
Inviting users: admins can invite a user from User Management. Until email is set up, the admin receives a one-time password to share; the user must choose their own password at first sign-in.
-pooler) and the direct one, and change sslmode=require to sslmode=verify-full in both (checks the server's certificate).npm run vercel-build, which applies pending migrations and builds. vercel.json runs the app in Frankfurt (fra1), next to the database; if your Neon project is in another region, change it to match, or every query crosses an ocean.DATABASE_URL (pooled), DIRECT_URL (direct), SESSION_SECRET, GEMINI_API_KEY, and DEMO_MODE if this is a demo.SENTRY_DSN and NEXT_PUBLIC_SENTRY_DSN (both the project's DSN); for readable stack traces also SENTRY_AUTH_TOKEN, SENTRY_ORG and SENTRY_PROJECT.BLOB_READ_WRITE_TOKEN).DATABASE_URL=... npm run db:seed.https://<your-app>/api/health should answer {"status":"ok"}.The repository contains an automated test suite implemented with Vitest. Integration tests run against their own PostgreSQL database (TEST_DATABASE_URL, rebuilt from the migrations before every run, so the app's data is never touched; in CI a PostgreSQL service container) with only the Next.js request context and the Gemini SDK stubbed; unit tests cover the AI wrapper, triage rules, skills and audit-trail events.
# Execute test suite once
npm test
# Run tests in watch mode
npm run test:watch
✓ tests/ai-triage.test.ts (10 tests)
✓ parseTriageResponse (4)
✓ accepts a valid answer and cleans it up
✓ rejects a category the admin has not configured
✓ rejects an unknown priority
✓ rejects an answer without self-help steps
✓ ruleBasedTriage (6)
✓ is always labelled as rules, never as AI
✓ prefers the more specific rule ("network printer" is a printer issue)
✓ matches English keywords on word boundaries only
✓ understands Arabic tickets and answers in Arabic
✓ raises priority to CRITICAL for outages affecting everyone
✓ only suggests categories that exist in the admin settings
✓ tests/gemini.test.ts (6 tests)
✓ Gemini client (6)
✓ treats the README placeholder key as not configured
✓ falls back to the second model when the first is rate-limited
✓ does not retry errors that another model would not fix
✓ fails when every model is rate-limited, so callers can fall back honestly
✓ disables thinking on 2.5 models for speed
✓ rejects a triage answer with a category the admin does not have
✓ tests/helpdesk.test.ts (126 tests)
✓ Test 1: User Login (7)
✓ valid credentials create a session for the user and redirect to their tickets
✓ IT_SUPPORT is redirected to their assigned queue
✓ wrong password is rejected without creating a session
✓ unknown email gets the same generic error (no account enumeration)
✓ "remember me" asks for a long-lived session, and email case does not matter
✓ maintenance mode keeps everyone but admins out
✓ accounts pending approval cannot log in
✓ Test 2: Ticket Creation (5)
✓ creates an OPEN ticket with the next number, an SLA deadline and an audit entry
✓ rejects missing or too-short fields without creating a ticket
✓ auto-assigns to a matching agent only when the admin setting is on
✓ auto-assignment understands agents saved with the old skill names
✓ redirects signed-out users to the login page
✓ Test 3: Role Authorization (6)
✓ EMPLOYEE cannot change ticket status
✓ EMPLOYEE cannot assign or take over tickets
✓ EMPLOYEE cannot promote themselves to ADMIN
✓ IT_SUPPORT cannot use admin-only actions
✓ admins can only give agents skills that are real categories
✓ IT_SUPPORT cannot change the status of another agent's ticket
✓ Test 4: Status Lifecycle State Machine (8)
✓ rejects skipping from OPEN to IN_PROGRESS
✓ rejects skipping from OPEN to RESOLVED
✓ rejects skipping from OPEN to CLOSED
✓ OPEN → ASSIGNED claims the unassigned ticket for the agent
✓ ASSIGNED → IN_PROGRESS → RESOLVED
✓ rejects going backwards from RESOLVED
✓ RESOLVED → CLOSED, after which every transition is rejected
✓ records each successful transition in the audit trail, and nothing for rejected ones
✓ Test 5: Data Isolation (8)
✓ ticket list page shows an EMPLOYEE only their own tickets
✓ ticket list page shows IT_SUPPORT every ticket in the "all" queue
✓ ticket detail page returns 404 for another employee's ticket
✓ getTicketDetails returns null when an EMPLOYEE requests another user ticket
✓ getTicketDetails returns the ticket to its EMPLOYEE owner without internal notes
✓ getTicketDetails returns any ticket, with internal notes, to IT_SUPPORT
✓ EMPLOYEE cannot comment on another employee's ticket
✓ EMPLOYEE comments are always public, even if they ask for an internal note
✓ Test 6: API Authentication & Upload Safety (7)
✓ upload rejects unauthenticated requests
✓ upload rejects HTML and SVG files that browsers would execute
✓ upload rejects files larger than 4 MB (the hosting limit per request)
✓ upload rejects more than 5 files at once
✓ upload stores an allowed file privately, under a random name, with the server-side type
✓ AI triage route rejects unauthenticated requests without calling Gemini
✓ translateAction rejects unauthenticated callers without calling Gemini
✓ Test 7: AI Features (6)
✓ triage returns the Gemini answer labelled as AI
✓ triage falls back to keyword rules, labelled as rules, when Gemini fails
✓ triage returns 503 instead of guessing when the admin disabled the fallback
✓ triage is refused when the admin turned AI triage off
✓ ticket summaries are only available to IT support and admins
✓ translation reports an honest error instead of inventing text
✓ Test 8: SLA Tracking (5)
✓ records when a ticket was resolved and that it met its SLA
✓ flags a ticket resolved after its deadline as an SLA breach
✓ records the close time, and reopening clears both times but keeps the breach
✓ the admin "SLA breaches" view lists overdue and late-resolved tickets only
✓ the "pending requests" link opens the users page filtered to pending accounts
✓ Test 9: Admin Settings (11)
✓ only admins can change settings
✓ rejects an unknown priority
✓ rejects negative SLA hours
✓ rejects fractional SLA hours
✓ rejects a malformed time
✓ rejects hours that end before they start
✓ rejects an invalid work day
✓ rejects no categories
✓ rejects a bad email
✓ rejects a domain without @
✓ saves valid settings
✓ Test 10: Invites & Passwords (10)
✓ only admins can invite users
✓ an invite creates an INVITED account and returns a one-time password
✓ signing in with the temporary password forces a password change
✓ the proxy keeps a user with a temporary password on the password page
✓ rejects the change when the current password is wrong
✓ rejects the change when the new password is too short
✓ rejects the change when the confirmation does not match
✓ rejects the change when the new password equals the old one
✓ setting a password activates the account and continues into the app
✓ any user can change their password later without being redirected
✓ Test 11: Sessions (10)
✓ an active user keeps their session, with role and name read from the database
✓ a role taken away ends admin access on the next request
✓ a SUSPENDED account is signed out and told why
✓ a REJECTED account is signed out and told why
✓ a PENDING account is signed out and told why
✓ a deleted account is signed out
✓ on the login page an ended session is cleared without a redirect loop
✓ changing the password signs out other devices but keeps this one
✓ maintenance mode ends the sessions of everyone but admins
✓ demo logins are only offered when DEMO_MODE=true
✓ Test 12: Sign-in Rate Limiting (4)
✓ locks an account for 15 minutes after 5 wrong passwords, even for the right password
✓ allows the account again once the 15-minute window has passed
✓ a successful sign-in resets the count of failures
✓ locks out an address that tries many accounts
✓ Test 13: Server-side Validation (12)
✓ a ticket needs one of the admin categories and a real priority
✓ a ticket can only use the requester's own uploads that are not attached elsewhere
✓ attachments are only served to people who may see the ticket
✓ tickets created at the same moment still get different numbers
✓ tickets can only be given to active IT support staff
✓ a closed ticket cannot be taken over or reassigned
✓ when two agents claim the same ticket at once, only one wins
✓ comments must exist, be non-empty and not too long, on a real ticket
✓ a ticket can be rated once, from 1 to 5, after it is resolved
✓ an admin cannot suspend or demote themselves
✓ the account request form does not reveal whether an email is registered
✓ limits account requests from one address
✓ Test 14: AI Usage Limit (3)
✓ counts each AI request against the user
✓ over the limit, triage answers with the keyword rules without calling Gemini
✓ over the limit, summaries and translation are refused
✓ Test 15: Review Fixes (3)
✓ the users page checks for an admin itself, not only in the layout
✓ claiming an unassigned ticket is logged as a claim, not "taken over from Unassigned"
✓ canned replies are read safely from the settings
✓ Test 16: Reopened SLA & Agents Who Leave (4)
✓ a reopened ticket gets a fresh SLA deadline but keeps an earlier breach
✓ when an agent is suspended, their unfinished tickets go back to the queue
✓ when an agent is made an employee, their unfinished tickets go back to the queue
✓ approving or keeping an agent does not touch their tickets
✓ Test 17: Audit Fixes (6)
✓ auth guards are not exported from a 'use server' file (where every export is a public endpoint)
✓ an unknown email still pays for a password check, so it takes as long as a wrong password
✓ an account request for a registered email still hashes the password
✓ a user can have at most 10 uploads waiting, and removing one frees a place
✓ a user can upload at most 30 files an hour
✓ only the uploader can delete an upload, and never once it belongs to a ticket
✓ Test 18: Lifecycle Consistency & Database Constraints (6)
✓ an agent cannot hand a colleague's ticket to someone else without taking it over
✓ an agent can assign an unassigned ticket, and an admin can reassign any open ticket
✓ confirming a resolution twice at once closes the ticket once
✓ reopening twice at once reopens the ticket once
✓ a ticket rated twice at once keeps one rating
✓ the database refuses statuses, priorities, roles and ratings outside the allowed values
✓ Test 19: Ticket List Paging (5)
✓ shows 20 tickets a page, most urgent first and newest first within a priority
✓ the counters describe every ticket in the queue, not only the page
✓ filters on the server: priority, status, search, ticket number
✓ a page past the end shows the last page, and junk values are ignored
✓ filters never widen the scope: an employee's search doesn't reach other people's tickets
✓ tests/history.test.ts (14 tests)
✓ ticket audit trail (14)
✓ stores the event, its data and an English sentence
✓ shows events in the viewer’s language with translated statuses
✓ has a translation for every event type in both languages
✓ falls back to the stored text for rows without an event
✓ recognises the old sentence "Ticket #101 created"
✓ recognises the old sentence "System auto-assigned ticket to Bob Williams based on category (Email & Communication)"
✓ recognises the old sentence "No available specialist found — ticket queued in unassigned"
✓ recognises the old sentence "Status changed: ASSIGNED → IN_PROGRESS"
✓ recognises the old sentence "Ticket claimed and status changed from OPEN to ASSIGNED"
✓ recognises the old sentence "Reassigned from Bob to Mike by Admin User"
✓ recognises the old sentence "Ticket reopened by requester. Reason: still broken"
✓ recognises the old sentence "CSAT Rating submitted: 5 Stars"
✓ round-trips: every sentence the app writes can be recognised again
✓ the queued ticket in the seed data keeps its category
✓ tests/skills.test.ts (4 tests)
✓ agent skills (4)
✓ maps the old short names to the current category names and removes duplicates
✓ survives empty or corrupt data
✓ only keeps skills that are current categories
✓ matches categories exactly, not as substrings of the JSON text
X-Forwarded-For, so a deployment must sit behind a reverse proxy that sets it (Vercel does).smart-helpdesk/
├── app/
│ ├── actions/ # Server Actions
│ │ ├── auth.ts # Login, register, logout
│ │ ├── account.ts # Change password (incl. first sign-in)
│ │ ├── admin.ts # Users: status, role & skills, invites
│ │ ├── settings.ts # Validated admin settings
│ │ ├── tickets.ts # Ticket CRUD & strict state machine
│ │ └── translate.ts # On-demand AI translation
│ ├── account/password/ # Change-password page
│ ├── admin/ # Administrator portal (settings, tickets, users)
│ ├── api/ # REST API routes
│ │ ├── ai/ # Gemini triage and ticket summaries
│ │ ├── files/[id]/ # Serves an attachment to those allowed to see it
│ │ ├── health/ # Liveness check (database)
│ │ └── upload/ # File attachments (type & size checked)
│ ├── components/ # Shared UI (drawer, AI summary, translate button, responsive sidebar…)
│ ├── login/ # Sign-in page and walk-through
│ ├── register/ # Account request
│ └── tickets/ # Ticket list, detail page and new-ticket form
├── lib/
│ ├── ai/triage.ts # AI answer validation + labelled keyword fallback
│ ├── gemini.ts # Gemini client: model fallback, triage, summaries, translation
│ ├── history.ts # Typed audit-trail events and their display
│ ├── session.ts # JWT session cookie via jose
│ ├── session-check.ts # Session re-checked against the database
│ ├── rate-limit.ts # Sign-in, account request and AI limits
│ ├── errors.ts # Error codes shown in the user's language
│ ├── demo.ts # 1-click demo logins (DEMO_MODE)
│ ├── storage.ts # Private file storage (Vercel Blob or local disk)
│ ├── ticket-query.ts # List filters and server-side paging (20 per page)
│ ├── ticket-status.ts # The ticket state machine
│ ├── monitoring.ts # What Sentry may collect (nothing private)
│ ├── settings.ts / skills.ts / sla.ts / uploads.ts / ticket-rules.ts
│ └── i18n/ # Arabic/English dictionaries and provider
├── prisma/
│ ├── schema.prisma # Database models
│ ├── migrations/ # Versioned SQL migrations (PostgreSQL)
│ └── seed.ts # Demo data
├── proxy.ts # Route protection (roles, forced password change)
├── instrumentation*.ts # Sentry error monitoring (server / browser), off without a DSN
├── scripts/
│ ├── run-ts.js # Run TypeScript files with plain Node
│ └── seed.js # npm run db:seed
├── .github/workflows/ci.yml # Lint, types, tests (PostgreSQL) and build on every push
├── tests/ # 160 Vitest tests (5 files)
├── AI-USAGE.md # How AI tools were used and how their output was checked
├── vitest.config.ts # Vitest configuration
└── README.md # Comprehensive enterprise documentation
| Requirement | Implementation Verification | Status |
|---|---|---|
| Role-Based Authentication | JWT with jose re-checked against the database, bcrypt hashing, sign-in rate limiting, roles EMPLOYEE, IT_SUPPORT, ADMIN | ✅ Complete |
| Data Isolation | Employees restricted to own tickets; IT/Admin view entire queue; verified via 8 automated tests | ✅ Complete |
| Ticket Lifecycle Machine | OPEN → ASSIGNED → IN_PROGRESS → RESOLVED → CLOSED; invalid/backward transitions rejected | ✅ Complete |
| Audit Logging | Every status change, assignment and comment logged in TicketHistory as a typed, translatable event | ✅ Complete |
| AI Copilot & Triage | Gemini suggests category, priority and self-help (schema-validated, Accept/Dismiss); summaries for IT staff | ✅ Complete |
| On-Demand AI Translation | Inline translation of tickets & comments into the viewer's language; clear error when AI is unavailable | ✅ Complete |
| Bilingual Localization | Native Arabic (RTL) & English (LTR) language support with persistent cookies/localStorage | ✅ Complete |
| Analytics Dashboard | KPI cards, SLA countdown badges and status chart; the list refreshes every 30s | ✅ Complete |
| Drawer Triage Workflow | Sliding TicketDrawer enabling rapid triage and updates without leaving the dashboard | ✅ Complete |
| Automated Testing | 160 unit & integration tests against the real code, all passing | ✅ Complete |
| Production Build | Clean Next.js 16 production build (npm run build) with zero TypeScript errors | ✅ Complete |
| AI Usage Report | AI-USAGE.md: tools, delegated vs. decided work, validation, AI mistakes caught, rejected proposals |
Engineered with precision for modern enterprise IT service excellence.
| ✅ Complete |