diff --git a/article/embed-gui-guide.md b/article/embed-gui-guide.md new file mode 100644 index 0000000..9a50a15 --- /dev/null +++ b/article/embed-gui-guide.md @@ -0,0 +1,345 @@ +# Встраиваем веб-GUI в консольную утилиту: практический гайд + +```metadata +id: 2 +title: "Встраиваем веб-GUI в консольную утилиту: практический гайд" +readTime: 15-20 минут +date: 2025-09-10 18:00 +author: Direct-Dev (Антон) +level: Средний +tags: #go #angular #spa #embed #static #cli #webui #devops +version: 1.0.2 +``` + +## Содержание + +- [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд) + - [Содержание](#содержание) + - [Введение](#введение) + - [Клонируем, собираем, запускаем](#клонируем-собираем-запускаем) + - [Структура проекта](#структура-проекта) + - [Идея и архитектура](#идея-и-архитектура) + - [Минимальный GUI](#минимальный-gui) + - [Сборка фронтенда](#сборка-фронтенда) + - [Встраивание в Go-сервис](#встраивание-в-go-сервис) + - [API: контракт и примеры](#api-контракт-и-примеры) + - [Запуск и проверка](#запуск-и-проверка) + - [SPA под произвольным префиксом (/ui/simple): base href и deploy-url](#spa-под-произвольным-префиксом-uisimple-base-href-и-deploy-url) + - [FAQ и типичные ошибки](#faq-и-типичные-ошибки) + +## Введение + +Допустим есть желание к консольной Go утилите прикрутить небольшой веб‑интерфейс, чтобы не гонять команды вручную в терминале или расширить круг пользователей. +Как вариант добавляем api в утилиту, делаем SPA на ангуляре, например, дальше кидаем в проект api скомпилированное spa, cобрали бинарь и отдаем статику прямо из Go‑бинарника (или из рядом лежащей папки). + +В качестве утилиты берем go-knocker - утилиту чтобы постучаться по портам ( такая штука повышающая безопасность серверов и устройств). Проект утилиты можно найти тут: + +в данном упрощенном интерфейсе используем только следующие поля и inline режим работы утилиты: + +на форме будут следующие поля + +- Targets (строка вида `tcp:host:port;udp:host:port...`) +- Delay (например `1s`) +- Флаг Wait connection +- Кнопка Execute + +Если нужен более «продвинутый» вариант — всегда можно нарастить поля и логику. + +## Клонируем, собираем, запускаем + +Источник репозитория: [`https://direct-dev.ru/gitea/GiteaAdmin/knock-gui`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui) + +1 Клонируем репозиторий и переключаемся на ветку for-article: + +```bash +# HTTPS клон +git clone https://direct-dev.ru/gitea/GiteaAdmin/knock-gui.git +cd knock-gui + +# Переход на ветку с упрощенным UI +git checkout for-article +``` + +2 переходим в папку фронта, ставим зависимости и собираем UI: + +```bash +cd ui +# Установим зависимости проекта (node/npm должны быть установлены) +npm ci +# Собираем продовую сборку +./build-for-embeding.sh ../back/cmd/public +cd .. + +# или через make +cd .. +make embed-ui +``` + +3 Запускаем бэкенд (Go 1.21+): + +```bash +# Обязательный пароль для Basic-Auth (GUI и API) +export GO_KNOCKER_SERVE_PASS=changeme +# Порт опционален (по умолчанию 8888) +export GO_KNOCKER_SERVE_PORT=8888 + +# Вариант A: запустить из исходников +cd back +go run ./ + +# или Вариант B: собрать бинарник и запустить +go build -o knocker-serve ./ +./knocker-serve serve + +# ну или так +make run PASS=superpass PORT=8888 +``` + +4 Открываем в браузере: + +```text +http://localhost:8888 +``` + +Браузер попросит логин/пароль (Basic‑Auth). Логин любой (например `knocker`), пароль — тот, что в `GO_KNOCKER_SERVE_PASS`. После этого UI загрузится, и та же авторизация «подхватится» для API‑вызовов. + +5 Заполняем нужные таргеты: tcp:10.10.10.10:8080;udp:10.10.10.20:8888 + +6 Нажимаем Execute — и смотрим результат. Ошибка авторизации? Проверьте пароль в переменной окружения и перезагрузите страницу. + +## Структура проекта + +Чтобы лучше ориентироваться, вот ключевые директории и файлы (сокращенно): + +```text +knock-gui/ +├── ui/ # Angular SPA (минимальный GUI) +│ ├── src/app/knock/ +│ │ ├── knock-page.component.ts +│ │ └── knock-page.component.html +│ ├── build-for-embeding.sh # Скрипт сборки и копирования артефактов в back +│ └── ... +├── back/ # Go backend (встроенная статика + API) +│ ├── cmd/ +│ │ ├── serve.go # запуск сервера, basic-auth, CORS +│ │ ├── static_routes.go # раздача встроенной статики (SPA routing) +│ │ └── knock_routes.go # эндпойнты API +│ ├── internal/knocker.go # логика работы +│ └── main.go +└── article/ # материалы статьи +``` + +## Идея и архитектура + +- **Фронтенд (Angular SPA)** — статические файлы (`index.html`, `*.js`, `*.css`). +- **Бэкенд (Go + Gin)** — раздаёт эти файлы и держит REST API. Для статики используем `embed.FS` — удобно распространять единый бинарник. +- **Безопасность** — простой Basic‑Auth. Переменная `GO_KNOCKER_SERVE_PASS` обязательна: без пароля сервер не стартует. + +Под капотом статику обслуживает `setupStaticRoutes`, обратите внимание на SPA‑маршрутизацию (фоллбэк на `index.html`): + +```1:30:/home/su/projects/articles/embed-gui-article/back/cmd/static_routes.go +// Если файл не найден и это маршрут SPA — показываем index.html +``` + +## Минимальный GUI + +Оставил только то, что реально нужно для «пнул порты». В main полный интерфейс ... + +Ключевой шаблон компонента: + +```12:60:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.html +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+``` + +Логика отправки запроса: + +```1:40:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.ts +this.http.post('/api/v1/knock-actions/execute', { + targets: v.targets, + delay: v.delay, + waitConnection: v.waitConnection +}).subscribe(...) +``` + +> ну да, ну да надо сервис и все такое ... + +Так как страницы GUI защищены Basic‑Auth, браузер после ввода пароля сам будет добавлять заголовок Authorization и к XHR‑запросам — отдельно в коде его прокидывать не нужно. + +## Сборка фронтенда + +Собираем Angular‑приложение и копируем артефакты туда, откуда Go будет отдавать статику. + +Скрипт сборки: + +```1:20:/home/su/projects/articles/embed-gui-article/ui/build-for-embeding.sh +#!/bin/bash +# Использование: ./build-for-embeding.sh /abs/path/to/back/cmd/public +npx ng build --configuration production +mkdir -p "$DESTINATION_DIR" +cp -r /home/su/projects/angular/project-front/dist/project-front/browser/* "$DESTINATION_DIR" +``` + +- После `ng build` файлы лежат в `ui/dist/project-front/browser/`. +- Мы копируем их в `back/cmd/public/`, откуда сервер раздаёт статику (а в релизе — всё упакуется в `embed.FS`). + +## Встраивание в Go-сервис + +Старт сервера и базовая авторизация — в `serve.go`: + +```1:26:/home/su/projects/articles/embed-gui-article/back/cmd/serve.go +//go:embed public/* +var embeddedFS embed.FS +... +pass := os.Getenv("GO_KNOCKER_SERVE_PASS") +if strings.TrimSpace(pass) == "" { + return fmt.Errorf("GO_KNOCKER_SERVE_PASS не задан — задайте пароль для доступа к GUI/API") +} +... +setupStaticRoutes(r, embeddedFS) +``` + +То есть без пароля сервер не стартует — и это хорошо. + +Альтернативно можно отдать файлы с диска, но `embed.FS` удобнее: один бинарник — и поехали. + +## API: контракт и примеры + +Endpoint: `POST /api/v1/knock-actions/execute` + +Request JSON: + +```json +{ + "targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53", + "delay": "1s", + "waitConnection": false +} +``` + +Response 200: + +```json +{ "status": "ok" } +``` + +Проверка через curl (не забудьте базовую авторизацию): + +```bash +curl -X POST http://localhost:8888/api/v1/knock-actions/execute \ + -u knocker:changeme \ + -H 'Content-Type: application/json' \ + -d '{"targets":"tcp:127.0.0.1:22","delay":"1s","waitConnection":false}' +``` + +## Запуск и проверка + +1 Собрать фронт и скопировать файлы: + +```bash +cd ui +./build-for-embeding.sh ../back/cmd/public +cd .. +``` + +2 Запустить бэкенд: + +```bash +export GO_KNOCKER_SERVE_PASS=changeme +export GO_KNOCKER_SERVE_PORT=8888 +cd back +go run ./ +``` + +3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API: + +```text +http://localhost:8888 +``` + +Если что-то не так — загляните в логи терминала и сетевую вкладку DevTools. + +## SPA под произвольным префиксом (/ui/simple): base href и deploy-url + +Иногда нужно отдавать SPA не с корня `/`, а, скажем, по пути `/ui/simple`. Для Angular это значит две вещи: правильный `` в `index.html` и корректные пути к ассетам. + +Вариант A: собрать с заданным base-href и deploy-url: + +```bash +# пример сборки под префикс /ui/simple +npx ng build --configuration production \ + --base-href /ui/simple/ \ + --deploy-url /ui/simple/ +``` + +Что это даёт: + +- В `dist/.../browser/index.html` будет ``. +- Все ссылки на бандлы/ассеты будут начинаться с `/ui/simple/`. + +Вариант B: поправить `index.html` вручную после сборки (минимальный вариант): + +```html + + +``` + +Важно: закрывающий слеш обязателен, иначе роутинг может «ехать». + +Настройка бэкенда: + +- Отгружайте содержимое собранной папки по маршруту `/ui/simple` (или скопируйте артефакты в `back/cmd/public/ui/simple/`). +- SPA‑фоллбэк должен отдавать `index.html` при запросах внутри префикса, если это не файловые ресурсы. + +Если у вас универсальный обработчик (как в `setupStaticRoutes`) на корне, два простых подхода: + +- Хранить файлы в `public/ui/simple/...` — тогда запросы к `/ui/simple/...` будут отработаны корректно. +- Сделать отдельный хендлер, который для путей с префиксом `/ui/simple` читает файлы из подкаталога, а на несуществующие файлы отвечает содержимым `public/ui/simple/index.html`. + +Пример маппинга структуры в `public/`: + +```text +public/ +├── ui/ +│ └── simple/ +│ ├── index.html # с +│ ├── main-*.js +│ ├── styles-*.css +│ └── assets/... +└── ... +``` + +Если при прямом заходе на вложенный роут `/ui/simple/some/child` видите 404 — значит фоллбэк не отрабатывает. Проверьте, что при отсутствии файла по этому пути сервер возвращает `public/ui/simple/index.html`. + +## FAQ и типичные ошибки + +- «Сервер ругается на пароль»: не задали `GO_KNOCKER_SERVE_PASS` или запустили в другой сессии. Экспортните переменную и перезапустите. +- «GUI просит логин/пароль, а потом 401 на API»: проверьте правильность пароля, перезагрузите страницу. Браузер должен автоматом подставлять Authorization. +- «Статика не находится»: проверьте, что после сборки файлы UI скопированы в `back/cmd/public/` и сервер перезапущен. +- «SPA роуты 404 при F5»: `static_routes.go` делает фоллбэк на `index.html`. Если меняли пути — не сломайте фоллбек. +- «CORS в разработке»: мы разрешили `http://localhost:4200` и `:8888`. Если запускаете UI отдельно, не забудьте CORS. + +--- + +Если хочется глубже — можно прикрутить версионирование артефактов, CI/CD и полноценный embed всех файлов. Но для старта достаточно описанных шагов — клон, сборка, копирование, запуск. Увидел проблему — открывайте тикет в репозитории: [`https://direct-dev.ru/gitea/GiteaAdmin/knock-gui`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui)