├── 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 |
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 |
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 |
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 |
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 |
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 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/assets/src/components/icons/Play.tsx:
--------------------------------------------------------------------------------
1 | export const Play = () => {
2 | return (
3 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |

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 |
16 |
17 |
24 |
31 |
39 |
46 |
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 |
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 |

42 |
43 | Sign in to your account
44 |
45 |
46 |
47 |
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 |
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 |
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 |
--------------------------------------------------------------------------------