├── pnpm-workspace.yaml ├── apps ├── client │ ├── src │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ ├── queries │ │ │ │ ├── audit │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useAuditLogs.ts │ │ │ │ ├── users │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useUsersExist.ts │ │ │ │ ├── tags │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useTags.ts │ │ │ │ │ └── useServiceTags.ts │ │ │ │ ├── providers │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useProviders.ts │ │ │ │ │ └── useProvider.ts │ │ │ │ ├── integrations │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useIntegrations.ts │ │ │ │ │ └── useIntegrationUrls.ts │ │ │ │ ├── alerts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useAlerts.ts │ │ │ │ │ ├── useDismissAlert.ts │ │ │ │ │ └── useUndismissAlert.ts │ │ │ │ ├── views │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useViews.ts │ │ │ │ │ ├── useDeleteView.ts │ │ │ │ │ ├── useSaveView.ts │ │ │ │ │ └── useActiveView.ts │ │ │ │ ├── services │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useService.ts │ │ │ │ │ ├── useServiceLogs.ts │ │ │ │ │ ├── useStopService.ts │ │ │ │ │ ├── useStartService.ts │ │ │ │ │ └── useServices.ts │ │ │ │ ├── index.ts │ │ │ │ └── queryKeys.ts │ │ │ ├── use-mobile.tsx │ │ │ └── useFormErrors.ts │ │ ├── components │ │ │ ├── ui │ │ │ │ ├── use-toast.ts │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── collapsible.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── slider.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ ├── hover-card.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── tag-badge.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── file-dropzone.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── accordion.tsx │ │ │ │ ├── card.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ └── calendar.tsx │ │ │ ├── Dashboard │ │ │ │ ├── FilterPanel │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ ├── icons │ │ │ │ ├── AppIcon.tsx │ │ │ │ ├── CoralogixIcon.tsx │ │ │ │ ├── KibanaIcon.tsx │ │ │ │ └── DatadogIcon.tsx │ │ │ ├── ThemeProvider.tsx │ │ │ ├── LogoutButton.tsx │ │ │ ├── ErrorAlert.tsx │ │ │ ├── ThemeToggle.tsx │ │ │ ├── ThemeButton.tsx │ │ │ ├── ProfileButton.tsx │ │ │ ├── TVModeLauncher.tsx │ │ │ ├── ActionButtons.tsx │ │ │ └── AuthGuard.tsx │ │ ├── main.tsx │ │ ├── types │ │ │ ├── index.ts │ │ │ └── SavedView.ts │ │ ├── lib │ │ │ ├── utils.ts │ │ │ ├── auth.ts │ │ │ ├── sslKeys.ts │ │ │ └── permissions.ts │ │ ├── utils │ │ │ └── alert.utils.ts │ │ ├── pages │ │ │ ├── index.ts │ │ │ └── NotFound.tsx │ │ ├── test │ │ │ ├── setup.ts │ │ │ ├── TestProviders.tsx │ │ │ └── test-utils.tsx │ │ ├── App.css │ │ └── App.tsx │ ├── bun.lockb │ ├── public │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── git.png │ │ │ ├── logo.png │ │ │ └── slack.png │ │ └── robots.txt │ ├── postcss.config.js │ ├── tsconfig.json │ ├── components.json │ ├── vitest.config.ts │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── tsconfig.app.json │ ├── eslint.config.js │ └── index.html └── server │ ├── src │ ├── bl │ │ ├── integrations │ │ │ └── integration-connector │ │ │ │ ├── integration-connector.ts │ │ │ │ ├── integration-connector-factory.ts │ │ │ │ ├── grafana-integration-connector.ts │ │ │ │ ├── datadog-integration-connector.ts │ │ │ │ └── kibana-integration-connector.ts │ │ ├── services │ │ │ ├── ServiceNotFound.ts │ │ │ └── services.bl.ts │ │ ├── providers │ │ │ ├── ProviderNotFound.ts │ │ │ └── provider-connector │ │ │ │ ├── providerConnectorFactory.ts │ │ │ │ ├── providerConnector.ts │ │ │ │ ├── k8sProviderConnector.ts │ │ │ │ └── vmProviderConnector.ts │ │ └── audit │ │ │ └── audit.bl.ts │ ├── utils │ │ ├── validators │ │ │ ├── sshpk.d.ts │ │ │ └── validators.ts │ │ └── isZodError.ts │ ├── api │ │ ├── v1 │ │ │ ├── audit │ │ │ │ ├── router.ts │ │ │ │ └── controller.ts │ │ │ ├── alerts │ │ │ │ ├── router.ts │ │ │ │ └── controller.ts │ │ │ ├── integrations │ │ │ │ └── router.ts │ │ │ ├── tags │ │ │ │ └── router.ts │ │ │ ├── views │ │ │ │ └── router.ts │ │ │ ├── providers │ │ │ │ └── router.ts │ │ │ ├── users │ │ │ │ └── router.ts │ │ │ ├── custom-fields │ │ │ │ └── router.ts │ │ │ ├── secrets │ │ │ │ └── router.ts │ │ │ └── services │ │ │ │ └── router.ts │ │ └── health.ts │ ├── index.ts │ ├── middleware │ │ └── auth.ts │ ├── dal │ │ ├── db.ts │ │ └── external-client │ │ │ └── grafana-client.ts │ └── jobs │ │ └── refresh-job.ts │ ├── .gitignore │ ├── tests │ ├── setup.ts │ ├── isZodError.test.ts │ ├── encryption.test.ts │ ├── test-integration.ts │ └── test-private-key.ts │ ├── eslint.config.mjs │ ├── tsconfig.json │ ├── jest.config.js │ └── package.json ├── assets └── images │ ├── tv-mode.png │ └── dashboard.png ├── .cursor └── mcp.json ├── packages └── shared │ ├── src │ ├── index.ts │ └── logger.ts │ ├── tsconfig.json │ ├── package.json │ └── eslint.config.mjs ├── .github ├── ISSUE_TEMPLATE │ ├── ui_issue.md │ ├── documentation.md │ ├── devops_issue.md │ ├── bug_report.md │ ├── feature_request.md │ ├── test-request.md │ └── tech-debt.yaml ├── labeler.yml ├── workflows │ ├── auto-pull-label.yml │ ├── greeting.yml │ ├── ci.yaml │ ├── sync-version.yml │ └── auto-issue-label.yml ├── PULL_REQUEST_TEMPLATE.md └── release-drafter.yml ├── .coderabbit.yaml ├── .gitignore ├── package.json ├── turbo.json ├── docker-compose.yml ├── docker-entrypoint.sh ├── configuration_example ├── docker-config.yml └── README.md ├── default-config.yml ├── .dockerignore ├── scripts └── start-docker.sh ├── docker-compose.dev.yml ├── SECURITY.md ├── Dockerfile.server ├── Dockerfile.client ├── CONTRIBUTING.md └── Dockerfile /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' -------------------------------------------------------------------------------- /apps/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/audit/index.ts: -------------------------------------------------------------------------------- 1 | export { useAuditLogs } from './useAuditLogs'; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/users/index.ts: -------------------------------------------------------------------------------- 1 | export { useUsersExist } from './useUsersExist'; -------------------------------------------------------------------------------- /apps/client/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/apps/client/bun.lockb -------------------------------------------------------------------------------- /assets/images/tv-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/assets/images/tv-mode.png -------------------------------------------------------------------------------- /assets/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/assets/images/dashboard.png -------------------------------------------------------------------------------- /apps/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/apps/client/public/favicon.ico -------------------------------------------------------------------------------- /apps/client/public/images/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/apps/client/public/images/git.png -------------------------------------------------------------------------------- /apps/client/public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/apps/client/public/images/logo.png -------------------------------------------------------------------------------- /apps/client/public/images/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alfaarghya/OpsiMate/main/apps/client/public/images/slack.png -------------------------------------------------------------------------------- /apps/client/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /apps/client/src/components/ui/use-toast.ts: -------------------------------------------------------------------------------- 1 | import { useToast, toast } from "@/hooks/use-toast"; 2 | 3 | export { useToast, toast }; 4 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/tags/index.ts: -------------------------------------------------------------------------------- 1 | export { useTags } from './useTags'; 2 | export { useServiceTags } from './useServiceTags'; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/providers/index.ts: -------------------------------------------------------------------------------- 1 | export { useProviders } from './useProviders'; 2 | export { useProvider } from './useProvider'; -------------------------------------------------------------------------------- /apps/client/src/components/Dashboard/FilterPanel/index.ts: -------------------------------------------------------------------------------- 1 | export { FilterPanel } from './FilterPanel'; 2 | export type { Filters } from './FilterPanel'; 3 | 4 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/integrations/index.ts: -------------------------------------------------------------------------------- 1 | export { useIntegrations } from './useIntegrations'; 2 | export { useIntegrationUrls } from './useIntegrationUrls'; -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "chrome-devtools": { 4 | "command": "npx", 5 | "args": ["chrome-devtools-mcp@latest"] 6 | } 7 | } 8 | } 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import App from './App.tsx' 3 | import './index.css' 4 | 5 | createRoot(document.getElementById("root")!).render(); 6 | -------------------------------------------------------------------------------- /apps/client/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root 4 | 5 | export { AspectRatio } 6 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/alerts/index.ts: -------------------------------------------------------------------------------- 1 | export { useAlerts } from './useAlerts'; 2 | export { useDismissAlert } from './useDismissAlert'; 3 | export { useUndismissAlert } from './useUndismissAlert'; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/views/index.ts: -------------------------------------------------------------------------------- 1 | export { useViews } from './useViews'; 2 | export { useSaveView } from './useSaveView'; 3 | export { useDeleteView } from './useDeleteView'; 4 | export { useActiveView } from './useActiveView'; -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './schemas'; 3 | export * from './logger'; 4 | 5 | // Explicitly export enums to ensure they're available 6 | export { Role, SecretType, ProviderType } from './types'; 7 | -------------------------------------------------------------------------------- /apps/client/src/components/Dashboard/index.ts: -------------------------------------------------------------------------------- 1 | export { useServiceFilters } from './useServiceFilters'; 2 | export { FilterPanel } from './FilterPanel'; 3 | export type { Filters } from './FilterPanel'; 4 | export { Dashboard } from './Dashboard'; 5 | 6 | -------------------------------------------------------------------------------- /apps/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: Googlebot 2 | Allow: / 3 | 4 | User-agent: Bingbot 5 | Allow: / 6 | 7 | User-agent: Twitterbot 8 | Allow: / 9 | 10 | User-agent: facebookexternalhit 11 | Allow: / 12 | 13 | User-agent: * 14 | Allow: / 15 | -------------------------------------------------------------------------------- /apps/server/src/bl/integrations/integration-connector/integration-connector.ts: -------------------------------------------------------------------------------- 1 | import {Integration, IntegrationUrls} from "@OpsiMate/shared"; 2 | 3 | export interface IntegrationConnector { 4 | getUrls(integration: Integration, tags: string[]): Promise; 5 | } -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/services/index.ts: -------------------------------------------------------------------------------- 1 | export { useServices } from './useServices'; 2 | export { useService } from './useService'; 3 | export { useServiceLogs } from './useServiceLogs'; 4 | export { useStartService } from './useStartService'; 5 | export { useStopService } from './useStopService'; -------------------------------------------------------------------------------- /apps/client/src/components/icons/AppIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ImgHTMLAttributes } from "react"; 2 | 3 | export const AppIcon = (props: ImgHTMLAttributes) => { 4 | return ( 5 | OpsiMate Logo 10 | ); 11 | }; -------------------------------------------------------------------------------- /apps/client/src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Client-side type definitions 2 | export enum Role { 3 | Admin = 'admin', 4 | Editor = 'editor', 5 | Viewer = 'viewer', 6 | } 7 | 8 | export interface User { 9 | id: number; 10 | email: string; 11 | fullName: string; 12 | role: Role; 13 | createdAt: string; 14 | } -------------------------------------------------------------------------------- /apps/server/src/utils/validators/sshpk.d.ts: -------------------------------------------------------------------------------- 1 | declare module "sshpk" { 2 | export interface Key { 3 | type: string; 4 | size: number; 5 | comment?: string; 6 | source?: string; 7 | } 8 | export function parseKey( 9 | data: string | Buffer, 10 | format?: string, 11 | name?: string 12 | ): Key; 13 | } 14 | -------------------------------------------------------------------------------- /apps/client/src/types/SavedView.ts: -------------------------------------------------------------------------------- 1 | import { Filters } from "@/components/Dashboard"; 2 | 3 | export interface SavedView { 4 | id: string; 5 | name: string; 6 | description?: string; 7 | createdAt: string; 8 | filters: Filters; 9 | visibleColumns: Record; 10 | searchTerm: string; 11 | isDefault?: number; 12 | } 13 | -------------------------------------------------------------------------------- /apps/server/src/bl/services/ServiceNotFound.ts: -------------------------------------------------------------------------------- 1 | class ServiceNotFound extends Error { 2 | constructor(public serviceId: number, message: string = "Service not found") { 3 | super(message); 4 | this.name = 'ServiceNotFound'; 5 | Object.setPrototypeOf(this, new.target.prototype); 6 | } 7 | } 8 | 9 | export {ServiceNotFound}; 10 | -------------------------------------------------------------------------------- /apps/server/src/bl/providers/ProviderNotFound.ts: -------------------------------------------------------------------------------- 1 | class ProviderNotFound extends Error { 2 | constructor(public provider: number, message: string = "Provider not found") { 3 | super(message); 4 | this.name = 'ProviderNotFound'; 5 | Object.setPrototypeOf(this, new.target.prototype); 6 | } 7 | } 8 | 9 | export { ProviderNotFound }; 10 | -------------------------------------------------------------------------------- /apps/client/src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /apps/client/src/components/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 2 | import { type ThemeProviderProps } from "next-themes/dist/types"; 3 | 4 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 5 | return ( 6 | 7 | {children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/client/src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 2 | 3 | const Collapsible = CollapsiblePrimitive.Root 4 | 5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 6 | 7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 8 | 9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 10 | -------------------------------------------------------------------------------- /apps/client/src/components/icons/CoralogixIcon.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export const CoralogixIcon = (props: SVGProps) => ( 4 | 9 | 10 | 11 | 12 | ); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ui_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: UI Issue 3 | about: Report an issue related to the user interface 4 | title: "[UI]" 5 | labels: UI 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the UI issue** 11 | A clear description of the problem with the UI. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain. 15 | 16 | **Additional context** 17 | Any other details or info. 18 | -------------------------------------------------------------------------------- /apps/server/src/utils/isZodError.ts: -------------------------------------------------------------------------------- 1 | import { ZodError } from "zod"; 2 | 3 | export const isZodError = (error: unknown): error is ZodError => { 4 | if(!(error instanceof Error)) return false; 5 | 6 | if(error instanceof ZodError) return true; 7 | if(error.constructor.name === "ZodError") return true; 8 | if("issues" in error && error.issues instanceof Array) return true; 9 | 10 | return false; 11 | } -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/index.ts: -------------------------------------------------------------------------------- 1 | // Export all query hooks 2 | export * from './services'; 3 | export * from './alerts'; 4 | export * from './providers'; 5 | export * from './tags'; 6 | export * from './integrations'; 7 | export * from './views'; 8 | export * from './users'; 9 | export * from './audit'; 10 | export * from './custom-fields'; 11 | 12 | // Export query keys 13 | export { queryKeys } from './queryKeys'; -------------------------------------------------------------------------------- /apps/client/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | 8 | /** 9 | * Remove duplicate objects from an array based on a key 10 | */ 11 | export function removeDuplicates(array: T[], key: keyof T): T[] { 12 | return Array.from(new Map(array.map(item => [item[key], item])).values()); 13 | } 14 | -------------------------------------------------------------------------------- /apps/server/src/api/v1/audit/router.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import { Router } from 'express'; 3 | import { AuditController } from './controller.js'; 4 | import PromiseRouter from "express-promise-router"; 5 | 6 | export default function createAuditRouter(auditController: AuditController): Router { 7 | const router = PromiseRouter(); 8 | router.get('/', auditController.getAuditLogsPaginated); 9 | return router; 10 | } -------------------------------------------------------------------------------- /apps/client/src/utils/alert.utils.ts: -------------------------------------------------------------------------------- 1 | import type { Alert } from "@OpsiMate/shared"; 2 | 3 | 4 | type Dict = Record; 5 | 6 | export const getAlertServiceId = (a: Alert): number | undefined => { 7 | const rec = a as unknown as Dict; 8 | const sid = rec.serviceId; 9 | if (typeof sid === 'number') return sid as number; 10 | 11 | const parts = a.id.split(':'); 12 | const n = Number(parts[1]); 13 | return Number.isFinite(n) ? n : undefined; 14 | }; 15 | -------------------------------------------------------------------------------- /apps/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "noImplicitAny": false, 13 | "noUnusedParameters": false, 14 | "skipLibCheck": true, 15 | "allowJs": true, 16 | "noUnusedLocals": false, 17 | "strictNullChecks": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Improvements or additions to documentation 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the documentation issue** 11 | A clear and concise description of what documentation needs improvement or addition. 12 | 13 | **Affected pages or sections** 14 | Which parts of the docs need work? 15 | 16 | **Additional context** 17 | Include any other relevant information. 18 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | DevOps: 2 | - 'devops*' 3 | - 'infrastructure/**' 4 | - 'ci/**' 5 | - 'docker/**' 6 | - '**/.github/**' 7 | 8 | documentation: 9 | - 'docs/**' 10 | - '**/*.md' 11 | - 'README.md' 12 | - 'documentation/**' 13 | 14 | Server: 15 | - '**/server/**' 16 | 17 | UI: 18 | - '**/client/**' 19 | - '**/*.css' 20 | - '**/*.scss' 21 | - '**/*.html' 22 | 23 | Test: 24 | - '**/*.test.*' 25 | - '**/tests/**' 26 | - '**/__tests__/**' 27 | - '**/test/**' 28 | -------------------------------------------------------------------------------- /apps/client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/index.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/devops_issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: DevOps Issue 3 | about: Report an issue related to DevOps or infrastructure 4 | title: "[DevOps]" 5 | labels: DevOps 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the DevOps issue** 11 | Details about the infrastructure or deployment problem. 12 | 13 | **Steps to reproduce or investigate** 14 | Steps to reproduce or links to logs, configs, etc. 15 | 16 | **Expected behavior** 17 | What you expected to happen. 18 | 19 | **Additional context** 20 | Additional relevant info. 21 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/views/useViews.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { viewsApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useViews = () => { 6 | return useQuery({ 7 | queryKey: queryKeys.views, 8 | queryFn: async () => { 9 | const response = await viewsApi.getViews(); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch views'); 12 | } 13 | return response.data || []; 14 | }, 15 | }); 16 | }; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/tags/useTags.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { providerApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useTags = () => { 6 | return useQuery({ 7 | queryKey: queryKeys.tags, 8 | queryFn: async () => { 9 | const response = await providerApi.getAllTags(); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch tags'); 12 | } 13 | return response.data || []; 14 | }, 15 | }); 16 | }; -------------------------------------------------------------------------------- /apps/client/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import react from '@vitejs/plugin-react-swc' 3 | import path from 'path' 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | test: { 8 | globals: true, 9 | environment: 'jsdom', 10 | setupFiles: './src/test/setup.ts', 11 | css: true, 12 | }, 13 | resolve: { 14 | alias: { 15 | '@': path.resolve(__dirname, './src'), 16 | '@OpsiMate/shared': path.resolve(__dirname, '../../packages/shared/src'), 17 | }, 18 | }, 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["ES2020"], 6 | "declaration": true, 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true 16 | }, 17 | "include": ["src/**/*"], 18 | "exclude": ["node_modules", "dist"] 19 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug to help us improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '...' 17 | 3. Scroll down to '...' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | What you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /apps/client/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Alerts } from "./Alerts"; 2 | export { default as Integrations } from "./Integrations"; 3 | export { default as Login } from "./Login"; 4 | export { default as MyProviders } from "./MyProviders"; 5 | export { default as NotFound } from "./NotFound"; 6 | export { default as Profile } from "./Profile"; 7 | export { default as Providers } from "./Providers"; 8 | export { default as Register } from "./Register"; 9 | export { default as Settings } from "./Settings"; 10 | export { default as TVMode } from "./TVMode"; 11 | -------------------------------------------------------------------------------- /apps/server/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | build/ 7 | 8 | # Database 9 | *.db 10 | *.sqlite 11 | *.sqlite3 12 | 13 | # Private keys and sensitive data 14 | data/ 15 | data/private-keys/ 16 | 17 | # Logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Environment variables 24 | .env 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | # IDE 31 | .vscode/ 32 | .idea/ 33 | *.swp 34 | *.swo 35 | 36 | # OS 37 | .DS_Store 38 | Thumbs.db -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of the problem. 12 | 13 | **Describe the solution you'd like** 14 | Describe the feature you want. 15 | 16 | **Describe alternatives you've considered** 17 | Any alternative solutions or features. 18 | 19 | **Additional context** 20 | Add any other context or screenshots. 21 | -------------------------------------------------------------------------------- /apps/server/tests/setup.ts: -------------------------------------------------------------------------------- 1 | // Global test setup 2 | import { Logger } from '@OpsiMate/shared'; 3 | 4 | // Mock the Kubernetes client to avoid ES module issues 5 | jest.mock('@kubernetes/client-node', () => ({ 6 | KubeConfig: jest.fn().mockImplementation(() => ({ 7 | loadFromDefault: jest.fn(), 8 | loadFromFile: jest.fn(), 9 | makeApiClient: jest.fn(), 10 | })), 11 | CoreV1Api: jest.fn(), 12 | AppsV1Api: jest.fn(), 13 | NetworkingV1Api: jest.fn(), 14 | })); 15 | 16 | // Increase timeout for integration tests 17 | jest.setTimeout(30000); -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/providers/useProviders.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { providerApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useProviders = () => { 6 | return useQuery({ 7 | queryKey: queryKeys.providers, 8 | queryFn: async () => { 9 | const response = await providerApi.getProviders(); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch providers'); 12 | } 13 | return response.data?.providers || []; 14 | }, 15 | }); 16 | }; -------------------------------------------------------------------------------- /apps/server/src/bl/providers/provider-connector/providerConnectorFactory.ts: -------------------------------------------------------------------------------- 1 | import { ProviderType } from '@OpsiMate/shared'; 2 | import {ProviderConnector} from "./providerConnector.js"; 3 | import {VMProviderConnector} from "./vmProviderConnector.js"; 4 | import {K8SProviderConnector} from "./k8sProviderConnector.js"; 5 | 6 | 7 | export function providerConnectorFactory(type: ProviderType): ProviderConnector { 8 | return providersMap[type]; 9 | } 10 | 11 | const providersMap = { 12 | [ProviderType.VM]: new VMProviderConnector(), 13 | [ProviderType.K8S]: new K8SProviderConnector(), 14 | } -------------------------------------------------------------------------------- /apps/server/src/api/health.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from 'express'; 2 | import PromiseRouter from "express-promise-router"; 3 | 4 | const router = PromiseRouter(); 5 | 6 | 7 | function healthCheck(req: Request, res: Response) { 8 | res.send('ok'); 9 | } 10 | 11 | router.get('/health', healthCheck); 12 | 13 | router.get('/', (_req: Request, res: Response) => { 14 | res.json({ 15 | message: 'Welcome to Opsimate server', 16 | status: 'Server is up and running', 17 | timestamp: new Date().toISOString() 18 | }); 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /.coderabbit.yaml: -------------------------------------------------------------------------------- 1 | chat: 2 | auto_reply: true 3 | reviews: 4 | poem: false 5 | profile: chill 6 | high_level_summary: true 7 | estimate_code_review_effort: false 8 | sequence_diagrams: false 9 | request_changes_workflow: false 10 | finishing_touches: 11 | docstrings: 12 | enabled: false 13 | unit_tests: 14 | enabled: false 15 | pre_merge_checks: 16 | docstrings: 17 | mode: off 18 | title: 19 | mode: off 20 | description: 21 | mode: off 22 | issue_assessment: 23 | mode: off 24 | custom_checks: [] 25 | related_issues: false 26 | -------------------------------------------------------------------------------- /apps/client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": false, 18 | "noUnusedParameters": false, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/services/useService.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { providerApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useService = (id: number) => { 6 | return useQuery({ 7 | queryKey: queryKeys.service(id), 8 | queryFn: async () => { 9 | const response = await providerApi.getServiceById(id); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch service'); 12 | } 13 | return response.data; 14 | }, 15 | enabled: !!id, 16 | }); 17 | }; -------------------------------------------------------------------------------- /apps/client/src/test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' 2 | import { cleanup } from '@testing-library/react' 3 | import { afterEach, vi } from 'vitest' 4 | 5 | Object.defineProperty(window, 'matchMedia', { 6 | writable: true, 7 | value: vi.fn().mockImplementation(query => ({ 8 | matches: false, 9 | media: query, 10 | onchange: null, 11 | addListener: vi.fn(), 12 | removeListener: vi.fn(), 13 | addEventListener: vi.fn(), 14 | removeEventListener: vi.fn(), 15 | dispatchEvent: vi.fn(), 16 | })), 17 | }) 18 | 19 | afterEach(() => { 20 | cleanup() 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | .env 15 | 16 | #Changes made for docker-compose to ignore data and config.yaml start here 17 | 18 | data 19 | config.yml 20 | 21 | #Changes made for docker-compose end here 22 | 23 | 24 | # Editor directories and files 25 | .vscode/* 26 | !.vscode/extensions.json 27 | .idea 28 | .DS_Store 29 | *.suo 30 | *.ntvs* 31 | *.njsproj 32 | *.sln 33 | *.sw? 34 | *.db 35 | private-keys/* 36 | *.timestamp-*.mjs 37 | .turbo -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/providers/useProvider.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { providerApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useProvider = (id: number) => { 6 | return useQuery({ 7 | queryKey: queryKeys.provider(id), 8 | queryFn: async () => { 9 | const response = await providerApi.getProvider(id); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch provider'); 12 | } 13 | return response.data; 14 | }, 15 | enabled: !!id, 16 | }); 17 | }; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/integrations/useIntegrations.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { integrationApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useIntegrations = () => { 6 | return useQuery({ 7 | queryKey: queryKeys.integrations, 8 | queryFn: async () => { 9 | const response = await integrationApi.getIntegrations(); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch integrations'); 12 | } 13 | return response.data?.integrations || []; 14 | }, 15 | }); 16 | }; -------------------------------------------------------------------------------- /.github/workflows/auto-pull-label.yml: -------------------------------------------------------------------------------- 1 | name: Auto Label PRs 2 | 3 | on: 4 | issues: 5 | types: [opened, edited] 6 | pull_request_target: # use pull_request_target for PRs (runs with base repo permissions) 7 | types: [opened, edited, synchronize] 8 | 9 | permissions: 10 | contents: read 11 | pull-requests: write 12 | issues: write 13 | 14 | jobs: 15 | label: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/labeler@v4 19 | with: 20 | repo-token: ${{ secrets.GITHUB_TOKEN }} 21 | configuration-path: '.github/labeler.yml' 22 | sync-labels: true 23 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/tags/useServiceTags.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { providerApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useServiceTags = (id: number) => { 6 | return useQuery({ 7 | queryKey: queryKeys.serviceTags(id), 8 | queryFn: async () => { 9 | const response = await providerApi.getServiceTags(id); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch service tags'); 12 | } 13 | return response.data || []; 14 | }, 15 | enabled: !!id, 16 | }); 17 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OpsiMate", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "turbo run build", 7 | "start": "pnpm --filter @OpsiMate/server start", 8 | "dev": "turbo run dev", 9 | "debug": "turbo run debug", 10 | "lint": "turbo run lint", 11 | "lint-fix": "turbo run lint-fix", 12 | "clean": "turbo run clean", 13 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 14 | }, 15 | "devDependencies": { 16 | "@turbo/gen": "^1.12.4", 17 | "prettier": "^3.2.5", 18 | "turbo": "^1.12.4" 19 | }, 20 | "packageManager": "pnpm@9.12.0" 21 | } 22 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/audit/useAuditLogs.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { auditApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useAuditLogs = (page = 1, pageSize = 20) => { 6 | return useQuery({ 7 | queryKey: queryKeys.auditLogs(page, pageSize), 8 | queryFn: async () => { 9 | const response = await auditApi.getAuditLogs(page, pageSize); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch audit logs'); 12 | } 13 | return response.data || { logs: [], total: 0 }; 14 | }, 15 | }); 16 | }; -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue Reference 2 | 3 | 4 | 5 | --- 6 | 7 | ## What Was Changed 8 | 9 | 10 | 11 | --- 12 | 13 | ## Why Was It Changed 14 | 15 | 16 | 17 | --- 18 | 19 | ## Screenshots 20 | 21 | 22 | 23 | --- 24 | 25 | ## Additional Context (Optional) 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION 🚀' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | template: | 4 | ## What's Changed. 5 | 6 | $CHANGES 7 | 8 | Thanks to our contributors: $CONTRIBUTORS 9 | categories: 10 | - title: 'New Features 🚀' 11 | labels: 12 | - enhancement 13 | - feature 14 | - UI 15 | - Server 16 | 17 | - title: 'Bug Fixes 🐛' 18 | labels: 19 | - bug 20 | - fix 21 | - DevOps 22 | - Test 23 | 24 | - title: 'Documentation 📚' 25 | labels: 26 | - documentation 27 | - docs 28 | 29 | exclude-labels: 30 | - ignore-for-release 31 | - do-not-merge -------------------------------------------------------------------------------- /apps/client/src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/users/useUsersExist.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { apiRequest } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useUsersExist = () => { 6 | return useQuery({ 7 | queryKey: queryKeys.usersExist, 8 | queryFn: async () => { 9 | const response = await apiRequest<{ exists: boolean }>('/users/exists', 'GET'); 10 | if (!response || !response.success) { 11 | throw new Error(response?.error || 'Failed to check if users exist'); 12 | } 13 | return response?.exists || false; 14 | }, 15 | staleTime: 5 * 60 * 1000, // 5 minutes 16 | }); 17 | }; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/alerts/useAlerts.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { alertsApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useAlerts = () => { 6 | return useQuery({ 7 | queryKey: queryKeys.alerts, 8 | queryFn: async () => { 9 | const response = await alertsApi.getAllAlerts(); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch alerts'); 12 | } 13 | return response.data?.alerts || []; 14 | }, 15 | staleTime: 30 * 1000, // 30 seconds 16 | refetchInterval: 30 * 1000, // Auto-refresh every 30 seconds 17 | }); 18 | }; -------------------------------------------------------------------------------- /apps/server/src/api/v1/alerts/router.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import PromiseRouter from 'express-promise-router'; 3 | import { AlertController } from './controller.js'; 4 | 5 | export default function createAlertRouter(controller: AlertController) { 6 | const router = PromiseRouter(); 7 | 8 | // GET all alerts 9 | router.get('/', controller.getAlerts.bind(controller)); 10 | 11 | // Dismiss an alert 12 | router.patch('/:id/dismiss', controller.dismissAlert.bind(controller)); 13 | 14 | // Undismiss an alert 15 | router.patch('/:id/undismiss', controller.undismissAlert.bind(controller)); 16 | 17 | return router; 18 | } -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**", ".next/**", "!.next/cache/**"] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^lint"] 11 | }, 12 | "lint-fix": { 13 | "dependsOn": ["^lint-fix"] 14 | }, 15 | "dev": { 16 | "cache": false, 17 | "persistent": true, 18 | "dependsOn": ["^build"] 19 | }, 20 | "debug": { 21 | "cache": false, 22 | "persistent": true, 23 | "dependsOn": ["^build"] 24 | }, 25 | "clean": { 26 | "cache": false 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /apps/client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import path from "path"; 4 | import { componentTagger } from "lovable-tagger"; 5 | import * as yaml from 'js-yaml'; 6 | import * as fs from 'fs'; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(({ mode }) => ({ 10 | server: { 11 | host: "::", 12 | port: 8080, 13 | }, 14 | plugins: [ 15 | react(), 16 | mode === 'development' && 17 | componentTagger(), 18 | ].filter(Boolean), 19 | resolve: { 20 | alias: { 21 | "@": path.resolve(__dirname, "./src"), 22 | }, 23 | }, 24 | optimizeDeps: { 25 | include: ['@OpsiMate/shared'] 26 | } 27 | })); 28 | -------------------------------------------------------------------------------- /apps/client/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/queryKeys.ts: -------------------------------------------------------------------------------- 1 | // Query keys for consistent caching across the application 2 | export const queryKeys = { 3 | services: ['services'] as const, 4 | alerts: ['alerts'] as const, 5 | providers: ['providers'] as const, 6 | tags: ['tags'] as const, 7 | integrations: ['integrations'] as const, 8 | views: ['views'] as const, 9 | usersExist: ['usersExist'] as const, 10 | service: (id: number) => ['service', id] as const, 11 | provider: (id: number) => ['provider', id] as const, 12 | serviceLogs: (id: number) => ['serviceLogs', id] as const, 13 | serviceTags: (id: number) => ['serviceTags', id] as const, 14 | auditLogs: (page: number, pageSize: number) => ['audit', page, pageSize] as const, 15 | }; -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/services/useServiceLogs.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import { providerApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useServiceLogs = (id: number) => { 6 | return useQuery({ 7 | queryKey: queryKeys.serviceLogs(id), 8 | queryFn: async () => { 9 | const response = await providerApi.getServiceLogs(id); 10 | if (!response.success) { 11 | throw new Error(response.error || 'Failed to fetch service logs'); 12 | } 13 | return response.data || []; 14 | }, 15 | enabled: !!id, 16 | staleTime: 10 * 1000, // 10 seconds 17 | refetchInterval: 10 * 1000, // Auto-refresh every 10 seconds 18 | }); 19 | }; -------------------------------------------------------------------------------- /apps/server/src/api/v1/integrations/router.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import PromiseRouter from 'express-promise-router'; 3 | import { IntegrationController } from './controller.js'; 4 | 5 | export default function createIntegrationRouter(controller: IntegrationController) { 6 | const router = PromiseRouter(); 7 | 8 | // CRUD API 9 | router.get('/', controller.getIntegrations); 10 | router.post('/', controller.createIntegration); 11 | router.put('/:integrationId', controller.updateIntegration); 12 | router.delete('/:integrationId', controller.deleteIntegration); 13 | 14 | router.get('/:integrationId/urls', controller.getIntegrationUrls); 15 | 16 | return router; 17 | } 18 | -------------------------------------------------------------------------------- /apps/server/src/api/v1/tags/router.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-misused-promises */ 2 | import PromiseRouter from 'express-promise-router'; 3 | import { TagController } from './controller.js'; 4 | 5 | export default function createTagRouter(tagController: TagController) { 6 | const router = PromiseRouter(); 7 | 8 | // Tag management routes 9 | router.get('/', tagController.getAllTagsHandler); 10 | router.post('/', tagController.createTagHandler); 11 | 12 | // Parameterized tag routes 13 | router.get('/:tagId', tagController.getTagByIdHandler); 14 | router.put('/:tagId', tagController.updateTagHandler); 15 | router.delete('/:tagId', tagController.deleteTagHandler); 16 | 17 | return router; 18 | } 19 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@OpsiMate/shared", 3 | "version": "1.0.0", 4 | "private": true, 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.js", 11 | "types": "./dist/index.d.ts" 12 | } 13 | }, 14 | "scripts": { 15 | "build": "tsc", 16 | "dev": "tsc --watch", 17 | "clean": "rm -rf dist", 18 | "lint": "eslint src --ext .ts", 19 | "lint-fix": "eslint src --ext .ts --fix" 20 | }, 21 | "dependencies": { 22 | "zod": "^3.23.8" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^22.5.5", 26 | "eslint": "^9.9.0", 27 | "typescript": "^5.5.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/client/src/components/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | import { LogOut } from "lucide-react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface LogoutButtonProps { 6 | collapsed: boolean; 7 | onLogout: () => void; 8 | } 9 | 10 | export function LogoutButton({ collapsed, onLogout }: LogoutButtonProps) { 11 | return ( 12 | 23 | ); 24 | } -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/views/useDeleteView.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { viewsApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useDeleteView = () => { 6 | const queryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: async (viewId: string) => { 10 | const response = await viewsApi.deleteView(viewId); 11 | if (!response.success) { 12 | throw new Error(response.error || 'Failed to delete view'); 13 | } 14 | return response.data; 15 | }, 16 | onSuccess: () => { 17 | // Invalidate and refetch views 18 | queryClient.invalidateQueries({ queryKey: queryKeys.views }); 19 | }, 20 | }); 21 | }; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | backend: 5 | image: opsimate/backend:latest 6 | container_name: opsimate-backend 7 | ports: 8 | - "3001:3001" 9 | volumes: 10 | - ./data/database:/app/data/database 11 | - ./data/private-keys:/app/data/private-keys 12 | - ./config.yml:/app/config/config.yml 13 | environment: 14 | - NODE_ENV=production 15 | - HOST=0.0.0.0 16 | restart: unless-stopped 17 | 18 | frontend: 19 | image: opsimate/frontend:latest 20 | container_name: opsimate-frontend 21 | ports: 22 | - "8080:8080" 23 | environment: 24 | - NODE_ENV=production 25 | - API_URL=http://backend:3001 26 | - HOST=0.0.0.0 27 | restart: unless-stopped 28 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/alerts/useDismissAlert.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { alertsApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useDismissAlert = () => { 6 | const queryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: async (alertId: string) => { 10 | const response = await alertsApi.dismissAlert(alertId); 11 | if (!response.success) { 12 | throw new Error(response.error || 'Failed to dismiss alert'); 13 | } 14 | return response.data; 15 | }, 16 | onSuccess: () => { 17 | // Invalidate and refetch alerts 18 | queryClient.invalidateQueries({ queryKey: queryKeys.alerts }); 19 | }, 20 | }); 21 | }; -------------------------------------------------------------------------------- /apps/server/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | import js from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default [ 6 | js.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | ...tseslint.configs.recommendedTypeChecked, 9 | { 10 | files: ['**/*.ts'], 11 | languageOptions: { 12 | parserOptions: { 13 | project: './tsconfig.json', 14 | }, 15 | }, 16 | rules: { 17 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 18 | '@typescript-eslint/no-explicit-any': 'warn', 19 | '@typescript-eslint/no-floating-promises': 'error', 20 | 'no-console': 'warn', 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /packages/shared/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // eslint.config.js 2 | import js from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default [ 6 | js.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | ...tseslint.configs.recommendedTypeChecked, 9 | { 10 | files: ['**/*.ts'], 11 | languageOptions: { 12 | parserOptions: { 13 | project: './tsconfig.json', 14 | }, 15 | }, 16 | rules: { 17 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 18 | '@typescript-eslint/no-explicit-any': 'warn', 19 | '@typescript-eslint/no-floating-promises': 'error', 20 | 'no-console': 'warn', 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/alerts/useUndismissAlert.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { alertsApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | 5 | export const useUndismissAlert = () => { 6 | const queryClient = useQueryClient(); 7 | 8 | return useMutation({ 9 | mutationFn: async (alertId: string) => { 10 | const response = await alertsApi.undismissAlert(alertId); 11 | if (!response.success) { 12 | throw new Error(response.error || 'Failed to undismiss alert'); 13 | } 14 | return response.data; 15 | }, 16 | onSuccess: () => { 17 | // Invalidate and refetch alerts 18 | queryClient.invalidateQueries({ queryKey: queryKeys.alerts }); 19 | }, 20 | }); 21 | }; -------------------------------------------------------------------------------- /apps/client/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": false, 19 | "noUnusedLocals": false, 20 | "noUnusedParameters": false, 21 | "noImplicitAny": false, 22 | "noFallthroughCasesInSwitch": false, 23 | 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": ["./src/*"] 27 | } 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /apps/client/src/hooks/queries/views/useSaveView.ts: -------------------------------------------------------------------------------- 1 | import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 | import { viewsApi } from '@/lib/api'; 3 | import { queryKeys } from '../queryKeys'; 4 | import { SavedView } from '@/types/SavedView'; 5 | 6 | export const useSaveView = () => { 7 | const queryClient = useQueryClient(); 8 | 9 | return useMutation({ 10 | mutationFn: async (view: SavedView) => { 11 | const response = await viewsApi.saveView(view); 12 | if (!response.success) { 13 | throw new Error(response.error || 'Failed to save view'); 14 | } 15 | return response.data; 16 | }, 17 | onSuccess: () => { 18 | // Invalidate and refetch views 19 | queryClient.invalidateQueries({ queryKey: queryKeys.views }); 20 | }, 21 | }); 22 | }; -------------------------------------------------------------------------------- /apps/client/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "moduleResolution": "nodenext", 5 | "target": "ES2022", 6 | "lib": ["ES2022"], 7 | "outDir": "./dist", 8 | "rootDir": "./", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowSyntheticDefaultImports": true, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "resolveJsonModule": true, 17 | "declaration": true, 18 | "declarationMap": true, 19 | "sourceMap": true 20 | }, 21 | "include": [ 22 | "src/**/*", 23 | "tests/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist", 28 | "**/*.test.ts", 29 | "**/*.spec.ts" 30 | ], 31 | } -------------------------------------------------------------------------------- /apps/server/src/bl/providers/provider-connector/providerConnector.ts: -------------------------------------------------------------------------------- 1 | import {Provider, DiscoveredService, Service, ServiceType, ContainerDetails, DiscoveredPod} from "@OpsiMate/shared"; 2 | 3 | export interface ProviderConnector { 4 | discoverServices(provider: Provider): Promise; 5 | 6 | startService(provider: Provider, serviceName: string, serviceType?: ServiceType): Promise; 7 | 8 | stopService(provider: Provider, serviceName: string, serviceType?: ServiceType, containerDetails?: ContainerDetails): Promise; 9 | 10 | getServiceLogs(provider: Provider, service: Service): Promise; 11 | 12 | testConnection(provider: Provider): Promise<{success: boolean, error?: string}>; 13 | 14 | getServicePods(provider: Provider, service: Service): Promise; 15 | } -------------------------------------------------------------------------------- /apps/server/src/bl/integrations/integration-connector/integration-connector-factory.ts: -------------------------------------------------------------------------------- 1 | import {IntegrationType} from '@OpsiMate/shared'; 2 | import {IntegrationConnector} from "./integration-connector.js"; 3 | import {GrafanaIntegrationConnector} from "./grafana-integration-connector.js"; 4 | import {KibanaIntegrationConnector} from "./kibana-integration-connector.js"; 5 | import {DatadogIntegrationConnector} from "./datadog-integration-connector.js"; 6 | 7 | 8 | export function integrationConnectorFactory(type: IntegrationType): IntegrationConnector { 9 | return integrationsMap[type]; 10 | } 11 | 12 | const integrationsMap = { 13 | [IntegrationType.Grafana]: new GrafanaIntegrationConnector(), 14 | [IntegrationType.Kibana]: new KibanaIntegrationConnector(), 15 | [IntegrationType.Datadog]: new DatadogIntegrationConnector(), 16 | } -------------------------------------------------------------------------------- /apps/server/src/bl/audit/audit.bl.ts: -------------------------------------------------------------------------------- 1 | import { AuditLogRepository } from '../../dal/auditLogRepository.js'; 2 | import { AuditLog } from '@OpsiMate/shared'; 3 | 4 | export class AuditBL { 5 | constructor(private auditLogRepository: AuditLogRepository) {} 6 | 7 | async logAction(params: Omit): Promise { 8 | await this.auditLogRepository.insertAuditLog(params); 9 | } 10 | 11 | async getAuditLogsPaginated(page: number, pageSize: number): Promise<{ logs: AuditLog[]; total: number }> { 12 | const offset = (page - 1) * pageSize; 13 | const [logs, total] = await Promise.all([ 14 | this.auditLogRepository.getAuditLogs(offset, pageSize), 15 | this.auditLogRepository.countAuditLogs(), 16 | ]); 17 | return { logs, total }; 18 | } 19 | } -------------------------------------------------------------------------------- /apps/client/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /apps/server/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/src', '/tests'], 5 | testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest', 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'json'], 10 | collectCoverageFrom: [ 11 | 'src/**/*.ts', 12 | '!src/**/*.d.ts', 13 | ], 14 | setupFilesAfterEnv: ['/tests/setup.ts'], 15 | transformIgnorePatterns: [ 16 | 'node_modules/(?!(@kubernetes/client-node)/)' 17 | ], 18 | moduleNameMapper: { 19 | '^@OpsiMate/shared$': '/../../packages/shared/dist/index.js', 20 | }, 21 | // Handle ES modules in node_modules 22 | extensionsToTreatAsEsm: ['.ts'], 23 | globals: { 24 | 'ts-jest': { 25 | useESM: true 26 | } 27 | } 28 | }; -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Docker entrypoint script for Opsimate 4 | # Handles config file mounting and startup 5 | 6 | set -e 7 | 8 | echo "Starting Opsimate container..." 9 | 10 | # Check if a custom config file is mounted 11 | if [ -f "/app/config/config.yml" ]; then 12 | echo "Using mounted config file: /app/config/config.yml" 13 | export CONFIG_FILE="/app/config/config.yml" 14 | elif [ -f "/app/config/default-config.yml" ]; then 15 | echo "Using default container config: /app/config/default-config.yml" 16 | export CONFIG_FILE="/app/config/default-config.yml" 17 | else 18 | echo "No config file found, using project default" 19 | export CONFIG_FILE="/app/config.yml" 20 | fi 21 | 22 | # Print config file being used 23 | echo "Config file: $CONFIG_FILE" 24 | 25 | # Execute the main command 26 | exec "$@" 27 | -------------------------------------------------------------------------------- /apps/client/src/components/ErrorAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AlertCircle } from 'lucide-react'; 3 | import { Alert, AlertDescription } from './ui/alert'; 4 | import { cn } from '@/lib/utils'; 5 | 6 | interface ErrorAlertProps { 7 | message: string; 8 | className?: string; 9 | } 10 | 11 | export const ErrorAlert: React.FC = ({ message, className }) => { 12 | return ( 13 | 20 | 21 | 22 | {message} 23 | 24 | 25 | ); 26 | }; -------------------------------------------------------------------------------- /configuration_example/docker-config.yml: -------------------------------------------------------------------------------- 1 | # Opsimate Docker Configuration 2 | # This configuration is optimized for Docker container deployment 3 | 4 | # Server configuration 5 | server: 6 | port: 3001 # Backend API server port 7 | host: "0.0.0.0" # Bind to all interfaces in container 8 | 9 | # Client configuration (not used in production container) 10 | client: 11 | port: 8080 # Frontend development server port (for reference) 12 | api_url: "http://localhost:3001/api/v1" # Backend API URL 13 | 14 | # Database configuration 15 | database: 16 | path: "/app/data/database/opsimate.db" # SQLite database file path (mounted volume) 17 | 18 | # Security configuration 19 | security: 20 | private_keys_path: "/app/data/private-keys" # SSH private keys directory (mounted volume) 21 | 22 | # VM configuration 23 | vm: 24 | try_with_sudo: true # Run docker commands with sudo -------------------------------------------------------------------------------- /apps/client/src/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from "react-router-dom"; 2 | import { useEffect } from "react"; 3 | 4 | const NotFound = () => { 5 | const location = useLocation(); 6 | 7 | useEffect(() => { 8 | console.error( 9 | "404 Error: User attempted to access non-existent route:", 10 | location.pathname 11 | ); 12 | }, [location.pathname]); 13 | 14 | return ( 15 |
16 |
17 |

404

18 |

Oops! Page not found

19 | 20 | Return to Home 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default NotFound; 28 | -------------------------------------------------------------------------------- /apps/client/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export type TextareaProps = React.TextareaHTMLAttributes 6 | 7 | const Textarea = React.forwardRef( 8 | ({ className, ...props }, ref) => { 9 | return ( 10 |