article feature
This commit is contained in:
2
Makefile
2
Makefile
@@ -43,7 +43,7 @@ back-build: embed-ui back-deps
|
||||
cd $(BACK_DIR) && go build -o knocker-serve .
|
||||
|
||||
run: back-build
|
||||
cd $(BACK_DIR) && GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve serve
|
||||
cd $(BACK_DIR) && GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve -v serve
|
||||
|
||||
run-bg: back-build
|
||||
cd $(BACK_DIR) && nohup env GO_KNOCKER_SERVE_PASS=$(PASS) GO_KNOCKER_SERVE_PORT=$(PORT) ./knocker-serve serve > /tmp/knocker.log 2>&1 & echo $$! && sleep 1 && tail -n +1 /tmp/knocker.log | sed -n '1,60p'
|
||||
|
@@ -12,19 +12,22 @@ version: 1.0.0
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Введение](#введение)
|
||||
2. [Идея и архитектура](#идея-и-архитектура)
|
||||
3. [Минимальный GUI](#минимальный-gui)
|
||||
4. [Сборка фронтенда](#сборка-фронтенда)
|
||||
5. [Встраивание в Go-сервис](#встраивание-в-go-сервис)
|
||||
6. [API: контракт и примеры](#api-контракт-и-примеры)
|
||||
7. [Запуск и проверка](#запуск-и-проверка)
|
||||
8. [FAQ и типичные ошибки](#faq-и-типичные-ошибки)
|
||||
- [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд)
|
||||
- [Содержание](#содержание)
|
||||
- [Введение](#введение)
|
||||
- [Идея и архитектура](#идея-и-архитектура)
|
||||
- [Минимальный GUI](#минимальный-gui)
|
||||
- [Сборка фронтенда](#сборка-фронтенда)
|
||||
- [Встраивание в Go-сервис](#встраивание-в-go-сервис)
|
||||
- [API: контракт и примеры](#api-контракт-и-примеры)
|
||||
- [Запуск и проверка](#запуск-и-проверка)
|
||||
- [FAQ и типичные ошибки](#faq-и-типичные-ошибки)
|
||||
|
||||
## Введение
|
||||
|
||||
Задача: добавить простой веб-интерфейс к консольной утилите, собрать его один раз и отдавать статические файлы прямо из бинарника или из каталога рядом. В качестве примера используем минимальный UI со следующими полями:
|
||||
- Targets (строка вида `tcp:host:port;udp:host:port`)
|
||||
|
||||
- Targets (строка вида `tcp:host:port;udp:host:port...`)
|
||||
- Delay (например `1s`)
|
||||
- Флаг Wait connection
|
||||
- Кнопка Execute
|
||||
@@ -42,6 +45,7 @@ version: 1.0.0
|
||||
Мы упростили компонент до минимума в ветке `for-article`. Основной экран — одна форма и кнопка запуска.
|
||||
|
||||
Ключевой шаблон компонента:
|
||||
|
||||
```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 (Минимальный UI)">
|
||||
@@ -69,6 +73,7 @@ version: 1.0.0
|
||||
```
|
||||
|
||||
Логика отправки запроса:
|
||||
|
||||
```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,
|
||||
@@ -82,6 +87,7 @@ this.http.post('/api/v1/knock-actions/execute', {
|
||||
Собираем 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
|
||||
@@ -96,12 +102,14 @@ cp -r /home/su/projects/angular/project-front/dist/project-front/browser/* "$DES
|
||||
## Встраивание в Go-сервис
|
||||
|
||||
Вариант А: раздача файлов из папки (`back/cmd/public`).
|
||||
|
||||
- Положите файлы фронта в `back/cmd/public`.
|
||||
- Убедитесь, что роутер отдаёт `/` и `/*` из этой папки.
|
||||
|
||||
Вариант Б: `embed.FS` в бинарник.
|
||||
|
||||
Пример (концептуально):
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
@@ -128,6 +136,7 @@ func main() {
|
||||
Endpoint: `POST /api/v1/knock-actions/execute`
|
||||
|
||||
Request JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"targets": "tcp:127.0.0.1:22;udp:1.2.3.4:53",
|
||||
@@ -137,11 +146,13 @@ Request JSON:
|
||||
```
|
||||
|
||||
Response 200:
|
||||
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
Пример curl:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/knock-actions/execute \
|
||||
-H 'Content-Type: application/json' \
|
||||
@@ -150,17 +161,19 @@ curl -X POST http://localhost:8080/api/v1/knock-actions/execute \
|
||||
|
||||
## Запуск и проверка
|
||||
|
||||
1) Собрать фронт и скопировать файлы:
|
||||
1 Собрать фронт и скопировать файлы:
|
||||
|
||||
```bash
|
||||
cd /home/su/projects/articles/embed-gui-article/ui
|
||||
./build-for-embeding.sh /home/su/projects/articles/embed-gui-article/back/cmd/public
|
||||
```
|
||||
|
||||
2) Запустить бэкенд (варианты):
|
||||
2 Запустить бэкенд (варианты):
|
||||
|
||||
- Через make/скрипт, если есть.
|
||||
- Локально `go run ./back` (или соответствующая команда в вашем проекте).
|
||||
|
||||
3) Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API.
|
||||
3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API.
|
||||
|
||||
## FAQ и типичные ошибки
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,7 @@ func setupKnockRoutes(api *gin.RouterGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := knocker.ExecuteWithConfig(&config, req.Verbose, req.WaitConnection); err != nil {
|
||||
if err := knocker.ExecuteWithConfig(&config, true || req.Verbose, req.WaitConnection); err != nil {
|
||||
c.JSON(400, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"port-knocker/cmd"
|
||||
)
|
||||
|
||||
// Version и BuildTime устанавливаются при сборке через ldflags
|
||||
|
||||
var (
|
||||
Version = "v1.0.10"
|
||||
BuildTime = "unknown"
|
||||
|
@@ -112,6 +112,7 @@ git push origin main
|
||||
# Сборка бинарников
|
||||
log_info "Собираем бинарники для всех платформ..."
|
||||
export VERSION_NUM="${VERSION#v}"
|
||||
# shellcheck disable=SC2155
|
||||
export BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
|
||||
|
||||
# Функция сборки для платформы
|
||||
@@ -155,6 +156,7 @@ log_info "Создаем Git тег..."
|
||||
# Читаем release-notes.md и сохраняем содержимое в переменную NOTES
|
||||
NOTES=$(cat docs/scripts/release-notes.md)
|
||||
# Заменяем все переменные вида $VERSION в NOTES на их значения
|
||||
# shellcheck disable=SC2001
|
||||
NOTES=$(echo "$NOTES" | sed "s/\\\$VERSION/$VERSION/g")
|
||||
|
||||
git tag -a "$VERSION" -m "$NOTES"
|
||||
|
BIN
knocker-serve
BIN
knocker-serve
Binary file not shown.
@@ -1,9 +1,8 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { BasicKnockPageComponent } from './basic-knock/basic-knock-page.component';
|
||||
import { FsaKnockPageComponent } from './fsa-knock/fsa-knock-page.component';
|
||||
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', component: BasicKnockPageComponent },
|
||||
{ path: 'fsa', component: FsaKnockPageComponent },
|
||||
{ path: '**', redirectTo: '' }
|
||||
];
|
@@ -15,7 +15,7 @@ import { KnockPageComponent } from '../knock/knock-page.component';
|
||||
template: `
|
||||
<div class="container">
|
||||
<!-- Встраиваем основной компонент в базовом режиме -->
|
||||
<app-knock-page [enableFSA]="false" [canUseFSA]="canUseFSA"></app-knock-page>
|
||||
<app-knock-page></app-knock-page>
|
||||
</div>
|
||||
|
||||
<!-- Информационное модальное окно -->
|
||||
|
@@ -1,132 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CardModule } from 'primeng/card';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { DialogModule } from 'primeng/dialog';
|
||||
import { KnockPageComponent } from '../knock/knock-page.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-fsa-knock-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule, RouterModule, CardModule, ButtonModule, DialogModule, KnockPageComponent
|
||||
],
|
||||
template: `
|
||||
<div class="container">
|
||||
<div *ngIf="!isFSASupported" class="text-center">
|
||||
<h3>File System Access API не поддерживается</h3>
|
||||
<p>Эта функциональность требует браузер с поддержкой File System Access API:</p>
|
||||
<ul class="text-left mt-3">
|
||||
<li>Google Chrome 86+</li>
|
||||
<li>Microsoft Edge 86+</li>
|
||||
<li>Opera 72+</li>
|
||||
</ul>
|
||||
<p class="mt-3">Ваш браузер: <strong>{{ browserInfo }}</strong></p>
|
||||
<button pButton
|
||||
type="button"
|
||||
label="Перейти к основной версии"
|
||||
class="p-button-outlined mt-3"
|
||||
routerLink="/">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isFSASupported">
|
||||
<!-- Встраиваем основной компонент с поддержкой FSA -->
|
||||
<app-knock-page [enableFSA]="true" [canUseFSA]="true"></app-knock-page>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информационное модальное окно -->
|
||||
<p-dialog header="🚀 Расширенная версия с File System Access"
|
||||
[(visible)]="showInfoDialog"
|
||||
[modal]="true"
|
||||
[closable]="true"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
styleClass="info-dialog">
|
||||
<div class="dialog-content">
|
||||
<p class="mb-3">
|
||||
Эта версия поддерживает прямое редактирование файлов на диске.
|
||||
Файлы будут автоматически перезаписываться после шифрования/дешифрования.
|
||||
</p>
|
||||
<div class="p-3 bg-green-50 border-round">
|
||||
<p class="text-sm mb-2">
|
||||
✅ <strong>Доступные возможности:</strong>
|
||||
</p>
|
||||
<ul class="text-sm mb-0">
|
||||
<li>Прямое открытие файлов с диска</li>
|
||||
<li>Автоматическое сохранение изменений</li>
|
||||
<li>Перезапись зашифрованных файлов "на месте"</li>
|
||||
<li>Быстрая работа без диалогов загрузки/скачивания</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</p-dialog>
|
||||
`,
|
||||
styles: [`
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-link {
|
||||
color: #3b82f6;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.info-link:hover {
|
||||
color: #1d4ed8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.bg-green-50 {
|
||||
background-color: #f0fdf4;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
min-width: 450px;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class FsaKnockPageComponent {
|
||||
isFSASupported = false;
|
||||
browserInfo = '';
|
||||
showInfoDialog = false;
|
||||
|
||||
constructor() {
|
||||
this.checkFSASupport();
|
||||
this.getBrowserInfo();
|
||||
}
|
||||
|
||||
private checkFSASupport() {
|
||||
const w = window as any;
|
||||
this.isFSASupported = typeof w.showOpenFilePicker === 'function';
|
||||
}
|
||||
|
||||
private getBrowserInfo() {
|
||||
const ua = navigator.userAgent;
|
||||
if (ua.includes('Chrome') && !ua.includes('Edg/')) {
|
||||
this.browserInfo = 'Google Chrome';
|
||||
} else if (ua.includes('Edg/')) {
|
||||
this.browserInfo = 'Microsoft Edge';
|
||||
} else if (ua.includes('Opera') || ua.includes('OPR/')) {
|
||||
this.browserInfo = 'Opera';
|
||||
} else if (ua.includes('Firefox')) {
|
||||
this.browserInfo = 'Mozilla Firefox';
|
||||
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
||||
this.browserInfo = 'Safari';
|
||||
} else {
|
||||
this.browserInfo = 'Неизвестный браузер';
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,5 +1,5 @@
|
||||
<div class="container">
|
||||
<p-card header="Port Knocker (Минимальный UI)">
|
||||
<p-card header="Port Knocker (Minimal UI)">
|
||||
<form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid">
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
@@ -8,7 +8,7 @@
|
||||
pInputText
|
||||
type="text"
|
||||
formControlName="targets"
|
||||
placeholder="tcp:host:port;udp:host:port"
|
||||
placeholder="tcp:host:port;udp:host:port;...;tcp:host:port"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user