diff --git a/Makefile b/Makefile
index ef62d66..49f02d5 100644
--- a/Makefile
+++ b/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 {+-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'
diff --git a/back/cmd/knock_routes.go b/back/cmd/knock_routes.go
index 1b30229..6596def 100644
--- a/back/cmd/knock_routes.go
+++ b/back/cmd/knock_routes.go
@@ -58,7 +58,7 @@ func setupKnockRoutes(api *gin.RouterGroup) {
				c.JSON(400, gin.H{"error": "targets is required in inline mode"})
				return
			}
			config, err := parseInlineTargetsWithWait(req.Targets, req.Delay, [-req.WaitConnection)-]{+req.WaitConnection, req.Gateway)+}
			if err != nil {
				c.JSON(400, gin.H{"error": fmt.Sprintf("invalid targets: %v", err)})
				return
@@ -71,7 +71,7 @@ func setupKnockRoutes(api *gin.RouterGroup) {
				}
			}

			if err := knocker.ExecuteWithConfig(&config, {+true ||+} req.Verbose, req.WaitConnection); err != nil {			
				c.JSON(400, gin.H{"error": err.Error()})
				return
			}
@@ -81,7 +81,7 @@ func setupKnockRoutes(api *gin.RouterGroup) {
}

// parseInlineTargetsWithWait парсит inline строку целей в Config с поддержкой waitConnection
func parseInlineTargetsWithWait(targets, delay string, waitConnection [-bool)-]{+bool, gateway string)+} (internal.Config, error) {
	var config internal.Config

	// Парсим targets
@@ -93,13 +93,18 @@ func parseInlineTargetsWithWait(targets, delay string, waitConnection bool) (int
		}

		parts := strings.Split(targetStr, ":")
		if [-len(parts) !=-]{+!(len(parts) ==+} 3 {+|| len(parts) == 4)+} {
			return config, fmt.Errorf("invalid target format: %s (expected [-protocol:host:port)",-]{+protocol:host:port or protocol:host:port:gateway)",+} targetStr)
		}

		protocol := strings.TrimSpace(parts[0])
		host := strings.TrimSpace(parts[1])
		portStr := strings.TrimSpace(parts[2])
		
		
		{+if len(parts) == 4 {+}
{+			gateway = strings.TrimSpace(parts[3])+}
{+		}+}

		if protocol != "tcp" && protocol != "udp" {
			return config, fmt.Errorf("unsupported protocol: %s (only tcp/udp supported)", protocol)
@@ -128,6 +133,7 @@ func parseInlineTargetsWithWait(targets, delay string, waitConnection bool) (int
			Ports:          []int{port},
			Delay:          targetDelay,
			WaitConnection: waitConnection,
			{+Gateway:        gateway,+}
		}

		config.Targets = append(config.Targets, target)
diff --git a/back/go.mod b/back/go.mod
index eb15378..c4a9a82 100644
--- a/back/go.mod
+++ b/back/go.mod
@@ -6,6 +6,7 @@ require (
	github.com/gin-contrib/cors v1.7.2
	github.com/gin-gonic/gin v1.10.0
	github.com/spf13/cobra v1.8.0
	{+golang.org/x/sys v0.20.0+}
	gopkg.in/yaml.v3 v3.0.1
)

@@ -35,7 +36,6 @@ require (
	golang.org/x/arch v0.8.0 // indirect
	golang.org/x/crypto v0.23.0 // indirect
	golang.org/x/net v0.25.0 // indirect
[-	golang.org/x/sys v0.20.0 // indirect-]
	golang.org/x/text v0.15.0 // indirect
	google.golang.org/protobuf v1.34.1 // indirect
)
diff --git a/back/internal/knocker.go b/back/internal/knocker.go
index b946717..2aa66c4 100644
--- a/back/internal/knocker.go
+++ b/back/internal/knocker.go
@@ -10,9 +10,12 @@ import (
	"math/rand"
	"net"
	"os"
	{+"regexp"+}
	"strings"
	{+"syscall"+}
	"time"

	{+"golang.org/x/sys/unix"+}
	"gopkg.in/yaml.v3"
)

@@ -278,14 +281,18 @@ func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
	}

	// Вычисляем таймаут как половину интервала между пакетами
	timeout := [-time.Duration(target.Delay) / 2-]
[-	if timeout < 100*time.Millisecond {-]
[-		timeout = 100 * time.Millisecond-]{+max(time.Duration(target.Delay)/2,+}
		// минимальный таймаут
		[-}-]{+100*time.Millisecond)+}

	for i, port := range target.Ports {
		if verbose {
			{+if target.Gateway != "" {+}
{+				fmt.Printf("  Отправка пакета на %s:%d (%s) через шлюз %s\n", target.Host, port, protocol, target.Gateway)+}
{+			} else {+}
				fmt.Printf("  Отправка пакета на %s:%d (%s)\n", target.Host, port, protocol)
			{+}+}

		}

		if err := pk.sendPacket(target.Host, port, protocol, target.WaitConnection, timeout, target.Gateway); err != nil {
@@ -314,7 +321,8 @@ func (pk *PortKnocker) knockTarget(target Target, verbose bool) error {
}

// sendPacket отправляет один пакет на указанный хост и порт
{+// sendPacket_backup — резервная копия прежней реализации+}
func (pk *PortKnocker) [-sendPacket(host-]{+SendPacket_backup(host+} string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error {
	address := net.JoinHostPort(host, fmt.Sprintf("%d", port))

	var conn net.Conn
@@ -381,8 +389,8 @@ func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitCo
	return nil
}

// [-sendPacketWithoutConnection отправляет пакет без установления соединения-]{+sendPacketWithoutConnection_backup — резервная копия прежней реализации+}
func (pk *PortKnocker) [-sendPacketWithoutConnection(host-]{+SendPacketWithoutConnection_backup(host+} string, port int, protocol string, localAddr net.Addr) error {
	address := net.JoinHostPort(host, fmt.Sprintf("%d", port))

	switch protocol {
@@ -440,6 +448,155 @@ func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protoc
	return nil
}

{+// parseGateway возвращает локальные адреса для TCP/UDP и (опционально) имя интерфейса,+}
{+// если в gateway передано, например, "eth1". Также поддерживается IP[:port].+}
{+func parseGateway(gateway string) (tcpLocal *net.TCPAddr, udpLocal *net.UDPAddr, ifaceName string, err error) {+}
{+	gateway = strings.TrimSpace(gateway)+}
{+	if gateway == "" {+}
{+		return nil, nil, "", nil+}
{+	}+}

{+	// Если это похоже на имя интерфейса (буквы/цифры/дефисы, без точного IP)+}
{+	isIfaceName := regexp.MustCompile(`^[A-Za-z0-9_-]+$`).MatchString(gateway) && net.ParseIP(gateway) == nil+}
{+	if isIfaceName {+}
{+		// Привязка по интерфейсу. LocalAddr оставим пустым — маршрут выберется ядром,+}
{+		// а SO_BINDTODEVICE закрепит интерфейс.+}
{+		return nil, nil, gateway, nil+}
{+	}+}

{+	// Иначе трактуем как IP[:port]+}
{+	host := gateway+}
{+	if !strings.Contains(gateway, ":") {+}
{+		host = gateway + ":0"+}
{+	}+}

{+	tcpLocal, err = net.ResolveTCPAddr("tcp", host)+}
{+	if err != nil {+}
{+		return nil, nil, "", fmt.Errorf("не удалось разрешить локальный TCP адрес %s: %w", host, err)+}
{+	}+}
{+	udpLocal, err = net.ResolveUDPAddr("udp", host)+}
{+	if err != nil {+}
{+		return nil, nil, "", fmt.Errorf("не удалось разрешить локальный UDP адрес %s: %w", host, err)+}
{+	}+}
{+	return tcpLocal, udpLocal, "", nil+}
{+}+}

{+// buildDialer создаёт net.Dialer с опциональной привязкой к LocalAddr и интерфейсу (Linux SO_BINDTODEVICE)+}
{+func buildDialer(protocol string, tcpLocal *net.TCPAddr, udpLocal *net.UDPAddr, timeout time.Duration, ifaceName string) *net.Dialer {+}
{+	d := &net.Dialer{Timeout: timeout}+}
{+	if protocol == "tcp" && tcpLocal != nil {+}
{+		d.LocalAddr = tcpLocal+}
{+	}+}
{+	if protocol == "udp" && udpLocal != nil {+}
{+		d.LocalAddr = udpLocal+}
{+	}+}
{+	if strings.TrimSpace(ifaceName) != "" {+}
{+		d.Control = func(network, address string, c syscall.RawConn) error {+}
{+			var ctrlErr error+}
{+			err := c.Control(func(fd uintptr) {+}
{+				// Привязка сокета к интерфейсу по имени+}
{+				ctrlErr = unix.SetsockoptString(int(fd), unix.SOL_SOCKET, unix.SO_BINDTODEVICE, ifaceName)+}
{+			})+}
{+			if err != nil {+}
{+				return err+}
{+			}+}
{+			return ctrlErr+}
{+		}+}
{+	}+}
{+	return d+}
{+}+}

{+// sendPacket — обновлённая реализация с поддержкой UDPAddr и SO_BINDTODEVICE+}
{+func (pk *PortKnocker) sendPacket(host string, port int, protocol string, waitConnection bool, timeout time.Duration, gateway string) error {+}
{+	address := net.JoinHostPort(host, fmt.Sprintf("%d", port))+}

{+	tcpLocal, udpLocal, ifaceName, err := parseGateway(gateway)+}
{+	if err != nil {+}
{+		return err+}
{+	}+}

{+	switch protocol {+}
{+	case "tcp":+}
{+		dialer := buildDialer("tcp", tcpLocal, nil, timeout, ifaceName)+}
{+		conn, err := dialer.Dial("tcp", address)+}
{+		if err != nil {+}
{+			if waitConnection {+}
{+				return fmt.Errorf("не удалось подключиться к %s: %w", address, err)+}
{+			}+}
{+			// без ожидания — пробуем best-effort отправку+}
{+			return pk.sendPacketWithoutConnection(host, port, protocol, dialer.LocalAddr)+}
{+		}+}
{+		defer conn.Close()+}
{+		_, err = conn.Write([]byte{})+}
{+		if err != nil {+}
{+			return fmt.Errorf("не удалось отправить пакет: %w", err)+}
{+		}+}
{+		return nil+}

{+	case "udp":+}
{+		dialer := buildDialer("udp", nil, udpLocal, timeout, ifaceName)+}
{+		conn, err := dialer.Dial("udp", address)+}
{+		if err != nil {+}
{+			if waitConnection {+}
{+				return fmt.Errorf("не удалось подключиться к %s: %w", address, err)+}
{+			}+}
{+			return pk.sendPacketWithoutConnection(host, port, protocol, dialer.LocalAddr)+}
{+		}+}
{+		defer conn.Close()+}
{+		_, err = conn.Write([]byte{})+}
{+		if err != nil {+}
{+			return fmt.Errorf("не удалось отправить пакет: %w", err)+}
{+		}+}
{+		return nil+}
{+	default:+}
{+		return fmt.Errorf("неподдерживаемый протокол: %s", protocol)+}
{+	}+}
{+}+}

{+// sendPacketWithoutConnection — обновлённая реализация best-effort без ожидания соединения+}
{+func (pk *PortKnocker) sendPacketWithoutConnection(host string, port int, protocol string, localAddr net.Addr) error {+}
{+	address := net.JoinHostPort(host, fmt.Sprintf("%d", port))+}

{+	switch protocol {+}
{+	case "udp":+}
{+		// Используем Dialer с коротким таймаутом, локальный адрес может быть *net.UDPAddr+}
{+		var udpLocal *net.UDPAddr+}
{+		if la, ok := localAddr.(*net.UDPAddr); ok {+}
{+			udpLocal = la+}
{+		}+}
{+		d := &net.Dialer{Timeout: 200 * time.Millisecond}+}
{+		if udpLocal != nil {+}
{+			d.LocalAddr = udpLocal+}
{+		}+}
{+		conn, err := d.Dial("udp", address)+}
{+		if err != nil {+}
{+			return nil+}
{+		} // best-effort+}
{+		defer conn.Close()+}
{+		_, err = conn.Write([]byte{})+}
{+		if err != nil {+}
{+			return nil+}
{+		}+}
{+	case "tcp":+}
{+		// Короткий таймаут и игнор ошибок+}
{+		var tcpLocal *net.TCPAddr+}
{+		if la, ok := localAddr.(*net.TCPAddr); ok {+}
{+			tcpLocal = la+}
{+		}+}
{+		d := &net.Dialer{Timeout: 100 * time.Millisecond}+}
{+		if tcpLocal != nil {+}
{+			d.LocalAddr = tcpLocal+}
{+		}+}
{+		conn, err := d.Dial("tcp", address)+}
{+		if err != nil {+}
{+			return nil+}
{+		}+}
{+		defer conn.Close()+}
{+		_, _ = conn.Write([]byte{})+}
{+	}+}
{+	return nil+}
{+}+}

// showEasterEgg показывает забавный ASCII-арт
func (pk *PortKnocker) showEasterEgg() {
	fmt.Println("\n🎯 🎯 🎯  EASTER EGG ACTIVATED! 🎯 🎯 🎯")
diff --git a/back/main.go b/back/main.go
index 845fa06..d06f9fe 100644
--- a/back/main.go
+++ b/back/main.go
@@ -7,7 +7,7 @@ import (
	"port-knocker/cmd"
)

[-// Version и BuildTime устанавливаются при сборке через ldflags-]
var (
	Version   = "v1.0.10"
	BuildTime = "unknown"
diff --git a/back/scripts/quick-release.sh b/back/scripts/quick-release.sh
index 8024937..0cb3133 100755
--- a/back/scripts/quick-release.sh
+++ b/back/scripts/quick-release.sh
@@ -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"
diff --git a/knocker-serve b/knocker-serve
deleted file mode 100755
index c959979..0000000
Binary files a/knocker-serve and /dev/null differ
diff --git a/ui/src/app/app.routes.ts b/ui/src/app/app.routes.ts
index 69181c5..6599deb 100644
--- a/ui/src/app/app.routes.ts
+++ b/ui/src/app/app.routes.ts
@@ -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: '' }
];
diff --git a/ui/src/app/basic-knock/basic-knock-page.component.ts b/ui/src/app/basic-knock/basic-knock-page.component.ts
index f2be37d..cabf2c5 100644
--- a/ui/src/app/basic-knock/basic-knock-page.component.ts
+++ b/ui/src/app/basic-knock/basic-knock-page.component.ts
@@ -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>

    <!-- Информационное модальное окно -->
diff --git a/ui/src/app/fsa-knock/fsa-knock-page.component.ts b/ui/src/app/fsa-knock/fsa-knock-page.component.ts
deleted file mode 100644
index 00c1c44..0000000
--- a/ui/src/app/fsa-knock/fsa-knock-page.component.ts
+++ /dev/null
@@ -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 = 'Неизвестный браузер';-]
[-    }-]
[-  }-]
[-}-]
diff --git a/ui/src/app/knock/knock-page.component.html b/ui/src/app/knock/knock-page.component.html
index 07ca8f5..c36bfda 100644
--- a/ui/src/app/knock/knock-page.component.html
+++ b/ui/src/app/knock/knock-page.component.html
@@ -1,76 +1,19 @@
<div class="container">
  <p-card [-[header]="cardHeader">-]
[-    <ng-template pTemplate="header">-]
[-      <div class="flex justify-content-between align-items-center">-]
[-        <h1 style="margin-left: 1rem">Port Knocker</h1>-]
[-        <!-- <div class="animated-title" [class.animating]="isAnimating">-]
[-            <span *ngIf="cardHeader">{{ cardHeader }}</span>-]
[-          </div> -->-]
[-        <div class="flex gap-2">-]
[-          <button-]
[-            *ngIf="!enableFSA"-]
[-            pButton-]
[-            type="button"-]
[-            label="📁 Info"-]
[-            class="p-button-text p-button-sm"-]
[-            (click)="showInfoDialog = true"-]
[-          ></button>-]
[-          <button-]
[-            *ngIf="canUseFSA && !enableFSA"-]
[-            pButton-]
[-            type="button"-]
[-            label="🚀 FSA Version"-]
[-            class="p-button-text p-button-sm"-]
[-            routerLink="/fsa"-]
[-          ></button>-]
[-        </div>-]
[-      </div>-]
[-    </ng-template>-]{+header="Port Knocker (Minimal UI)">+}
    <form [formGroup]="form" (ngSubmit)="execute()" class="p-fluid">
      <div class="grid">
        <div [-class="col-12 md:col-6">-]
[-          <label>Password</label>-]
[-          <p-password-]
[-            formControlName="password"-]
[-            [feedback]="false"-]
[-            toggleMask-]
[-            inputStyleClass="w-full"-]
[-            placeholder="GO_KNOCKER_SERVE_PASS"-]
[-          ></p-password>-]
[-          <div class="mt-1 text-sm" *ngIf="!form.value.password || wrongPass">-]
[-            <span class="text-red-500" *ngIf="wrongPass">Invalid password</span>-]
[-            <span class="text-600" *ngIf="!wrongPass && !form.value.password"-]
[-              >Password is required</span-]
[-            >-]
[-          </div>-]
[-        </div>-]

[-        <div class="col-12 md:col-6">-]
[-          <label>Mode</label>-]
[-          <p-dropdown-]
[-            formControlName="mode"-]
[-            [options]="[-]
[-              { label: 'Inline', value: 'inline' },-]
[-              { label: 'YAML', value: 'yaml' }-]
[-            ]"-]
[-            optionLabel="label"-]
[-            optionValue="value"-]
[-            class="w-full"-]
[-          ></p-dropdown>-]
[-        </div>-]

[-        <div class="col-12" *ngIf="form.value.mode === 'inline'">-]{+class="col-12">+}
          <label>Targets</label>
          <input
            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>

        <div class="col-12 [-md:col-4" *ngIf="form.value.mode === 'inline'">-]{+md:col-6">+}
          <label>Delay</label>
          <input
            pInputText
@@ -81,83 +24,11 @@
          />
        </div>

        <div [-class="col-6 md:col-4 flex align-items-center gap-2">-]
[-          <p-checkbox formControlName="verbose" [binary]="true"></p-checkbox>-]
[-          <label class="checkbox-label">Verbose</label>-]
[-        </div>-]

[-        <div class="col-6 md:col-4-]{+class="col-12 md:col-6+} flex align-items-center gap-2">
          <p-checkbox formControlName="waitConnection" [-[binary]="true"-]
[-          ></p-checkbox>-]{+[binary]="true"></p-checkbox>+}
          <label class="checkbox-label">Wait connection</label>
        </div>

[-        <div class="col-12">-]
[-          <label>Gateway</label>-]
[-          <input-]
[-            pInputText-]
[-            type="text"-]
[-            formControlName="gateway"-]
[-            placeholder="optional local ip:port"-]
[-            class="w-full"-]
[-          />-]
[-        </div>-]

[-        <div class="col-12" *ngIf="form.value.mode === 'yaml'">-]
[-          <label>YAML</label>-]
[-          <textarea-]
[-            pInputTextarea-]
[-            formControlName="configYAML"-]
[-            rows="12"-]
[-            placeholder="paste YAML or ENCRYPTED:"-]
[-            class="w-full"-]
[-          ></textarea>-]
[-        </div>-]

[-        <!-- File controls directly under YAML -->-]
[-        <div class="col-12" *ngIf="form.value.mode === 'yaml'">-]
[-          <div class="flex flex-wrap gap-2 align-items-center">-]
[-            <!-- FSA version -->-]
[-            <button-]
[-              *ngIf="enableFSA"-]
[-              pButton-]
[-              type="button"-]
[-              label="Open File (with write access)"-]
[-              (click)="openFileWithWriteAccess()"-]
[-              class="p-button-outlined"-]
[-            ></button>-]
[-            <span-]
[-              *ngIf="enableFSA && selectedFileName"-]
[-              class="text-sm text-600"-]
[-              >{{ selectedFileName }}</span-]
[-            >-]

[-            <!-- Basic version -->-]
[-            <p-fileUpload-]
[-              *ngIf="!enableFSA"-]
[-              mode="basic"-]
[-              name="file"-]
[-              chooseLabel="Choose File"-]
[-              (onSelect)="onFileUpload($event)"-]
[-              [customUpload]="true"-]
[-              [auto]="false"-]
[-              accept=".yaml,.yml,.txt,.encrypted"-]
[-              [maxFileSize]="1048576"-]
[-            ></p-fileUpload>-]
[-            <input-]
[-              *ngIf="!enableFSA && !isYamlEncrypted()"-]
[-              pInputText-]
[-              type="text"-]
[-              class="w-full md:w-6"-]
[-              placeholder="Server file path (optional)"-]
[-              formControlName="serverFilePath"-]
[-            />-]
[-          </div>-]
[-        </div>-]

[-        <!-- Row 1: Execute full width -->-]
        <div class="col-12">
          <button
            pButton
@@ -165,58 +36,7 @@
            label="Execute"
            class="w-full"
            [loading]="executing"
            [disabled]="executing || [-!form.value.password || wrongPass"-]
[-            [ngClass]="{ 'p-button-danger': !form.value.password || wrongPass }"-]
[-          ></button>-]
[-        </div>-]
[-        <!-- Row 2: Encrypt / Decrypt half/half on desktop; stacked on mobile (только для YAML режима) -->-]
[-        <div class="col-12 md:col-6" *ngIf="!isInlineMode()">-]
[-          <button-]
[-            pButton-]
[-            type="button"-]
[-            label="Encrypt"-]
[-            (click)="encrypt()"-]
[-            class="p-button-secondary w-full"-]
[-            [disabled]="-]
[-              executing ||-]
[-              !form.value.password ||-]
[-              wrongPass ||-]
[-              isYamlEncrypted()-]
[-            "-]
[-          ></button>-]
[-        </div>-]
[-        <div class="col-12 md:col-6" *ngIf="!isInlineMode()">-]
[-          <button-]
[-            pButton-]
[-            type="button"-]
[-            label="Decrypt"-]
[-            (click)="decrypt()"-]
[-            class="p-button-secondary w-full"-]
[-            [disabled]="-]
[-              executing ||-]
[-              !form.value.password ||-]
[-              wrongPass ||-]
[-              !isYamlEncrypted()-]
[-            "-]
[-          ></button>-]
[-        </div>-]
[-        <!-- Row 3: Download actions half/half on desktop; stacked on mobile (только для YAML режима) -->-]
[-        <div class="col-12 md:col-6" *ngIf="!isInlineMode()">-]
[-          <button-]
[-            pButton-]
[-            type="button"-]
[-            label="Download YAML"-]
[-            (click)="downloadYaml()"-]
[-            class="p-button-text w-full"-]
[-          ></button>-]
[-        </div>-]
[-        <div class="col-12 md:col-6" *ngIf="!isInlineMode()">-]
[-          <button-]
[-            pButton-]
[-            type="button"-]
[-            label="Download Result"-]
[-            (click)="downloadResult()"-]
[-            class="p-button-text w-full"-]{+form.invalid"+}
          ></button>
        </div>
      </div>
@@ -231,87 +51,15 @@
          [mode]="executing ? 'indeterminate' : 'determinate'"
        ></p-progressBar>
        <div class="mt-2 text-600" *ngIf="executing">
          Elapsed: {{ elapsedMs / 1000 | number : [-"1.1-1"-]{+'1.1-1'+} }}s
        </div>
        <div class="mt-2 text-600" *ngIf="!executing && [-elapsedMs > 0">-]
[-          Last run:-]{+result">+}
          {{ [-elapsedMs / 1000 | number : "1.1-1" }}s-]
[-          <span *ngIf="lastRunTime" class="ml-2 text-500">-]
[-            ({{ lastRunTime | date : "short" }})-]
[-          </span>-]{+result }}+}
{+        </div>+}
{+        <div class="mt-2 text-red-600" *ngIf="!executing && error">+}
{+          {{ error }}+}
        </div>
      </div>
    </div>
  </p-card>
</div>

[-<!-- Модальное окно с результатом выполнения -->-]
[-<p-dialog-]
[-  header="Результат выполнения"-]
[-  [(visible)]="showResultDialog"-]
[-  [modal]="true"-]
[-  [closable]="true"-]
[-  [draggable]="false"-]
[-  [resizable]="false"-]
[-  styleClass="result-dialog"-]
[->-]
[-  <div class="dialog-content">-]
[-    <div *ngIf="result" class="mb-3">-]
[-      <h4 class="text-green-600 mb-2">✅ Успешно выполнено</h4>-]
[-      <pre class="bg-gray-50 p-3 border-round text-sm">{{ result }}</pre>-]
[-    </div>-]
[-    <div *ngIf="error" class="mb-3">-]
[-      <h4 class="text-red-600 mb-2">❌ Ошибка</h4>-]
[-      <pre class="bg-red-50 p-3 border-round text-sm text-red-700">{{-]
[-        error-]
[-      }}</pre>-]
[-    </div>-]
[-    <div *ngIf="lastRunTime" class="text-sm text-600">-]
[-      Время выполнения: {{ elapsedMs / 1000 | number : "1.1-1" }}s-]
[-      <br />-]
[-      Завершено: {{ lastRunTime | date : "short" }}-]
[-    </div>-]
[-  </div>-]
[-  <ng-template pTemplate="footer">-]
[-    <button-]
[-      pButton-]
[-      type="button"-]
[-      label="OK"-]
[-      class="p-button-primary"-]
[-      (click)="showResultDialog = false"-]
[-    ></button>-]
[-  </ng-template>-]
[-</p-dialog>-]

[-<!-- Информационное модальное окно -->-]
[-<p-dialog-]
[-  header="📁 Базовая версия"-]
[-  [(visible)]="showInfoDialog"-]
[-  [modal]="true"-]
[-  [closable]="true"-]
[-  [draggable]="false"-]
[-  [resizable]="false"-]
[-  styleClass="info-dialog"-]
[->-]
[-  <div class="dialog-content">-]
[-    <p class="mb-3">-]
[-      Эта версия работает в любом браузере, но файлы загружаются/скачиваются-]
[-      через стандартные диалоги браузера.-]
[-    </p>-]
[-    <div *ngIf="canUseFSA" class="p-3 bg-blue-50 border-round">-]
[-      <p class="text-sm mb-2">-]
[-        💡 <strong>Доступна расширенная версия!</strong>-]
[-      </p>-]
[-      <p class="text-sm mb-3">-]
[-        Ваш браузер поддерживает прямое редактирование файлов на диске.-]
[-      </p>-]
[-      <button-]
[-        pButton-]
[-        type="button"-]
[-        label="Перейти к расширенной версии"-]
[-        class="p-button-success p-button-sm"-]
[-        routerLink="/fsa"-]
[-        (click)="showInfoDialog = false"-]
[-      ></button>-]
[-    </div>-]
[-  </div>-]
[-</p-dialog>-]
diff --git a/ui/src/app/knock/knock-page.component.ts b/ui/src/app/knock/knock-page.component.ts
index bd7fd1d..37fb436 100644
--- a/ui/src/app/knock/knock-page.component.ts
+++ b/ui/src/app/knock/knock-page.component.ts
@@ -1,384 +1,81 @@
import { Component, [-inject, Input-]{+inject+} } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { FormBuilder, ReactiveFormsModule, [-Validators, FormsModule-]{+Validators+} } from '@angular/forms';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { InputTextModule } from 'primeng/inputtext';
[-import { PasswordModule } from 'primeng/password';-]
[-import { DropdownModule } from 'primeng/dropdown';-]
import { CheckboxModule } from 'primeng/checkbox';
[-import { InputTextareaModule } from 'primeng/inputtextarea';-]
import { ButtonModule } from 'primeng/button';
import { CardModule } from 'primeng/card';
import { DividerModule } from 'primeng/divider';
[-import { FileUploadModule } from 'primeng/fileupload';-]
import { ProgressBarModule } from 'primeng/progressbar';
[-import { DialogModule } from 'primeng/dialog';-]
[-import * as yaml from 'js-yaml';-]
[-import { environment } from '../../environments/environment';-]

@Component({
  selector: 'app-knock-page',
  standalone: true,
  imports: [
    CommonModule,
    RouterModule,
    ReactiveFormsModule,[-FormsModule,-]
    InputTextModule,[-PasswordModule, DropdownModule,-]
    CheckboxModule,[-InputTextareaModule,-]
    ButtonModule,
    CardModule,
    DividerModule,[-FileUploadModule,-]
    ProgressBarModule,[-DialogModule-]
  ],
  templateUrl: './knock-page.component.html',
  styleUrls: [-['./knock-page.component.scss']-]{+['./knock-page.component.scss'],+}
})
export class KnockPageComponent {
  private readonly http = inject(HttpClient);
  private readonly fb = inject(FormBuilder);

[-  @Input() enableFSA = false; // Включает File System Access API функциональность-]
[-  @Input() canUseFSA = false; // Доступна ли FSA версия-]

[-  // cardHeader = 'Port Knocker GUI';-]
[-  cardHeader = '';-]
[-  animatedTitle = 'Knock Knock Knock on the heaven\'s door ...';-]
[-  showInfoDialog = false;-]
[-  isAnimating = false;-]
[-  showResultDialog = false;-]

  executing = false;
  {+elapsedMs = 0;+}
  private timerId: any = null;
  private startTs = 0;
[-elapsedMs = 0;-]
[-  lastRunTime: Date | null = null;-]
[-  wrongPass = false;-]
[-  selectedFileName: string | null = null;-]
[-  private fileHandle: any = null; // FileSystemFileHandle-]
[-  private isSyncing = false;-]
  result: string | null = null;
  error: string | null = null;

  form = this.fb.group({
[-password: ['', Validators.required],-]
[-    mode: ['inline', Validators.required],-]    targets: [-['tcp:127.0.0.1:22'],-]{+['tcp:127.0.0.1:22', Validators.required],+}
    delay: [-['1s'],-]
[-    verbose: [true],-]{+['1s', Validators.required],+}
    waitConnection: [false],
[-    gateway: [''],-]
[-    configYAML: [''],-]
[-    serverFilePath: ['']-]
  });

[-  constructor() {-]
[-    // Загружаем сохраненное состояние из localStorage-]
[-    this.loadStateFromLocalStorage();-]
[-    -]
[-    // Запускаем анимацию заголовка-]
[-    this.startTitleAnimation();-]
[-    -]
[-    // Сбрасываем индикатор неверного пароля при изменении поля-]
[-    this.form.get('password')?.valueChanges.subscribe(() => {-]
[-      this.wrongPass = false;-]
[-    });-]

[-    // React on YAML text changes: extract path and sync to serverFilePath-]
[-    this.form.get('configYAML')?.valueChanges.subscribe((val) => {-]
[-      if (this.isSyncing) return;-]
[-      if (this.isInlineMode() || this.isYamlEncrypted()) return;-]
[-      try {-]
[-        const p = this.extractPathFromYaml(String(val ?? ''));-]
[-        const currentPath = this.form.value.serverFilePath || '';-]
[-        if (p && p !== currentPath) {-]
[-          this.isSyncing = true;-]
[-          this.form.patchValue({ serverFilePath: p });-]
[-          this.isSyncing = false;-]
[-        }-]
[-      } catch {}-]
[-    });-]

[-    // React on serverFilePath changes: update YAML path field-]
[-    this.form.get('serverFilePath')?.valueChanges.subscribe((newPath) => {-]
[-      if (this.isSyncing) return;-]
[-      this.onServerPathChange(newPath || '');-]
[-    });-]

[-    // Подписка на изменение режима для автоматического преобразования-]
[-    this.setupModeConversion();-]
[-    -]
[-    // Подписки на изменения полей для автосохранения в localStorage-]
[-    this.setupAutoSave();-]

[-    // File System Access API detection (для обратной совместимости)-]
[-    // Логика FSA теперь находится в отдельных компонентах-]
[-  }-]

[-  private authHeader(pass: string) {-]
[-    // Basic auth с пользователем "knocker"-]
[-    const token = btoa(`knocker:${pass}`);-]
[-    return { Authorization: `Basic ${token}` };-]
[-  }-]

  execute() {
    this.error = null;
    this.result = null;
[-    this.wrongPass = false;-]
    if (this.form.invalid) return;

    const v = this.form.value;
    const body: any = {
      targets: v.targets,
      delay: v.delay,
[-      verbose: v.verbose,-]
      waitConnection: v.waitConnection,
      [-gateway: v.gateway,-]{+verbose: true,+}
    };
[-if (v.mode === 'yaml') {-]
[-      body.config_yaml = v.configYAML;-]
[-      delete body.targets;-]
[-      delete body.delay;-]
[-    }-]
    this.executing = true;
    this.startTimer();

    
    this.http.post('/api/v1/knock-actions/execute', [-body, {-]
[-      headers: this.authHeader(v.password || '')-]
[-    }).subscribe({-]{+body).subscribe({+}
      next: () => {
        this.executing = false;
        this.stopTimer();
[-this.lastRunTime = new Date();-]        this.result = `Done in [-${(this.elapsedMs/1000).toFixed(2)}s`;-]
[-        this.showResultDialog = true;-]{+${(this.elapsedMs / 1000).toFixed(2)}s`;+}
      },
      error: (e: HttpErrorResponse) => {
        this.executing = false;
        this.stopTimer();
[-if (e.status === 401) {-]
[-          this.wrongPass = true;-]
[-        }-]        this.error = [-(e.error?.error)-]{+e.error?.error+} || e.message;[-this.showResultDialog = true;-]
[-      }-]
[-    });-]
[-  }-]

[-  encrypt() {-]
[-    this.error = null;-]
[-    this.result = null;-]
[-    const v = this.form.value;-]
[-    if (this.isInlineMode() || this.isYamlEncrypted() || !v.password || this.wrongPass) {-]
[-      return;-]
[-    }-]
[-    -]
[-    // Проверяем есть ли path в YAML самом  -]
[-    const pathFromYaml = this.getPathFromYaml(v.configYAML || '');-]
[-    const serverFilePath = (this.form.value.serverFilePath || '').trim();-]
[-    -]
[-    let url: string;-]
[-    let body: any;-]
[-    -]
[-    if (pathFromYaml) {-]
[-      // Если path в YAML - используем /encrypt, сервер сам найдет path в YAML-]
[-      url = '/api/v1/knock-actions/encrypt';-]
[-      body = { yaml: v.configYAML };-]
[-    } else if (serverFilePath) {-]
[-      // Если path только в serverFilePath - используем /encrypt-file-]
[-      url = '/api/v1/knock-actions/encrypt-file';-]
[-      body = { path: serverFilePath };-]
[-    } else {-]
[-      // Нет пути - обычное шифрование содержимого-]
[-      url = '/api/v1/knock-actions/encrypt';-]
[-      body = { yaml: v.configYAML };-]
[-    }-]
[-    -]
[-    this.http.post(url, body, {-]
[-      headers: this.authHeader(v.password || '')-]
[-    }).subscribe({-]
[-      next: async (res: any) => {-]
[-        const encrypted: string = res.encrypted || '';-]
[-        -]
[-        // Всегда обновляем YAML поле зашифрованным содержимым-]
[-        this.form.patchValue({ configYAML: encrypted });-]
[-        -]
[-        if (pathFromYaml) {-]
[-          this.result = `Encrypted (YAML path: ${pathFromYaml})`;-]
[-          // НЕ сохраняем файл клиентом - сервер уже записал по path из YAML-]
[-        } else if (serverFilePath) {-]
[-          this.result = `Encrypted (server path: ${serverFilePath})`;-]
[-          // НЕ сохраняем файл клиентом - сервер записал по serverFilePath-]
[-        } else {-]
[-          this.result = 'Encrypted';-]
[-          // Только сохраняем в файл если НЕТ серверного пути-]
[-          await this.saveBackToFileIfPossible(encrypted, this.selectedFileName);-]
[-        }-]
[-      },-]
[-      error: (e: HttpErrorResponse) => this.error = (e.error && e.error.error) || e.message-]
[-    });-]
[-  }-]

[-  decrypt() {-]
[-    this.error = null;-]
[-    this.result = null;-]
[-    const v = this.form.value;-]
[-    if (this.isInlineMode() || !this.isYamlEncrypted() || !v.password || this.wrongPass) {-]
[-      return;-]
[-    }-]
[-    -]
[-    // Для зашифрованного YAML поле serverFilePath недоступно - используем только decrypt-]
[-    const url = '/api/v1/knock-actions/decrypt';-]
[-    const body = { encrypted: v.configYAML as string };-]
[-    -]
[-    this.http.post(url, body, {-]
[-      headers: this.authHeader(v.password || '')-]
[-    }).subscribe({-]
[-      next: async (res: any) => {-]
[-        const plain: string = res.yaml || '';-]
[-        this.form.patchValue({ configYAML: plain });-]
[-        this.result = 'Decrypted';-]
[-        -]
[-        // Извлекаем path из расшифрованного YAML и обновляем serverFilePath-]
[-        const pathFromDecrypted = this.getPathFromYaml(plain);-]
[-        if (pathFromDecrypted) {-]
[-          this.isSyncing = true;-]
[-          this.form.patchValue({ serverFilePath: pathFromDecrypted });-]
[-          this.isSyncing = false;-]
[-          this.result += ` (found path: ${pathFromDecrypted})`;-]
[-        }-]
[-        -]
[-        // НЕ делаем download - сервер уже обработал файл согласно path в YAML-]
      },
[-      error: (e: HttpErrorResponse) => this.error = (e.error && e.error.error) || e.message-]
    });
  }

[-  onFileSelected(event: Event) {-]
[-    const input = event.target as HTMLInputElement;-]
[-    if (!input.files || input.files.length === 0) return;-]
[-    const file = input.files[0];-]
[-    this.selectedFileName = file.name;-]
[-    const reader = new FileReader();-]
[-    reader.onload = () => {-]
[-      const text = String(reader.result || '');-]
[-      this.form.patchValue({ configYAML: text, mode: 'yaml' });-]
[-      // Sync path from YAML into serverFilePath-]
[-      const p = this.extractPathFromYaml(text);-]
[-      if (p) {-]
[-        this.isSyncing = true;-]
[-        this.form.patchValue({ serverFilePath: p });-]
[-        this.isSyncing = false;-]
[-      }-]
[-    };-]
[-    reader.readAsText(file);-]
[-    this.fileHandle = null; // обычная загрузка не даёт handle на запись-]
[-  }-]

[-  downloadYaml() {-]
[-    const yaml = this.form.value.configYAML || '';-]
[-    this.triggerDownload('config.yaml', yaml);-]
[-  }-]

[-  downloadResult() {-]
[-    const content = this.result || this.form.value.configYAML || '';-]
[-    const name = (content || '').startsWith('ENCRYPTED:') ? 'config.encrypted' : 'config.yaml';-]
[-    this.triggerDownload(name, content);-]
[-  }-]

[-  onFileUpload(event: any) {-]
[-    const files: File[] = event?.files || event?.currentFiles || [];-]
[-    if (!files.length) return;-]
[-    const file = files[0];-]
[-    this.selectedFileName = file.name;-]
[-    const reader = new FileReader();-]
[-    reader.onload = () => {-]
[-      const text = String(reader.result || '');-]
[-      this.form.patchValue({ configYAML: text, mode: 'yaml' });-]
[-      const p = this.extractPathFromYaml(text);-]
[-      if (!p) {-]
[-        return;-]
[-      }-]
[-      this.isSyncing = true;-]
[-      this.form.patchValue({ serverFilePath: p });-]
[-      this.isSyncing = false;-]
[-    };-]
[-    reader.readAsText(file);-]
[-    this.fileHandle = null;-]
[-  }-]

[-  async openFileWithWriteAccess() {-]
[-    try {-]
[-      const w: any = window as any;-]
[-      if (!w || typeof w.showOpenFilePicker !== 'function') {-]
[-        this.error = 'File System Access API is not supported by this browser.';-]
[-        return;-]
[-      }-]
[-      const [handle] = await w.showOpenFilePicker({-]
[-        types: [{ description: 'YAML/Encrypted', accept: { 'text/plain': ['.yaml', '.yml', '.encrypted', '.txt'] } }]-]
[-      });-]
[-      this.fileHandle = handle;-]
[-      const file = await handle.getFile();-]
[-      this.selectedFileName = file.name;-]
[-      const text = await file.text();-]
[-      this.form.patchValue({ configYAML: text, mode: 'yaml' });-]
[-      const p = this.extractPathFromYaml(text);-]
[-      if (p) {-]
[-        this.isSyncing = true;-]
[-        this.form.patchValue({ serverFilePath: p });-]
[-        this.isSyncing = false;-]
[-      }-]
[-      this.result = `Opened: ${file.name}`;-]
[-      this.error = null;-]
[-    } catch (e: any) {-]
[-      // user cancelled or error-]
[-    }-]
[-  }-]

[-  // Helpers for UI state-]
[-  isInlineMode(): boolean {-]
[-    return (this.form.value.mode === 'inline');-]
[-  }-]
[-  isYamlEncrypted(): boolean {-]
[-    const s = (this.form.value.configYAML || '').toString().trim();-]
[-    return s.startsWith('ENCRYPTED:');-]
[-  }-]

[-  private triggerDownload(filename: string, text: string) {-]
[-    const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });-]
[-    const url = URL.createObjectURL(blob);-]
[-    const a = document.createElement('a');-]
[-    a.href = url;-]
[-    a.download = filename;-]
[-    a.click();-]
[-    URL.revokeObjectURL(url);-]
[-  }-]

[-  private async saveBackToFileIfPossible(content: string, filename: string | null) {-]
[-    try {-]
[-      const w: any = window as any;-]
[-      if (this.fileHandle && typeof this.fileHandle.createWritable === 'function') {-]
[-        const writable = await this.fileHandle.createWritable();-]
[-        await writable.write(content);-]
[-        await writable.close();-]
[-        return;-]
[-      }-]
[-      if (w && typeof w.showSaveFilePicker === 'function') {-]
[-        const handle = await w.showSaveFilePicker({-]
[-          suggestedName: filename || 'config.yaml',-]
[-          types: [{ description: 'YAML/Encrypted', accept: { 'text/plain': ['.yaml', '.yml', '.encrypted', '.txt'] } }]-]
[-        });-]
[-        const writable = await handle.createWritable();-]
[-        await writable.write(content);-]
[-        await writable.close();-]
[-        return;-]
[-      } else if (filename) {-]
[-        this.triggerDownload(filename, content);-]
[-      } else {-]
[-        this.triggerDownload('config.yaml', content);-]
[-      }-]
[-    } catch {-]
[-      if (filename) {-]
[-        this.triggerDownload(filename, content);-]
[-      } else {-]
[-        this.triggerDownload('config.yaml', content);-]
[-      }-]
[-    }-]
[-  }-]

  private startTimer() {
    this.elapsedMs = 0;
    this.startTs = Date.now();
@@ -401,380 +98,4 @@ export class KnockPageComponent {
      this.timerId = null;
    }
  }

[-  // YAML path helpers-]
[-  private getPathFromYaml(text: string): string {-]
[-    return this.extractPathFromYaml(text);-]
[-  }-]

[-  private extractPathFromYaml(text: string): string {-]
[-    try {-]
[-      const doc: any = yaml.load(text);-]
[-      if (doc && typeof doc === 'object' && typeof doc.path === 'string') {-]
[-        return doc.path;-]
[-      }-]
[-    } catch {}-]
[-    return '';-]
[-  }-]

[-  onServerPathChange(newPath: string) {-]
[-    if (this.isInlineMode() || this.isYamlEncrypted()) return;-]
[-    environment.log('onServerPathChange', newPath);-]
[-    const current = String(this.form.value.configYAML || '');-]
[-    try {-]
[-      const doc: any = current.trim() ? yaml.load(current) : {};-]
[-      if (!doc || typeof doc !== 'object') return;-]
[-      (doc as any).path = newPath || '';-]
[-      this.isSyncing = true;-]
[-      this.form.patchValue({ configYAML: yaml.dump(doc, { lineWidth: 120 }) }, { emitEvent: true });-]
[-      this.isSyncing = false;-]
[-    } catch {}-]
[-  }-]

[-  // LocalStorage functionality-]
[-  private readonly STORAGE_KEY = 'knocker-ui-state';-]

[-  private loadStateFromLocalStorage() {-]
[-    try {-]
[-      const saved = localStorage.getItem(this.STORAGE_KEY);-]
[-      if (!saved) return;-]
[-      -]
[-      const state = JSON.parse(saved);-]
[-      environment.log('Loading saved state:', state);-]
[-      -]
[-      // Применяем сохраненные значения к форме-]
[-      const patchData: any = {};-]
[-      -]
[-      if (state.mode !== undefined) patchData.mode = state.mode;-]
[-      if (state.targets !== undefined) patchData.targets = state.targets;-]
[-      if (state.delay !== undefined) patchData.delay = state.delay;-]
[-      if (state.verbose !== undefined) patchData.verbose = state.verbose;-]
[-      if (state.waitConnection !== undefined) patchData.waitConnection = state.waitConnection;-]
[-      if (state.configYAML !== undefined) patchData.configYAML = state.configYAML;-]
[-      -]
[-      if (Object.keys(patchData).length > 0) {-]
[-        this.form.patchValue(patchData);-]
[-        -]
[-        // Если загружен YAML, извлекаем path и устанавливаем в serverFilePath-]
[-        if (state.configYAML) {-]
[-          const pathFromYaml = this.getPathFromYaml(state.configYAML);-]
[-          if (pathFromYaml) {-]
[-            this.isSyncing = true;-]
[-            this.form.patchValue({ serverFilePath: pathFromYaml });-]
[-            this.isSyncing = false;-]
[-            environment.log('Extracted path from loaded YAML:', pathFromYaml);-]
[-          }-]
[-        }-]
[-      }-]
[-    } catch (e) {-]
[-      console.warn('Failed to load state from localStorage:', e);-]
[-    }-]
[-  }-]

[-  private saveStateToLocalStorage() {-]
[-    try {-]
[-      const formValue = this.form.value;-]
[-      const state = {-]
[-        mode: formValue.mode,-]
[-        targets: formValue.targets,-]
[-        delay: formValue.delay,-]
[-        verbose: formValue.verbose,-]
[-        waitConnection: formValue.waitConnection,-]
[-        configYAML: formValue.configYAML-]
[-      };-]
[-      -]
[-      localStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));-]
[-      environment.log('State saved to localStorage:', state);-]
[-    } catch (e) {-]
[-      console.warn('Failed to save state to localStorage:', e);-]
[-    }-]
[-  }-]

[-  private setupAutoSave() {-]
[-    // Подписываемся на изменения нужных полей-]
[-    const fieldsToWatch = ['mode', 'targets', 'delay', 'verbose', 'waitConnection', 'configYAML'];-]
[-    -]
[-    fieldsToWatch.forEach(fieldName => {-]
[-      this.form.get(fieldName)?.valueChanges.subscribe(() => {-]
[-        // Небольшая задержка, чтобы не сохранять на каждое нажатие клавиши-]
[-        setTimeout(() => this.saveStateToLocalStorage(), 300);-]
[-      });-]
[-    });-]
[-  }-]

[-  // Автоматическое преобразование между режимами-]
[-  private setupModeConversion() {-]
[-    let previousMode = this.form.value.mode;-]
[-    -]
[-    this.form.get('mode')?.valueChanges.subscribe((newMode) => {-]
[-      if (this.isSyncing) return;-]
[-      -]
[-      environment.log(`Mode changed from ${previousMode} to ${newMode}`);-]
[-      -]
[-      if (previousMode === 'inline' && newMode === 'yaml') {-]
[-        this.handleModeChangeToYaml();-]
[-      } else if (previousMode === 'yaml' && newMode === 'inline') {-]
[-        this.handleModeChangeToInline();-]
[-      }-]
[-      -]
[-      previousMode = newMode;-]
[-    });-]
[-  }-]

[-  private handleModeChangeToYaml() {-]
[-    try {-]
[-      // Проверяем есть ли сохраненный YAML в localStorage-]
[-      const savedYaml = this.getSavedConfigYAML();-]
[-      -]
[-      if (savedYaml?.trim()) {-]
[-        // Используем сохраненный YAML-]
[-        environment.log('Using saved YAML from localStorage');-]
[-        this.isSyncing = true;-]
[-        this.form.patchValue({ configYAML: savedYaml });-]
[-        this.isSyncing = false;-]
[-      } else {-]
[-        // Конвертируем из inline-]
[-        environment.log('Converting inline to YAML');-]
[-        this.convertInlineToYaml();-]
[-      }-]
[-      -]
[-      // После установки YAML (из localStorage или конвертации) извлекаем path-]
[-      setTimeout(() => this.extractAndSetServerPath(), 100);-]
[-      -]
[-    } catch (e) {-]
[-      console.warn('Failed to handle mode change to YAML:', e);-]
[-    }-]
[-  }-]

[-  private handleModeChangeToInline() {-]
[-    try {-]
[-      // Проверяем есть ли сохраненные inline значения в localStorage-]
[-      const savedTargets = this.getSavedTargets();-]
[-      -]
[-      if (savedTargets && savedTargets.trim()) {-]
[-        // Используем сохраненные inline значения-]
[-        environment.log('Using saved inline targets from localStorage');-]
[-        this.isSyncing = true;-]
[-        this.form.patchValue({ targets: savedTargets });-]
[-        this.isSyncing = false;-]
[-      } else {-]
[-        // Конвертируем из YAML-]
[-        environment.log('Converting YAML to inline');-]
[-        this.convertYamlToInline();-]
[-      }-]
[-      -]
[-    } catch (e) {-]
[-      console.warn('Failed to handle mode change to inline:', e);-]
[-    }-]
[-  }-]

[-  private getSavedConfigYAML(): string | null {-]
[-    try {-]
[-      const saved = localStorage.getItem(this.STORAGE_KEY);-]
[-      if (!saved) return null;-]
[-      const state = JSON.parse(saved);-]
[-      return state.configYAML || null;-]
[-    } catch {-]
[-      return null;-]
[-    }-]
[-  }-]

[-  private getSavedTargets(): string | null {-]
[-    try {-]
[-      const saved = localStorage.getItem(this.STORAGE_KEY);-]
[-      if (!saved) return null;-]
[-      const state = JSON.parse(saved);-]
[-      return state.targets || null;-]
[-    } catch {-]
[-      return null;-]
[-    }-]
[-  }-]

[-  private extractAndSetServerPath() {-]
[-    try {-]
[-      const yamlContent = this.form.value.configYAML || '';-]
[-      if (!yamlContent.trim()) return;-]
[-      -]
[-      const config: any = yaml.load(yamlContent);-]
[-      if (config && typeof config === 'object' && config.path) {-]
[-        environment.log('Extracted path from YAML:', config.path);-]
[-        this.isSyncing = true;-]
[-        this.form.patchValue({ serverFilePath: config.path });-]
[-        this.isSyncing = false;-]
[-      }-]
[-    } catch (e) {-]
[-      console.warn('Failed to extract path from YAML:', e);-]
[-    }-]
[-  }-]

[-  private convertInlineToYaml() {-]
[-    try {-]
[-      const formValue = this.form.value;-]
[-      const targets = (formValue.targets || '').split(';').filter(t => t.trim());-]
[-      -]
[-      // Создаем YAML конфигурацию из inline параметров-]
[-      const config: any = {-]
[-        targets: targets.map(target => {-]
[-          const [protocol, address] = target.trim().split(':');-]
[-          const [host, port] = address ? address.split(':') : ['', ''];-]
[-          -]
[-          return {-]
[-            protocol: protocol || 'tcp',-]
[-            host: host || '127.0.0.1',-]
[-            ports: [parseInt(port) || 22],-]
[-            wait_connection: formValue.waitConnection || false-]
[-          };-]
[-        }),-]
[-        delay: formValue.delay || '1s'-]
[-      };-]
[-      -]
[-      const yamlString = yaml.dump(config, { lineWidth: 120 });-]
[-      -]
[-      this.isSyncing = true;-]
[-      this.form.patchValue({ configYAML: yamlString });-]
[-      this.isSyncing = false;-]
[-      -]
[-      environment.log('Converted inline to YAML:', config);-]
[-    } catch (e) {-]
[-      console.warn('Failed to convert inline to YAML:', e);-]
[-    }-]
[-  }-]

[-  private convertYamlToInline() {-]
[-    try {-]
[-      const yamlContent = this.form.value.configYAML || '';-]
[-      if (!yamlContent.trim()) {-]
[-        // Если YAML пустой, используем значения по умолчанию-]
[-        this.isSyncing = true;-]
[-        this.form.patchValue({ -]
[-          targets: 'tcp:127.0.0.1:22',-]
[-          delay: '1s',-]
[-          waitConnection: false-]
[-        });-]
[-        this.isSyncing = false;-]
[-        return;-]
[-      }-]
[-      -]
[-      const config: any = yaml.load(yamlContent);-]
[-      if (!config || !config.targets || !Array.isArray(config.targets)) {-]
[-        console.warn('Invalid YAML structure for conversion');-]
[-        return;-]
[-      }-]
[-      -]
[-      // Конвертируем targets в строку - создаем отдельную строку для каждого порта-]
[-      const targetStrings: string[] = [];-]
[-      config.targets.forEach((target: any) => {-]
[-        const protocol = target.protocol || 'tcp';-]
[-        const host = target.host || '127.0.0.1';-]
[-        // Поддерживаем как ports (массив), так и port (единственное число) для обратной совместимости-]
[-        const ports = target.ports || [target.port] || [22];-]
[-        -]
[-        if (Array.isArray(ports)) {-]
[-          // Создаем отдельную строку для каждого порта-]
[-          ports.forEach(port => {-]
[-            targetStrings.push(`${protocol}:${host}:${port}`);-]
[-          });-]
[-        } else {-]
[-          targetStrings.push(`${protocol}:${host}:${ports}`);-]
[-        }-]
[-      });-]
[-      -]
[-      const targetsString = targetStrings.join(';');-]
[-      const delay = config.delay || '1s';-]
[-      -]
[-      // Берем wait_connection из первой цели (если есть)-]
[-      const waitConnection = config.targets[0]?.wait_connection || false;-]
[-      -]
[-      this.isSyncing = true;-]
[-      this.form.patchValue({ -]
[-        targets: targetsString,-]
[-        delay: delay,-]
[-        waitConnection: waitConnection-]
[-      });-]
[-      this.isSyncing = false;-]
[-      -]
[-      environment.log('Converted YAML to inline:', { targets: targetsString, delay, waitConnection });-]
[-    } catch (e) {-]
[-      console.warn('Failed to convert YAML to inline:', e);-]
[-    }-]
[-  }-]

[-  // Публичный метод для очистки сохраненного состояния (опционально)-]
[-  clearSavedState() {-]
[-    try {-]
[-      localStorage.removeItem(this.STORAGE_KEY);-]
[-      environment.log('Saved state cleared from localStorage');-]
[-      -]
[-      // Сбрасываем форму к значениям по умолчанию-]
[-      this.form.patchValue({-]
[-        mode: 'inline',-]
[-        targets: 'tcp:127.0.0.1:22',-]
[-        delay: '1s',-]
[-        verbose: true,-]
[-        waitConnection: false,-]
[-        configYAML: ''-]
[-      });-]
[-    } catch (e) {-]
[-      console.warn('Failed to clear saved state:', e);-]
[-    }-]
[-  }-]

[-  // Анимация заголовка-]
[-  private startTitleAnimation() {-]
[-    if (this.isAnimating) return;-]
[-    this.isAnimating = true;-]
[-    -]
[-    // Первая анимация: по буквам-]
[-    this.animateByLetters();-]
[-  }-]

[-  private animateByLetters() {-]
[-    let currentIndex = 0;-]
[-    const letters = this.animatedTitle.split('');-]
[-    -]
[-    const interval = setInterval(() => {-]
[-      if (currentIndex < letters.length) {-]
[-        this.cardHeader = letters.slice(0, currentIndex + 1).join('');-]
[-        currentIndex++;-]
[-      } else {-]
[-        clearInterval(interval);-]
[-        // Ждем 2 секунды и начинаем исчезать-]
[-        setTimeout(() => {-]
[-          this.fadeOutTitle();-]
[-        }, 2000);-]
[-      }-]
[-    }, 100); // 100ms между буквами-]
[-  }-]

[-  private fadeOutTitle() {-]
[-    let opacity = 1;-]
[-    const fadeInterval = setInterval(() => {-]
[-      opacity -= 0.1;-]
[-      if (opacity <= 0) {-]
[-        clearInterval(fadeInterval);-]
[-        this.cardHeader = '';-]
[-        // Ждем 1 секунду и начинаем анимацию по словам-]
[-        setTimeout(() => {-]
[-          this.animateByWords();-]
[-        }, 1000);-]
[-      }-]
[-    }, 50);-]
[-  }-]

[-  private animateByWords() {-]
[-    const words = this.animatedTitle.split(' ');-]
[-    let currentIndex = 0;-]
[-    -]
[-    const interval = setInterval(() => {-]
[-      if (currentIndex < words.length) {-]
[-        this.cardHeader = words.slice(0, currentIndex + 1).join(' ');-]
[-        currentIndex++;-]
[-      } else {-]
[-        clearInterval(interval);-]
[-        this.isAnimating = false;-]
[-      }-]
[-    }, 300); // 300ms между словами-]
[-  }-]
}


