├── config ├── routes.yaml ├── packages │ ├── lock.yaml │ ├── twig.yaml │ ├── doctrine_migrations.yaml │ ├── rate_limiter.yaml │ ├── validator.yaml │ ├── routing.yaml │ ├── nelmio_cors.yaml │ ├── web_profiler.yaml │ ├── framework.yaml │ ├── lexik_jwt_authentication.yaml │ ├── api_platform.yaml │ ├── security.yaml │ └── doctrine.yaml ├── routes │ ├── api_platform.yaml │ ├── security.yaml │ ├── framework.yaml │ └── web_profiler.yaml ├── code │ ├── phpstan.neon │ ├── phpunit.xml.dist │ ├── .php-cs-fixer.dist.php │ └── rector.php ├── preload.php ├── services_test.yaml ├── app │ ├── supervisord_servers.yaml │ └── app_credentials.yaml ├── services.yaml └── bundles.php ├── docker ├── php │ ├── conf.d │ │ └── memory_limit.ini │ ├── fpm-conf.d │ │ └── zz-docker.conf │ └── php.ini ├── supervisor │ ├── supervisord-dist.conf │ └── supervisord-dev.conf └── nginx │ ├── dist │ └── nginx-default.conf │ ├── dev │ ├── supervisord-monitor.local.crt │ ├── nginx-default.conf │ └── supervisord-monitor.local.key │ └── nginx.conf ├── assets ├── .env.dist ├── src │ ├── api │ │ ├── api │ │ │ ├── index.ts │ │ │ └── api.ts │ │ ├── use-logout.tsx │ │ ├── use-login.tsx │ │ ├── use-get-me.tsx │ │ └── use-get-supervisors.tsx │ ├── config │ │ ├── index.ts │ │ ├── theme.ts │ │ └── env.ts │ ├── vite-env.d.ts │ ├── providers │ │ ├── router │ │ │ ├── index.ts │ │ │ ├── provider.tsx │ │ │ ├── private-route.tsx │ │ │ ├── public-route.tsx │ │ │ └── config.tsx │ │ ├── react-query │ │ │ ├── index.ts │ │ │ └── provider.tsx │ │ ├── session │ │ │ ├── index.ts │ │ │ └── context.tsx │ │ └── clock │ │ │ └── context.tsx │ ├── const │ │ ├── index.ts │ │ ├── routes.ts │ │ └── api-endpoints.ts │ ├── pages │ │ ├── home │ │ │ └── page.tsx │ │ ├── settings │ │ │ └── page.tsx │ │ └── login │ │ │ └── page.tsx │ ├── index.tsx │ ├── components │ │ ├── PageLoader.tsx │ │ ├── icons │ │ │ ├── Erase.tsx │ │ │ ├── Stop.tsx │ │ │ ├── Booklet.tsx │ │ │ ├── ShredFile.tsx │ │ │ ├── Close.tsx │ │ │ ├── Warning.tsx │ │ │ ├── Play.tsx │ │ │ ├── LockClosed.tsx │ │ │ ├── Reload.tsx │ │ │ ├── ChevronDown.tsx │ │ │ ├── ChevronUp.tsx │ │ │ ├── Skull.tsx │ │ │ └── Duplicate.tsx │ │ ├── ui │ │ │ ├── Input.tsx │ │ │ ├── TimeDisplay.tsx │ │ │ └── Checkbox.tsx │ │ ├── MonitorPageContent.tsx │ │ ├── monitor │ │ │ ├── Uptime.tsx │ │ │ └── SupervisordSkeleton.tsx │ │ ├── Header.tsx │ │ ├── Program.tsx │ │ └── Server.tsx │ ├── app │ │ ├── app.tsx │ │ └── index.css │ ├── hooks │ │ ├── useLocalStorage.ts │ │ └── useSettings.ts │ └── util │ │ ├── checkSupervisorManageResult.ts │ │ ├── trimIpPort.tsx │ │ └── toastManager.ts ├── .env.example ├── .yarnrc.yml ├── scripts │ └── gen-types │ │ ├── templates │ │ ├── http-client.ejs │ │ ├── api.ejs │ │ ├── interface-data-contract.ejs │ │ ├── enum-data-contract.ejs │ │ ├── type-data-contract.ejs │ │ ├── route-type.ejs │ │ ├── object-field-jsdoc.ejs │ │ ├── route-types.ejs │ │ ├── route-docs.ejs │ │ ├── data-contracts.ejs │ │ ├── route-name.ejs │ │ ├── data-contract-jsdoc.ejs │ │ └── procedure-call.ejs │ │ └── index.ts ├── postcss.config.cjs ├── vite.config.ts ├── tsconfig.node.json ├── .prettierrc ├── index.html ├── .gitignore ├── tsconfig.json ├── tailwind.config.cjs ├── package.json ├── public │ └── logo.svg └── @types │ └── api.d.ts ├── tests ├── Functional │ ├── resources │ │ ├── .gitignore │ │ ├── run_supervisord.sh │ │ ├── install.sh │ │ ├── Dockerfile │ │ ├── supervisord.conf │ │ └── supervisord_servers.yaml │ └── SupervisorApiClientTest.php ├── Unit │ └── XmlRpc │ │ └── resources │ │ ├── methodCall_listMethods.xml │ │ ├── methodResponse_error.xml │ │ ├── methodResponse_listMethods.xml │ │ ├── methodResponse_invalid.xml │ │ ├── methodResponse_systemMulticall.xml │ │ ├── methodCall_invalidSystemMulticall.xml │ │ ├── methodCall_systemMulticall.xml │ │ └── methodCall_systemMulticall_same_method_bug.xml └── bootstrap.php ├── .env.test ├── src ├── DTO │ ├── XmlRpc │ │ ├── CallInterface.php │ │ ├── MultiCallDTO.php │ │ ├── OperationResult.php │ │ ├── FaultDTO.php │ │ ├── ResponseDTO.php │ │ └── CallDTO.php │ ├── AuthByCredentialsDTO.php │ ├── Supervisord │ │ ├── ProcessGroup.php │ │ ├── ChangedProcess.php │ │ ├── ChangedProcesses.php │ │ ├── ProcessLog.php │ │ └── Process.php │ ├── EnvVar │ │ ├── AppCredentialsItem.php │ │ └── SupervisorServer.php │ ├── SupervisorManageResult.php │ └── SupervisorManage.php ├── Exception │ ├── BaseException.php │ └── XmlRpcException.php ├── Kernel.php ├── Controller │ ├── LogoutController.php │ └── LoginController.php ├── State │ ├── UserMeProvider.php │ ├── SupervisorsCollectionProvider.php │ └── SupervisorProcessor.php ├── Repository │ └── SupervisordServerStateRepository.php ├── EventSubscriber │ ├── InvalidJWTSubscriber.php │ └── RenewJWTSubscriber.php ├── Enum │ └── SupervisorManageTypeEnum.php ├── Service │ └── Symfony │ │ ├── SupervisordServerEnvVarProcessor.php │ │ └── AppCredentialsEnvVarProcessor.php ├── Command │ ├── CollectSupervisordServersDataCommand.php │ ├── CollectSupervisordServerDataCommand.php │ └── ProvideSupervisorServerJsonCommand.php ├── ApiResource │ ├── User.php │ └── Supervisor.php └── Entity │ └── SupervisordServerState.php ├── bin ├── database_provision.sh ├── generate_jwt_if_not_exists.sh └── console ├── public └── index.php ├── .gitignore ├── migrations └── Version20250427163901.php ├── .github └── workflows │ ├── deploy.yml │ └── client.yml ├── Makefile ├── .env ├── docker-compose.yml ├── Dockerfile └── composer.json /config/routes.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker/php/conf.d/memory_limit.ini: -------------------------------------------------------------------------------- 1 | memory_limit = 1G -------------------------------------------------------------------------------- /assets/.env.dist: -------------------------------------------------------------------------------- 1 | VITE_API_URL='http://localhost:8080/api' 2 | -------------------------------------------------------------------------------- /assets/src/api/api/index.ts: -------------------------------------------------------------------------------- 1 | export { $api } from './api'; 2 | -------------------------------------------------------------------------------- /assets/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { ENV } from './env'; 2 | -------------------------------------------------------------------------------- /tests/Functional/resources/.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /venv/ 3 | -------------------------------------------------------------------------------- /assets/.env.example: -------------------------------------------------------------------------------- 1 | VITE_API_URL='http://127.0.0.1:8080/api' 2 | -------------------------------------------------------------------------------- /assets/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /config/packages/lock.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | lock: '%env(LOCK_DSN)%' 3 | -------------------------------------------------------------------------------- /assets/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableTelemetry: 0 2 | 3 | nodeLinker: node-modules 4 | -------------------------------------------------------------------------------- /assets/src/providers/router/index.ts: -------------------------------------------------------------------------------- 1 | export { AppRouter } from './provider'; 2 | -------------------------------------------------------------------------------- /assets/src/providers/react-query/index.ts: -------------------------------------------------------------------------------- 1 | export { ReactQueryProvider } from './provider'; 2 | -------------------------------------------------------------------------------- /tests/Functional/resources/run_supervisord.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./venv/bin/supervisord -c supervisord.conf 3 | -------------------------------------------------------------------------------- /config/routes/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | resource: . 3 | type: api_platform 4 | prefix: /api 5 | -------------------------------------------------------------------------------- /assets/src/const/index.ts: -------------------------------------------------------------------------------- 1 | export { API_ENDPOINTS } from './api-endpoints'; 2 | export { ROUTES } from './routes'; 3 | -------------------------------------------------------------------------------- /config/routes/security.yaml: -------------------------------------------------------------------------------- 1 | _security_logout: 2 | resource: security.route_loader.logout 3 | type: service 4 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/http-client.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { apiConfig, generateResponses, config } = it; 3 | %> 4 | -------------------------------------------------------------------------------- /assets/src/providers/session/index.ts: -------------------------------------------------------------------------------- 1 | export { type ISessionContext, SessionProvider, useSession } from './context'; 2 | -------------------------------------------------------------------------------- /assets/src/config/theme.ts: -------------------------------------------------------------------------------- 1 | export enum Theme { 2 | dark = 'dark', 3 | light = 'light', 4 | system = 'system', 5 | } 6 | -------------------------------------------------------------------------------- /assets/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | file_name_pattern: '*.twig' 3 | 4 | when@test: 5 | twig: 6 | strict_variables: true 7 | -------------------------------------------------------------------------------- /tests/Functional/resources/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python -m venv venv 4 | 5 | ./venv/bin/pip install supervisor supervisor_twiddler 6 | -------------------------------------------------------------------------------- /config/routes/framework.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | _errors: 3 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 4 | prefix: /_error 5 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodCall_listMethods.xml: -------------------------------------------------------------------------------- 1 | 2 | system.listMethods 3 | 4 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | -------------------------------------------------------------------------------- /config/code/phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | checkGenericClassInNonGenericObjectType: false 4 | tmpDir: ../../var/.tmp/.phpstan.cache/ 5 | -------------------------------------------------------------------------------- /src/DTO/XmlRpc/CallInterface.php: -------------------------------------------------------------------------------- 1 | { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | 'DoctrineMigrations': '%kernel.project_dir%/migrations' 4 | enable_profiler: false 5 | -------------------------------------------------------------------------------- /config/packages/rate_limiter.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | rate_limiter: 3 | auth_by_credentials_api: 4 | policy: 'fixed_window' 5 | limit: 3 6 | interval: '1 minute' 7 | 8 | -------------------------------------------------------------------------------- /assets/src/const/routes.ts: -------------------------------------------------------------------------------- 1 | export const ROUTES = { 2 | HOME: '/', 3 | SETTINGS: '/settings', 4 | LOGIN: '/login', 5 | } as const; 6 | 7 | export type RouteValues = (typeof ROUTES)[keyof typeof ROUTES]; 8 | -------------------------------------------------------------------------------- /assets/src/providers/router/provider.tsx: -------------------------------------------------------------------------------- 1 | import { RouterProvider } from 'react-router-dom'; 2 | 3 | import { router } from './config'; 4 | 5 | export const AppRouter = () => ; 6 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /assets/src/index.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | 3 | import { App } from '~/app/app'; 4 | 5 | import '~/app/index.css'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render(); 8 | -------------------------------------------------------------------------------- /config/services_test.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | supervisors_servers: [ { "ip": "supervisord-monitor-supervisord-monitor-supervisord-monitor-test-server-1","port": 9555,"name": "default","username": "default","password": "default" } ] 3 | -------------------------------------------------------------------------------- /bin/database_provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Create database if it does not exist 4 | if [ ! -f var/data_dev.db ]; then 5 | php bin/console doctrine:database:create -n 6 | fi 7 | 8 | php bin/console doctrine:migrations:migrate -n 9 | -------------------------------------------------------------------------------- /src/DTO/AuthByCredentialsDTO.php: -------------------------------------------------------------------------------- 1 | new Kernel( 8 | $context['APP_ENV'], 9 | (bool)$context['APP_DEBUG'] 10 | ); 11 | -------------------------------------------------------------------------------- /assets/src/const/api-endpoints.ts: -------------------------------------------------------------------------------- 1 | export const API_ENDPOINTS = { 2 | SUPERVISORS: () => '/supervisors', 3 | MANAGE_SUPERVISORS: () => `/supervisors/manage`, 4 | ME: () => `/users/me`, 5 | LOGIN: () => `/auth/login`, 6 | LOGOUT: () => `/auth/logout`, 7 | } as const; 8 | -------------------------------------------------------------------------------- /assets/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tsconfigPaths()], 8 | }); 9 | -------------------------------------------------------------------------------- /assets/src/api/use-logout.tsx: -------------------------------------------------------------------------------- 1 | import { $api } from 'src/api/api'; 2 | import { useMutation } from '@tanstack/react-query'; 3 | import { API_ENDPOINTS } from 'src/const'; 4 | 5 | export const useLogout = () => 6 | useMutation({ 7 | mutationFn: () => $api.post(API_ENDPOINTS.LOGOUT(), []), 8 | }); 9 | -------------------------------------------------------------------------------- /assets/src/api/api/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | import { ENV } from '~/config'; 4 | 5 | export const $api = axios.create({ 6 | baseURL: ENV.VITE_API_URL, 7 | validateStatus(status) { 8 | return status >= 200 && status < 300; 9 | }, 10 | withCredentials: true, 11 | }); 12 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | /dev/null 2>&1 || [ ! -f config/jwt/private.pem ] || [ ! -f config/jwt/public.pem ]; then 5 | php bin/console lexik:jwt:generate-keypair -n --overwrite 6 | fi 7 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/.env'); 9 | } 10 | 11 | if ($_SERVER['APP_DEBUG']) { 12 | umask(0000); 13 | } 14 | -------------------------------------------------------------------------------- /assets/src/api/use-login.tsx: -------------------------------------------------------------------------------- 1 | import { $api } from 'src/api/api'; 2 | import { useMutation } from '@tanstack/react-query'; 3 | import { API_ENDPOINTS } from 'src/const'; 4 | 5 | export const useLogin = () => 6 | useMutation({ 7 | mutationFn: (data: ApiUserAuthByCredentialsDTO) => $api.post(API_ENDPOINTS.LOGIN(), data), 8 | }); 9 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | # Enables validator auto-mapping support. 4 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 5 | #auto_mapping: 6 | # App\Entity\: [] 7 | 8 | when@test: 9 | framework: 10 | validation: 11 | not_compromised_password: false 12 | -------------------------------------------------------------------------------- /config/app/supervisord_servers.yaml: -------------------------------------------------------------------------------- 1 | # SUPERVISORS_SERVERS Env var fallback. Can be used with a large number of servers. 2 | # If no SUPERVISORS_SERVERS environment variable has been set this configuration will be taken. 3 | supervisors_servers: 4 | - ip: 127.0.0.1 5 | port: 9551 6 | name: default 7 | username: default 8 | password: default 9 | -------------------------------------------------------------------------------- /docker/php/fpm-conf.d/zz-docker.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | error_log = /proc/self/fd/2 3 | 4 | [www] 5 | pm.max_children = 20 6 | pm.start_servers = 5 7 | pm.min_spare_servers = 3 8 | pm.max_spare_servers = 7 9 | ; if we send this to /proc/self/fd/1, it never appears 10 | access.log = /proc/self/fd/1 11 | 12 | catch_workers_output = yes 13 | decorate_workers_output = no 14 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | 9 | when@prod: 10 | framework: 11 | router: 12 | strict_requirements: null 13 | -------------------------------------------------------------------------------- /config/app/app_credentials.yaml: -------------------------------------------------------------------------------- 1 | # APP_CREDENTIALS Env var fallback. Can be used with a large number of users. 2 | # If no APP_CREDENTIALS environment variable has been set this configuration will be taken. 3 | app_credentials: 4 | - username: admin 5 | password: admin 6 | roles: 7 | - ROLE_MANAGER 8 | 9 | - username: guest 10 | password: guest 11 | roles: [ ] 12 | -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | origin_regex: true 4 | allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] 5 | allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] 6 | allow_headers: ['Content-Type', 'Authorization'] 7 | expose_headers: ['Link'] 8 | max_age: 3600 9 | paths: 10 | '^/': null 11 | -------------------------------------------------------------------------------- /src/Exception/XmlRpcException.php: -------------------------------------------------------------------------------- 1 | value)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /assets/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "bracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "jsxSingleQuote": true, 12 | "endOfLine": "lf", 13 | "plugins": ["prettier-plugin-tailwindcss"], 14 | "singleAttributePerLine": false 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | 3 | /config/jwt/*.pem 4 | /.env.local 5 | /.env.local.php 6 | /.env.*.local 7 | 8 | /config/secrets/prod/prod.decrypt.private.php 9 | 10 | /public/bundles/ 11 | 12 | /var/cache 13 | /var/log 14 | /var/.tmp/ 15 | /vendor/ 16 | 17 | /phpunit.xml 18 | .phpunit.result.cache 19 | 20 | /.php-cs-fixer.php 21 | 22 | /docker-compose.override.yml 23 | /var/data_dev.db 24 | /var/data_dev.db-journal 25 | -------------------------------------------------------------------------------- /assets/src/components/PageLoader.tsx: -------------------------------------------------------------------------------- 1 | import { SupervisordSkeleton } from '~/components/monitor/SupervisordSkeleton'; 2 | 3 | export const PageLoader = () => { 4 | return ( 5 |
6 | {Array.from({ length: 7 }).map((_, index) => ( 7 | 8 | ))} 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Supervisord Monitor 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | intercept_redirects: false 5 | 6 | framework: 7 | profiler: 8 | only_exceptions: false 9 | collect_serializer_data: true 10 | 11 | when@test: 12 | web_profiler: 13 | toolbar: false 14 | intercept_redirects: false 15 | 16 | framework: 17 | profiler: { collect: false } 18 | -------------------------------------------------------------------------------- /assets/src/components/icons/Erase.tsx: -------------------------------------------------------------------------------- 1 | export const Erase = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/interface-data-contract.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { contract, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | %> 5 | export interface <%~ contract.name %> { 6 | <% for (const field of contract.$content) { %> 7 | <%~ includeFile('./object-field-jsdoc.ejs', { ...it, field }) %> 8 | <%~ field.name %><%~ field.isRequired ? '' : '?' %>: <%~ field.value %><%~ field.isNullable ? ' | null' : ''%>; 9 | <% } %> 10 | } 11 | -------------------------------------------------------------------------------- /tests/Functional/resources/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk add bash python3 py3-pip 4 | 5 | RUN pip install --break-system-packages supervisor supervisor_twiddler 6 | 7 | ARG UID=1000 8 | ARG GID=1000 9 | RUN addgroup -g $GID app && adduser -D -u $UID -G app app && addgroup app www-data 10 | 11 | USER app 12 | 13 | COPY --chown=app:app . /var/www/supervisord-monitor 14 | 15 | WORKDIR /var/www/supervisord-monitor 16 | 17 | CMD ["supervisord", "-c", "supervisord.conf"] 18 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/enum-data-contract.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { contract, utils, config } = it; 3 | const { formatDescription, require, _ } = utils; 4 | const { name, $content } = contract; 5 | %> 6 | <% if (config.generateUnionEnums) { %> 7 | export type <%~ name %> = <%~ _.map($content, ({ value }) => value).join(" | ") %> 8 | <% } else { %> 9 | export enum <%~ name %> { 10 | <%~ _.map($content, ({ key, value }) => `${key} = ${value}`).join(",\n") %> 11 | } 12 | <% } %> 13 | -------------------------------------------------------------------------------- /assets/src/components/icons/Stop.tsx: -------------------------------------------------------------------------------- 1 | export const Stop = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/DTO/XmlRpc/MultiCallDTO.php: -------------------------------------------------------------------------------- 1 | */ 13 | public function toArray(): array 14 | { 15 | return array_map(static fn(CallDTO $call): array => $call->toArray(), $this->calls); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker/php/php.ini: -------------------------------------------------------------------------------- 1 | session.auto_start = Off 2 | short_open_tag = Off 3 | 4 | # http://symfony.com/doc/current/performance.html 5 | opcache.interned_strings_buffer = 16 6 | opcache.max_accelerated_files = 20000 7 | opcache.memory_consumption = 256 8 | realpath_cache_size = 4096K 9 | realpath_cache_ttl = 600 10 | 11 | memory_limit = 128M 12 | post_max_size = 6M 13 | upload_max_filesize = 5M 14 | 15 | display_errors = Off 16 | display_startup_errors = Off 17 | 18 | error_log = /tmp/php-fpm-error.log 19 | -------------------------------------------------------------------------------- /assets/src/components/icons/Booklet.tsx: -------------------------------------------------------------------------------- 1 | export const Booklet = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/src/providers/react-query/provider.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 3 | import { ReactNode } from 'react'; 4 | 5 | const queryClient = new QueryClient(); 6 | 7 | export const ReactQueryProvider = ({ children }: { children: ReactNode }) => ( 8 | 9 | {children} 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | supervisordServers: '%env(supervisordServers:SUPERVISORS_SERVERS)%' 3 | appCredentials: '%env(appCredentials:APP_CREDENTIALS)%' 4 | collectIntervalInMicroseconds: '%env(int:COLLECT_INTERVAL_IN_MICROSECONDS)%' 5 | 6 | services: 7 | _defaults: 8 | autowire: true 9 | autoconfigure: true 10 | 11 | App\: 12 | resource: '../src/' 13 | exclude: 14 | - '../src/Kernel.php' 15 | - '../src/{DTO,Enum,Exception}' 16 | - '../src/Service/SupervisorApiClient.php' 17 | -------------------------------------------------------------------------------- /assets/.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 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # yarn 27 | .yarn/* 28 | !.yarn/cache 29 | !.yarn/patches 30 | !.yarn/plugins 31 | !.yarn/releases 32 | !.yarn/sdks 33 | !.yarn/versions 34 | 35 | .env 36 | -------------------------------------------------------------------------------- /assets/src/config/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const envSchema = z.object({ 4 | PROD: z.boolean(), 5 | DEV: z.boolean(), 6 | VITE_API_URL: z.string(), 7 | }); 8 | 9 | export type EnvType = z.infer; 10 | 11 | export function validateEnv(): EnvType { 12 | const result = envSchema.safeParse(import.meta.env); 13 | 14 | if (!result.success) { 15 | throw new Error('Invalid environment variables:' + result.error); 16 | } 17 | 18 | return result.data; 19 | } 20 | 21 | export const ENV = validateEnv(); 22 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodResponse_error.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | faultCode 7 | 300 8 | 9 | 10 | faultString 11 | Invalid parameters 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/type-data-contract.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { contract, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | 5 | %> 6 | <% if (contract.$content.length) { %> 7 | export type <%~ contract.name %> = { 8 | <% for (const field of contract.$content) { %> 9 | <%~ includeFile('./object-field-jsdoc.ejs', { ...it, field }) %> 10 | <%~ field.field %>; 11 | <% } %> 12 | }<%~ utils.isNeedToAddNull(contract) ? ' | null' : ''%> 13 | <% } else { %> 14 | export type <%~ contract.name %> = Record; 15 | <% } %> 16 | -------------------------------------------------------------------------------- /assets/src/components/icons/ShredFile.tsx: -------------------------------------------------------------------------------- 1 | export const ShredFile = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | { 8 | const { status } = useSession(); 9 | 10 | return ( 11 | <> 12 | {status === 'loading' ? : status === 'authenticated' ? children : } 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { AppRouter } from '~/providers/router'; 2 | import { ReactQueryProvider } from '~/providers/react-query'; 3 | import { SessionProvider } from '~/providers/session'; 4 | import { StrictMode } from 'react'; 5 | import { ClockProvider } from '~/providers/clock/context'; 6 | 7 | export const App = () => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /assets/src/components/icons/Close.tsx: -------------------------------------------------------------------------------- 1 | export const Close = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/src/providers/router/public-route.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router'; 2 | 3 | import { useSession } from '~/providers/session'; 4 | import { ROUTES } from '~/const'; 5 | import { PageLoader } from '~/components/PageLoader'; 6 | import { ReactNode } from 'react'; 7 | 8 | export const PublicRoute = ({ children }: { children: ReactNode }) => { 9 | const { status } = useSession(); 10 | 11 | return ( 12 | <> 13 | {status === 'loading' ? : status === 'unauthenticated' ? children : } 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /assets/src/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useState } from 'react'; 2 | 3 | export function useLocalStorage(key: string, initialValue: T): [T, Dispatch>] { 4 | const fromLocal = () => { 5 | const item = window.localStorage.getItem(key); 6 | return item ? (JSON.parse(item) as T) : initialValue; 7 | }; 8 | 9 | const [storedValue, setStoredValue] = useState(fromLocal()); 10 | 11 | return [ 12 | storedValue, 13 | value => { 14 | window.localStorage.setItem(key, JSON.stringify(value)); 15 | setStoredValue(value); 16 | }, 17 | ]; 18 | } 19 | -------------------------------------------------------------------------------- /assets/src/api/use-get-me.tsx: -------------------------------------------------------------------------------- 1 | import { $api } from 'src/api/api'; 2 | import { API_ENDPOINTS } from 'src/const'; 3 | import { useQuery, useQueryClient } from '@tanstack/react-query'; 4 | 5 | export const useGetMe = () => 6 | useQuery({ 7 | queryKey: ['me'], 8 | queryFn: async () => $api.get(API_ENDPOINTS.ME()), 9 | refetchOnMount: false, 10 | refetchOnWindowFocus: false, 11 | retry: 0, 12 | retryDelay: 5000, 13 | }); 14 | 15 | export const useInvalidateMe = () => { 16 | const queryClient = useQueryClient(); 17 | 18 | return () => queryClient.invalidateQueries({ queryKey: ['me'] }); 19 | }; 20 | -------------------------------------------------------------------------------- /assets/src/components/icons/Warning.tsx: -------------------------------------------------------------------------------- 1 | export const Warning = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/src/components/icons/Play.tsx: -------------------------------------------------------------------------------- 1 | export const Play = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/src/components/icons/LockClosed.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | interface LockClosedProps extends SVGProps {} 4 | 5 | export const LockClosed = (props: LockClosedProps) => { 6 | return ( 7 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /src/DTO/XmlRpc/OperationResult.php: -------------------------------------------------------------------------------- 1 | isFault) { 14 | return new self(false, true, $response->getFirstFault()->message); 15 | } 16 | 17 | if (false === $response->bool()) { 18 | return new self(false, false, 'Operation failed'); 19 | } 20 | 21 | return new self(true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/src/util/checkSupervisorManageResult.ts: -------------------------------------------------------------------------------- 1 | import { toastManager } from '~/util/toastManager'; 2 | 3 | export const checkSupervisorManageResult = (result: ApiSupervisorSupervisorManageResult) => { 4 | if (result?.operationResult) { 5 | if (result.operationResult.isFault) { 6 | toastManager.error('Operation got fault: ' + result.operationResult.error); 7 | } 8 | 9 | return result.operationResult.ok; 10 | } 11 | 12 | if (result?.changedProcesses) { 13 | if (!result.changedProcesses.ok) { 14 | toastManager.error('Got error while changing processes: ' + result.changedProcesses.error); 15 | } 16 | return result.changedProcesses.ok; 17 | } 18 | 19 | return false; 20 | }; 21 | -------------------------------------------------------------------------------- /src/DTO/XmlRpc/FaultDTO.php: -------------------------------------------------------------------------------- 1 | $data */ 12 | public static function fromArray(array $data): self 13 | { 14 | /** @var array{faultCode: int, faultString: string} $data */ 15 | return new self(code: $data['faultCode'], message: $data['faultString']); 16 | } 17 | 18 | /** @param array $data */ 19 | public static function is(array $data): bool 20 | { 21 | return isset($data['faultCode'], $data['faultString']); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | handle_all_throwables: true 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | 19 | when@test: 20 | framework: 21 | test: true 22 | session: 23 | storage_factory_id: session.storage.factory.mock_file 24 | -------------------------------------------------------------------------------- /src/Controller/LogoutController.php: -------------------------------------------------------------------------------- 1 | headers->clearCookie( 18 | name: 'api_token', 19 | path: '/api', 20 | domain: (string)getenv('API_HOST'), 21 | secure: true, 22 | httpOnly: true, 23 | sameSite: 'strict' 24 | ); 25 | 26 | return $response; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/src/util/trimIpPort.tsx: -------------------------------------------------------------------------------- 1 | /** Utility function to trim string for fit in server container*/ 2 | export const trimIpPort = (input: string, postfix: string = ''): string => { 3 | const nameLen = 4 | window.innerWidth < 640 5 | ? 35 6 | : window.innerWidth < 1280 7 | ? 55 8 | : window.innerWidth < 1536 9 | ? 50 10 | : window.innerWidth < 1920 11 | ? 15 12 | : 40; 13 | 14 | if (input.length <= nameLen) { 15 | return input; 16 | } 17 | 18 | const postfixLength = postfix ? postfix.length + 1 : 0; 19 | const truncatedLength = nameLen - postfixLength; 20 | 21 | return input.substring(0, truncatedLength) + '...' + (postfix ? postfix : ''); 22 | }; 23 | -------------------------------------------------------------------------------- /src/DTO/SupervisorManageResult.php: -------------------------------------------------------------------------------- 1 | operationResult = $typedResult; 19 | } 20 | 21 | if ($typedResult instanceof ChangedProcesses) { 22 | $this->changedProcesses = $typedResult; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/src/components/icons/Reload.tsx: -------------------------------------------------------------------------------- 1 | export const Reload = () => { 2 | return ( 3 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /assets/src/app/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body, 7 | #root { 8 | height: 100%; 9 | } 10 | 11 | body { 12 | min-width: 375px; 13 | } 14 | 15 | /* Hide scrollbar for Chrome, Safari and Opera */ 16 | .no-scrollbar::-webkit-scrollbar { 17 | display: none; 18 | } 19 | 20 | /* Hide scrollbar for IE, Edge and Firefox */ 21 | .no-scrollbar { 22 | -ms-overflow-style: none; /* IE and Edge */ 23 | scrollbar-width: none; /* Firefox */ 24 | } 25 | 26 | @layer base { 27 | body { 28 | @apply dark:bg-black dark:text-white; 29 | } 30 | } 31 | 32 | @layer components { 33 | .program-title { 34 | @apply text-sm text-blue-500 hover:text-blue-700 dark:text-white; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /config/packages/lexik_jwt_authentication.yaml: -------------------------------------------------------------------------------- 1 | lexik_jwt_authentication: 2 | secret_key: '%env(resolve:JWT_SECRET_KEY)%' 3 | public_key: '%env(resolve:JWT_PUBLIC_KEY)%' 4 | pass_phrase: '%env(JWT_PASSPHRASE)%' 5 | token_ttl: 604800 6 | user_id_claim: email 7 | token_extractors: 8 | authorization_header: 9 | enabled: true 10 | prefix: Bearer 11 | name: Authorization 12 | cookie: 13 | enabled: true 14 | name: api_token 15 | set_cookies: 16 | api_token: 17 | samesite: strict 18 | path: /api 19 | domain: '%env(API_HOST)%' 20 | secure: true 21 | httpOnly: true 22 | 23 | when@test: 24 | lexik_jwt_authentication: 25 | encoder: 26 | service: app.mock.jwt_encoder 27 | -------------------------------------------------------------------------------- /config/packages/api_platform.yaml: -------------------------------------------------------------------------------- 1 | api_platform: 2 | title: Hello API Platform 3 | version: 1.0.0 4 | formats: 5 | json: [ 'application/json' ] 6 | docs_formats: 7 | json: [ 'application/json' ] 8 | html: [ 'text/html' ] 9 | error_formats: 10 | json: [ 'application/json' ] 11 | defaults: 12 | stateless: true 13 | cache_headers: 14 | vary: [ 'Content-Type', 'Authorization', 'Origin' ] 15 | extra_properties: 16 | standard_put: true 17 | rfc_7807_compliant_errors: true 18 | pagination_enabled: false 19 | keep_legacy_inflector: false 20 | use_symfony_listeners: true 21 | 22 | when@dist: 23 | api_platform: 24 | enable_docs: false 25 | enable_entrypoint: false 26 | enable_swagger_ui: false 27 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | password_hashers: 3 | Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 4 | algorithm: plaintext 5 | providers: 6 | all_users: 7 | lexik_jwt: 8 | class: App\ApiResource\User 9 | firewalls: 10 | dev: 11 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 12 | security: false 13 | api: 14 | pattern: ^/api/ 15 | stateless: true 16 | provider: all_users 17 | jwt: ~ 18 | main: 19 | lazy: true 20 | provider: all_users 21 | 22 | access_control: 23 | - { path: ^/api/auth, roles: PUBLIC_ACCESS } 24 | - { path: ^/api/docs, roles: PUBLIC_ACCESS } 25 | - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } 26 | -------------------------------------------------------------------------------- /src/State/UserMeProvider.php: -------------------------------------------------------------------------------- 1 | */ 13 | final readonly class UserMeProvider 14 | implements ProviderInterface 15 | { 16 | public function __construct(private Security $security) {} 17 | 18 | public function provide( 19 | Operation $operation, 20 | array $uriVariables = [], 21 | array $context = [] 22 | ): User { 23 | /** @var User $user */ 24 | $user = $this->security->getUser(); 25 | 26 | return $user; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 6 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 7 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 8 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 9 | ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true], 10 | Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], 11 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 12 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 13 | ]; 14 | -------------------------------------------------------------------------------- /src/DTO/Supervisord/ChangedProcess.php: -------------------------------------------------------------------------------- 1 | $data */ 17 | public static function fromArray(array $data): self 18 | { 19 | /** @var array{name: string, group: string, status: int, description: string} $data */ 20 | return new self( 21 | name: $data['name'], 22 | group: $data['group'], 23 | status: $data['status'], 24 | description: $data['description'] 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /assets/src/components/icons/ChevronDown.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | interface ChevronDownProps extends SVGProps {} 4 | 5 | export const ChevronDown = (props: ChevronDownProps) => { 6 | return ( 7 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /assets/src/components/icons/ChevronUp.tsx: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | interface ChevronUpProps extends SVGProps {} 4 | 5 | export const ChevronUp = (props: ChevronUpProps) => { 6 | return ( 7 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /assets/src/components/ui/Input.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes } from 'react'; 2 | 3 | interface InputProps extends InputHTMLAttributes { 4 | text: string; 5 | } 6 | 7 | export const Input = ({ text, ...props }: InputProps) => { 8 | return ( 9 |
10 | 13 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | "moduleResolution": "bundler", 10 | // "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "esModuleInterop": true, 21 | 22 | "baseUrl": ".", 23 | "paths": { 24 | "~/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src", "@types/api.d.ts"], 28 | "references": [{ "path": "./tsconfig.node.json" }] 29 | } 30 | -------------------------------------------------------------------------------- /assets/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ['class'], 4 | prefix: '', 5 | content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 6 | theme: { 7 | extend: { 8 | colors: { 9 | softYellow: { 10 | 500: '#F6F49D', 11 | }, 12 | softRed: { 13 | 500: '#FF7676', 14 | }, 15 | softGreen: { 16 | 500: '#5DAE8B', 17 | }, 18 | softBlue: { 19 | 500: '#466C95', 20 | }, 21 | }, 22 | fontFamily: { 23 | sans: ['Inter', 'sans-serif'], 24 | }, 25 | screens: { 26 | xs: '480px', 27 | '4xl': '3840px', 28 | }, 29 | }, 30 | }, 31 | variants: ['dark', 'dark-hover', 'dark-group-hover', 'dark-even', 'dark-odd'], 32 | }; 33 | -------------------------------------------------------------------------------- /assets/src/components/icons/Skull.tsx: -------------------------------------------------------------------------------- 1 | export const Skull = () => { 2 | return ( 3 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/route-type.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { route, utils, config } = it; 3 | const { _, pascalCase, require } = utils; 4 | const { query, payload, pathParams, headers } = route.request; 5 | 6 | const routeDocs = includeFile("./route-docs", { config, route, utils }); 7 | const routeNamespace = pascalCase(route.routeName.usage); 8 | 9 | %> 10 | 11 | /** 12 | <%~ routeDocs.description %> 13 | 14 | <%~ routeDocs.lines %> 15 | 16 | */ 17 | export namespace <%~ routeNamespace %> { 18 | export type RequestParams = <%~ (pathParams && pathParams.type) || '{}' %>; 19 | export type RequestQuery = <%~ (query && query.type) || '{}' %>; 20 | export type RequestBody = <%~ (payload && payload.type) || 'never' %>; 21 | export type RequestHeaders = <%~ (headers && headers.type) || '{}' %>; 22 | export type ResponseBody = <%~ route.response.type %>; 23 | } -------------------------------------------------------------------------------- /src/Repository/SupervisordServerStateRepository.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | system.listMethods 7 | 8 | 9 | system.methodSignature 10 | 11 | 12 | system.methodHelp 13 | 14 | 15 | system.multicall 16 | 17 | 18 | system.shutdown 19 | 20 | 21 | sample.add 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/DTO/Supervisord/ChangedProcesses.php: -------------------------------------------------------------------------------- 1 | isFault) { 17 | return new self(false, [], $response->getFirstFault()->message); 18 | } 19 | 20 | $changedProcesses = []; 21 | foreach ($response->getValue() as $changedProcess) { 22 | /** @var array $changedProcess */ 23 | $changedProcesses[] = ChangedProcess::fromArray($changedProcess); 24 | } 25 | 26 | return new self(true, $changedProcesses); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /migrations/Version20250427163901.php: -------------------------------------------------------------------------------- 1 | addSql(<<<'SQL' 20 | CREATE TABLE supervisord_server_state (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, groups CLOB NOT NULL --(DC2Type:json) 21 | , version VARCHAR(255) NOT NULL, ok BOOLEAN NOT NULL, server VARCHAR(255) NOT NULL, fail_error CLOB DEFAULT NULL) 22 | SQL); 23 | } 24 | 25 | public function down(Schema $schema): void 26 | { 27 | $this->addSql(<<<'SQL' 28 | DROP TABLE supervisord_server_state 29 | SQL); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DTO/Supervisord/ProcessLog.php: -------------------------------------------------------------------------------- 1 | $log */ 23 | public static function fromStringOrFault(string|array $log): ?self 24 | { 25 | if (is_array($log)) { 26 | return self::fromFault(FaultDTO::fromArray($log)); 27 | } else { 28 | return self::fromString($log); 29 | } 30 | } 31 | 32 | public static function fromFault(FaultDTO $fault): self 33 | { 34 | return new self('Failed to fetch logs. Error: '.$fault->message); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodResponse_invalid.xml: -------------------------------------------------------------------------------- 1 | This is an invalid xml file 2 | 3 | 4 | 5 | 6 | 7 | 8 | system.listMethods 9 | 10 | 11 | system.methodSignature 12 | 13 | 14 | system.methodHelp 15 | 16 | 17 | system.multicall 18 | 19 | 20 | system.shutdown 21 | 22 | 23 | sample.add 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/src/providers/clock/context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useEffect, useState } from 'react'; 2 | 3 | export interface IClockContext { 4 | clock: number; 5 | setClock: Dispatch>; 6 | } 7 | 8 | const ClockContext = createContext(null); 9 | 10 | export const ClockProvider = ({ children }: { children: ReactNode }) => { 11 | const [clock, setClock] = useState(0); 12 | 13 | useEffect(() => { 14 | const interval = setInterval(() => { 15 | setClock(prevClock => prevClock + 1); 16 | }, 1000); 17 | 18 | return () => clearInterval(interval); 19 | }); 20 | 21 | return {children}; 22 | }; 23 | 24 | export const useClock = () => { 25 | const context = useContext(ClockContext); 26 | if (!context) { 27 | throw new Error('useClock must be used within an ClockProvider'); 28 | } 29 | return context; 30 | }; 31 | -------------------------------------------------------------------------------- /config/code/phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | tests 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docker/supervisor/supervisord-dist.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile=/tmp/supervisord.log 3 | nodaemon=true 4 | loglevel=warn 5 | pidfile=/tmp/supervisord.pid 6 | 7 | [program:php-fpm] 8 | command = /usr/local/sbin/php-fpm -F 9 | autostart=true 10 | autorestart=true 11 | priority=10 12 | stopsignal=QUIT 13 | 14 | [program:nginx] 15 | command=/usr/sbin/nginx -e stderr -g "daemon off;" 16 | autostart=true 17 | autorestart=true 18 | priority=20 19 | stopsignal=QUIT 20 | 21 | [program:generate_jwt_if_not_exists] 22 | command=sh bin/generate_jwt_if_not_exists.sh 23 | autostart=true 24 | startsecs=0 25 | startretries=0 26 | autorestart=false 27 | priority=30 28 | stopsignal=QUIT 29 | 30 | [program:database_provision] 31 | command=sh bin/database_provision.sh 32 | autostart=true 33 | startsecs=0 34 | startretries=0 35 | autorestart=false 36 | priority=30 37 | stopsignal=QUIT 38 | 39 | [program:collect-supervisord-servers-data] 40 | command=php bin/console app:collect-supervisord-servers-data 41 | autostart=true 42 | autorestart=true 43 | -------------------------------------------------------------------------------- /src/DTO/EnvVar/SupervisorServer.php: -------------------------------------------------------------------------------- 1 | authenticated = null !== $this->username && null !== $this->password; 22 | 23 | $credentials = ''; 24 | if ($this->authenticated) { 25 | $credentials = $this->username.':'.$this->password.'@'; 26 | } 27 | 28 | $this->webOpenUrl = sprintf('http://%s%s:%d/', $credentials, $this->ip, $this->port); 29 | } 30 | 31 | #[Ignore] 32 | public function getUrl(): string 33 | { 34 | return sprintf('http://%s:%s/RPC2', $this->ip, $this->port); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/src/components/ui/TimeDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from 'react'; 2 | 3 | interface TimeDisplayProps extends HTMLAttributes { 4 | ts: number; 5 | } 6 | 7 | export const TimeDisplay = ({ ts, ...props }: TimeDisplayProps) => { 8 | const years = Math.floor(ts / (60 * 60 * 24 * 365)); 9 | const days = Math.floor(ts / (60 * 60 * 24)) % 365; 10 | const hours = Math.floor(ts / (60 * 60)) % 24; 11 | const minutes = Math.floor(ts / 60) % 60; 12 | const secs = Math.floor(ts) % 60; 13 | 14 | const formatNumber = (n: number) => n.toString().padStart(2, '0'); 15 | 16 | return ( 17 | 18 | {years > 0 && ( 19 | <> 20 | {formatNumber(years)}: 21 | 22 | )} 23 | {days > 0 && ( 24 | <> 25 | {formatNumber(days)}: 26 | 27 | )} 28 | {formatNumber(hours)}:{formatNumber(minutes)}:{formatNumber(secs)} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/object-field-jsdoc.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { field, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | 5 | const comments = _.uniq( 6 | _.compact([ 7 | field.title, 8 | field.description, 9 | field.deprecated && ` * @deprecated`, 10 | !_.isUndefined(field.format) && `@format ${field.format}`, 11 | !_.isUndefined(field.minimum) && `@min ${field.minimum}`, 12 | !_.isUndefined(field.maximum) && `@max ${field.maximum}`, 13 | !_.isUndefined(field.pattern) && `@pattern ${field.pattern}`, 14 | !_.isUndefined(field.example) && 15 | `@example ${_.isObject(field.example) ? JSON.stringify(field.example) : field.example}`, 16 | ]).reduce((acc, comment) => [...acc, ...comment.split(/\n/g)], []), 17 | ); 18 | %> 19 | <% if (comments.length === 1) { %> 20 | /** <%~ comments[0] %> */ 21 | <% } else if (comments.length) { %> 22 | /** 23 | <% comments.forEach(comment => { %> 24 | * <%~ comment %> 25 | 26 | <% }) %> 27 | */ 28 | <% } %> 29 | -------------------------------------------------------------------------------- /assets/src/components/icons/Duplicate.tsx: -------------------------------------------------------------------------------- 1 | export const Duplicate = () => { 2 | return ( 3 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /docker/supervisor/supervisord-dev.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile=syslog 3 | nodaemon=true 4 | loglevel=trace 5 | pidfile = /tmp/supervisord.pid 6 | 7 | [program:php-fpm] 8 | command = /usr/local/sbin/php-fpm -F 9 | autostart=true 10 | autorestart=true 11 | priority=10 12 | stopsignal=QUIT 13 | 14 | [program:nginx] 15 | command=/usr/sbin/nginx -e stderr -g "daemon off;" 16 | autostart=true 17 | autorestart=true 18 | priority=20 19 | stopsignal=QUIT 20 | 21 | [program:vite] 22 | command=npm run dev 23 | directory=/var/www/supervisord-monitor/assets 24 | autostart=true 25 | autorestart=true 26 | priority=20 27 | stopsignal=QUIT 28 | 29 | [program:collect-supervisord-servers-data] 30 | command=php bin/console app:collect-supervisord-servers-data 31 | autostart=true 32 | startsecs=0 33 | startretries=0 34 | autorestart=false 35 | priority=30 36 | stopsignal=QUIT 37 | 38 | [rpcinterface:supervisor] 39 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 40 | 41 | [inet_http_server] 42 | port=*:9551 43 | username=default 44 | password=default 45 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | env: 4 | IMAGE_WITH_TAG: ${{ format('{0}:{1}', secrets.CI_REGISTRY_IMAGE, github.run_number) }} 5 | LATEST_IMAGE: ${{ format('{0}:latest', secrets.CI_REGISTRY_IMAGE) }} 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Create .env file for frontend 18 | run: echo "VITE_API_URL='/api'" > assets/.env 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | - name: Login to Docker Registry 24 | run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login ${{ secrets.CI_REGISTRY }} -u ${{ secrets.DOCKER_USERNAME }} --password-stdin 25 | - name: Build and Push Docker Image (multi-arch) 26 | run: docker buildx build --platform linux/amd64,linux/arm64 --push -t $IMAGE_WITH_TAG -t $LATEST_IMAGE . 27 | -------------------------------------------------------------------------------- /assets/src/api/use-get-supervisors.tsx: -------------------------------------------------------------------------------- 1 | import { $api } from 'src/api/api'; 2 | import { API_ENDPOINTS } from 'src/const'; 3 | import { useQuery, useQueryClient } from '@tanstack/react-query'; 4 | import { useSettings } from '~/hooks/useSettings'; 5 | import { useClock } from '~/providers/clock/context'; 6 | 7 | export const useGetSupervisors = () => { 8 | const { syncRefresh } = useSettings(); 9 | const { setClock } = useClock(); 10 | 11 | return useQuery({ 12 | queryKey: ['getSupervisors'], 13 | queryFn: async () => 14 | $api 15 | .get(API_ENDPOINTS.SUPERVISORS() + (syncRefresh ? '?sync-refresh=true' : '')) 16 | .then(response => { 17 | setClock(0); 18 | return response; 19 | }), 20 | refetchOnMount: false, 21 | refetchOnWindowFocus: false, 22 | retry: 0, 23 | retryDelay: 5000, 24 | }); 25 | }; 26 | 27 | export const useInvalidateSupervisors = () => { 28 | const queryClient = useQueryClient(); 29 | 30 | return () => 31 | queryClient.invalidateQueries({ 32 | queryKey: ['getSupervisors'], 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/EventSubscriber/InvalidJWTSubscriber.php: -------------------------------------------------------------------------------- 1 | ['removeCookie', -1000], 18 | ]; 19 | } 20 | 21 | public function removeCookie(JWTInvalidEvent $event): void 22 | { 23 | $response = new Response(status: Response::HTTP_UNAUTHORIZED); 24 | 25 | $response->headers->clearCookie( 26 | name: 'api_token', 27 | path: '/api', 28 | domain: (string)getenv('API_HOST'), 29 | secure: true, 30 | httpOnly: true, 31 | sameSite: 'strict' 32 | ); 33 | 34 | $event->setResponse($response); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/route-types.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, config, routes, modelTypes } = it; 3 | const { _, pascalCase } = utils; 4 | const dataContracts = config.modular ? _.map(modelTypes, "name") : []; 5 | %> 6 | 7 | <% if (dataContracts.length) { %> 8 | import { <%~ dataContracts.join(", ") %> } from "./<%~ config.fileNames.dataContracts %>" 9 | <% } %> 10 | 11 | <% 12 | /* TODO: outOfModule, combined should be attributes of route, which will allow to avoid duplication of code */ 13 | %> 14 | 15 | <% if (routes.outOfModule) { %> 16 | <% for (const { routes: outOfModuleRoutes = [] } of routes.outOfModule) { %> 17 | <% for (const route of outOfModuleRoutes) { %> 18 | <%~ includeFile('./route-type.ejs', { ...it, route }) %> 19 | <% } %> 20 | <% } %> 21 | <% } %> 22 | 23 | <% if (routes.combined) { %> 24 | <% for (const { routes: combinedRoutes = [], moduleName } of routes.combined) { %> 25 | export namespace <%~ pascalCase(moduleName) %> { 26 | <% for (const route of combinedRoutes) { %> 27 | <%~ includeFile('./route-type.ejs', { ...it, route }) %> 28 | <% } %> 29 | } 30 | 31 | <% } %> 32 | <% } %> 33 | -------------------------------------------------------------------------------- /config/code/.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in(__DIR__.'/../../') 5 | ->exclude('var'); 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setRules([ 9 | // '@PSR2' => true, 10 | // '@Symfony' => true, 11 | // 'increment_style' => ['style' => 'post'], 12 | // 'global_namespace_import' => ['import_classes' => true, 'import_constants' => false, 'import_functions' => false], 13 | // 'use_nullable_type_declarations' => true, 14 | '@PSR12' => true, 15 | 'nullable_type_declaration' => ['syntax' => 'question_mark'], 16 | 'nullable_type_declaration_for_default_null_value' => ['use_nullable_type_declaration' => true], 17 | 'global_namespace_import' => [ 18 | 'import_classes' => true, 19 | 'import_constants' => false, 20 | 'import_functions' => false, 21 | ], 22 | 'concat_space' => ['spacing' => 'one'], 23 | 'function_declaration' => [ 24 | 'closure_fn_spacing' => 'none', 25 | 'closure_function_spacing' => 'none', 26 | ], 27 | 'class_definition' => ['single_item_single_line' => true], 28 | ]) 29 | ->setFinder($finder) 30 | ->setCacheFile('var/.tmp/.php-cs-fixer.cache'); 31 | -------------------------------------------------------------------------------- /docker/nginx/dist/nginx-default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | root /var/www/supervisord-monitor/public; 3 | listen 8080 default_server; 4 | 5 | add_header X-Frame-Options "SAMEORIGIN"; 6 | add_header X-XSS-Protection "1; mode=block"; 7 | add_header X-Content-Type-Options "nosniff"; 8 | 9 | error_log /dev/stderr info; 10 | access_log /dev/stdout main; 11 | 12 | location ~ ^/(api|_wdt|bundles|_profiler) { 13 | try_files $uri /index.php$is_args$args; 14 | } 15 | 16 | location ~ ^/index\\.php(/|$) { 17 | fastcgi_pass 127.0.0.1:9000; 18 | fastcgi_split_path_info ^(.+\\.php)(/.*)$; 19 | include fastcgi_params; 20 | 21 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 22 | fastcgi_param DOCUMENT_ROOT $realpath_root; 23 | 24 | fastcgi_buffer_size 128k; 25 | fastcgi_buffers 4 256k; 26 | fastcgi_busy_buffers_size 256k; 27 | 28 | internal; 29 | } 30 | 31 | location ~ \\.php$ { 32 | return 404; 33 | } 34 | 35 | location ~ ^/ { 36 | root /var/www/supervisord-monitor/assets/dist; 37 | try_files $uri /index.html; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docker/nginx/dev/supervisord-monitor.local.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDEzCCAfsCFFYftaMicQSrNPJYs97EgCaka4GEMA0GCSqGSIb3DQEBCwUAMEUx 3 | CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl 4 | cm5ldCBXaWRnaXRzIFB0eSBMdGQwIBcNMjQwMzE4MDk0NDA1WhgPMjEyNDAyMjMw 5 | OTQ0MDVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYD 6 | VQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQDR/pkLSHUrMTqWF6S/2atHaTiC3LhdsksDZVWTzaAVcVnl 8 | FuqoXkDugjLkA7x/VxDtgM5B38tSotW7RNLr28UyVONXkJXv/aNtBUatx9l2lY3c 9 | QmWBcqY9a07iy0ygYUYO/lfYU/IPuoQBSHJl2EsBWf1HqDndQO38pmK9irJkGwRX 10 | C3I0PqnyNyCQVT2F0OBBRT04B317SMGVOM1pEYA5Oh4hQ9uZAtJbfSIa9oEpTuqc 11 | vKXFHsWFTZ5+B1Mm7pU3khAwz7WnVJNbBrnWScHb5nO3owh/3zicdQS394X66Coe 12 | lEOk5sLv9re5ka+xWKLadZPLKFNZ33GQIjFrvsidAgMBAAEwDQYJKoZIhvcNAQEL 13 | BQADggEBAFuLqTUckiKW4S4kM9va/8DIOMwY0Cam9/9O/E430tjK035B0InFQaL+ 14 | jytuVYgvKVbmIJw8Bygx4plQq/znLtP6/gAoWElLp5KEZZdCKADLqMFiiM1QACFb 15 | VNOFpH6AKiLsC9vvGKAGOPU+IoePFsgulhCbsrjv8Pe3a0bTumaK6katnwnKod+G 16 | yDJJ6gkPJaVSL25wGUUfIoMSbs6agx1q8IluYPaHcshVkRhplTZmGAgZlPE9v8Ug 17 | PY93HogiovU+B1HUK+AvDWJsyuhem8f/7/m6wrkTfgBc2oIE8mgnz1XiTwRpjIek 18 | IhOYjiPDXqaTMbAFcAuu+vrJNld+5+U= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /assets/src/components/MonitorPageContent.tsx: -------------------------------------------------------------------------------- 1 | import { useGetSupervisors, useInvalidateSupervisors } from '~/api/use-get-supervisors'; 2 | import { Server } from '~/components/Server'; 3 | import { PageLoader } from '~/components/PageLoader'; 4 | import { useSettings } from '~/hooks/useSettings'; 5 | import { toastManager } from '~/util/toastManager'; 6 | import { useEffect } from 'react'; 7 | 8 | export const MonitorPageContent = () => { 9 | const { data, isLoading } = useGetSupervisors(); 10 | const { autoRefresh, autoRefreshInterval } = useSettings(); 11 | const invalidateSupervisors = useInvalidateSupervisors(); 12 | 13 | useEffect(() => { 14 | const interval = setInterval(() => { 15 | if (autoRefresh) { 16 | invalidateSupervisors().then(() => { 17 | toastManager.success('Data auto-refreshed'); 18 | }); 19 | } 20 | }, autoRefreshInterval * 1000); 21 | 22 | return () => clearInterval(interval); 23 | }, [autoRefresh, autoRefreshInterval, data]); 24 | 25 | if (isLoading) { 26 | return ; 27 | } 28 | 29 | return ( 30 |
31 | {data?.data?.map((item, index) => )} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/route-docs.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { config, route, utils } = it; 3 | const { _, formatDescription, fmtToJSDocLine, pascalCase, require } = utils; 4 | const { raw, request, routeName } = route; 5 | 6 | const jsDocDescription = raw.description ? 7 | ` * @description ${formatDescription(raw.description, true)}` : 8 | fmtToJSDocLine('No description', { eol: false }); 9 | const jsDocLines = _.compact([ 10 | _.size(raw.tags) && ` * @tags ${raw.tags.join(", ")}`, 11 | ` * @name ${pascalCase(routeName.usage)}`, 12 | raw.summary && ` * @summary ${raw.summary}`, 13 | ` * @request ${_.upperCase(request.method)}:${raw.route}`, 14 | raw.deprecated && ` * @deprecated`, 15 | routeName.duplicate && ` * @originalName ${routeName.original}`, 16 | routeName.duplicate && ` * @duplicate`, 17 | request.security && ` * @secure`, 18 | ...(config.generateResponses && raw.responsesTypes.length 19 | ? raw.responsesTypes.map( 20 | ({ type, status, description, isSuccess }) => 21 | ` * @response \`${status}\` \`${_.replace(_.replace(type, /\/\*/g, "\\*"), /\*\//g, "*\\")}\` ${description}`, 22 | ) 23 | : []), 24 | ]).map(str => str.trimEnd()).join("\n"); 25 | 26 | return { 27 | description: jsDocDescription, 28 | lines: jsDocLines, 29 | } 30 | %> 31 | -------------------------------------------------------------------------------- /docker/nginx/dev/nginx-default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | root /var/www/supervisord-monitor/public; 3 | 4 | listen 8080; 5 | listen 4430 ssl http2; 6 | 7 | server_name supervisord-monitor.local; 8 | ssl_certificate /etc/nginx/ssl/supervisord-monitor.local.crt; 9 | ssl_certificate_key /etc/nginx/ssl/supervisord-monitor.local.key; 10 | 11 | add_header X-Frame-Options "SAMEORIGIN"; 12 | add_header X-XSS-Protection "1; mode=block"; 13 | add_header X-Content-Type-Options "nosniff"; 14 | 15 | error_log /dev/stderr info; 16 | access_log /dev/stdout main; 17 | 18 | location ~ ^/(api|_wdt|bundles|_profiler) { 19 | try_files $uri /index.php$is_args$args; 20 | } 21 | 22 | location ~ ^/index\\.php(/|$) { 23 | fastcgi_pass 127.0.0.1:9000; 24 | fastcgi_split_path_info ^(.+\\.php)(/.*)$; 25 | include fastcgi_params; 26 | 27 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 28 | fastcgi_param DOCUMENT_ROOT $realpath_root; 29 | 30 | fastcgi_buffer_size 128k; 31 | fastcgi_buffers 4 256k; 32 | fastcgi_busy_buffers_size 256k; 33 | 34 | internal; 35 | } 36 | 37 | location ~ \\.php$ { 38 | return 404; 39 | } 40 | 41 | location ~ ^/ { 42 | proxy_pass http://127.0.0.1:5173; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assets/src/components/monitor/Uptime.tsx: -------------------------------------------------------------------------------- 1 | import { TimeDisplay } from '~/components/ui/TimeDisplay'; 2 | import { twMerge } from 'tailwind-merge'; 3 | import { useClock } from '~/providers/clock/context'; 4 | 5 | interface UptimeProps { 6 | process: ApiProcess; 7 | } 8 | 9 | export const Uptime = ({ process }: UptimeProps) => { 10 | const clock = useClock(); 11 | 12 | /** 13 | * STOPPED, EXITED, FATAL - Stop time 14 | * RUNNING - Start time 15 | * STARTING, STOPPING, BACKOFF, UNKNOWN - Do not show 16 | */ 17 | const isTimeless = !['STOPPED', 'EXITED', 'FATAL', 'RUNNING'].includes(process.stateName); 18 | if (isTimeless) { 19 | return <>; 20 | } 21 | 22 | const start = process.start; 23 | const stop = process.stop; 24 | const now = process.now + clock.clock; 25 | 26 | const isNotRunning = ['STOPPED', 'EXITED', 'FATAL'].includes(process.stateName); 27 | 28 | let duration = 0; 29 | 30 | if (isNotRunning) { 31 | // In case the program tried to start but did not stop because it failed to start(for example, it didn't find an executable file) 32 | if (start > 0 && stop === 0) { 33 | return <>; 34 | } 35 | 36 | duration = stop - start; 37 | } else if (process.stateName === 'RUNNING') { 38 | duration = now - start; 39 | } 40 | 41 | return ; 42 | }; 43 | -------------------------------------------------------------------------------- /.github/workflows/client.yml: -------------------------------------------------------------------------------- 1 | name: Frontend checker looking for bad code 🪓😵‍💫 2 | run-name: ${{ github.actor }} is checking frontend code ☺️ 3 | 4 | on: 5 | push: 6 | branches: 7 | - dev 8 | - main 9 | pull_request: 10 | branches: 11 | - dev 12 | - main 13 | 14 | jobs: 15 | pipeline: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node-version: [ 20.x ] 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Install corepack 24 | run: corepack enable 25 | 26 | - name: Set up Node.JS ${{matrix.node-version}} 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{matrix.node-version}} 30 | cache: "yarn" 31 | cache-dependency-path: ./assets/yarn.lock 32 | 33 | - name: Install yarn and dependencies 34 | working-directory: ./assets 35 | run: | 36 | npm install --global yarn 37 | corepack enable 38 | yarn set version from sources 39 | yarn install --immutable 40 | 41 | - name: Lint and Format 42 | working-directory: ./assets 43 | run: yarn format:check 44 | 45 | - name: Typecheck 46 | working-directory: ./assets 47 | run: yarn typecheck 48 | 49 | - name: Build project 50 | working-directory: ./assets 51 | run: yarn build 52 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | server_version: '16' 5 | profiling_collect_backtrace: '%kernel.debug%' 6 | use_savepoints: true 7 | orm: 8 | auto_generate_proxy_classes: true 9 | enable_lazy_ghost_objects: true 10 | report_fields_where_declared: true 11 | validate_xml_mapping: true 12 | naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware 13 | auto_mapping: true 14 | mappings: 15 | App: 16 | type: attribute 17 | is_bundle: false 18 | dir: '%kernel.project_dir%/src/Entity' 19 | prefix: 'App\Entity' 20 | alias: App 21 | 22 | when@test: 23 | doctrine: 24 | dbal: 25 | # "TEST_TOKEN" is typically set by ParaTest 26 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 27 | 28 | when@prod: 29 | doctrine: 30 | orm: 31 | auto_generate_proxy_classes: false 32 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 33 | query_cache_driver: 34 | type: pool 35 | pool: doctrine.system_cache_pool 36 | result_cache_driver: 37 | type: pool 38 | pool: doctrine.result_cache_pool 39 | 40 | framework: 41 | cache: 42 | pools: 43 | doctrine.result_cache_pool: 44 | adapter: cache.app 45 | doctrine.system_cache_pool: 46 | adapter: cache.system 47 | -------------------------------------------------------------------------------- /src/DTO/SupervisorManage.php: -------------------------------------------------------------------------------- 1 | type instanceof SupervisorManageTypeEnum) { 24 | $context->buildViolation('Type is required')->atPath('type')->addViolation(); 25 | 26 | return; 27 | } 28 | 29 | if (null === $this->server) { 30 | $context->buildViolation('Server is required')->atPath('server')->addViolation(); 31 | } 32 | 33 | if ($this->type->isGroupRelated() && null === $this->group) { 34 | $context->buildViolation('Group is required')->atPath('group')->addViolation(); 35 | } 36 | 37 | if ($this->type->isProcessRelated() && null === $this->process) { 38 | $context->buildViolation('Process is required')->atPath('process')->addViolation(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Functional/resources/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | logfile=syslog 3 | nodaemon=true 4 | loglevel=info 5 | pidfile = /tmp/supervisord.pid 6 | 7 | [program:tail-f-void] 8 | command = /usr/bin/tail -f /dev/null 9 | autostart=true 10 | autorestart=true 11 | priority=10 12 | stopsignal=QUIT 13 | numprocs=1 14 | process_name=%(process_num)s 15 | 16 | [program:tail-f-void-multiple] 17 | command = /usr/bin/tail -f /dev/null 18 | autostart=true 19 | autorestart=true 20 | priority=10 21 | stopsignal=QUIT 22 | numprocs=3 23 | process_name=%(program_name)s_%(process_num)02d 24 | 25 | [program:date-multiple] 26 | command = date 27 | autostart=true 28 | autorestart=true 29 | priority=10 30 | stopsignal=QUIT 31 | numprocs=3 32 | process_name=%(program_name)s_%(process_num)02d 33 | 34 | #[program:tail-f-void-multiple-many] 35 | #command = /usr/bin/tail -f /dev/null 36 | #autostart=true 37 | #autorestart=true 38 | #priority=10 39 | #stopsignal=QUIT 40 | #numprocs=300 41 | #process_name=%(program_name)s_%(process_num)03d 42 | 43 | [rpcinterface:supervisor] 44 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 45 | 46 | [rpcinterface:twiddler] 47 | supervisor.rpcinterface_factory = supervisor_twiddler.rpcinterface:make_twiddler_rpcinterface 48 | 49 | [inet_http_server] 50 | port=*:9555 51 | username=default 52 | password=default 53 | 54 | [supervisorctl] 55 | serverurl=http://127.0.0.1:9555 56 | username=default 57 | password=default 58 | -------------------------------------------------------------------------------- /config/code/rector.php: -------------------------------------------------------------------------------- 1 | paths([ 16 | $dir.'/config', 17 | $dir.'/public', 18 | $dir.'/src', 19 | $dir.'/tests', 20 | ]); 21 | 22 | $rectorConfig->skip([ 23 | 24 | ]); 25 | 26 | $rectorConfig->symfonyContainerXml( 27 | $dir.'/var/cache/dev/App_KernelDevDebugContainer.xml' 28 | ); 29 | 30 | // register rules 31 | $rectorConfig->rules([ 32 | FinalizeClassesWithoutChildrenRector::class, 33 | ReadOnlyPropertyRector::class, 34 | ReadOnlyClassRector::class, 35 | ]); 36 | 37 | // define sets of rules 38 | $rectorConfig->sets([ 39 | LevelSetList::UP_TO_PHP_82, 40 | SymfonySetList::SYMFONY_63, 41 | SymfonySetList::SYMFONY_CODE_QUALITY, 42 | SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, 43 | SetList::DEAD_CODE, 44 | SetList::CODE_QUALITY, 45 | SetList::TYPE_DECLARATION, 46 | ]); 47 | }; 48 | -------------------------------------------------------------------------------- /tests/Functional/resources/supervisord_servers.yaml: -------------------------------------------------------------------------------- 1 | supervisors_servers: 2 | - ip: 127.0.0.1 3 | port: 9551 4 | name: default 5 | username: default 6 | password: default 7 | - ip: supervisord-monitor-supervisord-monitor-test-server-1 8 | port: 9555 9 | name: supervisord-monitor-supervisord-monitor-test-server-1-1-1-1-1 10 | username: default 11 | password: default 12 | - ip: supervisord-monitor-supervisord-monitor-test-server-2 13 | port: 9555 14 | name: supervisord-monitor-supervisord-monitor-test-server-1-1-1-1-2 15 | username: default 16 | password: default 17 | - ip: supervisord-monitor-supervisord-monitor-test-server-3 18 | port: 9555 19 | name: supervisord-monitor-supervisord-monitor-test-server-1-1-1-1-3 20 | username: default 21 | password: default 22 | - ip: supervisord-monitor-supervisord-monitor-test-server-4 23 | port: 9555 24 | name: supervisord-monitor-supervisord-monitor-test-server-1-1-1-1-4 25 | username: default 26 | password: default 27 | - ip: supervisord-monitor-supervisord-monitor-test-server-5 28 | port: 9555 29 | name: supervisord-monitor-supervisord-monitor-test-server-1-1-1-1-5 30 | username: default 31 | password: default 32 | - ip: supervisord-monitor-supervisord-monitor-test-server-6 33 | port: 9555 34 | name: supervisord-monitor-supervisord-monitor-test-server-1-1-1-1-6 35 | username: default 36 | password: default 37 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/data-contracts.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { modelTypes, utils, config } = it; 3 | const { formatDescription, require, _, Ts } = utils; 4 | 5 | 6 | const buildGenerics = (contract) => { 7 | if (!contract.genericArgs || !contract.genericArgs.length) return ''; 8 | 9 | return '<' + contract.genericArgs.map(({ name, default: defaultType, extends: extendsType }) => { 10 | return [ 11 | name, 12 | extendsType && `extends ${extendsType}`, 13 | defaultType && `= ${defaultType}`, 14 | ].join('') 15 | }).join(',') + '>' 16 | } 17 | 18 | const dataContractTemplates = { 19 | enum: (contract) => { 20 | return `enum ${contract.name} {\r\n${contract.content} \r\n }`; 21 | }, 22 | interface: (contract) => { 23 | return `interface ${contract.name}${buildGenerics(contract)} {\r\n${contract.content}}`; 24 | }, 25 | type: (contract) => { 26 | return `type ${contract.name}${buildGenerics(contract)} = ${contract.content}`; 27 | }, 28 | } 29 | %> 30 | 31 | <% if (config.internalTemplateOptions.addUtilRequiredKeysType) { %> 32 | type <%~ config.Ts.CodeGenKeyword.UtilRequiredKeys %> = Omit & Required> 33 | <% } %> 34 | 35 | <% for (const contract of modelTypes) { %> 36 | <%~ includeFile('./data-contract-jsdoc.ejs', { ...it, data: { ...contract, ...contract.typeData } }) %> 37 | <%~ contract.internal ? '' : ''%> <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %> 38 | 39 | 40 | <% } %> 41 | -------------------------------------------------------------------------------- /src/Enum/SupervisorManageTypeEnum.php: -------------------------------------------------------------------------------- 1 | true, 28 | default => false 29 | }; 30 | } 31 | 32 | public function isProcessRelated(): bool 33 | { 34 | return match ($this) { 35 | self::StartProcess, self::StopProcess, self::RestartProcess, self::ClearProcessLogs, self::CloneProcess, self::RemoveProcess => true, 36 | default => false 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: down build create_network up 2 | 3 | build: 4 | docker compose build 5 | 6 | up: 7 | docker compose up -d --remove-orphans 8 | docker compose exec supervisord-monitor-app bin/console assets:install 9 | 10 | down: 11 | docker compose down 12 | 13 | down_force: 14 | docker compose down -v --rmi=all --remove-orphans 15 | 16 | console: 17 | if ! docker compose ps | grep -q supervisord-monitor; then make up; fi 18 | docker compose exec supervisord-monitor-app sh 19 | 20 | stop: 21 | docker compose stop 22 | 23 | create_network: 24 | docker network create --subnet 172.18.3.0/24 supervisord_monitor_network >/dev/null 2>&1 || true 25 | 26 | app_gen_jwt_keypair: 27 | docker compose exec supervisord-monitor-app php bin/console lexik:jwt:generate-keypair 28 | 29 | app_phpunit: 30 | docker compose exec supervisord-monitor-app composer run phpunit 31 | 32 | code_phpstan: 33 | docker compose exec supervisord-monitor-app composer run phpstan 34 | 35 | code_cs_fix: 36 | docker compose exec supervisord-monitor-app composer run cs-fixer 37 | 38 | code_rector: 39 | docker compose exec supervisord-monitor-app composer run rector 40 | 41 | code_cs_fix_diff: 42 | docker compose exec supervisord-monitor-app composer run cs-fixer-diff 43 | 44 | code_cs_fix_diff_status: 45 | if make code_cs_fix_diff; then \ 46 | printf '\n\n\n [OK] \n\n\n'; exit 0; \ 47 | else \ 48 | printf '\n\n\n [FAIL] \n\n\n'; exit 1; \ 49 | fi 50 | 51 | front_format_fix: ## Format frontend 52 | docker exec supervisord-monitor-app /bin/sh -c 'cd assets && npm run format:fix' 53 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/route-name.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { routeInfo, utils } = it; 3 | const { 4 | operationId, 5 | method, 6 | route, 7 | moduleName, 8 | responsesTypes, 9 | description, 10 | tags, 11 | summary, 12 | pathArgs, 13 | } = routeInfo; 14 | const { _, fmtToJSDocLine, require } = utils; 15 | 16 | const methodAliases = { 17 | get: (pathName, hasPathInserts) => 18 | _.camelCase(`${pathName}_${hasPathInserts ? "detail" : "list"}`), 19 | post: (pathName, hasPathInserts) => _.camelCase(`${pathName}_create`), 20 | put: (pathName, hasPathInserts) => _.camelCase(`${pathName}_update`), 21 | patch: (pathName, hasPathInserts) => _.camelCase(`${pathName}_partial_update`), 22 | delete: (pathName, hasPathInserts) => _.camelCase(`${pathName}_delete`), 23 | }; 24 | 25 | const createCustomOperationId = (method, route, moduleName) => { 26 | const hasPathInserts = /\{(\w){1,}\}/g.test(route); 27 | const splitedRouteBySlash = _.compact(_.replace(route, /\{(\w){1,}\}/g, "").split("/")); 28 | const routeParts = (splitedRouteBySlash.length > 1 29 | ? splitedRouteBySlash.splice(1) 30 | : splitedRouteBySlash 31 | ).join("_"); 32 | return routeParts.length > 3 && methodAliases[method] 33 | ? methodAliases[method](routeParts, hasPathInserts) 34 | : _.camelCase(_.lowerCase(method) + "_" + [moduleName].join("_")) || "index"; 35 | }; 36 | 37 | if (operationId) 38 | return _.camelCase(operationId); 39 | if (route === "/") 40 | return _.camelCase(`${_.lowerCase(method)}Root`); 41 | 42 | return createCustomOperationId(method, route, moduleName); 43 | %> -------------------------------------------------------------------------------- /assets/src/util/toastManager.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties, JSX } from 'react'; 2 | import toast from 'react-hot-toast'; 3 | 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | 6 | interface ToastOptions { 7 | duration?: number; 8 | className?: string; 9 | style?: CSSProperties; 10 | position?: 'top-center' | 'top-right' | 'top-left' | 'bottom-center' | 'bottom-right' | 'bottom-left'; 11 | } 12 | 13 | type Renderable = JSX.Element | string | null; 14 | 15 | type ValueFunction = (arg: TArg) => TValue; 16 | 17 | type ValueOrFunction = TValue | ValueFunction; 18 | 19 | const initialOptions: ToastOptions = { 20 | position: 'top-right', 21 | }; 22 | 23 | export const toastManager = { 24 | success(message: string, options?: ToastOptions) { 25 | toast.success(message, { ...initialOptions, ...options }); 26 | }, 27 | 28 | error(message: string, options?: ToastOptions) { 29 | toast.error(message, { ...initialOptions, ...options }); 30 | }, 31 | 32 | loading(message: string, options?: ToastOptions) { 33 | toast.loading(message, { ...initialOptions, ...options }); 34 | }, 35 | 36 | promise( 37 | promise: Promise, 38 | msgs: { 39 | loading: Renderable; 40 | success: ValueOrFunction; 41 | error: ValueOrFunction; 42 | }, 43 | opts?: ToastOptions 44 | ): Promise { 45 | return toast.promise(promise, { 46 | loading: msgs.loading, 47 | success: msgs.success, 48 | error: msgs.error, 49 | ...opts, 50 | }); 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodResponse_systemMulticall.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | success 11 | 12 | 1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | faultCode 21 | 22 | 300 23 | 24 | 25 | 26 | faultString 27 | 28 | Invalid parameters 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supervisord-monitor-frontend", 3 | "description": "A frontend for supervisord-monitor", 4 | "author": "velenyx (https://github.com/velenyx)", 5 | "private": true, 6 | "version": "0.0.0", 7 | "type": "module", 8 | "engines": { 9 | "node": ">=20.0.0" 10 | }, 11 | "scripts": { 12 | "dev": "vite --host 0.0.0.0", 13 | "build": "vite build", 14 | "preview": "vite preview", 15 | "format:check": "prettier --check .", 16 | "format:fix": "prettier --write .", 17 | "typecheck": "tsc --noEmit --pretty", 18 | "generate:types": "npx esno scripts/gen-types" 19 | }, 20 | "dependencies": { 21 | "@hookform/resolvers": "^3.6.0", 22 | "@tanstack/react-query": "^5.40.0", 23 | "@tanstack/react-query-devtools": "^5.40.0", 24 | "axios": "1.8.2", 25 | "dotenv": "^16.4.5", 26 | "react": "^18.2.0", 27 | "react-dom": "^18.2.0", 28 | "react-hook-form": "^7.51.5", 29 | "react-hot-toast": "^2.4.0", 30 | "react-router-dom": "^6.6.2", 31 | "swagger-typescript-api": "^13.0.3", 32 | "tailwind-merge": "^2.3.0", 33 | "zod": "^3.23.8" 34 | }, 35 | "devDependencies": { 36 | "@types/react": "^18.0.26", 37 | "@types/react-dom": "^18.0.9", 38 | "@vitejs/plugin-react": "^3.0.0", 39 | "autoprefixer": "^10.4.13", 40 | "postcss": "^8.4.21", 41 | "prettier": "^3.2.5", 42 | "prettier-plugin-tailwindcss": "^0.6.1", 43 | "tailwindcss": "^3.2.4", 44 | "typescript": "^5.4.5", 45 | "vite": "6.2.6", 46 | "vite-tsconfig-paths": "^4.3.2" 47 | }, 48 | "packageManager": "yarn@4.2.2" 49 | } 50 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | # the latter taking precedence over the former: 3 | # 4 | # * .env contains default values for the environment variables needed by the app 5 | # * .env.local uncommitted file with local overrides 6 | # * .env.$APP_ENV committed environment-specific defaults 7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides 8 | # 9 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. 10 | # https://symfony.com/doc/current/configuration/secrets.html 11 | # 12 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). 13 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration 14 | 15 | ###> symfony/framework-bundle ### 16 | APP_ENV=prod 17 | APP_SECRET=a99a199bd3122ab1167d9b22dfe12624 18 | ###< symfony/framework-bundle ### 19 | 20 | ###> nelmio/cors-bundle ### 21 | CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' 22 | ###< nelmio/cors-bundle ### 23 | 24 | ###> lexik/jwt-authentication-bundle ### 25 | JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem 26 | JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem 27 | JWT_PASSPHRASE=73e14354e3850602ceb82c34c17c56e4aee66dbc9b0bda91652e2f57cb2200b3 28 | ###< lexik/jwt-authentication-bundle ### 29 | 30 | ###> symfony/lock ### 31 | LOCK_DSN=flock 32 | ###< symfony/lock ### 33 | 34 | ###> doctrine/doctrine-bundle ### 35 | DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db" 36 | ###< doctrine/doctrine-bundle ### 37 | -------------------------------------------------------------------------------- /assets/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router'; 2 | import { Link as ReactLink } from 'react-router-dom'; 3 | import { ROUTES, RouteValues } from '~/const/routes'; 4 | import { useLogout } from '~/api/use-logout'; 5 | 6 | interface HeaderLinks { 7 | title: string; 8 | url: RouteValues; 9 | } 10 | 11 | const HEADER_LINKS: HeaderLinks[] = [ 12 | { 13 | title: 'Dashboard', 14 | url: ROUTES.HOME, 15 | }, 16 | { 17 | title: 'Settings', 18 | url: ROUTES.SETTINGS, 19 | }, 20 | ]; 21 | 22 | export const Header = () => { 23 | const location = useLocation(); 24 | const useLogoutMutation = useLogout(); 25 | 26 | const handleLogout = () => { 27 | if (!window.confirm('Are you sure you want to logout?')) { 28 | return; 29 | } 30 | 31 | useLogoutMutation.mutateAsync(undefined, undefined).then(() => window.location.reload()); 32 | }; 33 | 34 | return ( 35 |
36 | 37 |
38 | {'logo'} 39 |
40 |
41 | {HEADER_LINKS.map(route => { 42 | const isActive = location.pathname == route.url; 43 | return ( 44 | 45 | 46 | 47 | ); 48 | })} 49 | 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /assets/src/providers/session/context.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; 2 | import { useGetMe } from '~/api/use-get-me'; 3 | 4 | export interface ISessionContext { 5 | user: ApiUser; 6 | setUser: (user: ApiUser) => void; 7 | status: 'loading' | 'authenticated' | 'unauthenticated'; 8 | setStatus: (status: 'loading' | 'authenticated' | 'unauthenticated') => void; 9 | } 10 | 11 | const SessionContext = createContext(null); 12 | 13 | export const SessionProvider = ({ children }: { children: ReactNode }) => { 14 | // @ts-ignore 15 | const [user, setUser] = useState(null); 16 | const [status, setStatus] = useState('loading'); 17 | 18 | const { data, isFetching, error } = useGetMe(); 19 | 20 | useEffect(() => { 21 | if (isFetching) { 22 | setStatus('loading'); 23 | } else if (error) { 24 | setStatus('unauthenticated'); 25 | } else if (data?.data) { 26 | setUser(data?.data); 27 | setStatus('authenticated'); 28 | } else { 29 | setStatus('unauthenticated'); 30 | } 31 | }, [data, error, isFetching]); 32 | 33 | return {children}; 34 | }; 35 | 36 | export const useSession = () => { 37 | const context = useContext(SessionContext); 38 | if (!context) { 39 | throw new Error('useSession must be used within an SessionProvider'); 40 | } 41 | return context; 42 | }; 43 | 44 | export const isHasRoleManager = () => { 45 | return useSession().user.roles.includes('ROLE_MANAGER'); 46 | }; 47 | -------------------------------------------------------------------------------- /assets/src/hooks/useSettings.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from '~/hooks/useLocalStorage'; 2 | import { Theme } from '~/config/theme'; 3 | import { useEffect } from 'react'; 4 | 5 | const defaultSettings = { 6 | autoRefresh: false, 7 | autoRefreshInterval: 10, 8 | syncRefresh: false, 9 | theme: Theme.system, 10 | allowMutators: false, 11 | }; 12 | 13 | export const useSettings = () => { 14 | const [settings, setSettings] = useLocalStorage('settings', defaultSettings); 15 | 16 | useEffect(() => { 17 | const root = window.document.documentElement; 18 | 19 | if (settings.theme !== Theme.system && root.classList.contains(settings.theme)) { 20 | return; 21 | } 22 | 23 | root.classList.remove('light', 'dark'); 24 | 25 | if (settings.theme === Theme.system) { 26 | const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 27 | 28 | root.classList.add(systemTheme); 29 | return; 30 | } 31 | 32 | root.classList.add(settings.theme); 33 | }, [settings.theme]); 34 | 35 | return { 36 | autoRefresh: settings.autoRefresh, 37 | setAutoRefresh: (v: boolean) => setSettings({ ...settings, autoRefresh: v }), 38 | 39 | autoRefreshInterval: settings.autoRefreshInterval, 40 | setAutoRefreshInterval: (v: number) => setSettings({ ...settings, autoRefreshInterval: v }), 41 | 42 | syncRefresh: settings.syncRefresh, 43 | setSyncRefresh: (v: boolean) => setSettings({ ...settings, syncRefresh: v }), 44 | 45 | theme: settings.theme, 46 | setTheme: (v: Theme) => setSettings({ ...settings, theme: v }), 47 | 48 | allowMutators: settings.allowMutators, 49 | setAllowMutators: (v: boolean) => setSettings({ ...settings, allowMutators: v }), 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: supervisord-monitor 2 | 3 | services: 4 | supervisord-monitor-app: 5 | container_name: supervisord-monitor-app 6 | user: app 7 | build: 8 | args: 9 | - BUILD_TYPE=dev 10 | - PUID=1000 11 | - PGID=1000 12 | - DEV_XDEBUG_AUTOSTART=trigger 13 | environment: 14 | - PHP_IDE_CONFIG=serverName=Docker 15 | - API_HOST=localhost 16 | - JWT_PASSPHRASE=73e14354e3850602ceb82c34c17c56e4aee66dbc9b0bda91652e2f57cb2200b3 17 | - COLLECT_INTERVAL_IN_MICROSECONDS=100000 # 0.1s 18 | volumes: 19 | - ./:/var/www/supervisord-monitor:cached 20 | - ./docker/nginx/dev/supervisord-monitor.local.crt:/etc/nginx/ssl/supervisord-monitor.local.crt 21 | - ./docker/nginx/dev/supervisord-monitor.local.key:/etc/nginx/ssl/supervisord-monitor.local.key 22 | - ./docker/supervisor/supervisord-dev.conf:/etc/supervisord.conf 23 | ports: 24 | - "8080:8080" # Dev mode(backend) - Deprecated 25 | - "5173:5173" # Dev mode(frontend) - Deprecated 26 | - "10011:8080" # Prod mode emulation - Deprecated 27 | - "80:8080" # Dev mode(for cookies setup) 28 | - "443:4430" # Dev mode(for cookies setup) 29 | networks: 30 | - supervisord_monitor_network 31 | 32 | supervisord-monitor-test-server: 33 | user: app 34 | build: 35 | context: tests/Functional/resources 36 | args: 37 | - UID=1000 38 | - GID=1000 39 | volumes: 40 | - ./tests/Functional/resources/:/var/www/supervisord-monitor:cached 41 | networks: 42 | - supervisord_monitor_network 43 | deploy: 44 | mode: replicated 45 | replicas: 6 46 | 47 | networks: 48 | supervisord_monitor_network: 49 | name: supervisord_monitor_network 50 | external: true 51 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/data-contract-jsdoc.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { data, utils } = it; 3 | const { formatDescription, require, _ } = utils; 4 | 5 | const stringify = (value) => _.isObject(value) ? JSON.stringify(value) : _.isString(value) ? `"${value}"` : value 6 | 7 | const jsDocLines = _.compact([ 8 | data.title, 9 | data.description && formatDescription(data.description), 10 | !_.isUndefined(data.deprecated) && data.deprecated && '@deprecated', 11 | !_.isUndefined(data.format) && `@format ${data.format}`, 12 | !_.isUndefined(data.minimum) && `@min ${data.minimum}`, 13 | !_.isUndefined(data.multipleOf) && `@multipleOf ${data.multipleOf}`, 14 | !_.isUndefined(data.exclusiveMinimum) && `@exclusiveMin ${data.exclusiveMinimum}`, 15 | !_.isUndefined(data.maximum) && `@max ${data.maximum}`, 16 | !_.isUndefined(data.minLength) && `@minLength ${data.minLength}`, 17 | !_.isUndefined(data.maxLength) && `@maxLength ${data.maxLength}`, 18 | !_.isUndefined(data.exclusiveMaximum) && `@exclusiveMax ${data.exclusiveMaximum}`, 19 | !_.isUndefined(data.maxItems) && `@maxItems ${data.maxItems}`, 20 | !_.isUndefined(data.minItems) && `@minItems ${data.minItems}`, 21 | !_.isUndefined(data.uniqueItems) && `@uniqueItems ${data.uniqueItems}`, 22 | !_.isUndefined(data.default) && `@default ${stringify(data.default)}`, 23 | !_.isUndefined(data.pattern) && `@pattern ${data.pattern}`, 24 | !_.isUndefined(data.example) && `@example ${stringify(data.example)}` 25 | ]).join('\n').split('\n'); 26 | %> 27 | <% if (jsDocLines.every(_.isEmpty)) { %> 28 | <% } else if (jsDocLines.length === 1) { %> 29 | /** <%~ jsDocLines[0] %> */ 30 | <% } else if (jsDocLines.length) { %> 31 | /** 32 | <% for (jsDocLine of jsDocLines) { %> 33 | * <%~ jsDocLine %> 34 | 35 | <% } %> 36 | */ 37 | <% } %> 38 | -------------------------------------------------------------------------------- /docker/nginx/dev/supervisord-monitor.local.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDR/pkLSHUrMTqW 3 | F6S/2atHaTiC3LhdsksDZVWTzaAVcVnlFuqoXkDugjLkA7x/VxDtgM5B38tSotW7 4 | RNLr28UyVONXkJXv/aNtBUatx9l2lY3cQmWBcqY9a07iy0ygYUYO/lfYU/IPuoQB 5 | SHJl2EsBWf1HqDndQO38pmK9irJkGwRXC3I0PqnyNyCQVT2F0OBBRT04B317SMGV 6 | OM1pEYA5Oh4hQ9uZAtJbfSIa9oEpTuqcvKXFHsWFTZ5+B1Mm7pU3khAwz7WnVJNb 7 | BrnWScHb5nO3owh/3zicdQS394X66CoelEOk5sLv9re5ka+xWKLadZPLKFNZ33GQ 8 | IjFrvsidAgMBAAECggEAJWCMux5lhI+ZnveMYn2K6AYJgflpc3v1sCAMUGeMM+Te 9 | HFGs6NUF964DAuTLW1sS70M68yyzUv0az99bL5IJkoDbik148qORwCjtQKdOxLWv 10 | 72F+EcFnZ40/GE0ZUX6e6rJIzn96mWQYdOYBPrF2AEKVO3js+72/3nV7I8OZJwn5 11 | VrJpNILOcINvRcT4ZdHRjR776UIWwsWsg2+hEcxwThDSvpj2YWAVSs5ABOfmKZyx 12 | 8YsQKtUBMR5UrLmpX9WMMXK3TeQwFScl7w7jtsZmMAp5iMOgWPGKHmmtK33+hGgv 13 | B++xgDZMFIbtpCJhW9V5o4WWRTAsDZewxBolBucyHQKBgQD2Njlpm9DtZ61SGPW2 14 | kylrAqhn/j+lqpCtMokyZ09jUkCz3m/BNbb4L8D9zkDHxN/sRyFfFmp0vtycHr2E 15 | PuHP5Lz0SNclxfIs/LKGPKjPD+Qf8rzCEupFbTyK5whNEzmMcPPyuiLbZmPT9zgw 16 | qSNUHstRnP+ne1hTYEIFqOR5+wKBgQDaV8dZGFRj1BUDvyc1xjVwvznxjXuO74sr 17 | /zk9Wps+iM0cO9srkG3U+Noq8iP6tt2nqhk1ZCwHx7PtWMNSLS51ETGpIyBlAy0X 18 | Vh//Pozj/uMmuxBuT5h32Vm2E344389fJsYvbfZ7yVOjPJD8n9CWRu3sLcxLnon2 19 | iQAT3GScRwKBgQCRtnGwANlbR1qaFc+Fp/6BKGeGdEAyyYkqF5h+zgl73HgSe3hk 20 | Pmf05j4vd1t7Xxau/UHQxrFmOnbRppe+poB5ywPRBzLdVhMHcN4u98NoGB2Ikt4H 21 | da5UPFvyUNzm0JPkfAzEAEkU07oM/miw08jmxfrKaEIdWrBhV7x7IQNm9QKBgHkX 22 | 88SS/MK87ca9LkbhnePg+obgO+WjLuGA1EMVzEHbZz5AaCZ6HJ/gQEdPMesrnjUN 23 | 3ptA6jaKaFy7POCHlFty5ML0a1P6dfiaWHacP1F3nI1vdAZp+JqHnfygRQHQBtez 24 | znihmPFAUDWZMqQZEns17WBuaf6Kd+OWSce6FMajAoGAHFzEjwxlmzgUeZdMBWQl 25 | LGKMkaJsXgKkL2LQiZ1SzweQxm9ZK8aWp6mbhYt3eVoNUdIs+Qe6YJmcllgAJxzc 26 | i08aNT9Aetc1BGbG3W+06iI3q4XI2AuusxbiZTOQS4XM6mW3Z5s7vyy+2bVGWL8P 27 | z+Pclj5/f1B/iZnZlxNOx9U= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /src/Service/Symfony/SupervisordServerEnvVarProcessor.php: -------------------------------------------------------------------------------- 1 | 'string', 23 | ]; 24 | } 25 | 26 | /** @return array */ 27 | public function getEnv(string $prefix, string $name, Closure $getEnv): array 28 | { 29 | $type = SupervisorServer::class.'[]'; 30 | 31 | try { 32 | $data = $getEnv($name); 33 | 34 | /** @var SupervisorServer[] $servers */ 35 | $servers = $this->serializer->deserialize($data, $type, 'json'); 36 | } catch (EnvNotFoundException) { 37 | $data = Yaml::parseFile('/var/www/supervisord-monitor/config/app/supervisord_servers.yaml'); 38 | $data = is_array($data) ? $data['supervisors_servers'] ?? [] : []; 39 | 40 | /** @var SupervisorServer[] $servers */ 41 | $servers = $this->serializer->denormalize($data, $type, 'json'); 42 | } 43 | 44 | 45 | $result = []; 46 | 47 | foreach ($servers as $server) { 48 | $result[$server->name] = $server; 49 | } 50 | 51 | return $result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Service/Symfony/AppCredentialsEnvVarProcessor.php: -------------------------------------------------------------------------------- 1 | 'string', 23 | ]; 24 | } 25 | 26 | /** @return array */ 27 | public function getEnv(string $prefix, string $name, Closure $getEnv): array 28 | { 29 | $type = AppCredentialsItem::class.'[]'; 30 | 31 | try { 32 | $data = $getEnv($name); 33 | 34 | /** @var AppCredentialsItem[] $items */ 35 | $items = $this->serializer->deserialize($data, $type, 'json'); 36 | } catch (EnvNotFoundException) { 37 | $data = Yaml::parseFile('/var/www/supervisord-monitor/config/app/app_credentials.yaml'); 38 | $data = is_array($data) ? $data['app_credentials'] ?? [] : []; 39 | 40 | /** @var AppCredentialsItem[] $items */ 41 | $items = $this->serializer->denormalize($data, $type, 'json'); 42 | } 43 | 44 | $result = []; 45 | 46 | foreach ($items as $item) { 47 | $pass = sprintf('%s:%s', $item->username, $item->password); 48 | 49 | $result[$pass] = $item; 50 | } 51 | 52 | return $result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/CollectSupervisordServersDataCommand.php: -------------------------------------------------------------------------------- 1 | $supervisorServers */ 20 | public function __construct( 21 | public SupervisorApiClient $api, 22 | #[Autowire(param: 'supervisordServers')] private readonly array $supervisorServers, 23 | #[Autowire(param: 'collectIntervalInMicroseconds')] private readonly int $collectIntervalInMicroseconds, 24 | ) { 25 | parent::__construct(); 26 | } 27 | 28 | protected function execute(InputInterface $input, OutputInterface $output): int 29 | { 30 | // @phpstan-ignore-next-line 31 | while (true) { 32 | $this->collect($output); 33 | usleep($this->collectIntervalInMicroseconds); 34 | } 35 | } 36 | 37 | private function collect(OutputInterface $output): void 38 | { 39 | foreach ($this->supervisorServers as $server) { 40 | $process = new Process(['bin/console', CollectSupervisordServerDataCommand::COMMAND, $server->name]); 41 | $process->mustRun(); 42 | 43 | $out = $process->getOutput(); 44 | if ('' !== $out) { 45 | $output->writeln($out); 46 | } 47 | 48 | $err = $process->getErrorOutput(); 49 | if ('' !== $err) { 50 | $output->writeln(''.$err.''); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ApiResource/User.php: -------------------------------------------------------------------------------- 1 | username; 47 | } 48 | 49 | public function getRoles(): array 50 | { 51 | return array_values(array_unique(array_merge(['ROLE_USER'], $this->roles))); 52 | } 53 | 54 | public function eraseCredentials(): void {} 55 | 56 | public function getUserIdentifier(): string 57 | { 58 | return $this->username; 59 | } 60 | 61 | /** @param array{roles?: string[]} $payload */ 62 | public static function createFromPayload($username, array $payload): self 63 | { 64 | return new self($username, $payload['roles'] ?? []); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Controller/LoginController.php: -------------------------------------------------------------------------------- 1 | $appCredentials */ 23 | public function __construct( 24 | private AuthenticationSuccessHandler $successHandler, 25 | private RateLimiterFactory $authByCredentialsApiLimiter, 26 | #[Autowire(param: 'appCredentials')] private array $appCredentials 27 | ) {} 28 | 29 | public function __invoke(#[MapRequestPayload] AuthByCredentialsDTO $DTO, Request $request): Response 30 | { 31 | $limiter = $this->authByCredentialsApiLimiter->create($DTO->login); 32 | if (false === $limiter->consume()->isAccepted()) { 33 | return new JsonResponse(data: ['detail' => 'Too many requests'], status: Response::HTTP_TOO_MANY_REQUESTS); 34 | } 35 | 36 | $providedCredentials = sprintf('%s:%s', $DTO->login, $DTO->password); 37 | 38 | $found = $this->appCredentials[$providedCredentials] ?? null; 39 | 40 | if (null === $found) { 41 | return new JsonResponse(data: ['detail' => 'Invalid credentials'], status: Response::HTTP_UNAUTHORIZED); 42 | } 43 | 44 | $user = new User($found->username, $found->roles); 45 | 46 | return $this->successHandler->handleAuthenticationSuccess($user); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/src/providers/router/config.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter, Outlet } from 'react-router-dom'; 2 | import { NotFoundPage } from '~/pages/not-found/page'; 3 | import { SettingsPage } from '~/pages/settings/page'; 4 | import { ROUTES } from '~/const'; 5 | import { LoginPage } from '~/pages/login/page'; 6 | import { PrivateRoute } from './private-route'; 7 | import { PublicRoute } from '~/providers/router/public-route'; 8 | import { Toaster } from 'react-hot-toast'; 9 | import { Header } from '~/components/Header'; 10 | import { MainPage } from '~/pages/home/page'; 11 | import { useSettings } from '~/hooks/useSettings'; 12 | 13 | export const MainLayout = ({ children }: { children: any }) => { 14 | useSettings(); // Call for set theme class 15 | 16 | return ( 17 |
18 | 19 |
20 | {children} 21 |
22 | ); 23 | }; 24 | 25 | const ErrorPage = () =>
ERROROORORO SDJSDOJSDIJIO!!!!!!! ERRROR CRITIIACALLLL!!!!!!!!!!!!!
; 26 | 27 | export const router = createBrowserRouter([ 28 | { 29 | children: [ 30 | { 31 | path: ROUTES.HOME, 32 | element: ( 33 | 34 | 35 | 36 | 37 | 38 | ), 39 | errorElement: , 40 | children: [ 41 | { 42 | index: true, 43 | element: , 44 | }, 45 | { 46 | path: ROUTES.SETTINGS, 47 | element: , 48 | }, 49 | { 50 | path: '*', 51 | element: , 52 | }, 53 | ], 54 | }, 55 | { 56 | element: ( 57 | 58 | 59 | 60 | ), 61 | errorElement: , 62 | children: [ 63 | { 64 | path: ROUTES.LOGIN, 65 | element: , 66 | }, 67 | ], 68 | }, 69 | ], 70 | }, 71 | ]); 72 | -------------------------------------------------------------------------------- /src/DTO/Supervisord/Process.php: -------------------------------------------------------------------------------- 1 | description); 32 | if (count($explodedDesc) == 2) { 33 | [$this->descPid, $uptime] = $explodedDesc; 34 | $uptime = str_replace("uptime ", "", $uptime); 35 | $this->descUptime = $uptime; 36 | } 37 | } 38 | 39 | /** 40 | * @param array $data 41 | */ 42 | public static function fromArray(array $data): self 43 | { 44 | return new self( 45 | name: (string)$data['name'], 46 | group: (string)$data['group'], 47 | start: (int)$data['start'], 48 | stop: (int)$data['stop'], 49 | now: (int)$data['now'], 50 | state: (int)$data['state'], 51 | stateName: (string)$data['statename'], 52 | spawnErr: (string)$data['spawnerr'], 53 | exitStatus: (int)$data['exitstatus'], 54 | logfile: (string)$data['logfile'], 55 | stdoutLogfile: (string)$data['stdout_logfile'], 56 | stderrLogfile: (string)$data['stderr_logfile'], 57 | pid: (int)$data['pid'], 58 | description: (string)$data['description'], 59 | ); 60 | } 61 | 62 | public function getFullProcessName(): string 63 | { 64 | return $this->group.':'.$this->name; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodCall_invalidSystemMulticall.xml: -------------------------------------------------------------------------------- 1 | 2 | system.multicall 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | params 12 | 13 | 14 | 15 | 16 | john 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | methodName 28 | 29 | another.method 30 | 31 | 32 | 33 | params 34 | 35 | 36 | 37 | 38 | doe 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Генерирует TypeScript объявления API из спецификации Swagger (OpenAPI). 3 | * 4 | * @param {Object} argv Объект аргументов командной строки, обработанный с помощью yargs. 5 | * @param {string} argv.name Имя выходного файла. По умолчанию 'api.d.ts'. 6 | * @param {string} argv.input Путь к входному файлу спецификации Swagger (OpenAPI). 7 | * @param {string} argv.output Путь к директории, куда будет сохранён выходной файл. По умолчанию './@types'. 8 | * 9 | * Этот скрипт поддерживает настройку входных и выходных путей, имени выходного файла, 10 | * а также дополнительную кастомизацию сгенерированного кода через параметры командной строки. 11 | * 12 | * Пример использования: 13 | * ```bash 14 | * node generate-api.mjs -n api.d.ts -i ./path/to/swagger.json -o ./path/to/output/directory 15 | * ``` 16 | * 17 | * Кастомизация генерации кода реализуется через функцию `codeGenConstructs`, позволяя изменять форматирование 18 | * или добавлять специфические аннотации к полям типов. Например, поля типов могут быть помечены как `readonly`, 19 | * если это указано в спецификации Swagger. 20 | */ 21 | 22 | import path from 'path'; 23 | import { fileURLToPath } from 'url'; 24 | 25 | import { generateApi } from 'swagger-typescript-api'; 26 | import yargs from 'yargs'; 27 | import { hideBin } from 'yargs/helpers'; 28 | 29 | import 'dotenv/config'; 30 | import * as process from 'process'; 31 | 32 | const __filename = fileURLToPath(import.meta.url); 33 | const __dirname = path.dirname(__filename); 34 | 35 | const { argv } = yargs(hideBin(process.argv)).options({ 36 | name: { type: 'string', demandOption: true, alias: 'n', default: 'api.d.ts' }, 37 | input: { 38 | type: 'string', 39 | demandOption: true, 40 | alias: 'i', 41 | default: path.resolve(__dirname, './@types/swagger.json'), 42 | }, 43 | output: { type: 'string', demandOption: true, alias: 'o', default: './@types' }, 44 | }); 45 | 46 | const { name, input, output } = argv as { name: string; input: string; output: string }; 47 | const inputPath = path.resolve(process.cwd(), input); 48 | const outputPath = path.resolve(process.cwd(), output); 49 | 50 | generateApi({ 51 | name, 52 | url: `${process.env.VITE_API_URL}/docs.json`, 53 | input: inputPath, 54 | output: outputPath, 55 | httpClientType: 'axios', 56 | typePrefix: 'Api', 57 | templates: path.resolve(__dirname, './templates'), 58 | codeGenConstructs: constructs => ({ 59 | ...constructs, 60 | TypeField: ({ readonly, key, value }) => [...(readonly ? ['readonly'] : []), key, ': ', value].join(''), 61 | }), 62 | }).finally(() => process.exit(0)); 63 | -------------------------------------------------------------------------------- /assets/src/components/ui/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { InputHTMLAttributes } from 'react'; 2 | 3 | interface CheckboxProps extends InputHTMLAttributes { 4 | text: string; 5 | } 6 | 7 | export const Checkbox = ({ text, ...props }: CheckboxProps) => { 8 | return ( 9 |
10 | 15 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodCall_systemMulticall.xml: -------------------------------------------------------------------------------- 1 | 2 | system.multicall 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | methodName 12 | 13 | my.method 14 | 15 | 16 | 17 | params 18 | 19 | 20 | 21 | 22 | john 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | methodName 34 | 35 | another.method 36 | 37 | 38 | 39 | params 40 | 41 | 42 | 43 | 44 | doe 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tests/Unit/XmlRpc/resources/methodCall_systemMulticall_same_method_bug.xml: -------------------------------------------------------------------------------- 1 | 2 | system.multicall 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | methodName 12 | 13 | my.method 14 | 15 | 16 | 17 | params 18 | 19 | 20 | 21 | 22 | john 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | methodName 34 | 35 | my.method 36 | 37 | 38 | 39 | params 40 | 41 | 42 | 43 | 44 | doe 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/Entity/SupervisordServerState.php: -------------------------------------------------------------------------------- 1 | > $groups */ 18 | #[ORM\Column(type: 'json', options: ['jsonb' => true])] 19 | public array $groups = []; 20 | 21 | #[ORM\Column] 22 | public string $version = ''; 23 | 24 | #[ORM\Column] 25 | public bool $ok = true; 26 | 27 | #[ORM\Column] 28 | public string $server = ''; 29 | 30 | #[ORM\Column(type: 'text', nullable: true)] 31 | public ?string $failError = null; 32 | 33 | public function getId(): ?int 34 | { 35 | return $this->id; 36 | } 37 | 38 | /** @return array */ 39 | public function getGroups(): array 40 | { 41 | $fn = static fn(array $group): ProcessGroup => new ProcessGroup($group['name'], $group['processes']); 42 | 43 | return array_map($fn, $this->groups); 44 | } 45 | 46 | /** @param array $groups */ 47 | public function setGroups(array $groups): self 48 | { 49 | /** @var array> $groups */ 50 | 51 | $this->groups = $groups; 52 | 53 | return $this; 54 | } 55 | 56 | public function getVersion(): string 57 | { 58 | return $this->version; 59 | } 60 | 61 | public function setVersion(string $version): self 62 | { 63 | $this->version = $version; 64 | 65 | return $this; 66 | } 67 | 68 | public function isOk(): bool 69 | { 70 | return $this->ok; 71 | } 72 | 73 | public function setOk(bool $ok): self 74 | { 75 | $this->ok = $ok; 76 | 77 | return $this; 78 | } 79 | 80 | public function getServer(): string 81 | { 82 | return $this->server; 83 | } 84 | 85 | public function setServer(string $server): self 86 | { 87 | $this->server = $server; 88 | 89 | return $this; 90 | } 91 | 92 | public function getFailError(): ?string 93 | { 94 | return $this->failError; 95 | } 96 | 97 | public function setFailError(?string $failError): self 98 | { 99 | $this->failError = $failError; 100 | 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/State/SupervisorsCollectionProvider.php: -------------------------------------------------------------------------------- 1 | $servers */ 20 | public function __construct( 21 | public SupervisorApiClient $supervisorApiClient, 22 | private SupervisordServerStateRepository $supervisordServerStateRepository, 23 | #[Autowire(param: 'supervisordServers')] private array $servers, 24 | private EntityManagerInterface $entityManager 25 | ) {} 26 | 27 | /** @return Supervisor[] */ 28 | public function provide(Operation $operation, array $uriVariables = [], array $context = []): array 29 | { 30 | /** @var Request $request */ 31 | $request = $context['request']; 32 | 33 | /** @var Supervisor[] $result */ 34 | $result = []; 35 | 36 | if ($request->query->get('sync-refresh') === 'true') { 37 | foreach ($this->servers as $server) { 38 | $result[] = $this->supervisorApiClient->getSupervisor($server); 39 | } 40 | 41 | return $result; 42 | } 43 | 44 | $states = $this->supervisordServerStateRepository->findAll(); 45 | 46 | // Sort by server name from $this->servers 47 | $newStates = []; 48 | foreach ($this->servers as $server) { 49 | foreach ($states as $state) { 50 | if ($state->getServer() === $server->name) { 51 | $newStates[] = $state; 52 | } 53 | } 54 | } 55 | 56 | foreach ($newStates as $state) { 57 | $server = $this->servers[$state->getServer()] ?? null; 58 | 59 | if (null === $server) { 60 | $this->entityManager->remove($state); 61 | $this->entityManager->flush(); 62 | 63 | continue; 64 | } 65 | 66 | $result[] = new Supervisor( 67 | $state->getGroups(), 68 | $state->getVersion(), 69 | $state->isOk(), 70 | $server, 71 | $state->getFailError() 72 | ); 73 | } 74 | 75 | return $result; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Command/CollectSupervisordServerDataCommand.php: -------------------------------------------------------------------------------- 1 | $supervisorServers */ 25 | public function __construct( 26 | private readonly SupervisorApiClient $api, 27 | private readonly EntityManagerInterface $entityManager, 28 | #[Autowire(param: 'supervisordServers')] private readonly array $supervisorServers, 29 | private readonly SupervisordServerStateRepository $supervisordServerStateRepository 30 | ) { 31 | parent::__construct(); 32 | } 33 | 34 | protected function configure(): void 35 | { 36 | $this->addArgument('server', InputArgument::REQUIRED, 'Server name'); 37 | } 38 | 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | $server = $input->getArgument('server'); 42 | 43 | if (is_int($server)) { 44 | $server = (string)$server; 45 | } 46 | if (!is_string($server)) { 47 | $output->writeln('Excepted to be string'); 48 | 49 | return self::FAILURE; 50 | } 51 | if (!isset($this->supervisorServers[$server])) { 52 | $output->writeln('Server not found'); 53 | 54 | return self::FAILURE; 55 | } 56 | 57 | $result = $this->api->getSupervisor($this->supervisorServers[$server]); 58 | 59 | $state = $this->supervisordServerStateRepository->findOneBy(['server' => $result->server->name]); 60 | if (null === $state) { 61 | $state = new SupervisordServerState(); 62 | $this->entityManager->persist($state); 63 | } 64 | 65 | $state 66 | ->setGroups($result->groups) 67 | ->setVersion($result->version) 68 | ->setOk($result->ok) 69 | ->setServer($result->server->name) 70 | ->setFailError($result->failError); 71 | 72 | $this->entityManager->flush(); 73 | 74 | return self::SUCCESS; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ApiResource/Supervisor.php: -------------------------------------------------------------------------------- 1 | $groups */ 36 | public function __construct( 37 | public array $groups, 38 | public string $version, 39 | public bool $ok, 40 | public SupervisorServer $server, 41 | public ?string $failError, 42 | ) {} 43 | 44 | /** @param array $data */ 45 | public static function fromArray(array $data, string $version, SupervisorServer $server): self 46 | { 47 | /** @var array $groups */ 48 | $groups = []; 49 | /** @var array $processes */ 50 | $processes = []; 51 | 52 | /** @var array> $data */ 53 | foreach ($data as $item) { 54 | $process = Process::fromArray($item); 55 | $processes[$process->group][] = $process; 56 | } 57 | 58 | foreach ($processes as $groupName => $group) { 59 | $groups[] = new ProcessGroup(name: (string)$groupName, processes: $group); 60 | } 61 | 62 | return new self(groups: $groups, version: $version, ok: true, server: $server, failError: null); 63 | } 64 | 65 | public static function fail(SupervisorServer $server, string $error): self 66 | { 67 | return new self(groups: [], version: '', ok: false, server: $server, failError: $error); 68 | } 69 | 70 | /** @return Process[] */ 71 | #[Ignore] 72 | public function getProcesses(): array 73 | { 74 | /** @var Process[] $result */ 75 | $result = []; 76 | 77 | foreach ($this->groups as $group) { 78 | foreach ($group->processes as $process) { 79 | $result[] = $process; 80 | } 81 | } 82 | 83 | return $result; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /assets/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docker/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | # user nginx; 2 | worker_processes auto; 3 | 4 | events { 5 | worker_connections 1024; 6 | } 7 | 8 | pid /tmp/nginx.pid; 9 | 10 | http { 11 | include /etc/nginx/mime.types; 12 | default_type application/octet-stream; 13 | 14 | log_format main '$remote_addr [$time_local] "$host" "$request" ' 15 | '$status $body_bytes_sent "$http_referer" ' 16 | '"$http_user_agent" "$http_x_forwarded_for"'; 17 | 18 | #access_log /dev/null; 19 | error_log /tmp/nginx_error.log; 20 | access_log /tmp/nginx_access.log; 21 | 22 | sendfile on; 23 | #tcp_nopush on; 24 | 25 | keepalive_timeout 65; 26 | 27 | 28 | # Compression 29 | # Enable Gzip compressed. 30 | gzip on; 31 | 32 | # Enable compression both for HTTP/1.0 and HTTP/1.1. 33 | gzip_http_version 1.1; 34 | 35 | # Compression level (1-9). 36 | # 5 is a perfect compromise between size and cpu usage, offering about 37 | # 75% reduction for most ascii files (almost identical to level 9). 38 | gzip_comp_level 5; 39 | 40 | # Don't compress anything that's already small and unlikely to shrink much 41 | # if at all (the default is 20 bytes, which is bad as that usually leads to 42 | # larger files after gzipping). 43 | gzip_min_length 256; 44 | 45 | # Compress data even for clients that are connecting to us via proxies, 46 | # identified by the "Via" header (required for CloudFront). 47 | gzip_proxied any; 48 | 49 | # Tell proxies to cache both the gzipped and regular version of a resource 50 | # whenever the client's Accept-Encoding capabilities header varies; 51 | # Avoids the issue where a non-gzip capable client (which is extremely rare 52 | # today) would display gibberish if their proxy gave them the gzipped version. 53 | gzip_vary on; 54 | 55 | # Compress all output labeled with one of the following MIME-types. 56 | gzip_types 57 | application/atom+xml 58 | application/javascript 59 | application/json 60 | application/rss+xml 61 | application/vnd.ms-fontobject 62 | application/x-font-ttf 63 | application/x-web-app-manifest+json 64 | application/xhtml+xml 65 | application/xml 66 | font/opentype 67 | image/svg+xml 68 | image/x-icon 69 | text/css 70 | text/plain 71 | text/x-component; 72 | # text/html is always compressed by HttpGzipModule 73 | 74 | # Cache 75 | fastcgi_cache_path /dev/shm levels=1:2 keys_zone=dwchiang:16m inactive=60m max_size=256m; 76 | fastcgi_cache_key "$scheme$request_method$host$request_uri$query_string"; 77 | 78 | include /etc/nginx/conf.d/*.conf; 79 | 80 | # For non-root 81 | client_body_temp_path /tmp/client_temp; 82 | proxy_temp_path /tmp/proxy_temp_path; 83 | fastcgi_temp_path /tmp/fastcgi_temp; 84 | uwsgi_temp_path /tmp/uwsgi_temp; 85 | scgi_temp_path /tmp/scgi_temp; 86 | } 87 | -------------------------------------------------------------------------------- /src/EventSubscriber/RenewJWTSubscriber.php: -------------------------------------------------------------------------------- 1 | [ 33 | /** 34 | * This has to be done after session cookie is set in 35 | * @see SessionListener at -1000. 36 | * @see StreamedResponseListener fires at -1024, 37 | * so we might as well skip it too. 38 | */ 39 | ['renew', -1026], 40 | ], 41 | Events::JWT_DECODED => [ 42 | ['checkRenewNeeded', -1000], 43 | ], 44 | ]; 45 | } 46 | 47 | public function checkRenewNeeded(JWTDecodedEvent $event): void 48 | { 49 | if (!$event->isValid()) { 50 | return; 51 | } 52 | 53 | $nowTs = Clock::get()->now()->getTimestamp(); 54 | 55 | /** @var array{iat: int, exp: int} $payload */ 56 | $payload = $event->getPayload(); 57 | 58 | $createdTs = $payload['iat']; 59 | $expireTs = $payload['exp']; 60 | 61 | $currentPeriod = $nowTs - $createdTs; 62 | $halfPeriod = ($expireTs - $createdTs) / 2; 63 | 64 | if ($currentPeriod > $halfPeriod) { 65 | $this->renewNeeded = true; 66 | } 67 | } 68 | 69 | public function renew(ResponseEvent $event): void 70 | { 71 | $response = $event->getResponse(); 72 | 73 | if ( 74 | !$this->renewNeeded || 75 | $response instanceof StreamedResponse || 76 | $response->getStatusCode() !== Response::HTTP_OK 77 | ) { 78 | return; 79 | } 80 | 81 | $user = $this->security->getUser(); 82 | if (!$user instanceof UserInterface) { 83 | return; 84 | } 85 | 86 | $newResponse = $this->handler->handleAuthenticationSuccess($user); 87 | $response->headers = $newResponse->headers; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/State/SupervisorProcessor.php: -------------------------------------------------------------------------------- 1 | */ 19 | final readonly class SupervisorProcessor implements ProcessorInterface 20 | { 21 | /** @param array $servers */ 22 | public function __construct( 23 | private SupervisorApiClient $api, 24 | #[Autowire(param: 'supervisordServers')] private array $servers 25 | ) {} 26 | 27 | public function process( 28 | mixed $data, 29 | Operation $operation, 30 | array $uriVariables = [], 31 | array $context = [] 32 | ): SupervisorManageResult 33 | { 34 | $serverString = (string)$data->server; 35 | /** @var SupervisorManageTypeEnum $type */ 36 | $type = $data->type; 37 | $group = (string)$data->group; 38 | $process = (string)$data->process; 39 | $full = $group.':'.$process; 40 | 41 | $server = $this->servers[$serverString] ?? null; 42 | if (null === $server) { 43 | throw new BaseException(sprintf('Server "%s" not found', $serverString)); 44 | } 45 | 46 | $typedResult = match ($type) { 47 | Enum::StartAllProcesses => $this->api->startAllProcesses(server: $server), 48 | Enum::StopAllProcesses => $this->api->stopAllProcesses(server: $server), 49 | Enum::RestartAllProcesses => $this->api->restartAllProcesses(server: $server), 50 | Enum::ClearAllProcessLogs => $this->api->clearAllProcessLogs(server: $server), 51 | Enum::StartProcessGroup => $this->api->startProcessGroup(name: $group, server: $server), 52 | Enum::StopProcessGroup => $this->api->stopProcessGroup(name: $group, server: $server), 53 | Enum::RestartProcessGroup => $this->api->restartProcessGroup(name: $group, server: $server), 54 | Enum::StartProcess => $this->api->startProcess(name: $full, server: $server), 55 | Enum::StopProcess => $this->api->stopProcess(name: $full, server: $server), 56 | Enum::RestartProcess => $this->api->restartProcess(name: $full, server: $server), 57 | Enum::ClearProcessLogs => $this->api->clearProcessLogs(name: $full, server: $server), 58 | Enum::CloneProcess => $this->api->cloneProcess(name: $process, group: $group, server: $server), 59 | Enum::RemoveProcess => $this->api->removeProcess(name: $process, group: $group, server: $server), 60 | }; 61 | 62 | return new SupervisorManageResult(typedResult: $typedResult); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /assets/@types/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | /* 4 | * --------------------------------------------------------------- 5 | * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## 6 | * ## ## 7 | * ## AUTHOR: acacode ## 8 | * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## 9 | * --------------------------------------------------------------- 10 | */ 11 | 12 | interface ApiChangedProcess { 13 | name: string; 14 | group: string; 15 | status: number; 16 | description: string; 17 | } 18 | 19 | interface ApiChangedProcesses { 20 | ok: boolean; 21 | changedProcesses: ApiChangedProcess[]; 22 | error: string | null; 23 | } 24 | 25 | interface ApiOperationResult { 26 | ok: boolean; 27 | isFault: boolean; 28 | error: string | null; 29 | } 30 | 31 | interface ApiProcess { 32 | errLog: ApiProcessLog | null; 33 | outLog: ApiProcessLog | null; 34 | descPid: string; 35 | descUptime: string; 36 | name: string; 37 | group: string; 38 | start: number; 39 | stop: number; 40 | now: number; 41 | state: number; 42 | stateName: string; 43 | spawnErr: string; 44 | exitStatus: number; 45 | logfile: string; 46 | stdoutLogfile: string; 47 | stderrLogfile: string; 48 | pid: number; 49 | description: string; 50 | fullProcessName: string; 51 | } 52 | 53 | interface ApiProcessGroup { 54 | name: string; 55 | processes: ApiProcess[]; 56 | } 57 | 58 | interface ApiProcessLog { 59 | log: string; 60 | } 61 | 62 | interface ApiSupervisor { 63 | groups: ApiProcessGroup[]; 64 | version: string; 65 | ok: boolean; 66 | server: ApiSupervisorServer; 67 | failError: string | null; 68 | } 69 | 70 | interface ApiSupervisorSupervisorManage { 71 | type: 72 | | 'start_all_processes' 73 | | 'stop_all_processes' 74 | | 'restart_all_processes' 75 | | 'clear_all_process_log' 76 | | 'start_process_group' 77 | | 'stop_process_group' 78 | | 'restart_process_group' 79 | | 'start_process' 80 | | 'stop_process' 81 | | 'restart_process' 82 | | 'clear_process_log' 83 | | 'clone_process' 84 | | 'remove_process'; 85 | server: string | null; 86 | group: string | null; 87 | process: string | null; 88 | } 89 | 90 | interface ApiSupervisorSupervisorManageResult { 91 | operationResult: ApiOperationResult | null; 92 | changedProcesses: ApiChangedProcesses | null; 93 | } 94 | 95 | interface ApiSupervisorServer { 96 | webOpenUrl: string; 97 | authenticated: boolean; 98 | ip: string; 99 | port: number; 100 | name: string; 101 | username: string | null; 102 | password: string | null; 103 | } 104 | 105 | interface ApiUser { 106 | username: string; 107 | roles: string[]; 108 | userIdentifier: string; 109 | } 110 | 111 | interface ApiUserAuthByCredentialsDTO { 112 | login: string; 113 | password: string; 114 | } 115 | 116 | type ApiUserStdClass = object; 117 | -------------------------------------------------------------------------------- /src/DTO/XmlRpc/ResponseDTO.php: -------------------------------------------------------------------------------- 1 | |array|null $value */ 17 | public function __construct(public int|float|string|bool|array|null $value, bool $isFault = false) 18 | { 19 | if ($isFault === false && is_array($value)) { 20 | foreach ($value as $item) { 21 | if (is_array($item) && FaultDTO::is($item)) { 22 | $this->faultInValue[] = FaultDTO::fromArray($item); 23 | } 24 | } 25 | } 26 | 27 | $this->isFault = $isFault; 28 | } 29 | 30 | public function hasFault(): bool 31 | { 32 | return $this->isFault || $this->faultInValue !== []; 33 | } 34 | 35 | public function getFirstFault(): FaultDTO 36 | { 37 | if (!is_array($this->value)) { 38 | throw new XmlRpcException('Invalid value for fault'); 39 | } 40 | 41 | if ($this->faultInValue !== []) { 42 | return $this->faultInValue[0]; 43 | } 44 | 45 | if (!$this->isFault && !FaultDTO::is($this->value)) { 46 | throw new XmlRpcException('Response is not a fault'); 47 | } 48 | 49 | return FaultDTO::fromArray($this->value); 50 | } 51 | 52 | /** @return array */ 53 | public function getValue(): array 54 | { 55 | if (!is_array($this->value)) { 56 | throw XmlRpcException::invalidResponseValue($this); 57 | } 58 | 59 | /** @var array $value */ 60 | $value = $this->value; 61 | 62 | return $value; 63 | } 64 | 65 | public function bool(): bool 66 | { 67 | if (!is_bool($this->value)) { 68 | throw XmlRpcException::invalidResponseValue($this); 69 | } 70 | 71 | return $this->value; 72 | } 73 | 74 | /** @return bool[] */ 75 | public function boolArray(): array 76 | { 77 | if (!is_array($this->value)) { 78 | throw XmlRpcException::invalidResponseValue($this); 79 | } 80 | 81 | foreach ($this->value as $item) { 82 | if (!is_bool($item)) { 83 | throw new XmlRpcException('Invalid value: '. json_encode($item)); 84 | } 85 | } 86 | 87 | /** @var bool[] $value */ 88 | $value = $this->value; 89 | 90 | return $value; 91 | } 92 | 93 | /** @return array */ 94 | public function array(): array 95 | { 96 | if (!is_array($this->value)) { 97 | throw XmlRpcException::invalidResponseValue($this); 98 | } 99 | 100 | /** @var array $value */ 101 | $value = $this->value; 102 | 103 | return $value; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.gitlab.com/sdt7772416/infrastructure/node-alpha:latest AS build-frontend 2 | 3 | COPY assets/ /app 4 | WORKDIR /app 5 | RUN corepack enable && yarn set version from sources && yarn --immutable && yarn install && yarn build 6 | 7 | FROM php:8.3-fpm-alpine AS parent-supervisord-monitor-php-fpm 8 | 9 | ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ 10 | 11 | ARG PUID=1000 12 | ARG PGID=1000 13 | ARG BUILD_TYPE="dist" 14 | ARG DEV_HOST_IP=172.18.3.1 15 | ARG DEV_XDEBUG_AUTOSTART=trigger 16 | ARG DEV_XDEBUG_IDE_KEY=PHPSTORM 17 | 18 | RUN set -eux && \ 19 | chmod +x /usr/local/bin/install-php-extensions && sync && install-php-extensions @composer intl zip && \ 20 | apk add --no-cache curl git nginx openssh supervisor && \ 21 | if [ "${BUILD_TYPE}" = "dev" ]; then \ 22 | curl -1sLf 'https://dl.cloudsmith.io/public/symfony/stable/setup.alpine.sh' | /bin/sh; \ 23 | apk add --no-cache symfony-cli ranger vim nano vifm npm yarn && npm install -g corepack && corepack enable && \ 24 | install-php-extensions gd xdebug && touch /var/log/xdebug.log && chmod 0666 /var/log/xdebug.log && \ 25 | echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \ 26 | echo "xdebug.start_with_request=$DEV_XDEBUG_AUTOSTART" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \ 27 | echo "xdebug.client_host=$DEV_HOST_IP" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \ 28 | echo "xdebug.client_port=9003" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \ 29 | echo "xdebug.log=/var/log/xdebug.log" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \ 30 | echo "xdebug.idekey=$DEV_XDEBUG_IDE_KEY" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini; \ 31 | fi && \ 32 | mkdir -p /var/log/supervisor /etc/supervisor/conf.d/ && \ 33 | rm -rf /etc/nginx/nginx.conf /etc/nginx/conf.d/default.conf /var/cache/apk/* && \ 34 | addgroup -g $PGID app && adduser -D -u $PUID -G app app && addgroup app www-data 35 | 36 | COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf 37 | COPY ./docker/nginx/${BUILD_TYPE}/nginx-default.conf /etc/nginx/conf.d/default.conf 38 | COPY ./docker/supervisor/supervisord-${BUILD_TYPE}.conf /etc/supervisord.conf 39 | COPY ./docker/php/php.ini /usr/local/etc/php/php.ini 40 | COPY ./docker/php/conf.d/*.ini /usr/local/etc/php/conf.d/ 41 | COPY ./docker/php/fpm-conf.d/*.conf /usr/local/etc/php-fpm.d/ 42 | USER app 43 | COPY --chown=app:app . /var/www/supervisord-monitor 44 | COPY --from=build-frontend /app/dist /var/www/supervisord-monitor/assets/dist 45 | WORKDIR /var/www/supervisord-monitor 46 | 47 | RUN if [ "${BUILD_TYPE}" = "dev" ]; then cd assets && yarn set version from sources; fi && \ 48 | if [ "${BUILD_TYPE}" = "dist" ]; then \ 49 | composer install --no-dev && \ 50 | chmod -R g+w var && chgrp -R www-data var && mkdir config/jwt && rm -rf /home/app/.npm /home/app/.composer; \ 51 | fi; 52 | 53 | EXPOSE 8080 54 | 55 | # 100000 microseconds = 0.1 seconds 56 | ENV COLLECT_INTERVAL_IN_MICROSECONDS=100000 57 | 58 | CMD ["supervisord"] 59 | -------------------------------------------------------------------------------- /assets/src/components/monitor/SupervisordSkeleton.tsx: -------------------------------------------------------------------------------- 1 | export const SupervisordSkeleton = () => { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /assets/src/pages/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link as ReactLink } from 'react-router-dom'; 2 | import { isHasRoleManager } from '~/providers/session/context'; 3 | import { useSettings } from '~/hooks/useSettings'; 4 | import { toastManager } from '~/util/toastManager'; 5 | import { Checkbox } from '~/components/ui/Checkbox'; 6 | import { Theme } from '~/config/theme'; 7 | import { Input } from '~/components/ui/Input'; 8 | 9 | export const SettingsPage = () => { 10 | const isManager = isHasRoleManager(); 11 | const { 12 | autoRefresh, 13 | setAutoRefresh, 14 | autoRefreshInterval, 15 | setAutoRefreshInterval, 16 | syncRefresh, 17 | setSyncRefresh, 18 | theme, 19 | setTheme, 20 | allowMutators, 21 | setAllowMutators, 22 | } = useSettings(); 23 | 24 | return ( 25 |
26 |
27 |
Settings
28 | { 33 | setAutoRefresh(e.target.checked); 34 | toastManager.success(e.target.checked ? 'Auto refresh enabled' : 'Auto refresh disabled'); 35 | }} 36 | /> 37 | { 43 | const value = parseInt(e.target.value); 44 | if (value > 0) { 45 | setAutoRefreshInterval(value); 46 | toastManager.success(`Auto refresh set to ${value}`); 47 | } 48 | }} 49 | /> 50 | { 55 | setSyncRefresh(e.target.checked); 56 | toastManager.success(e.target.checked ? 'Sync refresh enabled' : 'Sync refresh disabled'); 57 | }} 58 | /> 59 | { 64 | setTheme(e.target.checked ? Theme.dark : Theme.light); 65 | toastManager.success(e.target.checked ? 'Use dark theme' : 'Use light theme'); 66 | }} 67 | /> 68 | {isManager && ( 69 | { 74 | setAllowMutators(e.target.checked); 75 | toastManager.success(e.target.checked ? 'Allow mutators enabled' : 'Allow mutators disabled'); 76 | }} 77 | /> 78 | )} 79 | 80 |
Go to dashboard
81 |
82 |
83 |
84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /assets/src/components/Program.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState } from 'react'; 2 | import { isHasRoleManager } from '~/providers/session/context'; 3 | import { useInvalidateSupervisors } from '~/api/use-get-supervisors'; 4 | import { useRestartProcessGroup, useStartProcessGroup, useStopProcessGroup } from '~/api/use-manage-supervisors'; 5 | import { ChevronUp } from '~/components/icons/ChevronUp'; 6 | import { ChevronDown } from '~/components/icons/ChevronDown'; 7 | import { Stop } from '~/components/icons/Stop'; 8 | import { Reload } from '~/components/icons/Reload'; 9 | import { Play } from '~/components/icons/Play'; 10 | 11 | import { Process } from '~/components/Process'; 12 | 13 | interface ProgramProps { 14 | group: ApiProcessGroup; 15 | server: ApiSupervisorServer; 16 | } 17 | 18 | export const Program = ({ group, server }: ProgramProps) => { 19 | const [isActive, setIsActive] = useState(false); 20 | 21 | const isManager = isHasRoleManager(); 22 | 23 | const isNoGroup = group.processes.length === 1 && isNaN(Number(group.processes[0].name)); 24 | const isSomeRunning = group.processes.some(process => process.stateName === 'RUNNING'); 25 | 26 | const invalidateSupervisors = useInvalidateSupervisors(); 27 | 28 | const stopProcessGroup = useStopProcessGroup(server.name, group.name); 29 | const restartProcessGroup = useRestartProcessGroup(server.name, group.name); 30 | const startProcessGroup = useStartProcessGroup(server.name, group.name); 31 | 32 | const onGroupClick = () => { 33 | setIsActive(!isActive); 34 | }; 35 | 36 | const onStopGroup = async () => { 37 | await stopProcessGroup.mutateAsync(); 38 | await invalidateSupervisors(); 39 | }; 40 | 41 | const onRestartGroup = async () => { 42 | await restartProcessGroup.mutateAsync(); 43 | await invalidateSupervisors(); 44 | }; 45 | 46 | const onStartGroup = async () => { 47 | await startProcessGroup.mutateAsync(); 48 | await invalidateSupervisors(); 49 | }; 50 | 51 | return ( 52 |
53 | {!isNoGroup && ( 54 |
55 |
56 |
57 | {isActive ? : } 58 |
59 | {group.name} 60 |
61 |
62 |
63 |
64 | {isManager ? ( 65 |
66 | {isSomeRunning ? ( 67 | <> 68 | 71 | 74 | 75 | ) : ( 76 | 79 | )} 80 |
81 | ) : ( 82 | 83 | )} 84 |
85 |
86 | )} 87 | {(isActive || isNoGroup) && 88 | group.processes.map((process, i) => )} 89 |
90 | ); 91 | }; 92 | -------------------------------------------------------------------------------- /assets/src/pages/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { useForm } from 'react-hook-form'; 3 | import { zodResolver } from '@hookform/resolvers/zod'; 4 | import { useLogin } from '~/api/use-login'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { toast } from 'react-hot-toast'; 7 | import { useInvalidateMe } from '~/api/use-get-me'; 8 | import { ROUTES } from '~/const'; 9 | 10 | const schema = z.object({ 11 | login: z.string(), 12 | password: z.string(), 13 | }); 14 | 15 | type Schema = z.infer; 16 | 17 | export const LoginPage = () => { 18 | const navigate = useNavigate(); 19 | 20 | const form = useForm({ 21 | resolver: zodResolver(schema), 22 | }); 23 | 24 | const login = useLogin(); 25 | const invalidateSession = useInvalidateMe(); 26 | 27 | const onSubmit = async (data: Schema) => { 28 | await login 29 | .mutateAsync(data) 30 | .then(() => { 31 | toast.success('Logged in successfully'); 32 | invalidateSession().then(() => navigate(ROUTES.HOME)); 33 | }) 34 | .catch(error => toast.error(error.response.data.detail)); 35 | }; 36 | 37 | return ( 38 | <> 39 |
40 |
41 | Supervisord Monitor 42 |

43 | Sign in to your account 44 |

45 |
46 | 47 |
48 |
49 |
50 | 53 |
54 | 59 |
60 |
61 | 62 |
63 |
64 | 67 |
68 |
69 | 76 |
77 |
78 | 79 |
80 | 86 |
87 |
88 |
89 |
90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "minimum-stability": "stable", 5 | "prefer-stable": true, 6 | "require": { 7 | "php": ">=8.3", 8 | "ext-ctype": "*", 9 | "ext-iconv": "*", 10 | "api-platform/core": "^3.3", 11 | "doctrine/dbal": "^3", 12 | "doctrine/doctrine-bundle": "^2.14", 13 | "doctrine/doctrine-migrations-bundle": "^3.4", 14 | "doctrine/orm": "^3.3", 15 | "lexik/jwt-authentication-bundle": "^3.0", 16 | "nelmio/cors-bundle": "^2.4", 17 | "phpdocumentor/reflection-docblock": "^5.4", 18 | "phpstan/phpdoc-parser": "^1.25", 19 | "symfony/asset": "7.0.*", 20 | "symfony/console": "7.0.*", 21 | "symfony/dotenv": "7.0.*", 22 | "symfony/expression-language": "7.0.*", 23 | "symfony/flex": "^2", 24 | "symfony/framework-bundle": "7.0.*", 25 | "symfony/http-client": "7.0.*", 26 | "symfony/lock": "7.0.*", 27 | "symfony/process": "7.0.*", 28 | "symfony/property-access": "7.0.*", 29 | "symfony/property-info": "7.0.*", 30 | "symfony/rate-limiter": "7.0.*", 31 | "symfony/runtime": "7.0.*", 32 | "symfony/security-bundle": "7.0.*", 33 | "symfony/serializer": "7.0.*", 34 | "symfony/twig-bundle": "7.0.*", 35 | "symfony/validator": "7.0.*", 36 | "symfony/yaml": "7.0.*" 37 | }, 38 | "config": { 39 | "allow-plugins": { 40 | "php-http/discovery": true, 41 | "symfony/flex": true, 42 | "symfony/runtime": true 43 | }, 44 | "sort-packages": true 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "App\\": "src/" 49 | } 50 | }, 51 | "autoload-dev": { 52 | "psr-4": { 53 | "App\\Tests\\": "tests/" 54 | } 55 | }, 56 | "replace": { 57 | "symfony/polyfill-ctype": "*", 58 | "symfony/polyfill-iconv": "*", 59 | "symfony/polyfill-php72": "*", 60 | "symfony/polyfill-php73": "*", 61 | "symfony/polyfill-php74": "*", 62 | "symfony/polyfill-php80": "*", 63 | "symfony/polyfill-php81": "*", 64 | "symfony/polyfill-php82": "*" 65 | }, 66 | "scripts": { 67 | "auto-scripts": { 68 | "cache:clear": "symfony-cmd" 69 | }, 70 | "post-install-cmd": [ 71 | "@auto-scripts" 72 | ], 73 | "post-update-cmd": [ 74 | "@auto-scripts" 75 | ], 76 | "phpstan": [ 77 | "vendor/bin/phpstan analyse src tests -c config/code/phpstan.neon" 78 | ], 79 | "cs-fixer": [ 80 | "vendor/bin/php-cs-fixer --config=config/code/.php-cs-fixer.dist.php fix" 81 | ], 82 | "cs-fixer-diff": [ 83 | "vendor/bin/php-cs-fixer --config=config/code/.php-cs-fixer.dist.php fix --dry-run --diff" 84 | ], 85 | "rector": [ 86 | "vendor/bin/rector process --config config/code/rector.php" 87 | ], 88 | "test": [ 89 | "@phpstan" 90 | ], 91 | "phpunit": [ 92 | "vendor/bin/phpunit -c config/code/phpunit.xml.dist tests" 93 | ] 94 | }, 95 | "conflict": { 96 | "symfony/symfony": "*" 97 | }, 98 | "extra": { 99 | "symfony": { 100 | "allow-contrib": false, 101 | "require": "7.0.*" 102 | } 103 | }, 104 | "require-dev": { 105 | "friendsofphp/php-cs-fixer": "^3.46", 106 | "phpstan/phpstan": "^1.10", 107 | "phpstan/phpstan-symfony": "^1.3", 108 | "phpunit/phpunit": "^10.5", 109 | "rector/rector": "^0.19", 110 | "symfony/browser-kit": "7.0.*", 111 | "symfony/css-selector": "7.0.*", 112 | "symfony/phpunit-bridge": "^7.0", 113 | "symfony/stopwatch": "7.0.*", 114 | "symfony/web-profiler-bundle": "7.0.*" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/Functional/SupervisorApiClientTest.php: -------------------------------------------------------------------------------- 1 | assertSame([], $problems); 83 | } 84 | 85 | public function testProcessClonedAndRemovedSuccessfully(): void 86 | { 87 | /** @var SupervisorServer[] $servers */ 88 | $servers = self::getContainer()->getParameter('supervisors_servers'); 89 | 90 | $server = $servers[0]; 91 | 92 | /** @var SupervisorApiClient $api */ 93 | $api = self::getContainer()->get(SupervisorApiClient::class); 94 | 95 | /** @var string $group */ 96 | $group = $api->getAllConfigInfo($server)[0]->group; 97 | $program = 'test'.time(); 98 | 99 | $result1 = $api->cloneProcess($program, $group, $server); 100 | $result2 = $api->removeProcess($group, $program, $server); 101 | 102 | $this->assertTrue($result1->ok && $result2->ok); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /assets/scripts/gen-types/templates/procedure-call.ejs: -------------------------------------------------------------------------------- 1 | <% 2 | const { utils, route, config } = it; 3 | const { requestBodyInfo, responseBodyInfo, specificArgNameResolver } = route; 4 | const { _, getInlineParseContent, getParseContent, parseSchema, getComponentByRef, require } = utils; 5 | const { parameters, path, method, payload, query, formData, security, requestParams } = route.request; 6 | const { type, errorType, contentTypes } = route.response; 7 | const { HTTP_CLIENT, RESERVED_REQ_PARAMS_ARG_NAMES } = config.constants; 8 | const routeDocs = includeFile("./route-docs", { config, route, utils }); 9 | const queryName = (query && query.name) || "query"; 10 | const pathParams = _.values(parameters); 11 | const pathParamsNames = _.map(pathParams, "name"); 12 | 13 | const isFetchTemplate = config.httpClientType === HTTP_CLIENT.FETCH; 14 | 15 | const requestConfigParam = { 16 | name: specificArgNameResolver.resolve(RESERVED_REQ_PARAMS_ARG_NAMES), 17 | optional: true, 18 | type: "RequestParams", 19 | defaultValue: "{}", 20 | } 21 | 22 | const argToTmpl = ({ name, optional, type, defaultValue }) => `${name}${!defaultValue && optional ? '?' : ''}: ${type}${defaultValue ? ` = ${defaultValue}` : ''}`; 23 | 24 | const rawWrapperArgs = config.extractRequestParams ? 25 | _.compact([ 26 | requestParams && { 27 | name: pathParams.length ? `{ ${_.join(pathParamsNames, ", ")}, ...${queryName} }` : queryName, 28 | optional: false, 29 | type: getInlineParseContent(requestParams), 30 | }, 31 | ...(!requestParams ? pathParams : []), 32 | payload, 33 | requestConfigParam, 34 | ]) : 35 | _.compact([ 36 | ...pathParams, 37 | query, 38 | payload, 39 | requestConfigParam, 40 | ]) 41 | 42 | const wrapperArgs = _ 43 | // Sort by optionality 44 | .sortBy(rawWrapperArgs, [o => o.optional]) 45 | .map(argToTmpl) 46 | .join(', ') 47 | 48 | // RequestParams["type"] 49 | const requestContentKind = { 50 | "JSON": "ContentType.Json", 51 | "URL_ENCODED": "ContentType.UrlEncoded", 52 | "FORM_DATA": "ContentType.FormData", 53 | "TEXT": "ContentType.Text", 54 | } 55 | // RequestParams["format"] 56 | const responseContentKind = { 57 | "JSON": '"json"', 58 | "IMAGE": '"blob"', 59 | "FORM_DATA": isFetchTemplate ? '"formData"' : '"document"' 60 | } 61 | 62 | const bodyTmpl = _.get(payload, "name") || null; 63 | const queryTmpl = (query != null && queryName) || null; 64 | const bodyContentKindTmpl = requestContentKind[requestBodyInfo.contentKind] || null; 65 | const responseFormatTmpl = responseContentKind[responseBodyInfo.success && responseBodyInfo.success.schema && responseBodyInfo.success.schema.contentKind] || null; 66 | const securityTmpl = security ? 'true' : null; 67 | 68 | const describeReturnType = () => { 69 | if (!config.toJS) return ""; 70 | 71 | switch(config.httpClientType) { 72 | case HTTP_CLIENT.AXIOS: { 73 | return `Promise>` 74 | } 75 | default: { 76 | return `Promise` 77 | } 78 | } 79 | } 80 | 81 | %> 82 | /** 83 | <%~ routeDocs.description %> 84 | 85 | *<% /* Here you can add some other JSDoc tags */ %> 86 | 87 | <%~ routeDocs.lines %> 88 | 89 | */ 90 | <%~ route.routeName.usage %><%~ route.namespace ? ': ' : ' = ' %>(<%~ wrapperArgs %>)<%~ config.toJS ? `: ${describeReturnType()}` : "" %> => 91 | <%~ config.singleHttpClient ? 'this.http.request' : 'this.request' %><<%~ type %>, <%~ errorType %>>({ 92 | path: `<%~ path %>`, 93 | method: '<%~ _.upperCase(method) %>', 94 | <%~ queryTmpl ? `query: ${queryTmpl},` : '' %> 95 | <%~ bodyTmpl ? `body: ${bodyTmpl},` : '' %> 96 | <%~ securityTmpl ? `secure: ${securityTmpl},` : '' %> 97 | <%~ bodyContentKindTmpl ? `type: ${bodyContentKindTmpl},` : '' %> 98 | <%~ responseFormatTmpl ? `format: ${responseFormatTmpl},` : '' %> 99 | ...<%~ _.get(requestConfigParam, "name") %>, 100 | })<%~ route.namespace ? ',' : '' %> 101 | -------------------------------------------------------------------------------- /src/DTO/XmlRpc/CallDTO.php: -------------------------------------------------------------------------------- 1 | |array|null>)|(array)|null $params 13 | */ 14 | public function __construct(public string $methodName, public ?array $params = []) {} 15 | 16 | public static function stopProcess(string $name): self 17 | { 18 | return self::new('supervisor.stopProcess', ['name' => $name, 'wait' => true]); 19 | } 20 | 21 | /** @param array|array{} $params */ 22 | public static function new(string $method, array $params = []): self 23 | { 24 | return new self($method, $params); 25 | } 26 | 27 | public static function startProcess(string $name): self 28 | { 29 | return self::new('supervisor.startProcess', ['name' => $name, 'wait' => true]); 30 | } 31 | 32 | public static function getSupervisorVersion(): self 33 | { 34 | return self::new('supervisor.getSupervisorVersion'); 35 | } 36 | 37 | public static function getAllProcessInfo(): self 38 | { 39 | return self::new('supervisor.getAllProcessInfo'); 40 | } 41 | 42 | public static function stopAllProcesses(): self 43 | { 44 | return self::new('supervisor.stopAllProcesses', ['wait' => true]); 45 | } 46 | 47 | public static function startAllProcesses(): self 48 | { 49 | return self::new('supervisor.startAllProcesses', ['wait' => true]); 50 | } 51 | 52 | public static function readProcessStderrLog(string $name, int $offset, int $length): self 53 | { 54 | return self::new( 55 | 'supervisor.readProcessStderrLog', 56 | ['name' => $name, 'offset' => $offset, 'length' => $length] 57 | ); 58 | } 59 | 60 | public static function readProcessStdoutLog(string $name, int $offset, int $length): self 61 | { 62 | return self::new( 63 | 'supervisor.readProcessStdoutLog', 64 | ['name' => $name, 'offset' => $offset, 'length' => $length] 65 | ); 66 | } 67 | 68 | public static function clearProcessLogs(string $name): self 69 | { 70 | return self::new('supervisor.clearProcessLogs', ['name' => $name]); 71 | } 72 | 73 | public static function clearAllProcessLogs(): self 74 | { 75 | return self::new('supervisor.clearAllProcessLogs'); 76 | } 77 | 78 | public static function listMethods(): self 79 | { 80 | return self::new('system.listMethods'); 81 | } 82 | 83 | public static function methodSignature(string $method): self 84 | { 85 | return self::new('system.methodSignature', ['method' => $method]); 86 | } 87 | 88 | public static function methodHelp(string $method): self 89 | { 90 | return self::new('system.methodHelp', ['method' => $method]); 91 | } 92 | 93 | public static function getAllConfigInfo(): self 94 | { 95 | return self::new('supervisor.getAllConfigInfo'); 96 | } 97 | 98 | /** Working with already defined groups in config(unload, load*) */ 99 | public static function addProgramToGroup(string $group, string $program, Config $config): self 100 | { 101 | return self::new( 102 | 'twiddler.addProgramToGroup', 103 | [ 104 | 'group_name' => $group, 105 | 'program_name' => $program, 106 | 'program_options' => $config->jsonSerialize(), 107 | ] 108 | ); 109 | } 110 | 111 | public static function removeProcessFromGroup(string $group, string $program): self 112 | { 113 | return self::new( 114 | 'twiddler.removeProcessFromGroup', 115 | [ 116 | 'group_name' => $group, 117 | 'program_name' => $program, 118 | ] 119 | ); 120 | } 121 | 122 | public static function stopProcessGroup(string $name): self 123 | { 124 | return self::new('supervisor.stopProcessGroup', ['name' => $name, 'wait' => true]); 125 | } 126 | 127 | public static function startProcessGroup(string $name): self 128 | { 129 | return self::new('supervisor.startProcessGroup', ['name' => $name, 'wait' => true,]); 130 | } 131 | 132 | /** @return array */ 133 | public function toArray(): array 134 | { 135 | return ['methodName' => $this->methodName, 'params' => $this->params]; 136 | } 137 | 138 | /** @return (array|array|null>)|(array) */ 139 | public function getParams(): array 140 | { 141 | if (null === $this->params) { 142 | return []; 143 | } 144 | 145 | return [$this->params]; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /assets/src/components/Server.tsx: -------------------------------------------------------------------------------- 1 | import { useInvalidateSupervisors } from '~/api/use-get-supervisors'; 2 | import { isHasRoleManager } from '~/providers/session/context'; 3 | import { useClearAllProcessLog, useRestartAll, useStartAll, useStopAll } from '~/api/use-manage-supervisors'; 4 | import { trimIpPort } from '~/util/trimIpPort'; 5 | import { LockClosed } from '~/components/icons/LockClosed'; 6 | import { ShredFile } from '~/components/icons/ShredFile'; 7 | import { Play } from '~/components/icons/Play'; 8 | import { Stop } from '~/components/icons/Stop'; 9 | import { Reload } from '~/components/icons/Reload'; 10 | import { Fragment } from 'react'; 11 | import { twMerge } from 'tailwind-merge'; 12 | import { Skull } from '~/components/icons/Skull'; 13 | 14 | import { Program } from '~/components/Program'; 15 | 16 | interface ServerProps { 17 | item: ApiSupervisor; 18 | } 19 | 20 | export const Server = ({ item }: ServerProps) => { 21 | const invalidateSupervisors = useInvalidateSupervisors(); 22 | const hasRoleManager = isHasRoleManager(); 23 | const startAll = useStartAll(item.server.name); 24 | const clearAllProcessLog = useClearAllProcessLog(item.server.name); 25 | const stopAll = useStopAll(item.server.name); 26 | const restartAll = useRestartAll(item.server.name); 27 | 28 | return ( 29 |
30 |
31 |
32 |
33 | 34 | {item.server.name} 35 | 36 | {item.ok ? ( 37 |
38 | 39 | {trimIpPort(item.server.ip, `:${item.server.port}`)} 40 | 41 | {item.server.authenticated && } 42 | {item.version} 43 |
44 | ) : ( 45 |
46 | error 47 |
48 | )} 49 |
50 | {item.ok && hasRoleManager ? ( 51 |
52 |
53 | 62 | 71 | 80 | 89 |
90 |
91 | ) : ( 92 | 93 | )} 94 |
95 |
96 | {item.ok ? ( 97 |
98 | {item.groups.map((value, index) => { 99 | const notLast = index !== item.groups.length - 1; 100 | 101 | return ( 102 |
103 | 104 |
105 | ); 106 | })} 107 |
108 | ) : ( 109 |
110 | 111 | 112 | 113 |
114 | Server is not available! 115 | Reason: {item.failError ?? 'Unknown error'} 116 |
117 |
118 | )} 119 |
120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /src/Command/ProvideSupervisorServerJsonCommand.php: -------------------------------------------------------------------------------- 1 | confirm('Import existing servers?', false)) { 24 | /** @var string $json */ 25 | $json = $io->ask('Enter servers json', validator: static function (string $v): string { 26 | Assert::string($v); 27 | 28 | return $v; 29 | }); 30 | 31 | /** @var array $arr */ 32 | $arr = (array)json_decode($json, true); 33 | 34 | $res = []; 35 | foreach ($arr as $v) { 36 | if (!isset($v['ip'], $v['port'], $v['name'])) { 37 | continue; 38 | } 39 | 40 | $res[$v['name']] = ['ip' => (string)$v['ip'], 'port' => (int)$v['port'], 'name' => (string)$v['name']]; 41 | 42 | if (isset($v['username'], $v['password'])) { 43 | $res[$v['name']]['username'] = (string)$v['username']; 44 | $res[$v['name']]['password'] = (string)$v['password']; 45 | } 46 | } 47 | 48 | $arr = array_values($res); 49 | } 50 | 51 | $arr = $this->builder($io, $arr); 52 | 53 | $output->writeln((string)json_encode($arr)); 54 | 55 | return Command::SUCCESS; 56 | } 57 | 58 | /** 59 | * @param array $arr 60 | * @return array 61 | */ 62 | private function builder(SymfonyStyle $io, array $arr = []): array 63 | { 64 | $this->print($io, $arr); 65 | 66 | $choice = $io->choice('What do you want to do?', ['Add', 'Delete', 'Quit'], 'Add'); 67 | 68 | if ('Delete' === $choice) { 69 | $names = array_column($arr, 'name'); 70 | if ([] === $names) { 71 | $io->warning('No one server exists'); 72 | 73 | return $this->builder($io, $arr); 74 | } 75 | 76 | $name = $io->choice('Which server do you want to delete?', $names); 77 | 78 | if (!in_array($name, array_column($arr, 'name'))) { 79 | $io->warning('Not found'); 80 | 81 | return $this->builder($io, $arr); 82 | } 83 | 84 | $arr = array_filter($arr, static fn($v): bool => ( 85 | is_array($v) && isset($v['name']) && is_string($v['name']) && $name !== $v['name']) 86 | ); 87 | 88 | return $this->builder($io, $arr); 89 | } 90 | 91 | if ('Add' === $choice) { 92 | $newItem = $this->getServerConfig($io); 93 | 94 | if (in_array($newItem[0]['name'], array_column($arr, 'name'))) { 95 | $io->warning('Already has name'); 96 | 97 | return $this->builder($io, $arr); 98 | } 99 | 100 | array_push($arr, ...$newItem); 101 | 102 | return $this->builder($io, $arr); 103 | } 104 | 105 | return $arr; 106 | } 107 | 108 | /** @param array $arr */ 109 | private function print(SymfonyStyle $io, array $arr): void 110 | { 111 | $count = count($arr); 112 | 113 | $table = $io->createTable(); 114 | $table->setHeaderTitle('Supervisor Servers'); 115 | $table->setHeaders(['ip', 'port', 'name', 'username', 'password']); 116 | 117 | /** @var array{ip: string, port: int, name: string, username?: string, password?: string} $v */ 118 | 119 | foreach ($arr as $v) { 120 | $table->addRow([$v['ip'], $v['port'], $v['name'], $v['username'] ?? '', $v['password'] ?? '']); 121 | } 122 | 123 | $table->render(); 124 | $io->text(sprintf('Total: %s', $count)); 125 | } 126 | 127 | /** @return array */ 128 | private function getServerConfig(SymfonyStyle $io): array 129 | { 130 | $ipFn = static function (string $v): string { 131 | Assert::ip($v); 132 | 133 | return $v; 134 | }; 135 | $portFn = static function (int $v): int { 136 | Assert::positiveInteger($v); 137 | Assert::lessThan($v, 65536); 138 | 139 | return $v; 140 | }; 141 | 142 | [$ip, $port, $name, $username, $password] = [ 143 | $io->ask('IP', '127.0.0.1', $ipFn), 144 | $io->ask('Port', '9551', $portFn), 145 | $io->ask('Name', 'default'), 146 | $io->ask('Username'), 147 | $io->ask('Password'), 148 | ]; 149 | 150 | $arr = ['ip' => $ip, 'port' => $port, 'name' => $name]; 151 | if (null !== $username && null !== $password) { 152 | $arr += ['username' => $username, 'password' => $password]; 153 | } 154 | 155 | /** @var array{ip: string, port: int, name: string, username?: string, password?: string} $arr */ 156 | 157 | return [$arr]; 158 | } 159 | } 160 | --------------------------------------------------------------------------------