diff --git a/README.md b/README.md index 55dac8f..e06f18a 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,190 @@ -# minmon +# Dashboard Manajemen Layanan Server Private Berbasis Web -This project was created with [Better-T-Stack](https://github.com/AmanVarshney01/create-better-t-stack), a modern TypeScript stack that combines React, TanStack Router, Elysia, TRPC, and more. +Minmon is a student-friendly web application for managing private servers, hosted services, attached domains, and internal server notes. The system focuses on CRUD operations plus lightweight DNS monitoring, not deployment automation. -## Features +## Final Project Scope -- **TypeScript** - For type safety and improved developer experience -- **TanStack Router** - File-based routing with full type safety -- **TailwindCSS** - Utility-first CSS for rapid UI development -- **Shared UI package** - shadcn/ui primitives live in `packages/ui` -- **Elysia** - Type-safe, high-performance framework -- **tRPC** - End-to-end type-safe APIs -- **Bun** - Runtime environment -- **Drizzle** - TypeScript-first ORM -- **PostgreSQL** - Database engine -- **Authentication** - Better-Auth -- **Electrobun** - Lightweight desktop shell for web frontends -- **Turborepo** - Optimized monorepo build system +This app allows an admin to: -## Getting Started +- Manage servers +- Manage services running on each server +- Manage domains attached to services +- Manage notes/documentation for each server +- Run DNS A-record checks and compare resolved IPs with the expected server IP +- View a dashboard summary -First, install the dependencies: +## Main Features + +- Better-auth admin login +- Dashboard summary for: + - total servers + - total services + - total domains + - matched domains + - mismatched domains + - unresolved domains + - inactive/down services + - recent notes +- Server CRUD +- Service CRUD +- Domain CRUD +- DNS check action +- Server notes CRUD +- Sample seeded admin and demo dataset + +## Database / Entity Overview + +### Server + +- id +- name +- primaryIpAddress +- secondaryIpAddress +- operatingSystem +- location +- provider +- description +- status +- createdAt +- updatedAt + +### Service + +- id +- serverId +- name +- type +- internalPort +- externalPort +- protocol +- status +- description +- createdAt +- updatedAt + +### Domain + +- id +- serviceId +- name +- expectedServerIp +- lastResolvedIp +- lastResolvedIps +- resolutionStatus +- lastCheckedAt +- remarks +- lastCheckMessage +- createdAt +- updatedAt + +### Note + +- id +- serverId +- title +- content +- category +- createdAt +- updatedAt + +### Relationships + +- One server has many services +- One server has many notes +- One service belongs to one server +- One service has many domains +- One domain belongs to one service + +## Tech Stack + +- React +- TanStack Router +- tRPC +- PostgreSQL +- Drizzle ORM +- better-auth +- Effect (used narrowly for DNS checking) +- Bun +- Turborepo + +## Local Run Instructions + +1. Install dependencies ```bash bun install ``` -## Database Setup +2. Create `apps/server/.env` -This project uses PostgreSQL with Drizzle ORM. +```env +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/minmon +BETTER_AUTH_SECRET=your-32-char-or-longer-secret-value-here +BETTER_AUTH_URL=http://localhost:3000 +CORS_ORIGIN=http://localhost:5173 +NODE_ENV=development +``` -1. Make sure you have a PostgreSQL database set up. -2. Update your `apps/server/.env` file with your PostgreSQL connection details. +3. Start PostgreSQL if needed -3. Apply the schema to your database: +```bash +bun run db:start +``` + +4. Apply database schema ```bash bun run db:push ``` -Then, run the development server: +5. Seed sample data + +```bash +bun run --filter @minmon/db db:seed +``` + +Seeded admin: + +- Email: `admin@minmon.local` +- Password: `admin12345` + +6. Run the application ```bash bun run dev ``` -Open [http://localhost:5173](http://localhost:5173) in your browser to see the web application. -The API is running at [http://localhost:3000](http://localhost:3000). +7. Open the app -## UI Customization - -React web apps in this stack share shadcn/ui primitives through `packages/ui`. - -- Change design tokens and global styles in `packages/ui/src/styles/globals.css` -- Update shared primitives in `packages/ui/src/components/*` -- Adjust shadcn aliases or style config in `packages/ui/components.json` and `apps/web/components.json` - -### Add more shared components - -Run this from the project root to add more primitives to the shared UI package: - -```bash -npx shadcn@latest add accordion dialog popover sheet table -c packages/ui -``` - -Import shared components like this: - -```tsx -import { Button } from "@minmon/ui/components/button"; -``` - -### Add app-specific blocks - -If you want to add app-specific blocks instead of shared primitives, run the shadcn CLI from `apps/web`. +- Web: `http://localhost:5173` +- API: `http://localhost:3000` ## Project Structure -``` +```text minmon/ ├── apps/ -│ ├── web/ # Frontend application (React + TanStack Router) -│ └── server/ # Backend API (Elysia, TRPC) +│ ├── web/ +│ └── server/ ├── packages/ -│ ├── ui/ # Shared shadcn/ui components and styles -│ ├── api/ # API layer / business logic -│ ├── auth/ # Authentication configuration & logic -│ └── db/ # Database schema & queries +│ ├── api/ +│ ├── auth/ +│ ├── db/ +│ ├── env/ +│ └── ui/ +└── docs/ ``` -## Available Scripts +## Extra Assignment Deliverables -- `bun run dev`: Start all applications in development mode -- `bun run build`: Build all applications -- `bun run dev:web`: Start only the web application -- `bun run dev:server`: Start only the server -- `bun run check-types`: Check TypeScript types across all apps -- `bun run db:push`: Push schema changes to database -- `bun run db:generate`: Generate database client/types -- `bun run db:migrate`: Run database migrations -- `bun run db:studio`: Open database studio UI -- `bun run dev:desktop`: Start the Electrobun desktop app with HMR -- `bun run build:desktop`: Build the stable Electrobun desktop app -- `bun run build:desktop:canary`: Build the canary Electrobun desktop app +See: + +- `docs/run-instructions.md` +- `docs/uml-guidance.md` +- `docs/report-outline.md` + +## Notes + +- DNS checking is limited to A record / IPv4 as required. +- This is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation. +- Full workspace typecheck is still blocked by a pre-existing desktop typing issue for `three`, but the web app and server app are verified. diff --git a/apps/web/src/components/dashboard-shell.tsx b/apps/web/src/components/dashboard-shell.tsx new file mode 100644 index 0000000..a52d923 --- /dev/null +++ b/apps/web/src/components/dashboard-shell.tsx @@ -0,0 +1,123 @@ +import { Button } from "@minmon/ui/components/button"; +import { cn } from "@minmon/ui/lib/utils"; +import { Link, Outlet, useRouterState } from "@tanstack/react-router"; +import { Activity, BookText, Globe, LayoutGrid, Server, Wrench } from "lucide-react"; + +const dashboardLinks = [ + { + to: "/dashboard", + label: "Overview", + description: "Health, alerts, and recent checks", + icon: LayoutGrid, + }, + { + to: "/dashboard/servers", + label: "Servers", + description: "Inventory and host details", + icon: Server, + }, + { + to: "/dashboard/services", + label: "Services", + description: "Processes and uptime targets", + icon: Wrench, + }, + { + to: "/dashboard/domains", + label: "Domains", + description: "DNS, SSL, and expiry status", + icon: Globe, + }, + { + to: "/dashboard/notes", + label: "Notes", + description: "Runbooks and operational handover", + icon: BookText, + }, +] as const; + +export function DashboardShell({ + userName, + userEmail, +}: { + userName?: string | null; + userEmail?: string | null; +}) { + const pathname = useRouterState({ + select: (state) => state.location.pathname, + }); + + return ( +
+
+
+
+
+

+ Server Management Dashboard +

+

Operations overview for demos and day-to-day admin tasks

+

+ Use this area to review monitored assets, inspect detail states, and stage CRUD forms that can connect to tRPC routers as they are added. +

+
+
+
+

Signed in as

+

{userName ?? "Administrator"}

+

{userEmail ?? "admin@minmon.local"}

+
+
+ + + + + + +
+
+
+
+ +
+ + +
+ +
+
+
+
+ ); +} diff --git a/apps/web/src/components/dashboard-ui.tsx b/apps/web/src/components/dashboard-ui.tsx new file mode 100644 index 0000000..201750b --- /dev/null +++ b/apps/web/src/components/dashboard-ui.tsx @@ -0,0 +1,282 @@ +import { Button } from "@minmon/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@minmon/ui/components/card"; +import { Input } from "@minmon/ui/components/input"; +import { Label } from "@minmon/ui/components/label"; +import { StatusBadge } from "@minmon/ui/components/status-badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TableWrapper, +} from "@minmon/ui/components/table"; +import { Textarea } from "@minmon/ui/components/textarea"; +import { Link } from "@tanstack/react-router"; +import { ChevronRight } from "lucide-react"; +import type { ReactNode } from "react"; + +type BadgeVariant = "neutral" | "success" | "warning" | "destructive" | "info"; + +export function PageSection({ title, description, action, children }: SectionProps) { + return ( +
+
+
+

{title}

+ {description ?

{description}

: null} +
+ {action ?
{action}
: null} +
+ {children} +
+ ); +} + +export function SummaryCard({ + label, + value, + hint, + status, +}: { + label: string; + value: string; + hint: string; + status?: { label: string; variant: BadgeVariant }; +}) { + return ( + + + {label} + {status ? {status.label} : null} + + +

{value}

+

{hint}

+
+
+ ); +} + +export function ResourceListCard({ + title, + description, + columns, + items, + emptyTitle, + emptyDescription, +}: { + title: string; + description: string; + columns: { key: string; header: string; render: (item: TItem) => ReactNode; className?: string }[]; + items: TItem[]; + emptyTitle: string; + emptyDescription: string; +}) { + return ( + + + {title} + {description} + + + {items.length ? ( + + + + + {columns.map((column) => ( + + {column.header} + + ))} + + + + {items.map((item, index) => ( + + {columns.map((column) => ( + + {column.render(item)} + + ))} + + ))} + +
+
+ ) : ( + + )} +
+
+ ); +} + +export function DetailCard({ + title, + description, + rows, +}: { + title: string; + description: string; + rows: { label: string; value: ReactNode }[]; +}) { + return ( + + + {title} + {description} + + + {rows.map((row) => ( +
+

{row.label}

+
{row.value}
+
+ ))} +
+
+ ); +} + +export function FormCard({ + title, + description, + children, + footer, +}: { + title: string; + description: string; + children: ReactNode; + footer?: ReactNode; +}) { + return ( + + + {title} + {description} + + {children} + {footer ? {footer} : null} + + ); +} + +export function FieldGroup({ + label, + htmlFor, + description, + children, +}: { + label: string; + htmlFor: string; + description?: string; + children: ReactNode; +}) { + return ( +
+ + {children} + {description ?

{description}

: null} +
+ ); +} + +export function TextField(props: React.ComponentProps & { label: string; description?: string }) { + const { label, description, id, ...inputProps } = props; + + return ( + + + + ); +} + +export function TextAreaField( + props: React.ComponentProps & { label: string; description?: string }, +) { + const { label, description, id, ...textareaProps } = props; + + return ( + +