+
+
Admin Access
+
Sign in to minmon
+
+ Use your administrator account to open the protected monitoring dashboard.
+
+
-
- {(field) => (
- field.handleChange(e.target.value)}
- errors={field.state.meta.errors}
- />
- )}
-
+
+
+ {(field) => (
+
+
Name
+
field.handleChange(e.target.value)}
+ />
+ {field.state.meta.errors.map((error) => (
+
+ {error?.message}
+
+ ))}
+
+ )}
+
+
-
- {(field) => (
- field.handleChange(e.target.value)}
- errors={field.state.meta.errors}
- />
- )}
-
+
+
+ {(field) => (
+
+
Email
+
field.handleChange(e.target.value)}
+ />
+ {field.state.meta.errors.map((error) => (
+
+ {error?.message}
+
+ ))}
+
+ )}
+
+
-
- {(field) => (
- field.handleChange(e.target.value)}
- errors={field.state.meta.errors}
- />
- )}
-
+
+
+ {(field) => (
+
+
Password
+
field.handleChange(e.target.value)}
+ />
+ {field.state.meta.errors.map((error) => (
+
+ {error?.message}
+
+ ))}
+
+ )}
+
+
({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}
>
{({ canSubmit, isSubmitting }) => (
-
- Sign Up
+
+ {isSubmitting ? "Submitting..." : "Sign Up"}
)}
-
+
Already have an account? Sign In
diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts
index af9cab6..83f00c8 100644
--- a/apps/web/src/routeTree.gen.ts
+++ b/apps/web/src/routeTree.gen.ts
@@ -17,14 +17,14 @@ import { Route as DashboardServicesRouteImport } from './routes/dashboard.servic
import { Route as DashboardServersRouteImport } from './routes/dashboard.servers'
import { Route as DashboardNotesRouteImport } from './routes/dashboard.notes'
import { Route as DashboardDomainsRouteImport } from './routes/dashboard.domains'
-import { Route as DashboardServicesNewRouteImport } from './routes/dashboard.services_.new'
-import { Route as DashboardServicesServiceIdRouteImport } from './routes/dashboard.services_.$serviceId'
-import { Route as DashboardServersNewRouteImport } from './routes/dashboard.servers_.new'
-import { Route as DashboardServersServerIdRouteImport } from './routes/dashboard.servers_.$serverId'
-import { Route as DashboardNotesNewRouteImport } from './routes/dashboard.notes_.new'
-import { Route as DashboardNotesNoteIdRouteImport } from './routes/dashboard.notes_.$noteId'
-import { Route as DashboardDomainsNewRouteImport } from './routes/dashboard.domains_.new'
-import { Route as DashboardDomainsDomainIdRouteImport } from './routes/dashboard.domains_.$domainId'
+import { Route as DashboardServicesNewRouteImport } from './routes/dashboard.services.new'
+import { Route as DashboardServicesServiceIdRouteImport } from './routes/dashboard.services.$serviceId'
+import { Route as DashboardServersNewRouteImport } from './routes/dashboard.servers.new'
+import { Route as DashboardServersServerIdRouteImport } from './routes/dashboard.servers.$serverId'
+import { Route as DashboardNotesNewRouteImport } from './routes/dashboard.notes.new'
+import { Route as DashboardNotesNoteIdRouteImport } from './routes/dashboard.notes.$noteId'
+import { Route as DashboardDomainsNewRouteImport } from './routes/dashboard.domains.new'
+import { Route as DashboardDomainsDomainIdRouteImport } from './routes/dashboard.domains.$domainId'
const LoginRoute = LoginRouteImport.update({
id: '/login',
@@ -67,57 +67,57 @@ const DashboardDomainsRoute = DashboardDomainsRouteImport.update({
getParentRoute: () => DashboardRoute,
} as any)
const DashboardServicesNewRoute = DashboardServicesNewRouteImport.update({
- id: '/services_/new',
- path: '/services/new',
- getParentRoute: () => DashboardRoute,
+ id: '/new',
+ path: '/new',
+ getParentRoute: () => DashboardServicesRoute,
} as any)
const DashboardServicesServiceIdRoute =
DashboardServicesServiceIdRouteImport.update({
- id: '/services_/$serviceId',
- path: '/services/$serviceId',
- getParentRoute: () => DashboardRoute,
+ id: '/$serviceId',
+ path: '/$serviceId',
+ getParentRoute: () => DashboardServicesRoute,
} as any)
const DashboardServersNewRoute = DashboardServersNewRouteImport.update({
- id: '/servers_/new',
- path: '/servers/new',
- getParentRoute: () => DashboardRoute,
+ id: '/new',
+ path: '/new',
+ getParentRoute: () => DashboardServersRoute,
} as any)
const DashboardServersServerIdRoute =
DashboardServersServerIdRouteImport.update({
- id: '/servers_/$serverId',
- path: '/servers/$serverId',
- getParentRoute: () => DashboardRoute,
+ id: '/$serverId',
+ path: '/$serverId',
+ getParentRoute: () => DashboardServersRoute,
} as any)
const DashboardNotesNewRoute = DashboardNotesNewRouteImport.update({
- id: '/notes_/new',
- path: '/notes/new',
- getParentRoute: () => DashboardRoute,
+ id: '/new',
+ path: '/new',
+ getParentRoute: () => DashboardNotesRoute,
} as any)
const DashboardNotesNoteIdRoute = DashboardNotesNoteIdRouteImport.update({
- id: '/notes_/$noteId',
- path: '/notes/$noteId',
- getParentRoute: () => DashboardRoute,
+ id: '/$noteId',
+ path: '/$noteId',
+ getParentRoute: () => DashboardNotesRoute,
} as any)
const DashboardDomainsNewRoute = DashboardDomainsNewRouteImport.update({
- id: '/domains_/new',
- path: '/domains/new',
- getParentRoute: () => DashboardRoute,
+ id: '/new',
+ path: '/new',
+ getParentRoute: () => DashboardDomainsRoute,
} as any)
const DashboardDomainsDomainIdRoute =
DashboardDomainsDomainIdRouteImport.update({
- id: '/domains_/$domainId',
- path: '/domains/$domainId',
- getParentRoute: () => DashboardRoute,
+ id: '/$domainId',
+ path: '/$domainId',
+ getParentRoute: () => DashboardDomainsRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteWithChildren
'/login': typeof LoginRoute
- '/dashboard/domains': typeof DashboardDomainsRoute
- '/dashboard/notes': typeof DashboardNotesRoute
- '/dashboard/servers': typeof DashboardServersRoute
- '/dashboard/services': typeof DashboardServicesRoute
+ '/dashboard/domains': typeof DashboardDomainsRouteWithChildren
+ '/dashboard/notes': typeof DashboardNotesRouteWithChildren
+ '/dashboard/servers': typeof DashboardServersRouteWithChildren
+ '/dashboard/services': typeof DashboardServicesRouteWithChildren
'/dashboard/': typeof DashboardIndexRoute
'/dashboard/domains/$domainId': typeof DashboardDomainsDomainIdRoute
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
@@ -131,10 +131,10 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/login': typeof LoginRoute
- '/dashboard/domains': typeof DashboardDomainsRoute
- '/dashboard/notes': typeof DashboardNotesRoute
- '/dashboard/servers': typeof DashboardServersRoute
- '/dashboard/services': typeof DashboardServicesRoute
+ '/dashboard/domains': typeof DashboardDomainsRouteWithChildren
+ '/dashboard/notes': typeof DashboardNotesRouteWithChildren
+ '/dashboard/servers': typeof DashboardServersRouteWithChildren
+ '/dashboard/services': typeof DashboardServicesRouteWithChildren
'/dashboard': typeof DashboardIndexRoute
'/dashboard/domains/$domainId': typeof DashboardDomainsDomainIdRoute
'/dashboard/domains/new': typeof DashboardDomainsNewRoute
@@ -150,19 +150,19 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteWithChildren
'/login': typeof LoginRoute
- '/dashboard/domains': typeof DashboardDomainsRoute
- '/dashboard/notes': typeof DashboardNotesRoute
- '/dashboard/servers': typeof DashboardServersRoute
- '/dashboard/services': typeof DashboardServicesRoute
+ '/dashboard/domains': typeof DashboardDomainsRouteWithChildren
+ '/dashboard/notes': typeof DashboardNotesRouteWithChildren
+ '/dashboard/servers': typeof DashboardServersRouteWithChildren
+ '/dashboard/services': typeof DashboardServicesRouteWithChildren
'/dashboard/': typeof DashboardIndexRoute
- '/dashboard/domains_/$domainId': typeof DashboardDomainsDomainIdRoute
- '/dashboard/domains_/new': typeof DashboardDomainsNewRoute
- '/dashboard/notes_/$noteId': typeof DashboardNotesNoteIdRoute
- '/dashboard/notes_/new': typeof DashboardNotesNewRoute
- '/dashboard/servers_/$serverId': typeof DashboardServersServerIdRoute
- '/dashboard/servers_/new': typeof DashboardServersNewRoute
- '/dashboard/services_/$serviceId': typeof DashboardServicesServiceIdRoute
- '/dashboard/services_/new': typeof DashboardServicesNewRoute
+ '/dashboard/domains/$domainId': typeof DashboardDomainsDomainIdRoute
+ '/dashboard/domains/new': typeof DashboardDomainsNewRoute
+ '/dashboard/notes/$noteId': typeof DashboardNotesNoteIdRoute
+ '/dashboard/notes/new': typeof DashboardNotesNewRoute
+ '/dashboard/servers/$serverId': typeof DashboardServersServerIdRoute
+ '/dashboard/servers/new': typeof DashboardServersNewRoute
+ '/dashboard/services/$serviceId': typeof DashboardServicesServiceIdRoute
+ '/dashboard/services/new': typeof DashboardServicesNewRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -210,14 +210,14 @@ export interface FileRouteTypes {
| '/dashboard/servers'
| '/dashboard/services'
| '/dashboard/'
- | '/dashboard/domains_/$domainId'
- | '/dashboard/domains_/new'
- | '/dashboard/notes_/$noteId'
- | '/dashboard/notes_/new'
- | '/dashboard/servers_/$serverId'
- | '/dashboard/servers_/new'
- | '/dashboard/services_/$serviceId'
- | '/dashboard/services_/new'
+ | '/dashboard/domains/$domainId'
+ | '/dashboard/domains/new'
+ | '/dashboard/notes/$noteId'
+ | '/dashboard/notes/new'
+ | '/dashboard/servers/$serverId'
+ | '/dashboard/servers/new'
+ | '/dashboard/services/$serviceId'
+ | '/dashboard/services/new'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
@@ -284,97 +284,134 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardDomainsRouteImport
parentRoute: typeof DashboardRoute
}
- '/dashboard/services_/new': {
- id: '/dashboard/services_/new'
- path: '/services/new'
+ '/dashboard/services/new': {
+ id: '/dashboard/services/new'
+ path: '/new'
fullPath: '/dashboard/services/new'
preLoaderRoute: typeof DashboardServicesNewRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardServicesRoute
}
- '/dashboard/services_/$serviceId': {
- id: '/dashboard/services_/$serviceId'
- path: '/services/$serviceId'
+ '/dashboard/services/$serviceId': {
+ id: '/dashboard/services/$serviceId'
+ path: '/$serviceId'
fullPath: '/dashboard/services/$serviceId'
preLoaderRoute: typeof DashboardServicesServiceIdRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardServicesRoute
}
- '/dashboard/servers_/new': {
- id: '/dashboard/servers_/new'
- path: '/servers/new'
+ '/dashboard/servers/new': {
+ id: '/dashboard/servers/new'
+ path: '/new'
fullPath: '/dashboard/servers/new'
preLoaderRoute: typeof DashboardServersNewRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardServersRoute
}
- '/dashboard/servers_/$serverId': {
- id: '/dashboard/servers_/$serverId'
- path: '/servers/$serverId'
+ '/dashboard/servers/$serverId': {
+ id: '/dashboard/servers/$serverId'
+ path: '/$serverId'
fullPath: '/dashboard/servers/$serverId'
preLoaderRoute: typeof DashboardServersServerIdRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardServersRoute
}
- '/dashboard/notes_/new': {
- id: '/dashboard/notes_/new'
- path: '/notes/new'
+ '/dashboard/notes/new': {
+ id: '/dashboard/notes/new'
+ path: '/new'
fullPath: '/dashboard/notes/new'
preLoaderRoute: typeof DashboardNotesNewRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardNotesRoute
}
- '/dashboard/notes_/$noteId': {
- id: '/dashboard/notes_/$noteId'
- path: '/notes/$noteId'
+ '/dashboard/notes/$noteId': {
+ id: '/dashboard/notes/$noteId'
+ path: '/$noteId'
fullPath: '/dashboard/notes/$noteId'
preLoaderRoute: typeof DashboardNotesNoteIdRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardNotesRoute
}
- '/dashboard/domains_/new': {
- id: '/dashboard/domains_/new'
- path: '/domains/new'
+ '/dashboard/domains/new': {
+ id: '/dashboard/domains/new'
+ path: '/new'
fullPath: '/dashboard/domains/new'
preLoaderRoute: typeof DashboardDomainsNewRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardDomainsRoute
}
- '/dashboard/domains_/$domainId': {
- id: '/dashboard/domains_/$domainId'
- path: '/domains/$domainId'
+ '/dashboard/domains/$domainId': {
+ id: '/dashboard/domains/$domainId'
+ path: '/$domainId'
fullPath: '/dashboard/domains/$domainId'
preLoaderRoute: typeof DashboardDomainsDomainIdRouteImport
- parentRoute: typeof DashboardRoute
+ parentRoute: typeof DashboardDomainsRoute
}
}
}
-interface DashboardRouteChildren {
- DashboardDomainsRoute: typeof DashboardDomainsRoute
- DashboardNotesRoute: typeof DashboardNotesRoute
- DashboardServersRoute: typeof DashboardServersRoute
- DashboardServicesRoute: typeof DashboardServicesRoute
- DashboardIndexRoute: typeof DashboardIndexRoute
+interface DashboardDomainsRouteChildren {
DashboardDomainsDomainIdRoute: typeof DashboardDomainsDomainIdRoute
DashboardDomainsNewRoute: typeof DashboardDomainsNewRoute
+}
+
+const DashboardDomainsRouteChildren: DashboardDomainsRouteChildren = {
+ DashboardDomainsDomainIdRoute: DashboardDomainsDomainIdRoute,
+ DashboardDomainsNewRoute: DashboardDomainsNewRoute,
+}
+
+const DashboardDomainsRouteWithChildren =
+ DashboardDomainsRoute._addFileChildren(DashboardDomainsRouteChildren)
+
+interface DashboardNotesRouteChildren {
DashboardNotesNoteIdRoute: typeof DashboardNotesNoteIdRoute
DashboardNotesNewRoute: typeof DashboardNotesNewRoute
+}
+
+const DashboardNotesRouteChildren: DashboardNotesRouteChildren = {
+ DashboardNotesNoteIdRoute: DashboardNotesNoteIdRoute,
+ DashboardNotesNewRoute: DashboardNotesNewRoute,
+}
+
+const DashboardNotesRouteWithChildren = DashboardNotesRoute._addFileChildren(
+ DashboardNotesRouteChildren,
+)
+
+interface DashboardServersRouteChildren {
DashboardServersServerIdRoute: typeof DashboardServersServerIdRoute
DashboardServersNewRoute: typeof DashboardServersNewRoute
+}
+
+const DashboardServersRouteChildren: DashboardServersRouteChildren = {
+ DashboardServersServerIdRoute: DashboardServersServerIdRoute,
+ DashboardServersNewRoute: DashboardServersNewRoute,
+}
+
+const DashboardServersRouteWithChildren =
+ DashboardServersRoute._addFileChildren(DashboardServersRouteChildren)
+
+interface DashboardServicesRouteChildren {
DashboardServicesServiceIdRoute: typeof DashboardServicesServiceIdRoute
DashboardServicesNewRoute: typeof DashboardServicesNewRoute
}
-const DashboardRouteChildren: DashboardRouteChildren = {
- DashboardDomainsRoute: DashboardDomainsRoute,
- DashboardNotesRoute: DashboardNotesRoute,
- DashboardServersRoute: DashboardServersRoute,
- DashboardServicesRoute: DashboardServicesRoute,
- DashboardIndexRoute: DashboardIndexRoute,
- DashboardDomainsDomainIdRoute: DashboardDomainsDomainIdRoute,
- DashboardDomainsNewRoute: DashboardDomainsNewRoute,
- DashboardNotesNoteIdRoute: DashboardNotesNoteIdRoute,
- DashboardNotesNewRoute: DashboardNotesNewRoute,
- DashboardServersServerIdRoute: DashboardServersServerIdRoute,
- DashboardServersNewRoute: DashboardServersNewRoute,
+const DashboardServicesRouteChildren: DashboardServicesRouteChildren = {
DashboardServicesServiceIdRoute: DashboardServicesServiceIdRoute,
DashboardServicesNewRoute: DashboardServicesNewRoute,
}
+const DashboardServicesRouteWithChildren =
+ DashboardServicesRoute._addFileChildren(DashboardServicesRouteChildren)
+
+interface DashboardRouteChildren {
+ DashboardDomainsRoute: typeof DashboardDomainsRouteWithChildren
+ DashboardNotesRoute: typeof DashboardNotesRouteWithChildren
+ DashboardServersRoute: typeof DashboardServersRouteWithChildren
+ DashboardServicesRoute: typeof DashboardServicesRouteWithChildren
+ DashboardIndexRoute: typeof DashboardIndexRoute
+}
+
+const DashboardRouteChildren: DashboardRouteChildren = {
+ DashboardDomainsRoute: DashboardDomainsRouteWithChildren,
+ DashboardNotesRoute: DashboardNotesRouteWithChildren,
+ DashboardServersRoute: DashboardServersRouteWithChildren,
+ DashboardServicesRoute: DashboardServicesRouteWithChildren,
+ DashboardIndexRoute: DashboardIndexRoute,
+}
+
const DashboardRouteWithChildren = DashboardRoute._addFileChildren(
DashboardRouteChildren,
)
diff --git a/apps/web/src/routes/dashboard.domains_.$domainId.tsx b/apps/web/src/routes/dashboard.domains.$domainId.tsx
similarity index 72%
rename from apps/web/src/routes/dashboard.domains_.$domainId.tsx
rename to apps/web/src/routes/dashboard.domains.$domainId.tsx
index 17248a7..5f15dc4 100644
--- a/apps/web/src/routes/dashboard.domains_.$domainId.tsx
+++ b/apps/web/src/routes/dashboard.domains.$domainId.tsx
@@ -1,14 +1,13 @@
-import { DetailCard, FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
-import { queryClient, trpc, trpcClient } from "@/utils/trpc";
import { Button } from "@minmon/ui/components/button";
import { StatusBadge } from "@minmon/ui/components/status-badge";
import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
-import { useEffect } from "react";
import { toast } from "sonner";
+import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/domains_/$domainId")({
+export const Route = createFileRoute("/dashboard/domains/$domainId")({
component: RouteComponent,
});
@@ -41,17 +40,8 @@ function RouteComponent() {
remarks: values.remarks || null,
}),
onSuccess: async () => {
- try {
- await trpcClient.domains.check.mutate({ id: domainId });
- toast.success("Domain updated and DNS re-checked");
- } catch (error) {
- toast.success("Domain updated");
- toast.error(
- error instanceof Error ? `DNS check failed: ${error.message}` : "DNS check failed",
- );
- }
- await queryClient.invalidateQueries({ queryKey: trpc.domains.byId.queryKey({ id: domainId }) });
- await queryClient.invalidateQueries({ queryKey: trpc.domains.list.queryKey() });
+ toast.success("Domain updated");
+ await queryClient.invalidateQueries();
},
onError: (error) => toast.error(error.message),
});
@@ -59,48 +49,32 @@ function RouteComponent() {
mutationFn: async () => trpcClient.domains.check.mutate({ id: domainId }),
onSuccess: async () => {
toast.success("DNS check completed");
- await queryClient.invalidateQueries({ queryKey: trpc.domains.byId.queryKey({ id: domainId }) });
- await queryClient.invalidateQueries({ queryKey: trpc.domains.list.queryKey() });
+ await queryClient.invalidateQueries();
},
onError: (error) => toast.error(error.message),
});
- const form = useForm({
- defaultValues: {
- serviceId: "",
- name: "",
- expectedServerIp: "",
- remarks: "",
- } satisfies DomainFormValues,
- onSubmit: async ({ value }) => {
- await updateDomain.mutateAsync(value);
- },
- });
+ if (domain.isLoading || services.isLoading) {
+ return
;
+ }
- useEffect(() => {
- if (!domain.data) {
- return;
- }
+ if (domain.isError || services.isError || !domain.data || !services.data) {
+ return
;
+ }
- form.reset({
+ const defaultValues: DomainFormValues = {
serviceId: domain.data.serviceId,
name: domain.data.name,
expectedServerIp: domain.data.expectedServerIp,
remarks: domain.data.remarks ?? "",
- });
- }, [domain.data, form]);
+ };
- if (!domain.data || !services.data) {
- if (domain.isError || services.isError) {
- return
;
- }
- return
;
- }
-
- const serviceOptions = services.data.map((item) => ({
- value: item.id,
- label: `${item.name} (${item.id})`,
- }));
+ const form = useForm({
+ defaultValues,
+ onSubmit: async ({ value }) => {
+ await updateDomain.mutateAsync(value);
+ },
+ });
return (
@@ -140,7 +114,7 @@ function RouteComponent() {
Check DNS
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save changes }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save changes"} }
>
@@ -148,15 +122,13 @@ function RouteComponent() {
>
{(field) => (
- field.handleChange(e.target.value)}
+ description={`Available: ${services.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
/>
)}
diff --git a/apps/web/src/routes/dashboard.domains_.new.tsx b/apps/web/src/routes/dashboard.domains.new.tsx
similarity index 76%
rename from apps/web/src/routes/dashboard.domains_.new.tsx
rename to apps/web/src/routes/dashboard.domains.new.tsx
index 76091aa..64abc47 100644
--- a/apps/web/src/routes/dashboard.domains_.new.tsx
+++ b/apps/web/src/routes/dashboard.domains.new.tsx
@@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
-import { FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/domains_/new")({
+export const Route = createFileRoute("/dashboard/domains/new")({
component: RouteComponent,
});
@@ -29,16 +29,8 @@ function RouteComponent() {
remarks: values.remarks || null,
}),
onSuccess: async (created) => {
- try {
- await trpcClient.domains.check.mutate({ id: created.id });
- toast.success("Domain created and DNS checked");
- } catch (error) {
- toast.success("Domain created");
- toast.error(
- error instanceof Error ? `DNS check failed: ${error.message}` : "DNS check failed",
- );
- }
- await queryClient.invalidateQueries({ queryKey: trpc.domains.list.queryKey() });
+ toast.success("Domain created");
+ await queryClient.invalidateQueries();
navigate({ to: "/dashboard/domains/$domainId", params: { domainId: created.id } });
},
onError: (error) => toast.error(error.message),
@@ -66,11 +58,6 @@ function RouteComponent() {
return
;
}
- const serviceOptions = services.data.map((item) => ({
- value: item.id,
- label: `${item.name} (${item.id})`,
- }));
-
return (
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save domain }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save domain"} }
>
}
>
{(field) => (
- field.handleChange(e.target.value)}
+ description={`Available: ${services.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
/>
)}
diff --git a/apps/web/src/routes/dashboard.domains.tsx b/apps/web/src/routes/dashboard.domains.tsx
index ff1d559..85054e4 100644
--- a/apps/web/src/routes/dashboard.domains.tsx
+++ b/apps/web/src/routes/dashboard.domains.tsx
@@ -1,11 +1,10 @@
import { Button } from "@minmon/ui/components/button";
-import { statusBadgeVariants } from "@minmon/ui/components/status-badge";
-import { cn } from "@minmon/ui/lib/utils";
+import { StatusBadge } from "@minmon/ui/components/status-badge";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, Link } from "@tanstack/react-router";
import { toast } from "sonner";
-import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
+import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
export const Route = createFileRoute("/dashboard/domains")({
@@ -94,29 +93,38 @@ function RouteComponent() {
header: "DNS status",
className: "w-36",
render: (item: (typeof domains.data)[number]) => (
- checkDns.mutate(item.id)}
- >
- {item.resolutionStatus}
-
+ {item.resolutionStatus}
),
},
{
key: "actions",
header: "Actions",
- className: "w-16",
+ className: "w-44",
render: (item: (typeof domains.data)[number]) => (
- removeDomain.mutate(item.id)}
- />
+
+ checkDns.mutate(item.id)}
+ >
+ Check DNS
+
+ {
+ if (confirm(`Delete domain \"${item.name}\"?`)) {
+ removeDomain.mutate(item.id);
+ }
+ }}
+ >
+ Delete
+
+
),
},
]}
diff --git a/apps/web/src/routes/dashboard.notes_.$noteId.tsx b/apps/web/src/routes/dashboard.notes.$noteId.tsx
similarity index 78%
rename from apps/web/src/routes/dashboard.notes_.$noteId.tsx
rename to apps/web/src/routes/dashboard.notes.$noteId.tsx
index 03c329b..c56fc07 100644
--- a/apps/web/src/routes/dashboard.notes_.$noteId.tsx
+++ b/apps/web/src/routes/dashboard.notes.$noteId.tsx
@@ -2,12 +2,11 @@ import { Button } from "@minmon/ui/components/button";
import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
-import { useEffect } from "react";
import { toast } from "sonner";
-import { DetailCard, FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/notes_/$noteId")({
+export const Route = createFileRoute("/dashboard/notes/$noteId")({
component: RouteComponent,
});
@@ -40,11 +39,19 @@ function RouteComponent() {
onError: (error) => toast.error(error.message),
});
+ if (note.isLoading || servers.isLoading) {
+ return ;
+ }
+
+ if (note.isError || servers.isError || !note.data || !servers.data) {
+ return ;
+ }
+
const defaultValues: NoteFormValues = {
- serverId: "",
- title: "",
- category: "",
- content: "",
+ serverId: note.data.serverId,
+ title: note.data.title,
+ category: note.data.category ?? "",
+ content: note.data.content,
};
const form = useForm({
@@ -54,33 +61,6 @@ function RouteComponent() {
},
});
- useEffect(() => {
- if (!note.data) {
- return;
- }
-
- form.reset({
- serverId: note.data.serverId,
- title: note.data.title,
- category: note.data.category ?? "",
- content: note.data.content,
- });
- }, [note.data, form]);
-
- if (note.isLoading || servers.isLoading) {
- return ;
- }
-
- if (note.isError || servers.isError || !note.data || !servers.data) {
- return ;
- }
-
- const serverMap = new Map(servers.data.map((item) => [item.id, item.name]));
- const serverOptions = servers.data.map((item) => ({
- value: item.id,
- label: `${item.name} (${item.id})`,
- }));
-
return (
@@ -88,7 +68,7 @@ function RouteComponent() {
title="Note summary"
description="Current note metadata and content preview."
rows={[
- { label: "Server", value: serverMap.get(note.data.serverId) || note.data.serverId },
+ { label: "Server ID", value: note.data.serverId },
{ label: "Category", value: note.data.category || "-" },
{ label: "Updated", value: new Date(note.data.updatedAt).toLocaleString() },
{ label: "Content", value: note.data.content },
@@ -113,22 +93,20 @@ function RouteComponent() {
Back
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save changes }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save changes"} }
>
}
>
{(field) => (
- field.handleChange(e.target.value)}
+ description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
/>
)}
diff --git a/apps/web/src/routes/dashboard.notes_.new.tsx b/apps/web/src/routes/dashboard.notes.new.tsx
similarity index 83%
rename from apps/web/src/routes/dashboard.notes_.new.tsx
rename to apps/web/src/routes/dashboard.notes.new.tsx
index f00231a..f7bf661 100644
--- a/apps/web/src/routes/dashboard.notes_.new.tsx
+++ b/apps/web/src/routes/dashboard.notes.new.tsx
@@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
-import { FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/notes_/new")({
+export const Route = createFileRoute("/dashboard/notes/new")({
component: RouteComponent,
});
@@ -58,11 +58,6 @@ function RouteComponent() {
return ;
}
- const serverOptions = servers.data.map((item) => ({
- value: item.id,
- label: `${item.name} (${item.id})`,
- }));
-
return (
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save note }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save note"} }
>
}
>
{(field) => (
- field.handleChange(e.target.value)}
+ description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
/>
)}
diff --git a/apps/web/src/routes/dashboard.notes.tsx b/apps/web/src/routes/dashboard.notes.tsx
index 7152a4d..be68bfa 100644
--- a/apps/web/src/routes/dashboard.notes.tsx
+++ b/apps/web/src/routes/dashboard.notes.tsx
@@ -3,7 +3,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, Link } from "@tanstack/react-router";
import { toast } from "sonner";
-import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
+import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
export const Route = createFileRoute("/dashboard/notes")({
@@ -76,13 +76,21 @@ function RouteComponent() {
{
key: "actions",
header: "Actions",
- className: "w-16",
+ className: "w-28",
render: (item: (typeof notes.data)[number]) => (
- removeNote.mutate(item.id)}
- />
+ onClick={() => {
+ if (confirm(`Delete note \"${item.title}\"?`)) {
+ removeNote.mutate(item.id);
+ }
+ }}
+ >
+ Delete
+
),
},
]}
diff --git a/apps/web/src/routes/dashboard.servers_.$serverId.tsx b/apps/web/src/routes/dashboard.servers.$serverId.tsx
similarity index 63%
rename from apps/web/src/routes/dashboard.servers_.$serverId.tsx
rename to apps/web/src/routes/dashboard.servers.$serverId.tsx
index 03c01fd..a0253f7 100644
--- a/apps/web/src/routes/dashboard.servers_.$serverId.tsx
+++ b/apps/web/src/routes/dashboard.servers.$serverId.tsx
@@ -2,13 +2,12 @@ import { Button } from "@minmon/ui/components/button";
import { StatusBadge } from "@minmon/ui/components/status-badge";
import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
-import { useEffect } from "react";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
-import { DetailCard, FormCard, PageSection, QueryStateCard, ResourceListCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/servers_/$serverId")({
+export const Route = createFileRoute("/dashboard/servers/$serverId")({
component: RouteComponent,
});
@@ -23,17 +22,10 @@ type ServerFormValues = {
status: "active" | "maintenance" | "inactive";
};
-const serverStatusOptions = [
- { value: "active", label: "Active" },
- { value: "maintenance", label: "Maintenance" },
- { value: "inactive", label: "Inactive" },
-] as const;
-
function RouteComponent() {
const { serverId } = Route.useParams();
const navigate = useNavigate();
const server = useQuery(trpc.servers.byId.queryOptions({ id: serverId }));
- const notes = useQuery(trpc.notes.list.queryOptions({ serverId }));
const updateServer = useMutation({
mutationFn: async (values: ServerFormValues) =>
@@ -53,30 +45,15 @@ function RouteComponent() {
onError: (error) => toast.error(error.message),
});
+ if (server.isLoading) {
+ return ;
+ }
+
+ if (server.isError || !server.data) {
+ return ;
+ }
+
const defaultValues: ServerFormValues = {
- name: "",
- primaryIpAddress: "",
- secondaryIpAddress: "",
- operatingSystem: "",
- location: "",
- provider: "",
- description: "",
- status: "active",
- };
-
- const form = useForm({
- defaultValues,
- onSubmit: async ({ value }) => {
- await updateServer.mutateAsync(value);
- },
- });
-
- useEffect(() => {
- if (!server.data) {
- return;
- }
-
- form.reset({
name: server.data.name,
primaryIpAddress: server.data.primaryIpAddress,
secondaryIpAddress: server.data.secondaryIpAddress ?? "",
@@ -85,16 +62,14 @@ function RouteComponent() {
provider: server.data.provider ?? "",
description: server.data.description ?? "",
status: server.data.status,
- });
- }, [server.data, form]);
+ };
- if (server.isLoading) {
- return ;
- }
-
- if (server.isError || !server.data) {
- return ;
- }
+ const form = useForm({
+ defaultValues,
+ onSubmit: async ({ value }) => {
+ await updateServer.mutateAsync(value);
+ },
+ });
return (
@@ -115,54 +90,6 @@ function RouteComponent() {
/>
- {notes.isLoading ? (
-
- ) : notes.isError || !notes.data ? (
-
- ) : (
-
- New note
-
- }
- >
- (
-
-
- {item.title}
-
- {item.content}
-
- ),
- },
- {
- key: "category",
- header: "Category",
- render: (item: (typeof notes.data)[number]) => item.category || "-",
- },
- {
- key: "updatedAt",
- header: "Updated",
- render: (item: (typeof notes.data)[number]) => new Date(item.updatedAt).toLocaleString(),
- },
- ]}
- items={notes.data}
- emptyTitle="No notes for this server"
- emptyDescription="Add a note to document maintenance, incidents, or internal server context."
- />
-
- )}
-
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save changes }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save changes"} }
>
}
@@ -192,7 +119,7 @@ function RouteComponent() {
{(field) => field.handleChange(e.target.value)} />}
{(field) => field.handleChange(e.target.value)} />}
{(field) => field.handleChange(e.target.value)} />}
-
{(field) => field.handleChange(value as ServerFormValues["status"])} placeholder="Select server status" options={[...serverStatusOptions]} />}
+
{(field) => field.handleChange(e.target.value as "active" | "maintenance" | "inactive")} />}
{(field) => field.handleChange(e.target.value)} />}
diff --git a/apps/web/src/routes/dashboard.servers_.new.tsx b/apps/web/src/routes/dashboard.servers.new.tsx
similarity index 89%
rename from apps/web/src/routes/dashboard.servers_.new.tsx
rename to apps/web/src/routes/dashboard.servers.new.tsx
index d17acee..8803cb2 100644
--- a/apps/web/src/routes/dashboard.servers_.new.tsx
+++ b/apps/web/src/routes/dashboard.servers.new.tsx
@@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
-import { FormCard, PageSection, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { FormCard, PageSection, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/servers_/new")({
+export const Route = createFileRoute("/dashboard/servers/new")({
component: RouteComponent,
});
@@ -21,12 +21,6 @@ type ServerFormValues = {
status: "active" | "maintenance" | "inactive";
};
-const serverStatusOptions = [
- { value: "active", label: "Active" },
- { value: "maintenance", label: "Maintenance" },
- { value: "inactive", label: "Inactive" },
-] as const;
-
function RouteComponent() {
const navigate = useNavigate();
const createServer = useMutation({
@@ -86,7 +80,7 @@ function RouteComponent() {
Cancel
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save server }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save server"} }
>
}
@@ -166,14 +160,13 @@ function RouteComponent() {
{(field) => (
- field.handleChange(value as ServerFormValues["status"])}
- placeholder="Select server status"
- options={[...serverStatusOptions]}
+ onChange={(e) => field.handleChange(e.target.value as "active" | "maintenance" | "inactive")}
+ placeholder="active"
/>
)}
diff --git a/apps/web/src/routes/dashboard.servers.tsx b/apps/web/src/routes/dashboard.servers.tsx
index 7c2c2cc..a1899e0 100644
--- a/apps/web/src/routes/dashboard.servers.tsx
+++ b/apps/web/src/routes/dashboard.servers.tsx
@@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, Link } from "@tanstack/react-router";
import { toast } from "sonner";
-import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
+import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
export const Route = createFileRoute("/dashboard/servers")({
@@ -99,13 +99,21 @@ function RouteComponent() {
{
key: "actions",
header: "Actions",
- className: "w-16",
+ className: "w-28",
render: (item: (typeof servers.data)[number]) => (
- removeServer.mutate(item.id)}
- />
+ onClick={() => {
+ if (confirm(`Delete server \"${item.name}\" and all related services, domains, and notes?`)) {
+ removeServer.mutate(item.id);
+ }
+ }}
+ >
+ Delete
+
),
},
]}
diff --git a/apps/web/src/routes/dashboard.services_.$serviceId.tsx b/apps/web/src/routes/dashboard.services.$serviceId.tsx
similarity index 67%
rename from apps/web/src/routes/dashboard.services_.$serviceId.tsx
rename to apps/web/src/routes/dashboard.services.$serviceId.tsx
index 015ba03..598ee1d 100644
--- a/apps/web/src/routes/dashboard.services_.$serviceId.tsx
+++ b/apps/web/src/routes/dashboard.services.$serviceId.tsx
@@ -3,12 +3,11 @@ import { StatusBadge } from "@minmon/ui/components/status-badge";
import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
-import { useEffect } from "react";
import { toast } from "sonner";
-import { DetailCard, FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { DetailCard, FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/services_/$serviceId")({
+export const Route = createFileRoute("/dashboard/services/$serviceId")({
component: RouteComponent,
});
@@ -23,29 +22,6 @@ type ServiceFormValues = {
description: string;
};
-const serviceTypeOptions = [
- { value: "web_app", label: "Web app" },
- { value: "mail", label: "Mail" },
- { value: "database", label: "Database" },
- { value: "api", label: "API" },
- { value: "proxy", label: "Proxy" },
- { value: "other", label: "Other" },
-] as const;
-
-const serviceProtocolOptions = [
- { value: "http", label: "HTTP" },
- { value: "https", label: "HTTPS" },
- { value: "tcp", label: "TCP" },
- { value: "udp", label: "UDP" },
- { value: "other", label: "Other" },
-] as const;
-
-const serviceStatusOptions = [
- { value: "active", label: "Active" },
- { value: "down", label: "Down" },
- { value: "maintenance", label: "Maintenance" },
-] as const;
-
function RouteComponent() {
const { serviceId } = Route.useParams();
const navigate = useNavigate();
@@ -72,41 +48,6 @@ function RouteComponent() {
onError: (error) => toast.error(error.message),
});
- const defaultValues: ServiceFormValues = {
- serverId: "",
- name: "",
- type: "web_app",
- internalPort: "",
- externalPort: "",
- protocol: "http",
- status: "active",
- description: "",
- };
-
- const form = useForm({
- defaultValues,
- onSubmit: async ({ value }) => {
- await updateService.mutateAsync(value);
- },
- });
-
- useEffect(() => {
- if (!service.data) {
- return;
- }
-
- form.reset({
- serverId: service.data.serverId,
- name: service.data.name,
- type: service.data.type,
- internalPort: service.data.internalPort?.toString() ?? "",
- externalPort: service.data.externalPort?.toString() ?? "",
- protocol: service.data.protocol,
- status: service.data.status,
- description: service.data.description ?? "",
- });
- }, [service.data, form]);
-
if (service.isLoading || servers.isLoading) {
return ;
}
@@ -115,10 +56,23 @@ function RouteComponent() {
return ;
}
- const serverOptions = servers.data.map((item) => ({
- value: item.id,
- label: `${item.name} (${item.id})`,
- }));
+ const defaultValues: ServiceFormValues = {
+ serverId: service.data.serverId,
+ name: service.data.name,
+ type: service.data.type,
+ internalPort: service.data.internalPort?.toString() ?? "",
+ externalPort: service.data.externalPort?.toString() ?? "",
+ protocol: service.data.protocol,
+ status: service.data.status,
+ description: service.data.description ?? "",
+ };
+
+ const form = useForm({
+ defaultValues,
+ onSubmit: async ({ value }) => {
+ await updateService.mutateAsync(value);
+ },
+ });
return (
@@ -155,32 +109,30 @@ function RouteComponent() {
Back
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save changes }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save changes"} }
>
}
>
{(field) => (
- field.handleChange(e.target.value)}
+ description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
/>
)}
{(field) => field.handleChange(e.target.value)} />}
- {(field) => field.handleChange(value as ServiceFormValues["type"])} placeholder="Select service type" options={[...serviceTypeOptions]} />}
+ {(field) => field.handleChange(e.target.value as ServiceFormValues["type"])} />}
{(field) => field.handleChange(e.target.value)} />}
{(field) => field.handleChange(e.target.value)} />}
- {(field) => field.handleChange(value as ServiceFormValues["protocol"])} placeholder="Select protocol" options={[...serviceProtocolOptions]} />}
- {(field) => field.handleChange(value as ServiceFormValues["status"])} placeholder="Select service status" options={[...serviceStatusOptions]} />}
+ {(field) => field.handleChange(e.target.value as ServiceFormValues["protocol"])} />}
+ {(field) => field.handleChange(e.target.value as ServiceFormValues["status"])} />}
{(field) => field.handleChange(e.target.value)} />}
diff --git a/apps/web/src/routes/dashboard.services_.new.tsx b/apps/web/src/routes/dashboard.services.new.tsx
similarity index 65%
rename from apps/web/src/routes/dashboard.services_.new.tsx
rename to apps/web/src/routes/dashboard.services.new.tsx
index 5139113..fd53725 100644
--- a/apps/web/src/routes/dashboard.services_.new.tsx
+++ b/apps/web/src/routes/dashboard.services.new.tsx
@@ -3,10 +3,10 @@ import { useForm } from "@tanstack/react-form";
import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { toast } from "sonner";
-import { FormCard, PageSection, QueryStateCard, SelectField, TextAreaField, TextField } from "@/components/dashboard-ui";
+import { FormCard, PageSection, QueryStateCard, TextAreaField, TextField } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
-export const Route = createFileRoute("/dashboard/services_/new")({
+export const Route = createFileRoute("/dashboard/services/new")({
component: RouteComponent,
});
@@ -21,29 +21,6 @@ type ServiceFormValues = {
description: string;
};
-const serviceTypeOptions = [
- { value: "web_app", label: "Web app" },
- { value: "mail", label: "Mail" },
- { value: "database", label: "Database" },
- { value: "api", label: "API" },
- { value: "proxy", label: "Proxy" },
- { value: "other", label: "Other" },
-] as const;
-
-const serviceProtocolOptions = [
- { value: "http", label: "HTTP" },
- { value: "https", label: "HTTPS" },
- { value: "tcp", label: "TCP" },
- { value: "udp", label: "UDP" },
- { value: "other", label: "Other" },
-] as const;
-
-const serviceStatusOptions = [
- { value: "active", label: "Active" },
- { value: "down", label: "Down" },
- { value: "maintenance", label: "Maintenance" },
-] as const;
-
function RouteComponent() {
const navigate = useNavigate();
const servers = useQuery(trpc.servers.list.queryOptions());
@@ -93,11 +70,6 @@ function RouteComponent() {
return
;
}
- const serverOptions = servers.data.map((item) => ({
- value: item.id,
- label: `${item.name} (${item.id})`,
- }));
-
return (
({ isSubmitting: state.isSubmitting })}>
- {({ isSubmitting }) => Save service }
+ {({ isSubmitting }) => {isSubmitting ? "Saving..." : "Save service"} }
>
}
>
{(field) => (
- field.handleChange(e.target.value)}
+ description={`Available: ${servers.data.map((item) => `${item.name} (${item.id})`).join(", ")}`}
/>
)}
{(field) => field.handleChange(e.target.value)} />}
- {(field) => field.handleChange(value as ServiceFormValues["type"])} placeholder="Select service type" options={[...serviceTypeOptions]} />}
+ {(field) => field.handleChange(e.target.value as ServiceFormValues["type"])} />}
{(field) => field.handleChange(e.target.value)} />}
{(field) => field.handleChange(e.target.value)} />}
- {(field) => field.handleChange(value as ServiceFormValues["protocol"])} placeholder="Select protocol" options={[...serviceProtocolOptions]} />}
- {(field) => field.handleChange(value as ServiceFormValues["status"])} placeholder="Select service status" options={[...serviceStatusOptions]} />}
+ {(field) => field.handleChange(e.target.value as ServiceFormValues["protocol"])} />}
+ {(field) => field.handleChange(e.target.value as ServiceFormValues["status"])} />}
{(field) => field.handleChange(e.target.value)} />}
diff --git a/apps/web/src/routes/dashboard.services.tsx b/apps/web/src/routes/dashboard.services.tsx
index 1aec048..b06ecb1 100644
--- a/apps/web/src/routes/dashboard.services.tsx
+++ b/apps/web/src/routes/dashboard.services.tsx
@@ -4,7 +4,7 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { createFileRoute, Link } from "@tanstack/react-router";
import { toast } from "sonner";
-import { ConfirmDeleteIconButton, PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
+import { PageSection, QueryStateCard, ResourceListCard } from "@/components/dashboard-ui";
import { queryClient, trpc, trpcClient } from "@/utils/trpc";
export const Route = createFileRoute("/dashboard/services")({
@@ -96,13 +96,21 @@ function RouteComponent() {
{
key: "actions",
header: "Actions",
- className: "w-16",
+ className: "w-28",
render: (item: (typeof services.data)[number]) => (
- removeService.mutate(item.id)}
- />
+ onClick={() => {
+ if (confirm(`Delete service \"${item.name}\" and its related domains?`)) {
+ removeService.mutate(item.id);
+ }
+ }}
+ >
+ Delete
+
),
},
]}
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
index 1ef3955..4ba9b8f 100644
--- a/apps/web/src/routes/index.tsx
+++ b/apps/web/src/routes/index.tsx
@@ -1,14 +1,51 @@
-import { createFileRoute, redirect } from "@tanstack/react-router";
+import { useQuery } from "@tanstack/react-query";
+import { createFileRoute } from "@tanstack/react-router";
-import { authClient } from "@/lib/auth-client";
+import { trpc } from "@/utils/trpc";
export const Route = createFileRoute("/")({
- beforeLoad: async () => {
- const session = await authClient.getSession();
-
- throw redirect({
- to: session.data ? "/dashboard" : "/login",
- });
- },
- component: () => null,
+ component: HomeComponent,
});
+
+const TITLE_TEXT = `
+ ██████╗ ███████╗████████╗████████╗███████╗██████╗
+ ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗
+ ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝
+ ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗
+ ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║
+ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
+
+ ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗
+ ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
+ ██║ ███████╗ ██║ ███████║██║ █████╔╝
+ ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗
+ ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗
+ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝
+ `;
+
+function HomeComponent() {
+ const healthCheck = useQuery(trpc.healthCheck.queryOptions());
+
+ return (
+
+
{TITLE_TEXT}
+
+
+ API Status
+
+
+
+ {healthCheck.isLoading
+ ? "Checking..."
+ : healthCheck.data
+ ? "Connected"
+ : "Disconnected"}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/utils/errors.ts b/apps/web/src/utils/errors.ts
deleted file mode 100644
index 0088e8d..0000000
--- a/apps/web/src/utils/errors.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-type AnyError = {
- message?: string;
- error?: { message?: string; statusText?: string };
-};
-
-export function formatErrorMessage(
- error: unknown,
- fallback = "Something went wrong",
-): string {
- if (!error) return fallback;
- if (typeof error === "string") return error;
-
- const e = error as AnyError;
-
- if (e.error && typeof e.error === "object") {
- const inner = e.error.message?.trim() || e.error.statusText?.trim();
- if (inner) return inner;
- }
-
- if (typeof e.message === "string" && e.message.trim()) {
- return e.message.trim();
- }
-
- return fallback;
-}
diff --git a/bun.lock b/bun.lock
index 47dc957..a1a5ee1 100644
--- a/bun.lock
+++ b/bun.lock
@@ -100,8 +100,8 @@
"@trpc/client": "catalog:",
"@trpc/server": "catalog:",
"dotenv": "catalog:",
- "drizzle-orm": "catalog:",
- "effect": "catalog:",
+ "drizzle-orm": "^0.45.1",
+ "effect": "^3.18.4",
"zod": "catalog:",
},
"devDependencies": {
@@ -134,7 +134,7 @@
"@minmon/env": "workspace:*",
"better-auth": "catalog:",
"dotenv": "catalog:",
- "drizzle-orm": "catalog:",
+ "drizzle-orm": "^0.45.1",
"pg": "^8.17.1",
"zod": "catalog:",
},
@@ -191,8 +191,6 @@
"@types/react-dom": "^19.2.3",
"better-auth": "1.5.5",
"dotenv": "^17.2.2",
- "drizzle-orm": "^0.45.1",
- "effect": "^3.18.4",
"elysia": "^1.4.28",
"next-themes": "^0.4.6",
"typescript": "^6",
diff --git a/docs/diagrams/activity.mmd b/docs/diagrams/activity.mmd
deleted file mode 100644
index ef83916..0000000
--- a/docs/diagrams/activity.mmd
+++ /dev/null
@@ -1,37 +0,0 @@
-flowchart LR
- subgraph Trigger[Trigger]
- A[Create / Update Domain]
- B[Manual Check List / Detail]
- end
-
- subgraph UI[Web UI]
- C[Submit form domain]
- D[Call domains.check]
- E[Invalidate query + toast]
- end
-
- subgraph API[tRPC domainsRouter]
- F[Save / load domain]
- G[Run domain check]
- end
-
- subgraph DNS[DNS Check]
- H[Resolve IPv4 A record]
- I{Expected IP found?}
- end
-
- subgraph Result[Persisted Status]
- J[MATCH]
- K[MISMATCH]
- L[UNRESOLVED]
- end
-
- A --> C --> F --> D
- B --> D
- D --> G --> H --> I
- I -- Ya --> J
- I -- Tidak --> K
- H -- Lookup gagal --> L
- J --> E
- K --> E
- L --> E
diff --git a/docs/diagrams/activity.png b/docs/diagrams/activity.png
deleted file mode 100644
index 40f9e85..0000000
Binary files a/docs/diagrams/activity.png and /dev/null differ
diff --git a/docs/diagrams/class.mmd b/docs/diagrams/class.mmd
deleted file mode 100644
index 1332c36..0000000
--- a/docs/diagrams/class.mmd
+++ /dev/null
@@ -1,61 +0,0 @@
-classDiagram
- direction LR
-
- class Server {
- +string id
- +string name
- +string primaryIpAddress
- +string secondaryIpAddress
- +string operatingSystem
- +string location
- +string provider
- +string description
- +server_status status
- +Date createdAt
- +Date updatedAt
- }
-
- class Service {
- +string id
- +string serverId
- +string name
- +service_type type
- +int internalPort
- +int externalPort
- +service_protocol protocol
- +service_status status
- +string description
- +Date createdAt
- +Date updatedAt
- }
-
- class Domain {
- +string id
- +string serviceId
- +string name
- +string expectedServerIp
- +string lastResolvedIp
- +string[] lastResolvedIps
- +domain_resolution_status resolutionStatus
- +Date lastCheckedAt
- +string remarks
- +string lastCheckMessage
- +Date createdAt
- +Date updatedAt
- }
-
- class Catatan {
- +string id
- +string serverId
- +string title
- +string content
- +string category
- +Date createdAt
- +Date updatedAt
- }
-
- Server "1" o-- "0..*" Service : memiliki
- Server "1" o-- "0..*" Catatan : memiliki
- Service "1" o-- "0..*" Domain : memiliki
-
- note for Domain "resolutionStatus: MATCH, MISMATCH, atau UNRESOLVED"
diff --git a/docs/diagrams/class.png b/docs/diagrams/class.png
deleted file mode 100644
index c285b11..0000000
Binary files a/docs/diagrams/class.png and /dev/null differ
diff --git a/docs/diagrams/collaboration.mmd b/docs/diagrams/collaboration.mmd
deleted file mode 100644
index 72d7b39..0000000
--- a/docs/diagrams/collaboration.mmd
+++ /dev/null
@@ -1,29 +0,0 @@
-graph TD
- Admin[Admin]
- Auth[Auth + Protected Route]
- UI[Dashboard Pages]
- Query[TanStack Query]
- TRPC[tRPC Client]
- Router[Protected Routers]
- DomainRouter[domainsRouter]
- Checker[DNS Check Module]
- DB[(PostgreSQL / Drizzle)]
- DNS[node:dns resolve4]
- Toast[Toast]
-
- subgraph Client[Client Side]
- Auth --> UI --> Query --> TRPC
- UI --> Toast
- end
-
- subgraph Server[Server Side]
- Router --> DB
- Router --> DomainRouter
- DomainRouter --> Checker
- Checker --> DNS
- Checker --> DB
- end
-
- Admin --> Auth
- TRPC --> Router
- Router --> TRPC
diff --git a/docs/diagrams/collaboration.png b/docs/diagrams/collaboration.png
deleted file mode 100644
index 2f15e93..0000000
Binary files a/docs/diagrams/collaboration.png and /dev/null differ
diff --git a/docs/diagrams/sequence.mmd b/docs/diagrams/sequence.mmd
deleted file mode 100644
index 673f80f..0000000
--- a/docs/diagrams/sequence.mmd
+++ /dev/null
@@ -1,29 +0,0 @@
-sequenceDiagram
- actor Admin
- participant UI as Web UI React
- participant TRPC as tRPC Client
- participant API as domainsRouter
- participant DB as PostgreSQL/Drizzle
- participant Checker as domain-check module
- participant DNS as DNS Resolver
-
- Admin->>UI: Isi form domain dan pilih service
- UI->>TRPC: domains.create({ serviceId, name, expectedServerIp? })
- TRPC->>API: Kirim request create domain
- API->>DB: Validasi service dan server parent
- API->>DB: Insert domain, default expected IP dari server parent jika kosong
- DB-->>API: Domain berhasil dibuat
- API-->>TRPC: Data domain baru
- TRPC-->>UI: Data domain baru
- UI->>TRPC: domains.check({ id: created.id }) otomatis
- TRPC->>API: Kirim request DNS check
- API->>DB: Ambil domain berdasarkan id
- API->>Checker: runAndPersistDomainCheck(...)
- Checker->>DNS: resolve4(domain.name)
- DNS-->>Checker: Daftar IPv4 atau error
- Checker->>DB: Update resolutionStatus dan hasil check
- DB-->>Checker: Domain terbaru
- Checker-->>API: { domain, result }
- API-->>TRPC: { domain, check }
- TRPC-->>UI: Hasil DNS check
- UI->>UI: Invalidate domains.list, navigate detail, tampilkan toast
diff --git a/docs/diagrams/sequence.png b/docs/diagrams/sequence.png
deleted file mode 100644
index cca0494..0000000
Binary files a/docs/diagrams/sequence.png and /dev/null differ
diff --git a/docs/diagrams/state-chart.mmd b/docs/diagrams/state-chart.mmd
deleted file mode 100644
index cdfec9a..0000000
--- a/docs/diagrams/state-chart.mmd
+++ /dev/null
@@ -1,10 +0,0 @@
-stateDiagram-v2
- [*] --> UNRESOLVED
- state "Checking DNS" as CHECKING
-
- UNRESOLVED --> CHECKING : create/update/manual check
- MATCH --> CHECKING : re-check
- MISMATCH --> CHECKING : re-check
- CHECKING --> MATCH : expected IP ditemukan
- CHECKING --> MISMATCH : resolved IP berbeda
- CHECKING --> UNRESOLVED : DNS lookup gagal
diff --git a/docs/diagrams/state-chart.png b/docs/diagrams/state-chart.png
deleted file mode 100644
index 0f536d5..0000000
Binary files a/docs/diagrams/state-chart.png and /dev/null differ
diff --git a/docs/diagrams/use-case.mmd b/docs/diagrams/use-case.mmd
deleted file mode 100644
index c3df4ef..0000000
--- a/docs/diagrams/use-case.mmd
+++ /dev/null
@@ -1,25 +0,0 @@
-flowchart LR
- Admin([Admin])
- Login((Sign in))
- Protected((Akses Dashboard Terproteksi))
- Dashboard((Melihat Dashboard Summary))
- Server((CRUD Server))
- Service((CRUD Service))
- Domain((CRUD Domain))
- AutoDns((Auto DNS Check saat Create/Update Domain))
- ManualDns((Manual DNS Check dari List/Detail Domain))
- Note((CRUD Catatan))
- Detail((Melihat Catatan pada Detail Server))
- Logout((Sign out))
-
- Admin --> Login
- Login --> Protected
- Protected --> Dashboard
- Protected --> Server
- Protected --> Service
- Protected --> Domain
- Protected --> Note
- Domain --> AutoDns
- Domain --> ManualDns
- Server --> Detail
- Admin --> Logout
diff --git a/docs/diagrams/use-case.png b/docs/diagrams/use-case.png
deleted file mode 100644
index 1e1dbb8..0000000
Binary files a/docs/diagrams/use-case.png and /dev/null differ
diff --git a/docs/minmon-homelab-report.tex b/docs/minmon-homelab-report.tex
deleted file mode 100644
index c7ec785..0000000
--- a/docs/minmon-homelab-report.tex
+++ /dev/null
@@ -1,568 +0,0 @@
-\documentclass[12pt,a4paper]{report}
-
-\usepackage[utf8]{inputenc}
-\usepackage[T1]{fontenc}
-\usepackage[indonesian]{babel}
-\usepackage[a4paper,margin=2.8cm]{geometry}
-\usepackage{graphicx}
-\usepackage{float}
-\usepackage{array}
-\usepackage{booktabs}
-\usepackage{longtable}
-\usepackage{hyperref}
-\usepackage{xcolor}
-\usepackage{listings}
-\usepackage{enumitem}
-\usepackage{titlesec}
-\usepackage{setspace}
-
-\hypersetup{
- colorlinks=true,
- linkcolor=blue!60!black,
- urlcolor=blue!60!black,
- pdftitle={Laporan Proyek Minmon Homelab},
- pdfauthor={[Nama Mahasiswa]}
-}
-
-\onehalfspacing
-\setlist[itemize]{noitemsep, topsep=4pt}
-\setlist[enumerate]{noitemsep, topsep=4pt}
-
-\renewcommand{\chaptername}{BAB}
-\titleformat{\chapter}[display]
- {\bfseries\Large}
- {\chaptername\ \thechapter}
- {0.5em}
- {\Large}
-
-\definecolor{codebg}{RGB}{248,248,248}
-\definecolor{bordergray}{RGB}{210,210,210}
-
-\lstdefinestyle{minmoncode}{
- backgroundcolor=\color{codebg},
- basicstyle=\ttfamily\small,
- breaklines=true,
- frame=single,
- rulecolor=\color{bordergray},
- showstringspaces=false,
- tabsize=2,
- keywordstyle=\color{blue!60!black}\bfseries,
- commentstyle=\color{green!40!black},
- stringstyle=\color{red!50!black}
-}
-
-\lstdefinelanguage{TypeScript}{
- keywords={import,from,export,const,return,if,await,async,type,string,number,boolean,null,undefined},
- sensitive=true,
- morecomment=[l]{//},
- morecomment=[s]{/*}{*/},
- morestring=[b]",
- morestring=[b]'
-}
-
-\newcommand{\uipath}[1]{\texttt{#1}}
-\newcommand{\uibutton}[1]{\textbf{#1}}
-\newcommand{\uiscreenshot}[2]{
- \begin{figure}[H]
- \centering
- \includegraphics[width=0.94\textwidth]{#1}
- \caption{#2}
- \end{figure}
-}
-
-\newcommand{\uidiagram}[2]{
- \begin{figure}[H]
- \centering
- \includegraphics[width=0.94\textwidth,height=0.72\textheight,keepaspectratio]{#1}
- \caption{#2}
- \end{figure}
-}
-
-\begin{document}
-
-\begin{titlepage}
- \centering
- {\Large \textbf{LAPORAN PROYEK DAN MANUAL BOOK}}\\[1.2cm]
- {\Huge \textbf{MINMON}}\\[0.4cm]
- {\Large Dashboard Inventaris dan Monitoring Homelab Berbasis Web}\\[1.2cm]
-
- \begin{tabular}{ll}
- \textbf{Nama Mahasiswa} & : \makebox[7cm]{\hrulefill}\\
- \textbf{NIM} & : \makebox[7cm]{\hrulefill}\\
- \textbf{Program Studi} & : \makebox[7cm]{\hrulefill}\\
- \textbf{Mata Kuliah} & : \makebox[7cm]{\hrulefill}\\
- \textbf{Dosen Pengampu} & : \makebox[7cm]{\hrulefill}\\
- \textbf{Institusi} & : \makebox[7cm]{\hrulefill}\\
- \textbf{Tahun Akademik} & : \makebox[7cm]{\hrulefill}
- \end{tabular}
-
- \vfill
-
- {\large Dokumen ini disusun untuk mendeskripsikan proyek \textit{Minmon} yang digunakan untuk mendata dan memonitor inventaris homelab, khususnya lingkungan kecil yang terdiri dari beberapa server dengan banyak service dan domain terkait.}\\[1cm]
-
- {\large \today}
-\end{titlepage}
-
-\tableofcontents
-\clearpage
-
-\chapter{Pendahuluan}
-
-\section{Latar Belakang}
-Homelab adalah lingkungan laboratorium komputer pribadi yang biasanya dibangun di rumah atau lingkungan kecil untuk belajar, mencoba, dan menjalankan layanan server secara mandiri. Dalam homelab, pengguna dapat mengelola server fisik, mini PC, virtual machine, container, jaringan lokal, serta berbagai aplikasi seperti dashboard, database, media server, reverse proxy, dan automation tools. Homelab modern tidak lagi hanya berisi satu mesin dengan satu layanan. Dalam praktiknya, satu homelab kecil dapat terdiri dari beberapa server, banyak service, serta sejumlah domain atau subdomain yang harus diarahkan ke alamat IP yang benar. Ketika jumlah aset ini bertambah, pencatatan manual menjadi sulit dipelihara karena data server, service, domain, dan catatan operasional tersebar di berbagai tempat.
-
-Minmon dikembangkan sebagai dashboard inventaris dan monitoring ringan berbasis web untuk membantu pemilik homelab mengelola aset tersebut dalam satu aplikasi terpusat. Aplikasi ini tidak ditujukan untuk deployment automation, melainkan untuk kebutuhan dokumentasi, inventaris, relasi data, dan validasi DNS sederhana. Dalam konteks penggunaan pribadi, proyek ini cocok untuk skenario homelab yang terdiri dari beberapa server dengan banyak service dan domain yang perlu dicatat serta dipantau.
-
-Implementasi saat ini menyediakan autentikasi admin, dashboard ringkasan, CRUD untuk server, service, domain, dan catatan, serta fitur pemeriksaan DNS A record. Selain itu, relasi antar entitas juga sudah diterapkan melalui select input agar pengguna tidak perlu memasukkan ID relasi secara manual. Dengan demikian, Minmon dapat berfungsi sebagai pusat data operasional homelab yang lebih rapi, konsisten, dan mudah diperluas.
-
-\section{Tujuan}
-Tujuan dari pengembangan proyek ini adalah sebagai berikut:
-
-\begin{itemize}
- \item Membangun aplikasi web yang dapat digunakan untuk mengelola inventaris homelab secara terstruktur.
- \item Menyediakan fitur CRUD untuk data server, service, domain, dan catatan dalam satu dashboard terproteksi.
- \item Menyediakan pemeriksaan DNS sederhana untuk memverifikasi apakah domain mengarah ke IP server yang diharapkan.
- \item Mempermudah dokumentasi relasi data, misalnya service yang berjalan pada server tertentu dan catatan yang melekat pada server tertentu.
- \item Menjadi contoh implementasi aplikasi web penuh berbasis React, tRPC, Drizzle ORM, PostgreSQL, dan better-auth.
-\end{itemize}
-
-\section{Manfaat}
-Manfaat dari proyek ini antara lain:
-
-\begin{itemize}
- \item Membantu pemilik homelab mendokumentasikan server, service, domain, dan catatan dalam satu tempat.
- \item Mengurangi kesalahan input relasi data karena hubungan antar data dipilih melalui field pilihan.
- \item Memudahkan pengecekan domain melalui fitur pemeriksaan DNS berbasis A record / IPv4.
- \item Memberikan gambaran ringkas kondisi homelab melalui ringkasan dashboard.
- \item Menjadi bahan laporan akademik dan contoh produk perangkat lunak yang benar-benar dapat dipakai.
-\end{itemize}
-
-\chapter{Metode Pengembangan}
-
-\section{Tahap Pengembangan}
-Tahap pengembangan proyek Minmon dapat dijelaskan secara ringkas sebagai berikut:
-
-\begin{enumerate}
- \item \textbf{Identifikasi kebutuhan}\\
- Menentukan kebutuhan utama untuk inventaris homelab, yaitu pengelolaan data server, service, domain, dan catatan, serta kebutuhan monitoring DNS sederhana.
-
- \item \textbf{Perancangan data dan relasi}\\
- Menentukan entitas inti, yaitu server, service, domain, dan catatan. Setiap service terkait ke satu server, setiap domain terkait ke satu service, dan setiap catatan terkait ke satu server.
-
- \item \textbf{Perancangan backend}\\
- Mengimplementasikan router tRPC untuk operasi daftar data, detail, tambah, ubah, hapus, serta ringkasan dashboard. Pemeriksaan DNS diimplementasikan sebagai operasi khusus pada data domain.
-
- \item \textbf{Perancangan frontend}\\
- Membangun halaman login, ringkasan dashboard, halaman daftar data, halaman tambah data, dan halaman detail/edit untuk setiap entitas. Form relasi dibuat menggunakan pilihan data agar lebih aman dan mudah dipakai.
-
- \item \textbf{Integrasi dan validasi}\\
- Menghubungkan frontend ke backend dengan TanStack Query dan tRPC client, lalu memastikan operasi data, pembaruan ulang data, serta tampilan ringkasan berjalan dengan baik.
-
- \item \textbf{Pengujian dan penyempurnaan}\\
- Melakukan pemeriksaan tipe, pengecekan alur CRUD, pengecekan DNS, perbaikan kestabilan urutan daftar data, dan penyempurnaan interaksi UI seperti hapus dua langkah dan pemicu DNS dari badge status.
-\end{enumerate}
-
-\section{Identitas/Keunikan Produk}
-Minmon memiliki identitas produk sebagai dashboard inventaris dan monitoring ringan untuk homelab. Keunikan produk ini dibanding pencatatan manual biasa adalah sebagai berikut:
-
-\begin{itemize}
- \item Fokus pada skenario homelab kecil-menengah, yaitu beberapa server dengan banyak service dan domain.
- \item Menggabungkan inventaris dan dokumentasi operasional dalam satu aplikasi.
- \item Menyediakan relasi data yang jelas antara server, service, domain, dan catatan.
- \item Memiliki pemeriksaan DNS A record yang terintegrasi langsung pada alur domain.
- \item Menampilkan catatan yang terkait langsung pada halaman detail server.
- \item Menggunakan autentikasi admin sehingga dashboard tidak dapat diakses tanpa sesi.
-\end{itemize}
-
-Untuk penggunaan pribadi, Minmon cocok sebagai pusat dokumentasi homelab yang tidak hanya menyimpan data inventaris, tetapi juga membantu memastikan domain masih mengarah ke IP yang benar dan memudahkan pelacakan catatan operasional pada setiap server.
-
-\section{Diagram-diagram UML (use case, class, activity, state chart, sequence, collaboration)}
-Bagian ini berisi rancangan UML yang disusun berdasarkan implementasi aktual Minmon. Diagram disisipkan sebagai gambar agar alur sistem dapat dibaca langsung di dalam laporan. Struktur diagram mencerminkan aktor admin, modul frontend React, router tRPC, database PostgreSQL melalui Drizzle ORM, dan modul pemeriksaan DNS.
-
-\subsection{Use Case Diagram}
-Use case utama dalam sistem ini meliputi:
-
-\begin{itemize}
- \item Admin sign in
- \item Admin mengakses ringkasan dashboard
- \item Admin mengelola server
- \item Admin mengelola service
- \item Admin mengelola domain
- \item Admin menjalankan pemeriksaan DNS dari halaman daftar atau detail domain
- \item UI menjalankan pemeriksaan DNS setelah domain dibuat atau diperbarui
- \item Admin mengelola catatan
- \item Admin melihat catatan pada detail server
- \item Admin sign out
-\end{itemize}
-
-\uidiagram{diagrams/use-case.png}{Use Case Diagram Minmon}
-
-\subsection{Class Diagram}
-Entitas utama pada sistem adalah:
-
-\begin{itemize}
- \item \textbf{Server}: id, name, primaryIpAddress, secondaryIpAddress, operatingSystem, location, provider, description, status, createdAt, updatedAt
- \item \textbf{Service}: id, serverId, name, type, internalPort, externalPort, protocol, status, description, createdAt, updatedAt
- \item \textbf{Domain}: id, serviceId, name, expectedServerIp, lastResolvedIp, lastResolvedIps, resolutionStatus, lastCheckedAt, remarks, lastCheckMessage, createdAt, updatedAt
- \item \textbf{Catatan}: id, serverId, title, content, category, createdAt, updatedAt
-\end{itemize}
-
-Hubungan utama:
-
-\begin{itemize}
- \item Server 1..* Service secara konseptual, tetapi secara database dapat 0..*.
- \item Server 1..* Catatan secara konseptual, tetapi secara database dapat 0..*.
- \item Service 1..* Domain secara konseptual, tetapi secara database dapat 0..*.
-\end{itemize}
-
-\uidiagram{diagrams/class.png}{Class Diagram Minmon}
-
-\subsection{Activity Diagram}
-Activity diagram berikut menampilkan alur pemeriksaan DNS yang sesuai dengan implementasi saat ini, yaitu dipicu oleh UI setelah domain ditambahkan/diperbarui atau dijalankan manual dari halaman daftar/detail domain:
-
-\begin{enumerate}
- \item Admin membuat, memperbarui, atau memeriksa domain.
- \item UI memanggil operasi \texttt{domains.create}, \texttt{domains.update}, atau \texttt{domains.check} sesuai aksi.
- \item Setelah create/update berhasil, UI memanggil \texttt{domains.check} untuk menjalankan pemeriksaan DNS.
- \item Backend melakukan resolve IPv4 A record.
- \item Backend membandingkan hasil resolve dengan \texttt{expectedServerIp}.
- \item Backend menyimpan status dan hasil resolve ke database.
- \item UI memperbarui ulang data dan menampilkan status baru.
-\end{enumerate}
-
-\uidiagram{diagrams/activity.png}{Activity Diagram DNS Check}
-
-\subsection{State Chart Diagram}
-State chart yang disarankan adalah state untuk status DNS domain:
-
-\begin{itemize}
- \item UNRESOLVED
- \item MATCH
- \item MISMATCH
-\end{itemize}
-
-Transisi utama:
-
-\begin{itemize}
- \item DNS berhasil dan IP cocok $\rightarrow$ MATCH
- \item DNS berhasil tetapi IP tidak cocok $\rightarrow$ MISMATCH
- \item DNS gagal resolve $\rightarrow$ UNRESOLVED
-\end{itemize}
-
-\uidiagram{diagrams/state-chart.png}{State Chart Domain DNS}
-
-\subsection{Sequence Diagram}
-Sequence diagram yang cocok untuk proyek ini adalah alur tambah domain dan pemeriksaan DNS lanjutan, karena UI langsung memanggil pemeriksaan DNS setelah domain berhasil dibuat:
-
-\begin{itemize}
- \item Admin
- \item Web UI
- \item tRPC client/API
- \item Database
- \item Domain check module
- \item DNS Resolver
-\end{itemize}
-
-\uidiagram{diagrams/sequence.png}{Sequence Diagram Create Domain and Check DNS}
-
-\subsection{Collaboration Diagram}
-Collaboration diagram menunjukkan interaksi aktual antara route terproteksi, halaman dashboard, TanStack Query, router tRPC, database, dan modul pemeriksaan DNS:
-
-\begin{itemize}
- \item Admin
- \item Dashboard UI dan route terproteksi
- \item better-auth client
- \item TanStack Query dan tRPC router
- \item Database
- \item Modul DNS lookup
-\end{itemize}
-
-\uidiagram{diagrams/collaboration.png}{Collaboration Diagram Minmon}
-
-\section{Coding Minimal 80 Baris}
-Bagian ini menampilkan kode asli dari file \uipath{packages/api/src/routers/services.ts}. Kode ini dipilih karena berjumlah 82 baris dan mewakili logika inti backend, yaitu protected CRUD untuk entitas service, validasi relasi ke server, penghitungan ringkasan domain, dan operasi database menggunakan Drizzle ORM.
-
-\begin{lstlisting}[style=minmoncode,language=TypeScript,caption={Kode servicesRouter: CRUD Service dan validasi relasi, 82 baris}]
-import { db } from "@minmon/db";
-import { domain, service } from "@minmon/db";
-import { count, desc, eq } from "drizzle-orm";
-
-import { protectedProcedure, router } from "../index";
-import {
- byIdSchema,
- serviceCreateSchema,
- serviceListSchema,
- serviceUpdateSchema,
-} from "./schemas";
-import {
- createEntityId,
- requireServer,
- requireService,
- stripUndefined,
-} from "./utils";
-
-export const servicesRouter = router({
- list: protectedProcedure.input(serviceListSchema).query(async ({ input }) => {
- if (!input?.serverId) {
- return db.select().from(service).orderBy(desc(service.createdAt), desc(service.id));
- }
-
- await requireServer(input.serverId);
-
- return db
- .select()
- .from(service)
- .where(eq(service.serverId, input.serverId))
- .orderBy(desc(service.createdAt), desc(service.id));
- }),
-
- byId: protectedProcedure.input(byIdSchema).query(async ({ input }) => {
- const item = await requireService(input.id);
- const [domainCount] = await db.select({ count: count() }).from(domain).where(eq(domain.serviceId, input.id));
-
- return {
- ...item,
- summary: {
- domains: domainCount?.count ?? 0,
- },
- };
- }),
-
- create: protectedProcedure.input(serviceCreateSchema).mutation(async ({ input }) => {
- await requireServer(input.serverId);
-
- const [created] = await db
- .insert(service)
- .values({
- id: createEntityId(),
- ...input,
- })
- .returning();
-
- return created;
- }),
-
- update: protectedProcedure.input(serviceUpdateSchema).mutation(async ({ input }) => {
- const { id, serverId, ...changes } = input;
- await requireService(id);
-
- if (serverId) {
- await requireServer(serverId);
- }
-
- const [updated] = await db
- .update(service)
- .set(stripUndefined({ serverId, ...changes }))
- .where(eq(service.id, id))
- .returning();
-
- return updated;
- }),
-
- delete: protectedProcedure.input(byIdSchema).mutation(async ({ input }) => {
- await requireService(input.id);
- await db.delete(service).where(eq(service.id, input.id));
- return { id: input.id };
- }),
-});
-\end{lstlisting}
-
-Penjelasan singkat kode di atas:
-\begin{itemize}
- \item \texttt{list} mengambil daftar service, baik seluruh service maupun service berdasarkan \texttt{serverId}, dengan urutan stabil berdasarkan \texttt{createdAt} dan \texttt{id}.
- \item \texttt{byId} mengambil detail service dan menambahkan ringkasan jumlah domain yang terhubung ke service tersebut.
- \item \texttt{create} memvalidasi server induk terlebih dahulu sebelum menyimpan service baru.
- \item \texttt{update} memastikan service yang diedit ada, lalu memvalidasi server baru jika relasi \texttt{serverId} diubah.
- \item \texttt{delete} memastikan data service ada sebelum dihapus dari database.
-\end{itemize}
-
-\chapter{Manual Book}
-
-\section{Gambaran Umum Penggunaan}
-Bagian ini menjelaskan cara menggunakan aplikasi Minmon berdasarkan implementasi saat ini. Gaya penjelasan dibuat seperti manual book: langkah demi langkah, ringkas, dan fokus pada hasil yang diharapkan.
-
-\section{Login ke Sistem}
-\textbf{Path akses:} \uipath{/login}
-
-\begin{enumerate}
- \item Jalankan aplikasi web dan server backend dari root proyek menggunakan perintah \texttt{bun run dev}.
- \item Buka browser ke alamat \uipath{http://localhost:5173}.
- \item Jika belum login, sistem akan mengarahkan ke \uipath{/login}.
- \item Masukkan email admin dan password.
- \item Tekan tombol \uibutton{Sign In}.
-\end{enumerate}
-
-\textbf{Hasil yang diharapkan:} pengguna masuk ke halaman \uipath{/dashboard}.
-
-Tampilan halaman login berisi form email dan password untuk admin. Apabila data benar, sistem membuat session melalui better-auth dan mengarahkan pengguna ke dashboard.
-
-\uiscreenshot{screenshots/manual-login.png}{Halaman Login Minmon}
-
-\section{Membuka Dashboard Summary}
-\textbf{Path akses:} \uipath{/dashboard}
-
-Halaman dashboard menampilkan ringkasan:
-
-\begin{itemize}
- \item total server,
- \item total service,
- \item total domain,
- \item jumlah DNS match / mismatch / unresolved,
- \item service yang inactive/down,
- \item catatan terbaru.
-\end{itemize}
-
-\textbf{Langkah penggunaan:}
-\begin{enumerate}
- \item Login sebagai admin.
- \item Setelah diarahkan ke dashboard, periksa kartu ringkasan di bagian atas.
- \item Gunakan link area untuk membuka halaman Servers, Services, Domains, atau Catatan.
-\end{enumerate}
-
-Dashboard summary berfungsi sebagai halaman awal setelah login. Pengguna dapat langsung melihat kondisi umum inventaris sebelum masuk ke halaman CRUD masing-masing modul.
-
-\uiscreenshot{screenshots/manual-dashboard.png}{Dashboard Summary Minmon}
-
-\section{Mengelola Data Server}
-\textbf{Path akses:} \uipath{/dashboard/servers}
-
-\subsection{Menambah Server}
-\begin{enumerate}
- \item Buka menu Servers.
- \item Klik tombol \uibutton{Add server}.
- \item Isi data server seperti nama, IP utama, lokasi, provider, dan status.
- \item Klik \uibutton{Save server}.
-\end{enumerate}
-
-\textbf{Hasil yang diharapkan:} data server tersimpan dan pengguna diarahkan ke halaman detail server.
-
-\uiscreenshot{screenshots/manual-servers-list.png}{Halaman Daftar Server}
-
-\uiscreenshot{screenshots/manual-server-create.png}{Form Tambah Server}
-
-\subsection{Melihat Detail Server}
-\textbf{Path akses:} \uipath{/dashboard/servers/\$serverId}
-
-Pada halaman ini pengguna dapat:
-
-\begin{itemize}
- \item melihat ringkasan server,
- \item mengedit data server,
- \item melihat catatan yang terkait dengan server tersebut.
-\end{itemize}
-
-Halaman detail server menjadi pusat informasi untuk satu server tertentu karena menampilkan data utama server dan catatan yang terhubung dengan server tersebut.
-
-\uiscreenshot{screenshots/manual-server-detail.png}{Halaman Detail Server}
-
-\section{Mengelola Data Service}
-\textbf{Path akses:} \uipath{/dashboard/services}
-
-\subsection{Menambah Service}
-\begin{enumerate}
- \item Buka menu Services.
- \item Klik \uibutton{Add service}.
- \item Pilih server induk pada field \textit{Server}.
- \item Isi nama service, tipe, port, protokol, status, dan deskripsi.
- \item Klik \uibutton{Save service}.
-\end{enumerate}
-
-\textbf{Hasil yang diharapkan:} service tersimpan dan terhubung ke server yang dipilih.
-
-\uiscreenshot{screenshots/manual-services-list.png}{Halaman Daftar Service}
-
-\uiscreenshot{screenshots/manual-service-create.png}{Form Tambah Service}
-
-\section{Mengelola Data Domain}
-\textbf{Path akses:} \uipath{/dashboard/domains}
-
-\subsection{Menambah Domain}
-\begin{enumerate}
- \item Buka menu Domains.
- \item Klik \uibutton{Add domain}.
- \item Pilih service induk pada field \textit{Service}.
- \item Isi nama domain dan expected IP bila diperlukan.
- \item Klik \uibutton{Save domain}.
-\end{enumerate}
-
-\textbf{Catatan:} jika expected IP dikosongkan, backend akan menggunakan IP server induk dari service terkait.
-
-\uiscreenshot{screenshots/manual-domain-create.png}{Form Tambah Domain}
-
-\subsection{Menjalankan DNS Check}
-\begin{enumerate}
- \item Buka halaman daftar domain.
- \item Tekan badge status DNS pada baris domain yang ingin dicek.
- \item Tunggu proses pemeriksaan selesai.
- \item Lihat hasil baru pada kolom status dan IP hasil resolve.
-\end{enumerate}
-
-\textbf{Hasil yang diharapkan:} status berubah menjadi \texttt{MATCH}, \texttt{MISMATCH}, atau \texttt{UNRESOLVED}.
-
-Pada halaman daftar domain, badge status DNS juga berfungsi sebagai tombol aksi. Ketika ditekan, sistem menjalankan proses pemeriksaan DNS dan memperbarui status pada baris domain tersebut.
-
-\uiscreenshot{screenshots/manual-domains-list.png}{Halaman Daftar Domain dan Status DNS}
-
-\section{Mengelola Data Catatan}
-\textbf{Path akses:} \uipath{/dashboard/notes}
-
-\subsection{Menambah Catatan}
-\begin{enumerate}
- \item Buka menu Catatan.
- \item Klik tombol tambah catatan.
- \item Pilih server pada field \textit{Server}.
- \item Isi judul, kategori, dan isi catatan.
- \item Klik tombol simpan catatan.
-\end{enumerate}
-
-\textbf{Hasil yang diharapkan:} catatan tersimpan dan terhubung ke server terpilih.
-
-\uiscreenshot{screenshots/manual-notes-list.png}{Halaman Daftar Catatan}
-
-\uiscreenshot{screenshots/manual-note-create.png}{Form Tambah Catatan}
-
-\subsection{Melihat Catatan pada Detail Server}
-\begin{enumerate}
- \item Buka halaman detail server.
- \item Scroll ke bagian catatan server.
- \item Lihat daftar catatan yang hanya terkait dengan server tersebut.
-\end{enumerate}
-
-\textbf{Hasil yang diharapkan:} catatan yang tampil hanyalah catatan dengan \texttt{serverId} yang sesuai.
-
-\section{Aksi Hapus Data}
-Pada daftar server, service, domain, dan catatan, aksi hapus saat ini menggunakan alur dua langkah:
-
-\begin{enumerate}
- \item Klik ikon trash.
- \item Tombol berubah menjadi ikon check sebagai konfirmasi.
- \item Klik ikon check untuk benar-benar menghapus data.
-\end{enumerate}
-
-\textbf{Catatan penting:}
-\begin{itemize}
- \item Hapus server akan menghapus service, domain, dan catatan terkait melalui cascade delete.
- \item Hapus service akan menghapus domain terkait melalui cascade delete.
-\end{itemize}
-
-\chapter{Kesimpulan dan Saran}
-
-\section*{Kesimpulan}
-\addcontentsline{toc}{section}{Kesimpulan}
-Minmon berhasil diimplementasikan sebagai aplikasi web untuk inventaris dan monitoring ringan homelab. Sistem ini sudah mencakup autentikasi admin, ringkasan dashboard, CRUD untuk server, service, domain, dan catatan, serta pemeriksaan DNS A record. Dalam konteks penggunaan homelab pribadi dengan beberapa server dan banyak service/domain, aplikasi ini sudah cukup representatif untuk membantu pencatatan aset, dokumentasi operasional, dan validasi domain.
-
-Keunggulan utama implementasi saat ini adalah struktur relasi yang jelas, form relasi berbasis select, ringkasan dashboard yang langsung informatif, serta adanya catatan yang bisa ditampilkan langsung pada halaman detail server. Selain itu, interaksi UI juga sudah dibuat lebih aman melalui delete dua langkah dan urutan list yang stabil.
-
-\section*{Saran}
-\addcontentsline{toc}{section}{Saran}
-Saran pengembangan untuk tahap berikutnya antara lain:
-
-\begin{itemize}
- \item Menambahkan fitur pencarian, filter, dan pagination pada halaman daftar data.
- \item Menambahkan histori hasil pemeriksaan DNS agar perubahan status domain dapat dilacak dari waktu ke waktu.
- \item Menambahkan multi-role authentication jika aplikasi ingin dipakai lebih dari satu pengguna.
- \item Menambahkan dukungan monitoring lain selain DNS A record, misalnya masa berlaku SSL, ping, atau health endpoint HTTP.
- \item Menambahkan histori aktivitas agar perubahan data server, service, domain, dan catatan dapat ditelusuri lebih mudah.
-\end{itemize}
-
-\end{document}
diff --git a/docs/report-outline.md b/docs/report-outline.md
index 140785d..ab57bd6 100644
--- a/docs/report-outline.md
+++ b/docs/report-outline.md
@@ -4,182 +4,109 @@
### 1.1 Latar Belakang
-- Pentingnya pendataan server, layanan, domain, dan dokumentasi internal secara terpusat
-- Kebutuhan dashboard web sederhana untuk membantu admin memantau aset server private
-- Kebutuhan pemeriksaan DNS dasar agar domain dapat diverifikasi terhadap IP server yang diharapkan
+- Importance of structured server documentation and monitoring
+- Need for a simple admin dashboard for managing private servers and hosted services
### 1.2 Rumusan Masalah
-- Bagaimana membangun aplikasi web terproteksi untuk mengelola server, service, domain, dan note dalam satu dashboard?
-- Bagaimana memeriksa apakah domain mengarah ke IP server yang benar secara sederhana?
-- Bagaimana menampilkan relasi data, misalnya note per server, langsung dari halaman detail?
+- How to manage server data, services, domains, and internal notes in one web app?
+- How to check whether a domain resolves to the expected server IP automatically?
### 1.3 Batasan Masalah
-- Aplikasi berupa dashboard CRUD + monitoring ringan
-- Hanya satu aktor admin
-- DNS checking terbatas pada A record / IPv4
-- Tidak mencakup SSH, deployment automation, Docker orchestration, atau reverse proxy management
-- Fokus implementasi pada web app + server app + PostgreSQL
+- CRUD + monitoring dashboard only
+- No SSH, no Docker orchestration, no real deployment automation
+- DNS checking limited to A record / IPv4
+- Single admin login
### 1.4 Tujuan
-- Membangun dashboard manajemen layanan server private berbasis web
-- Menyediakan CRUD untuk server, service, domain, dan note
-- Menyediakan pemeriksaan DNS sederhana dan ringkasan dashboard
+- Build a web-based dashboard for private server service management
+- Provide simple DNS verification and summary monitoring
### 1.5 Manfaat
-- Membantu admin mendokumentasikan dan memantau aset server
-- Membantu proses pembelajaran relasi data, CRUD, auth, dan monitoring dasar
-- Menjadi contoh implementasi full-stack berbasis React, tRPC, Drizzle, dan PostgreSQL
+- Helps admins manage server and service records
+- Helps students understand CRUD, relationships, and monitoring concepts
## BAB 2 - Tinjauan Pustaka
-### 2.1 Sistem Informasi Berbasis Web
+### 2.1 Web-Based Information Systems
-### 2.2 Konsep CRUD
+### 2.2 CRUD Concept
-### 2.3 Konsep Dashboard Monitoring
+### 2.3 Monitoring Dashboard Concept
-### 2.4 DNS dan Resolusi A Record
+### 2.4 DNS and A Record Resolution
-### 2.5 Otentikasi pada Aplikasi Web
-
-### 2.6 Teknologi yang Digunakan
+### 2.5 Technologies Used
- React
- TanStack Router
-- TanStack Query
- tRPC
- PostgreSQL
- Drizzle ORM
- better-auth
- Effect
-- Bun
## BAB 3 - Analisis dan Perancangan Sistem
### 3.1 Analisis Kebutuhan
-#### Kebutuhan Fungsional
-
-- Admin dapat sign in untuk mengakses dashboard
-- Sistem mengarahkan `/` ke `/dashboard` atau `/login` berdasarkan sesi
-- Admin dapat mengelola data server
-- Admin dapat mengelola data service
-- Admin dapat mengelola data domain
-- Admin dapat menjalankan DNS check untuk domain
-- Admin dapat mengelola note server
-- Admin dapat melihat note yang terkait langsung dari halaman detail server
-- Sistem menampilkan ringkasan dashboard dan data terbaru
-
-#### Kebutuhan Non-Fungsional
-
-- Antarmuka sederhana dan student-friendly
-- Relasi data dipilih melalui select input agar mengurangi kesalahan input ID manual
-- Data list ditampilkan dengan urutan stabil
-- Aksi hapus memakai konfirmasi dua langkah berbasis ikon
+- Functional requirements
+- Non-functional requirements
### 3.2 Perancangan Sistem
- Use case diagram
- Class diagram
- Activity diagram
-- State chart diagram
+- State chart
- Sequence diagram
- Collaboration diagram
### 3.3 Perancangan Database
-- Tabel `server`
-- Tabel `service`
-- Tabel `domain`
-- Tabel `note`
-- Tabel auth bawaan better-auth
-- Relasi:
- - server -> services
- - server -> notes
- - service -> domains
-- Aturan cascade delete pada relasi utama
+- Server table
+- Service table
+- Domain table
+- Note table
+- Auth tables
### 3.4 Perancangan Antarmuka
-- Halaman login (`/login`)
-- Redirect root (`/`)
-- Dashboard summary (`/dashboard`)
-- Halaman list/detail/form server
-- Halaman list/detail/form service
-- Halaman list/detail/form domain
-- Halaman list/detail/form note
-- Section note terkait pada detail server
+- Login page
+- Dashboard page
+- Server list/detail/form
+- Service list/detail/form
+- Domain list/detail/form
+- Note list/detail/form
## BAB 4 - Implementasi dan Pengujian
### 4.1 Implementasi Sistem
-#### Backend
-
-- Router tRPC untuk server, service, domain, note, dan dashboard summary
-- Validasi relasi parent-child pada create/update
-- DNS check menggunakan resolver IPv4 A record
-- Persist hasil DNS check ke database
-
-#### Frontend
-
-- Protected routes untuk area dashboard
-- CRUD forms dengan TanStack Form
-- Select input untuk relasi:
- - service -> server
- - domain -> service
- - note -> server
-- Resource tables dengan stable row key dan ordering stabil dari backend
-- Delete action berbasis ikon dengan dua klik konfirmasi
-- DNS status pada domain list sebagai tombol check dengan styling status badge
-
-#### Authentication
-
-- better-auth client + protected route redirect
-- Akses tanpa sesi diarahkan ke `/login`
+- Backend implementation
+- Frontend implementation
+- Authentication implementation
+- DNS check implementation
### 4.2 Pengujian
-- Pengujian login admin
-- Pengujian CRUD server
-- Pengujian CRUD service
-- Pengujian CRUD domain
-- Pengujian CRUD note
-- Pengujian select relasi pada form
-- Pengujian DNS check domain
-- Pengujian note terkait pada halaman detail server
-- Pengujian ringkasan dashboard
-- Pengujian typecheck web dan API
+- Login test
+- CRUD server test
+- CRUD service test
+- CRUD domain test
+- CRUD note test
+- DNS check test
+- Dashboard summary test
### 4.3 Hasil Pengujian
-- Tabel atau screenshot untuk setiap skenario berhasil
-- Bukti hasil DNS check (`MATCH`, `MISMATCH`, `UNRESOLVED`)
-- Bukti relasi data tampil benar di halaman detail dan list
+- Tables/screenshots of successful scenarios
### 4.4 Evaluasi
-#### Kelebihan
-
-- Struktur data relasional jelas
-- UI CRUD sudah konsisten dengan select untuk foreign key utama
-- Ringkasan dashboard dan detail page saling terhubung
-- DNS checking sudah terintegrasi langsung di alur domain
-
-#### Keterbatasan
-
-- DNS checking hanya untuk A record / IPv4
-- Belum ada multi-role auth
-- Belum ada fitur pagination, search, atau sorting manual dari UI
-- Full workspace typecheck masih terhalang issue pre-existing pada `apps/desktop`
-
-#### Saran Pengembangan
-
-- Tambah filter, search, dan pagination
-- Tambah histori DNS check
-- Tambah role/permission management
-- Tambah monitoring yang lebih luas selain DNS A record
+- Strengths of the system
+- Current limitations
+- Suggested future improvements
diff --git a/docs/run-instructions.md b/docs/run-instructions.md
index d59428e..268ed77 100644
--- a/docs/run-instructions.md
+++ b/docs/run-instructions.md
@@ -2,52 +2,18 @@
## Project Description
-Minmon is a student-friendly web dashboard for managing private servers, services, domains, and internal notes. The current implementation focuses on protected CRUD flows plus lightweight DNS A-record checking.
+Minmon is a student-friendly web dashboard for managing private servers, their hosted services, related domains, and server notes. It focuses on CRUD operations plus simple DNS monitoring, not deployment automation.
-## Current Main Features
+## Main Features
-- Admin sign-in with better-auth
-- Root route (`/`) redirects automatically:
- - logged in -> `/dashboard`
- - not logged in -> `/login`
-- Dashboard summary for:
- - total servers
- - total services
- - total domains
- - DNS match / mismatch / unresolved counts
- - inactive or down services
- - recent notes
+- Admin login with better-auth
+- Dashboard summary for servers, services, domains, DNS status, and inactive services
- Server CRUD
- Service CRUD
- Domain CRUD
-- Note CRUD
-- Domain DNS check from:
- - domain list
- - domain detail page
-- Server detail page includes notes that belong to that server
-- Foreign-key form fields use select inputs for related records:
- - service -> server
- - domain -> service
- - note -> server
-- Sample seeded admin and demo dataset
-
-## Current Route Structure
-
-- `/` -> auth-based redirect
-- `/login` -> sign-in page
-- `/dashboard` -> summary page
-- `/dashboard/servers` -> server list
-- `/dashboard/servers/new` -> create server
-- `/dashboard/servers/$serverId` -> server detail + edit + related notes
-- `/dashboard/services` -> service list
-- `/dashboard/services/new` -> create service
-- `/dashboard/services/$serviceId` -> service detail + edit
-- `/dashboard/domains` -> domain list
-- `/dashboard/domains/new` -> create domain
-- `/dashboard/domains/$domainId` -> domain detail + edit
-- `/dashboard/notes` -> note list
-- `/dashboard/notes/new` -> create note
-- `/dashboard/notes/$noteId` -> note detail + edit
+- DNS A-record check with match/mismatch/unresolved result
+- Server note/documentation CRUD
+- Sample seed data for demo use
## Local Setup
@@ -69,30 +35,19 @@ CORS_ORIGIN=http://localhost:5173
NODE_ENV=development
```
-3. Prepare web environment at `apps/web/.env`
-
-```env
-VITE_SERVER_URL=http://localhost:3000
-```
-
-4. Ensure the web app can reach the server
-
-- The web client uses the better-auth client with `env.VITE_SERVER_URL`
-- Make sure the web environment points to the running API server
-
-5. Start PostgreSQL if needed
+3. Start PostgreSQL if needed
```bash
bun run db:start
```
-6. Apply schema
+4. Apply schema
```bash
bun run db:push
```
-7. Seed sample data
+5. Seed sample data
```bash
bun run --filter @minmon/db db:seed
@@ -103,44 +58,17 @@ Seeded admin login:
- Email: `admin@minmon.local`
- Password: `admin12345`
-8. Run the apps
+6. Run the apps
```bash
bun run dev
```
-9. Open the app
+7. Open the app
- Web: `http://localhost:5173`
- API: `http://localhost:3000`
-## Current Behavior Notes
-
-### List Ordering
-
-- Main CRUD list pages are ordered by:
- - `createdAt DESC`
- - then `id DESC`
-- This keeps row ordering deterministic after refetch.
-
-### Delete Actions
-
-- List pages use a two-click destructive icon flow:
- - first click: trash icon
- - second click: check icon confirms delete
-- Confirm state resets on blur and timeout.
-
-### Domain DNS Check
-
-- Domain list uses the DNS status badge itself as the check trigger.
-- Domain detail page also provides a DNS check action.
-- DNS checking resolves IPv4 A records only.
-
-### Relationship Behavior
-
-- Deleting a server removes related services, domains, and notes through cascade rules in the database.
-- Deleting a service removes related domains through cascade rules.
-
## Helpful Commands
```bash
@@ -149,12 +77,9 @@ bun run db:push
bun run db:studio
bun run --filter web check-types
bun run --filter server check-types
-bun x tsc --noEmit -p packages/api/tsconfig.json
```
## Notes
- Full workspace typecheck may still fail because of a pre-existing `apps/desktop` typing issue for `three`.
-- The verified implementation path is the web app + server app + database push + database seed.
-- DNS checking is limited to A record / IPv4.
-- This project is not a deployment platform and does not include SSH, Docker orchestration, or reverse proxy automation.
+- The dashboard app itself, server app, DB push, and DB seed are verified.
diff --git a/docs/screenshots/manual-dashboard.png b/docs/screenshots/manual-dashboard.png
deleted file mode 100644
index 19f8224..0000000
Binary files a/docs/screenshots/manual-dashboard.png and /dev/null differ
diff --git a/docs/screenshots/manual-domain-create.png b/docs/screenshots/manual-domain-create.png
deleted file mode 100644
index 89feff5..0000000
Binary files a/docs/screenshots/manual-domain-create.png and /dev/null differ
diff --git a/docs/screenshots/manual-domains-list.png b/docs/screenshots/manual-domains-list.png
deleted file mode 100644
index dfb0058..0000000
Binary files a/docs/screenshots/manual-domains-list.png and /dev/null differ
diff --git a/docs/screenshots/manual-login.png b/docs/screenshots/manual-login.png
deleted file mode 100644
index d75d4ad..0000000
Binary files a/docs/screenshots/manual-login.png and /dev/null differ
diff --git a/docs/screenshots/manual-note-create.png b/docs/screenshots/manual-note-create.png
deleted file mode 100644
index b73f345..0000000
Binary files a/docs/screenshots/manual-note-create.png and /dev/null differ
diff --git a/docs/screenshots/manual-notes-list.png b/docs/screenshots/manual-notes-list.png
deleted file mode 100644
index 436deac..0000000
Binary files a/docs/screenshots/manual-notes-list.png and /dev/null differ
diff --git a/docs/screenshots/manual-server-create.png b/docs/screenshots/manual-server-create.png
deleted file mode 100644
index 2345e2a..0000000
Binary files a/docs/screenshots/manual-server-create.png and /dev/null differ
diff --git a/docs/screenshots/manual-server-detail.png b/docs/screenshots/manual-server-detail.png
deleted file mode 100644
index 7d06747..0000000
Binary files a/docs/screenshots/manual-server-detail.png and /dev/null differ
diff --git a/docs/screenshots/manual-servers-list.png b/docs/screenshots/manual-servers-list.png
deleted file mode 100644
index 990735e..0000000
Binary files a/docs/screenshots/manual-servers-list.png and /dev/null differ
diff --git a/docs/screenshots/manual-service-create.png b/docs/screenshots/manual-service-create.png
deleted file mode 100644
index e450186..0000000
Binary files a/docs/screenshots/manual-service-create.png and /dev/null differ
diff --git a/docs/screenshots/manual-services-list.png b/docs/screenshots/manual-services-list.png
deleted file mode 100644
index b9d9fa1..0000000
Binary files a/docs/screenshots/manual-services-list.png and /dev/null differ
diff --git a/docs/uml-guidance.md b/docs/uml-guidance.md
index ce4222e..158e9ca 100644
--- a/docs/uml-guidance.md
+++ b/docs/uml-guidance.md
@@ -8,16 +8,14 @@
### Main Use Cases
-- Sign in
-- Access protected dashboard
+- Login
- View dashboard summary
- Manage servers
- Manage services
- Manage domains
-- Check DNS for a domain
-- Manage notes
-- View notes related to a specific server
-- Sign out
+- Check DNS for domain
+- Manage server notes
+- Logout
## 2. Class Diagram
@@ -81,30 +79,19 @@
- Server 1..\* Service
- Server 1..\* Note
- Service 1..\* Domain
-- Service \*..1 Server
-- Domain \*..1 Service
-- Note \*..1 Server
-
-### Implementation Notes
-
-- Server detail page displays related notes for the selected server.
-- Service forms select an existing server.
-- Domain forms select an existing service.
-- Note forms select an existing server.
## 3. Activity Diagram
-Suggested activity: **Check DNS from Domain List**
+Suggested activity: **Check DNS**
Flow:
-- Admin opens domain list
-- Admin clicks the DNS status badge
-- System resolves IPv4 A record(s)
-- System compares resolved IPs with `expectedServerIp`
-- System stores check result in the domain record
-- System refreshes the list
-- System shows `MATCH`, `MISMATCH`, or `UNRESOLVED`
+- Admin opens domain list/detail
+- Admin clicks Check DNS
+- System resolves A record
+- System compares resolved IPs with expected server IP
+- System stores result
+- System shows Match / Mismatch / Unresolved
## 4. State Chart Diagram
@@ -112,15 +99,15 @@ Suggested state chart: **Domain DNS Status**
States:
-- UNRESOLVED
-- MATCH
-- MISMATCH
+- Unresolved
+- Match
+- Mismatch
Transitions:
-- DNS check success and expected IP found -> MATCH
-- DNS check success and expected IP not found -> MISMATCH
-- DNS resolution failure -> UNRESOLVED
+- Check DNS success with expected IP found -> Match
+- Check DNS success with expected IP missing -> Mismatch
+- Check DNS failure -> Unresolved
## 5. Sequence Diagram
@@ -136,41 +123,24 @@ Objects:
Flow:
-- Admin opens create domain page
-- UI loads available services
-- Admin selects a service and submits the form
-- API validates the service relationship
-- API stores the domain
-- Admin clicks DNS status / check action
-- UI calls `domains.check`
-- API resolves DNS A records
-- API updates domain check fields in the database
-- UI refetches and displays the new status
+- Admin submits domain form
+- UI sends create request
+- API validates and stores domain
+- Admin triggers DNS check
+- UI calls check mutation
+- API resolves DNS
+- API updates database
+- UI displays result
## 6. Collaboration Diagram
-Show collaboration between:
+Show interaction between:
- Admin
- Dashboard UI
-- better-auth client / auth middleware
-- tRPC routers
+- Auth module
+- API router
- Database
- DNS lookup module
-Suggested emphasis:
-
-- Protected route access
-- CRUD coordination between UI and API
-- Parent-child relationship selection in forms
-- DNS checking and persistence flow
-- Server detail page collaboration with note listing
-
-## 7. UI/Interaction Notes for Documentation
-
-Useful current UI details you can reflect in diagrams or captions:
-
-- `/` redirects to `/dashboard` or `/login` based on session state
-- Main list pages are ordered by `createdAt DESC`, then `id DESC`
-- Delete actions use a two-click icon confirmation flow
-- Domain DNS check is triggered from the DNS status badge in the list view
+Focus on how each module collaborates during CRUD and DNS checking.
diff --git a/package.json b/package.json
index 09cf3e1..a545b6c 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,6 @@
"elysia": "^1.4.28",
"@trpc/server": "^11.16.0",
"better-auth": "1.5.5",
- "drizzle-orm": "^0.45.1",
- "effect": "^3.18.4",
"next-themes": "^0.4.6",
"@trpc/client": "^11.16.0",
"@types/react-dom": "^19.2.3"
diff --git a/packages/api/package.json b/packages/api/package.json
index 43a9162..1223235 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -17,8 +17,8 @@
"@trpc/client": "catalog:",
"@trpc/server": "catalog:",
"dotenv": "catalog:",
- "drizzle-orm": "catalog:",
- "effect": "catalog:",
+ "drizzle-orm": "^0.45.1",
+ "effect": "^3.18.4",
"zod": "catalog:"
},
"devDependencies": {
diff --git a/packages/api/src/routers/domains.ts b/packages/api/src/routers/domains.ts
index 5f04711..9ba0b87 100644
--- a/packages/api/src/routers/domains.ts
+++ b/packages/api/src/routers/domains.ts
@@ -21,7 +21,7 @@ import {
export const domainsRouter = router({
list: protectedProcedure.input(domainListSchema).query(async ({ input }) => {
if (!input?.serviceId) {
- return db.select().from(domain).orderBy(desc(domain.createdAt), desc(domain.id));
+ return db.select().from(domain).orderBy(desc(domain.updatedAt));
}
await requireService(input.serviceId);
@@ -30,7 +30,7 @@ export const domainsRouter = router({
.select()
.from(domain)
.where(eq(domain.serviceId, input.serviceId))
- .orderBy(desc(domain.createdAt), desc(domain.id));
+ .orderBy(desc(domain.updatedAt));
}),
byId: protectedProcedure.input(byIdSchema).query(async ({ input }) => {
diff --git a/packages/api/src/routers/notes.ts b/packages/api/src/routers/notes.ts
index ccbd0a0..150df17 100644
--- a/packages/api/src/routers/notes.ts
+++ b/packages/api/src/routers/notes.ts
@@ -14,7 +14,7 @@ import {
export const notesRouter = router({
list: protectedProcedure.input(noteListSchema).query(async ({ input }) => {
if (!input?.serverId) {
- return db.select().from(note).orderBy(desc(note.createdAt), desc(note.id));
+ return db.select().from(note).orderBy(desc(note.updatedAt));
}
await requireServer(input.serverId);
@@ -23,7 +23,7 @@ export const notesRouter = router({
.select()
.from(note)
.where(eq(note.serverId, input.serverId))
- .orderBy(desc(note.createdAt), desc(note.id));
+ .orderBy(desc(note.updatedAt));
}),
byId: protectedProcedure.input(byIdSchema).query(async ({ input }) => {
diff --git a/packages/api/src/routers/servers.ts b/packages/api/src/routers/servers.ts
index 2e96623..b733d61 100644
--- a/packages/api/src/routers/servers.ts
+++ b/packages/api/src/routers/servers.ts
@@ -8,7 +8,7 @@ import { createEntityId, requireServer, stripUndefined } from "./utils";
export const serversRouter = router({
list: protectedProcedure.query(async () => {
- return db.select().from(server).orderBy(desc(server.createdAt), desc(server.id));
+ return db.select().from(server).orderBy(desc(server.updatedAt));
}),
byId: protectedProcedure.input(byIdSchema).query(async ({ input }) => {
diff --git a/packages/api/src/routers/services.ts b/packages/api/src/routers/services.ts
index 9e84963..8a45f93 100644
--- a/packages/api/src/routers/services.ts
+++ b/packages/api/src/routers/services.ts
@@ -19,7 +19,7 @@ import {
export const servicesRouter = router({
list: protectedProcedure.input(serviceListSchema).query(async ({ input }) => {
if (!input?.serverId) {
- return db.select().from(service).orderBy(desc(service.createdAt), desc(service.id));
+ return db.select().from(service).orderBy(desc(service.updatedAt));
}
await requireServer(input.serverId);
@@ -28,7 +28,7 @@ export const servicesRouter = router({
.select()
.from(service)
.where(eq(service.serverId, input.serverId))
- .orderBy(desc(service.createdAt), desc(service.id));
+ .orderBy(desc(service.updatedAt));
}),
byId: protectedProcedure.input(byIdSchema).query(async ({ input }) => {
diff --git a/packages/db/package.json b/packages/db/package.json
index 2b29b35..0381152 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -24,7 +24,7 @@
"better-auth": "catalog:",
"@minmon/env": "workspace:*",
"dotenv": "catalog:",
- "drizzle-orm": "catalog:",
+ "drizzle-orm": "^0.45.1",
"pg": "^8.17.1",
"zod": "catalog:"
},
diff --git a/packages/ui/src/components/button.tsx b/packages/ui/src/components/button.tsx
index 61e7223..cf471f6 100644
--- a/packages/ui/src/components/button.tsx
+++ b/packages/ui/src/components/button.tsx
@@ -1,10 +1,9 @@
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cn } from "@minmon/ui/lib/utils";
import { cva, type VariantProps } from "class-variance-authority";
-import { Loader2Icon } from "lucide-react";
const buttonVariants = cva(
- "group/button relative inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:scale-[0.98] disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
@@ -42,40 +41,14 @@ function Button({
className,
variant = "default",
size = "default",
- loading = false,
- disabled,
- children,
...props
-}: ButtonPrimitive.Props &
- VariantProps & {
- loading?: boolean;
- }) {
+}: ButtonPrimitive.Props & VariantProps) {
return (
- {loading ? (
-
-
-
- ) : null}
-
- {children}
-
-
+ />
);
}
diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx
deleted file mode 100644
index 00b1857..0000000
--- a/packages/ui/src/components/dialog.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-"use client";
-
-import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
-import { cn } from "@minmon/ui/lib/utils";
-import { XIcon } from "lucide-react";
-import * as React from "react";
-
-function Dialog({ ...props }: DialogPrimitive.Root.Props) {
- return ;
-}
-
-function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
- return ;
-}
-
-function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
- return ;
-}
-
-function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
- return ;
-}
-
-function DialogBackdrop({
- className,
- ...props
-}: DialogPrimitive.Backdrop.Props) {
- return (
-
- );
-}
-
-function DialogContent({
- className,
- children,
- showClose = true,
- ...props
-}: DialogPrimitive.Popup.Props & { showClose?: boolean }) {
- return (
-
-
-
- {children}
- {showClose ? (
-
-
- Close
-
- ) : null}
-
-
- );
-}
-
-function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
- return (
-
- );
-}
-
-function DialogDescription({
- className,
- ...props
-}: DialogPrimitive.Description.Props) {
- return (
-
- );
-}
-
-export {
- Dialog,
- DialogTrigger,
- DialogPortal,
- DialogClose,
- DialogBackdrop,
- DialogContent,
- DialogHeader,
- DialogFooter,
- DialogTitle,
- DialogDescription,
-};
diff --git a/packages/ui/src/components/select.tsx b/packages/ui/src/components/select.tsx
deleted file mode 100644
index 08a3c36..0000000
--- a/packages/ui/src/components/select.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-import * as React from "react"
-import { Select as SelectPrimitive } from "@base-ui/react/select"
-
-import { cn } from "@minmon/ui/lib/utils"
-import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
-
-const Select = SelectPrimitive.Root
-
-function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
- return (
-
- )
-}
-
-function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
- return (
-
- )
-}
-
-function SelectTrigger({
- className,
- size = "default",
- children,
- ...props
-}: SelectPrimitive.Trigger.Props & {
- size?: "sm" | "default"
-}) {
- return (
-
- {children}
-
- }
- />
-
- )
-}
-
-function SelectContent({
- className,
- children,
- side = "bottom",
- sideOffset = 4,
- align = "center",
- alignOffset = 0,
- alignItemWithTrigger = true,
- ...props
-}: SelectPrimitive.Popup.Props &
- Pick<
- SelectPrimitive.Positioner.Props,
- "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
- >) {
- return (
-
-
-
-
- {children}
-
-
-
-
- )
-}
-
-function SelectLabel({
- className,
- ...props
-}: SelectPrimitive.GroupLabel.Props) {
- return (
-
- )
-}
-
-function SelectItem({
- className,
- children,
- ...props
-}: SelectPrimitive.Item.Props) {
- return (
-
-
- {children}
-
-
- }
- >
-
-
-
- )
-}
-
-function SelectSeparator({
- className,
- ...props
-}: SelectPrimitive.Separator.Props) {
- return (
-
- )
-}
-
-function SelectScrollUpButton({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- )
-}
-
-function SelectScrollDownButton({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- )
-}
-
-export {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectLabel,
- SelectScrollDownButton,
- SelectScrollUpButton,
- SelectSeparator,
- SelectTrigger,
- SelectValue,
-}
diff --git a/packages/ui/src/components/sheet.tsx b/packages/ui/src/components/sheet.tsx
deleted file mode 100644
index c45fd81..0000000
--- a/packages/ui/src/components/sheet.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-"use client";
-
-import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
-import { cn } from "@minmon/ui/lib/utils";
-import { XIcon } from "lucide-react";
-import * as React from "react";
-
-function Sheet({ ...props }: DialogPrimitive.Root.Props) {
- return ;
-}
-
-function SheetTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
- return ;
-}
-
-function SheetClose({ ...props }: DialogPrimitive.Close.Props) {
- return ;
-}
-
-function SheetPortal({ ...props }: DialogPrimitive.Portal.Props) {
- return ;
-}
-
-function SheetBackdrop({
- className,
- ...props
-}: DialogPrimitive.Backdrop.Props) {
- return (
-
- );
-}
-
-type Side = "top" | "right" | "bottom" | "left";
-
-const sideClasses: Record = {
- left: "inset-y-0 left-0 h-full w-3/4 max-w-sm border-r data-open:slide-in-from-left data-closed:slide-out-to-left",
- right:
- "inset-y-0 right-0 h-full w-3/4 max-w-sm border-l data-open:slide-in-from-right data-closed:slide-out-to-right",
- top: "inset-x-0 top-0 w-full border-b data-open:slide-in-from-top data-closed:slide-out-to-top",
- bottom:
- "inset-x-0 bottom-0 w-full border-t data-open:slide-in-from-bottom data-closed:slide-out-to-bottom",
-};
-
-function SheetContent({
- className,
- children,
- side = "right",
- showClose = true,
- ...props
-}: DialogPrimitive.Popup.Props & {
- side?: Side;
- showClose?: boolean;
-}) {
- return (
-
-
-
- {children}
- {showClose ? (
-
-
- Close
-
- ) : null}
-
-
- );
-}
-
-function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function SheetTitle({ className, ...props }: DialogPrimitive.Title.Props) {
- return (
-
- );
-}
-
-function SheetDescription({
- className,
- ...props
-}: DialogPrimitive.Description.Props) {
- return (
-
- );
-}
-
-export {
- Sheet,
- SheetTrigger,
- SheetClose,
- SheetPortal,
- SheetBackdrop,
- SheetContent,
- SheetHeader,
- SheetTitle,
- SheetDescription,
-};
diff --git a/packages/ui/src/components/skeleton.tsx b/packages/ui/src/components/skeleton.tsx
index 8146c60..da1ef00 100644
--- a/packages/ui/src/components/skeleton.tsx
+++ b/packages/ui/src/components/skeleton.tsx
@@ -4,9 +4,6 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return (
diff --git a/packages/ui/src/components/table.tsx b/packages/ui/src/components/table.tsx
index 48056b1..5917743 100644
--- a/packages/ui/src/components/table.tsx
+++ b/packages/ui/src/components/table.tsx
@@ -21,10 +21,9 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return ;
}
-function TableHead({ className, scope = "col", ...props }: React.ComponentProps<"th">) {
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (