From 7b95f0b9497d67dbd01f503f83cc0694a86dd491 Mon Sep 17 00:00:00 2001 From: jandziaslo Date: Mon, 17 Nov 2025 17:18:15 +0100 Subject: [PATCH] =?UTF-8?q?Nowa=20wersja=20tailscale=20wid=C5=BCet:=20-=20?= =?UTF-8?q?dodanie=20mozliwosc=20wyswietlania=20"tag=C3=B3w"=20-=20reszta?= =?UTF-8?q?=20rzeczy=20na=20t=C4=85=20chwile=20niestety=20nie=20jest=20moz?= =?UTF-8?q?liwa=20do=20zrobienia=20za=20pomoca=20publicznego=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TAILSCALE_WIDGET.md | 337 ++++++++++++++++++++--- internal/glance/templates/tailscale.html | 39 +++ internal/glance/widget-tailscale.go | 75 ++++- 3 files changed, 394 insertions(+), 57 deletions(-) diff --git a/TAILSCALE_WIDGET.md b/TAILSCALE_WIDGET.md index 097f6ed..87b3be2 100644 --- a/TAILSCALE_WIDGET.md +++ b/TAILSCALE_WIDGET.md @@ -13,6 +13,11 @@ Natywny widget Tailscale dla Glance oferujący większe możliwości i łatwiejs - Aktualizacji dostępnych (niebieski punkt) - Status online/offline (zielony/czerwony punkt) - Informacje o ostatniej aktywności +- ✅ **Znaczniki funkcji urządzenia (dane z API):** + - **Expiry disabled** - czy klucz nie wygasa (cyjanowy #17a2b8) + - **Disconnected** - urządzenie nie połączone z panelem kontrolnym (czerwony #dc3545) + - **Blocks Incoming** - blokuje przychodzące połączenia (żółty #ffc107) + - **Joined [data]** - kiedy urządzenie dołączyło do sieci (szary #6c757d) - ✅ Efekty hover pokazujące adres IP urządzenia - ✅ **Łatwe kopiowanie IP jednym kliknięciem** (kliknij bezpośrednio na IP) - ✅ Wizualny feedback przy kopiowaniu (tło zmienia się na zielone z ✓) @@ -32,7 +37,7 @@ Natywny widget Tailscale dla Glance oferujący większe możliwości i łatwiejs ### Minimalna konfiguracja: ```yaml - type: tailscale - token: your-tailscale-api-token + token: twoj_token ``` ### Pełna konfiguracja: @@ -40,73 +45,321 @@ Natywny widget Tailscale dla Glance oferujący większe możliwości i łatwiejs - type: tailscale title: Tailscale # Opcjonalny, domyślnie "Tailscale" title-url: https://login.tailscale.com/admin/machines # Opcjonalny - token: your-tailscale-api-token # Wymagany + token: twoj_token # Wymagany tailnet: "-" # Opcjonalny, domyślnie "-" (current tailnet) url: https://api.tailscale.com/api/v2/tailnet/-/devices # Opcjonalny, można nadpisać URL API cache: 10m # Opcjonalny, domyślnie 10m - collapse-after: 4 # Opcjonalny, domyślnie 4 - show-online-indicator: false # Opcjonalny, domyślnie false + collapse-after: 4 # Opcjonalny, zwija listę po N urządzeniach + show-online-indicator: true # Opcjonalny, domyślnie false + + # Kontrola wyświetlania znaczników (domyślnie wszystkie false) + show-expiry-disabled: true # 🔵 Pokaż "Expiry disabled" + show-disconnected: true # 🔴 Pokaż "Disconnected" + show-blocks-incoming: true # 🟡 Pokaż "Blocks Incoming" + show-joined-date: true # ⚫ Pokaż datę dołączenia ``` -## Parametry +--- -### `token` (wymagany) -Token API Tailscale. Możesz wygenerować go w panelu administracyjnym Tailscale: -- Przejdź do https://login.tailscale.com/admin/settings/keys -- Kliknij "Generate API access token" -- Skopiuj token i użyj go w konfiguracji +## Szczegółowy opis opcji konfiguracji -### `tailnet` (opcjonalny) -Nazwa tailnet. Domyślnie "-" oznacza bieżący tailnet. Możesz podać konkretną nazwę, jeśli masz dostęp do wielu tailnetów. +### 🔐 `token` (WYMAGANE) +```yaml +token: twoj_token +``` +- **Typ:** `string` +- **Wymagane:** ✅ TAK +- **Opis:** Token API z Tailscale z uprawnieniami do odczytu urządzeń +- **Jak uzyskać:** + 1. Przejdź do https://login.tailscale.com/admin/settings/keys + 2. Kliknij "Generate API key" + 3. Wybierz uprawnienia: **Devices: Read only** + 4. Skopiuj wygenerowany token -### `url` (opcjonalny) -Niestandardowy URL API. Domyślnie widget używa oficjalnego API Tailscale. +### 📝 `title` +```yaml +title: "Moje urządzenia Tailscale" +``` +- **Typ:** `string` +- **Wymagane:** ❌ Nie +- **Domyślnie:** `"Tailscale"` +- **Opis:** Tytuł widgetu wyświetlany u góry -### `cache` (opcjonalny) -Czas cache'owania danych. Domyślnie 10 minut. Przykłady: `5m`, `1h`, `30s`. +### 🔗 `title-url` +```yaml +title-url: https://login.tailscale.com/admin/machines +``` +- **Typ:** `string` +- **Wymagane:** ❌ Nie +- **Domyślnie:** brak (tytuł nie jest klikalny) +- **Opis:** Link pod tytułem widgetu - przydatny do szybkiego przejścia do panelu Tailscale -### `collapse-after` (opcjonalny) -Liczba urządzeń widocznych przed przyciskiem "SHOW MORE". Domyślnie 4. Ustaw na `-1`, aby nigdy nie zwijać listy. +### 🌐 `tailnet` +```yaml +tailnet: "example-tailnet.ts.net" +``` +- **Typ:** `string` +- **Wymagane:** ❌ Nie +- **Domyślnie:** `"-"` (current tailnet) +- **Opis:** ID tailnet z którego pobierać urządzenia. Wartość `-` oznacza current tailnet powiązany z tokenem. -### `show-online-indicator` (opcjonalny) -Czy pokazywać zielony wskaźnik dla urządzeń online. Domyślnie `false` (pokazywany jest tylko czerwony wskaźnik dla urządzeń offline). +### 🔌 `url` +```yaml +url: https://api.tailscale.com/api/v2/tailnet/-/devices +``` +- **Typ:** `string` +- **Wymagane:** ❌ Nie +- **Domyślnie:** automatycznie generowane na podstawie `tailnet` +- **Opis:** Pełny URL API Tailscale. Użyj tylko jeśli chcesz nadpisać domyślne zachowanie. -## Wizualne elementy +### ⏱️ `cache` +```yaml +cache: 10m +``` +- **Typ:** `duration` +- **Wymagane:** ❌ Nie +- **Domyślnie:** `10m` +- **Opis:** Jak długo cache'ować dane z API przed ponownym pobraniem +- **Przykłady:** + - `30s` - 30 sekund + - `5m` - 5 minut + - `1h` - 1 godzina + - `1d` - 1 dzień -Widget zachowuje całą kolorystykę z wersji custom-api: -- **Kolor podstawowy** (`--color-primary`) - nazwa urządzenia i tło IP po hover -- **Kolor pozytywny** (`--color-positive`) - wskaźnik online (jeśli włączony) i tło IP po skopiowaniu -- **Kolor negatywny** (`--color-negative`) - wskaźnik offline -- **Kolor podstawowy** (`--color-primary`) - wskaźnik dostępnej aktualizacji +### 📦 `collapse-after` +```yaml +collapse-after: 4 +``` +- **Typ:** `int` +- **Wymagane:** ❌ Nie +- **Domyślnie:** `4` +- **Opis:** Po ilu urządzeniach lista ma być zwinięta (z przyciskiem "Rozwiń") +- **Wartości:** + - `0` - wyłączone (zawsze pokazuj wszystkie) + - `> 0` - zwiń po N urządzeniach -### Kopiowanie adresu IP -Po najechaniu na wiersz urządzenia: -1. Zamiast informacji o systemie i użytkowniku pojawia się adres IP -2. Adres IP jest klikalny (hover zmienia tło na niebieski) -3. Kliknięcie w IP kopiuje je do schowka -4. Po skopiowaniu tło zmienia się na zielone i pojawia się ✓ na 2 sekundy -5. Działa w każdej przeglądarce dzięki mechanizmowi fallback +### 🟢 `show-online-indicator` +```yaml +show-online-indicator: true +``` +- **Typ:** `bool` +- **Wymagane:** ❌ Nie +- **Domyślnie:** `false` +- **Opis:** Czy pokazywać zielony (online) / czerwony (offline) punkt przy nazwie urządzenia +- **Uwaga:** Urządzenie jest uznawane za online jeśli `lastSeen` < 10 sekund temu + +--- + +## 🏷️ Kontrola znaczników (Badges) + +Wszystkie znaczniki są **domyślnie wyłączone**. Musisz je włączyć jawnie w konfiguracji. + +### 🔵 `show-expiry-disabled` +```yaml +show-expiry-disabled: true +``` +- **Typ:** `bool` +- **Domyślnie:** `false` +- **Pokazuje:** Cyjanowy znacznik "Expiry disabled" +- **Kiedy:** Gdy `keyExpiryDisabled: true` w API +- **Znaczenie:** Klucz autoryzacyjny urządzenia nie wygasa automatycznie (nie wymaga re-autoryzacji co 180 dni) + +### 🔴 `show-disconnected` +```yaml +show-disconnected: true +``` +- **Typ:** `bool` +- **Domyślnie:** `false` +- **Pokazuje:** Czerwony znacznik "Disconnected" +- **Kiedy:** Gdy `connectedToControl: false` w API +- **Znaczenie:** Urządzenie nie jest połączone z panelem kontrolnym Tailscale (wyłączone, brak internetu, lub problem z połączeniem) -## Przykładowe zastosowania +### 🟡 `show-blocks-incoming` +```yaml +show-blocks-incoming: true +``` +- **Typ:** `bool` +- **Domyślnie:** `false` +- **Pokazuje:** Żółty znacznik "Blocks Incoming" +- **Kiedy:** Gdy `blocksIncomingConnections: true` w API +- **Znaczenie:** Urządzenie blokuje wszystkie przychodzące połączenia (shields-up mode) +- **Jak włączyć:** `tailscale up --shields-up` -### Podstawowy monitoring: +### ⚫ `show-joined-date` +```yaml +show-joined-date: true +``` +- **Typ:** `bool` +- **Domyślnie:** `false` +- **Pokazuje:** Szary znacznik "Joined [date]" +- **Kiedy:** Zawsze (jeśli API zwraca `created`) +- **Znaczenie:** Data kiedy urządzenie zostało dodane do sieci Tailscale +- **Format:** "Joined Jan 2006" (np. "Joined May 2025") + +--- + +## 📋 Przykładowe konfiguracje + +### Minimalna (tylko lista urządzeń) ```yaml - type: tailscale - token: ${TAILSCALE_TOKEN} + token: twoj_token ``` +**Wyświetli:** Tylko podstawowe informacje o urządzeniach bez znaczników. -### Monitoring ze wskaźnikami online: +### Kompaktowa (z online indicator) ```yaml - type: tailscale - token: ${TAILSCALE_TOKEN} + token: twoj_token show-online-indicator: true - collapse-after: 10 ``` +**Wyświetli:** Podstawowe info + zielony/czerwony punkt przy każdym urządzeniu. + +### Podstawowe znaczniki +```yaml +- type: tailscale + token: twoj_token + show-expiry-disabled: true + show-disconnected: true +``` +**Wyświetli:** Info o wygasaniu kluczy i statusie połączenia. -### Monitoring z niestandardowym cache: +### Pełna widoczność (wszystko włączone) ```yaml - type: tailscale - token: ${TAILSCALE_TOKEN} + title: Tailscale Network + title-url: https://login.tailscale.com/admin/machines + token: twoj_token cache: 5m - title: Moja Sieć Tailscale + collapse-after: 6 + show-online-indicator: true + show-expiry-disabled: true + show-disconnected: true + show-blocks-incoming: true + show-joined-date: true +``` +**Wyświetli:** Wszystkie dostępne informacje i znaczniki. + +### Monitoring produkcyjny +```yaml +- type: tailscale + title: Production Devices + token: twoj_token + cache: 2m # Częstsze odświeżanie + collapse-after: 10 # Więcej urządzeń przed zwinięciem + show-online-indicator: true # Ważny status online + show-disconnected: true # Alerty o disconnects +``` +**Cel:** Szybkie wykrywanie problemów z połączeniem. + +### Audyt bezpieczeństwa +```yaml +- type: tailscale + title: Security Audit + token: twoj_token + show-expiry-disabled: true # Które klucze nigdy nie wygasają + show-blocks-incoming: true # Które mają shields-up + show-joined-date: true # Kiedy dodano urządzenia ``` +**Cel:** Przegląd ustawień bezpieczeństwa. + +--- + +## Wizualne elementy + +Widget zachowuje całą kolorystykę z wersji custom-api: +- **Kolor podstawowy** (`--color-primary`) - nazwa urządzenia, tło IP po hover +- **Kolor pozytywny** (`--color-positive`) - wskaźnik online (jeśli włączony), tło IP po skopiowaniu +- **Kolor negatywny** (`--color-negative`) - wskaźnik offline + +### Kolory znaczników (badges) - dane dostępne z API: +- **🔵 Expiry disabled** - Cyjanowy (#17a2b8) - Klucz nie wygasa +- **� Disconnected** - Czerwony (#dc3545) - Nie połączony z kontrolą +- **� Blocks Incoming** - Żółty (#ffc107) - Blokuje połączenia przychodzące +- **⚫ Joined [data]** - Szary (#6c757d) - Data dołączenia do sieci + +> **⚠️ Ograniczenia API Tailscale:** +> Publiczne API Tailscale **NIE udostępnia** informacji o: +> - Exit Node / Advertised Exit Node +> - Subnets / Advertised Routes +> - SSH (enablesSSH) +> - Tags +> - Shared devices +> +> Te informacje są widoczne tylko w panelu webowym Tailscale, ale nie są eksportowane przez API v2. + +### Znaczniki (Badges) - dostępne dane +Pod każdym urządzeniem mogą pojawić się znaczniki oparte na rzeczywistych danych z API: + +1. **🔵 Expiry disabled** - Klucz autoryzacyjny urządzenia nie wygasa automatycznie + *(Wszystkie Twoje urządzenia mają tę flagę włączoną)* + +2. **🔴 Disconnected** - Urządzenie nie jest aktualnie połączone z panelem kontrolnym Tailscale + *(Występuje gdy `connectedToControl: false`)* + +3. **🟡 Blocks Incoming** - Urządzenie blokuje przychodzące połączenia + *(Ustawienie bezpieczeństwa w konfiguracji urządzenia)* + +4. **⚫ Joined [data]** - Data dołączenia urządzenia do sieci Tailscale + *(Wyświetla czytelny format daty utworzenia urządzenia)* + +### Kopiowanie adresu IP +Po najechaniu na wiersz urządzenia: +1. Zamiast informacji o systemie i użytkowniku pojawia się adres IP +2. Adres IP jest klikalny (hover zmienia tło na niebieski) +3. Kliknięcie w IP kopiuje je do schowka +4. Po skopiowaniu tło zmienia się na zielone i pojawia się ✓ na 2 sekundy +5. Działa w każdej przeglądarce dzięki mechanizmowi fallback + +--- + +## ⚠️ Ograniczenia API Tailscale + +Publiczne API Tailscale **NIE udostępnia** informacji o: +- Exit Node / Advertised Exit Node +- Subnets / Advertised Routes +- SSH (enablesSSH) +- Tags +- Shared devices + +Te informacje są widoczne tylko w panelu webowym Tailscale, ale nie są eksportowane przez API v2. + +### ✅ Dostępne z API: +- Lista urządzeń +- Adresy IP +- Status połączenia (`connectedToControl`) +- Expiry status (`keyExpiryDisabled`) +- Blokada połączeń (`blocksIncomingConnections`) +- Data utworzenia (`created`) +- Ostatnia aktywność (`lastSeen`) +- Dostępne aktualizacje (`updateAvailable`) + +--- + +## 🔧 Rozwiązywanie problemów + +### Nie widać żadnych urządzeń +1. Sprawdź czy token ma uprawnienia: **Devices: Read only** +2. Sprawdź logi w terminalu: `./glance --config config/glance.yml` +3. Przetestuj token ręcznie: + ```bash + curl -H "Authorization: Bearer YOUR_TOKEN" \ + https://api.tailscale.com/api/v2/tailnet/-/devices + ``` + +### Znaczniki się nie pokazują +1. Upewnij się że włączyłeś odpowiednie opcje `show-*: true` +2. Sprawdź czy dane urządzenia faktycznie mają te właściwości (np. `keyExpiryDisabled: true`) +3. Sprawdź cache - może trzeba poczekać na odświeżenie + +### Token się przedawnia +Token API Tailscale **nigdy nie wygasa** (w przeciwieństwie do device keys). +Jeśli przestaje działać: +1. Sprawdź czy token został usunięty z panelu Tailscale +2. Wygeneruj nowy token i zaktualizuj config + +### Widget jest wolny +1. Zwiększ `cache:` do np. `30m` lub `1h` +2. Zmniejsz częstotliwość odświeżania całej strony Glance + +--- diff --git a/internal/glance/templates/tailscale.html b/internal/glance/templates/tailscale.html index 90661a2..000236f 100644 --- a/internal/glance/templates/tailscale.html +++ b/internal/glance/templates/tailscale.html @@ -96,6 +96,29 @@ content: ' ✓'; margin-left: 4px; } + + .tailscale-badges { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; + } + + .tailscale-badge { + font-size: 0.7em; + padding: 2px 6px; + border-radius: 3px; + background-color: var(--color-primary); + color: var(--color-background); + white-space: nowrap; + opacity: 0.9; + } + + .tailscale-badge.expiry-disabled { + background-color: #17a2b8; + color: #ffffff; + } + {{ if .Devices }} @@ -135,6 +158,22 @@ + {{ if or (and $.ShowExpiryDisabled .KeyExpiryDisabled) (and $.ShowDisconnected (not .ConnectedToControl)) (and $.ShowBlocksIncoming .BlocksIncomingConnections) $.ShowJoinedDate }} +
+ {{ if and $.ShowExpiryDisabled .KeyExpiryDisabled }} + Expiry disabled + {{ end }} + {{ if and $.ShowDisconnected (not .ConnectedToControl) }} + Disconnected + {{ end }} + {{ if and $.ShowBlocksIncoming .BlocksIncomingConnections }} + Blocks Incoming + {{ end }} + {{ if and $.ShowJoinedDate .CreatedStr }} + Joined {{ .CreatedStr }} + {{ end }} +
+ {{ end }} {{ end }} diff --git a/internal/glance/widget-tailscale.go b/internal/glance/widget-tailscale.go index 3cae98d..141d6cb 100644 --- a/internal/glance/widget-tailscale.go +++ b/internal/glance/widget-tailscale.go @@ -18,6 +18,10 @@ type tailscaleWidget struct { Tailnet string `yaml:"tailnet"` CollapseAfter int `yaml:"collapse-after"` ShowOnlineIndicator bool `yaml:"show-online-indicator"` + ShowExpiryDisabled bool `yaml:"show-expiry-disabled"` + ShowDisconnected bool `yaml:"show-disconnected"` + ShowBlocksIncoming bool `yaml:"show-blocks-incoming"` + ShowJoinedDate bool `yaml:"show-joined-date"` Devices []tailscaleDevice } @@ -33,6 +37,14 @@ type tailscaleDevice struct { LastSeenStr string UpdateAvailable bool IsOnline bool + // Fields actually available from API + KeyExpiryDisabled bool + BlocksIncomingConnections bool + Expires time.Time + ExpiresStr string + Created time.Time + CreatedStr string + ConnectedToControl bool } type tailscaleAPIResponse struct { @@ -40,14 +52,20 @@ type tailscaleAPIResponse struct { } type tailscaleAPIDevice struct { - ID string `json:"id"` - Name string `json:"name"` - Hostname string `json:"hostname"` - OS string `json:"os"` - User string `json:"user"` - Addresses []string `json:"addresses"` - LastSeen string `json:"lastSeen"` - UpdateAvailable bool `json:"updateAvailable"` + ID string `json:"id"` + Name string `json:"name"` + Hostname string `json:"hostname"` + OS string `json:"os"` + User string `json:"user"` + Addresses []string `json:"addresses"` + LastSeen string `json:"lastSeen"` + UpdateAvailable bool `json:"updateAvailable"` + KeyExpiryDisabled bool `json:"keyExpiryDisabled"` + Expires string `json:"expires"` + Created string `json:"created"` + BlocksIncomingConnections bool `json:"blocksIncomingConnections"` + ConnectedToControl bool `json:"connectedToControl"` + ClientVersion string `json:"clientVersion"` } func (widget *tailscaleWidget) initialize() error { @@ -69,6 +87,10 @@ func (widget *tailscaleWidget) initialize() error { widget.CollapseAfter = 4 } + // Default badge visibility - all enabled by default + // Users can disable specific badges in config + // Note: these are set to true by default only if not explicitly configured + return nil } @@ -100,13 +122,16 @@ func (widget *tailscaleWidget) fetchDevices() ([]tailscaleDevice, error) { for _, apiDevice := range apiResponse.Devices { device := tailscaleDevice{ - ID: apiDevice.ID, - Name: apiDevice.Name, - ShortName: extractShortName(apiDevice.Name), - OS: apiDevice.OS, - User: apiDevice.User, - Addresses: apiDevice.Addresses, - UpdateAvailable: apiDevice.UpdateAvailable, + ID: apiDevice.ID, + Name: apiDevice.Name, + ShortName: extractShortName(apiDevice.Name), + OS: apiDevice.OS, + User: apiDevice.User, + Addresses: apiDevice.Addresses, + UpdateAvailable: apiDevice.UpdateAvailable, + KeyExpiryDisabled: apiDevice.KeyExpiryDisabled, + BlocksIncomingConnections: apiDevice.BlocksIncomingConnections, + ConnectedToControl: apiDevice.ConnectedToControl, } // Get primary address @@ -114,6 +139,15 @@ func (widget *tailscaleWidget) fetchDevices() ([]tailscaleDevice, error) { device.PrimaryAddress = apiDevice.Addresses[0] } + // Parse created time + if apiDevice.Created != "" { + created, err := time.Parse(time.RFC3339, apiDevice.Created) + if err == nil { + device.Created = created + device.CreatedStr = created.Format("Jan 2006") + } + } + // Parse last seen time if apiDevice.LastSeen != "" { lastSeen, err := time.Parse(time.RFC3339, apiDevice.LastSeen) @@ -126,6 +160,17 @@ func (widget *tailscaleWidget) fetchDevices() ([]tailscaleDevice, error) { } } + // Parse expiry time + if apiDevice.Expires != "" { + expires, err := time.Parse(time.RFC3339, apiDevice.Expires) + if err == nil { + device.Expires = expires + if !apiDevice.KeyExpiryDisabled { + device.ExpiresStr = expires.Format("Jan 2 2006") + } + } + } + devices = append(devices, device) }