Files
knock-gui/article/embed-gui-guide.md
2025-09-12 13:53:32 +06:00

346 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Встраиваем веб-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 - утилиту чтобы постучаться по портам ( такая штука повышающая безопасность серверов и устройств). Проект утилиты можно найти тут: <https://github.com/Direct-Dev-Ru/port-knocker.git>
в данном упрощенном интерфейсе используем только следующие поля и 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
```
Браузер попросит логин/пароль (BasicAuth). Логин любой (например `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` — удобно распространять единый бинарник.
- **Безопасность** — простой BasicAuth. Переменная `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
<div class="container">
<p-card header="Port Knocker (Minimal UI)">
<form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid">
<div class="grid">
<div class="col-12">
<label>Targets</label>
<input pInputText type="text" formControlName="targets" placeholder="tcp:host:port;udp:host:port" class="w-full" />
</div>
<div class="col-12 md:col-6">
<label>Delay</label>
<input pInputText type="text" formControlName="delay" placeholder="1s" class="w-full" />
</div>
<div class="col-12 md:col-6 flex align-items-center gap-2">
<p-checkbox formControlName="waitConnection" [binary]="true"></p-checkbox>
<label class="checkbox-label">Wait connection</label>
</div>
<div class="col-12">
<button pButton type="submit" label="Execute" class="w-full" [loading]="executing" [disabled]="executing || form.invalid"></button>
</div>
</div>
</form>
</p-card>
</div>
```
Логика отправки запроса:
```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 защищены BasicAuth, браузер после ввода пароля сам будет добавлять заголовок 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 это значит две вещи: правильный `<base href>` в `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` будет `<base href="/ui/simple/">`.
- Все ссылки на бандлы/ассеты будут начинаться с `/ui/simple/`.
Вариант B: поправить `index.html` вручную после сборки (минимальный вариант):
```html
<!-- ui/dist/project-front/browser/index.html -->
<base href="/ui/simple/">
```
Важно: закрывающий слеш обязателен, иначе роутинг может «ехать».
Настройка бэкенда:
- Отгружайте содержимое собранной папки по маршруту `/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 # с <base href="/ui/simple/">
│ ├── 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)