Compare commits

...
8 Commits
Author SHA1 Message Date
Tomas Dvorak 18046aee71 feat(site): overhaul domain and monitor detail views
Build Docker images / Hub (push) Failing after 1m21s
Refactor the domain and monitor detail pages to use a new modular
component architecture. This includes:

- Implementing `domain-info-sections.tsx` and `monitor-info-sections.tsx`
  to provide structured, card-based information layouts.
- Enhancing domain details with new sections for DNS, SSL, SEO, and
  hosting information.
- Improving monitor details with enhanced uptime visualizations and
  response time statistics.
- Updating the PageSpeed check API to support an optional API key from
  environment variables.
- Cleaning up UI components and improving code formatting in the
  monitors table.
- Updating localization files to support new UI strings.
2026-05-18 18:27:12 +02:00
Tomas Dvorak fe5c7eaa95 feat(hub,site): enhance domain intelligence and monitor performance
Build Docker images / Hub (push) Failing after 1m35s
Implement comprehensive domain data collection including provider detection (DNS, hosting, email, CA), HTTP headers, TLS certificate chains, and SEO metadata. Added PageSpeed Insights integration for monitors to track Core Web Vitals.

- **hub**:
  - Add provider detection logic for DNS, email, and hosting.
  - Expand `Domain` entity to include SEO, headers, certificates, and enhanced registration details.
  - Implement automated collection of TLD, WHOIS raw data, and host country codes.
  - Update scheduler to track changes in providers and security settings (privacy/transfer lock).
  - Add PageSpeed check endpoint to monitor API.
- **site**:
  - Update domain table and detail views to display new intelligence (providers, headers, SEO).
  - Implement PageSpeed metrics visualization with Core Web Vitals status indicators.
  - Add display options for provider information in the domain list.
- **db**:
  - Add migration for new domain collection fields.
2026-05-14 13:33:03 +02:00
Tomas Dvorak 0dd7db8a82 feat(hub,site): enhance domain management and monitor UI
Build Docker images / Hub (push) Failing after 54s
Implement manual domain expiry overrides, improve subdomain discovery via CT logs, and enhance the monitoring dashboard with favicons and configurable display options.

hub:
- allow manual expiry and creation date overrides in domain API when WHOIS lookup fails
- implement JSON parsing for crt.sh certificate transparency log searches in subdomain discovery
- update monitor API routes to use curly brace syntax for path parameters

site:
- add manual registration date and period inputs to domain dialog
- implement monitor favicon support using Google's favicon service
- add configurable display options (uptime pills, heartbeat dots) to monitors table
- update localization files to include new UI elements
2026-05-10 10:24:28 +02:00
Tomas Dvorak b6f40af67f feat(hub): improve WHOIS lookup reliability and enhance site UI
Build Docker images / Hub (push) Failing after 52s
Implement enhanced WHOIS lookup strategies, specifically targeting .eu
domains through EURid web scraping and alternative services to
improve data accuracy for expiry dates.

- Add EURid web scraping and alternative WHOIS service support for .eu domains
- Increase timeouts for .eu domain lookups in TCP and native WHOIS
- Improve domain scheduler to prevent overwriting valid data with zero-value dates
- Enhance site UI with subdomain indicators in domain tables
- Add filtering capabilities to the calendar view
- Implement drag-and-drop reordering for systems table
- Add new debug and test utilities for WHOIS and date parsing logic
2026-05-08 11:07:34 +02:00
Tomas Dvorak 1af18872d5 Revert "ui(site): enhance table components and visibility"
Build Docker images / Hub (push) Failing after 1m9s
This reverts commit 298c514087.
2026-05-07 11:14:05 +02:00
Tomas Dvorak 298c514087 ui(site): enhance table components and visibility
Refactor domain, monitor, and system tables to improve visual clarity and information density.

- Update badge and pill styles in `domains-table` and `monitors-table` to use solid background colors for better contrast.
- Integrate `ContainersTable` into `MonitorCard` and `SystemCard` to provide nested container visibility.
- Simplify `MonitorsTable` by removing the unused network view mode.
- Improve layout and spacing in various table components for better readability.
2026-05-07 10:36:21 +02:00
Tomas Dvorak 77b24ae9e5 docs(site): update localization files
Build Docker images / Hub (push) Failing after 1m6s
Update translation files across multiple languages to include new UI strings and fix reference mappings in the subdomain list and monitoring components.
2026-05-05 18:40:57 +02:00
Tomas Dvorak 7ea9a069f9 feat(site): implement subdomain discovery and enhanced monitoring dashboard
This commit introduces a comprehensive subdomain discovery system and significantly upgrades the monitoring and domain management user interfaces.

Key changes include:
- **Subdomain Discovery**: Added a new service in the hub that performs advanced subdomain discovery using DNS brute forcing, Certificate Transparency (CT) log searches, pattern enumeration, and HTTP probing.
- **Enhanced Domain Management**:
    - Added API endpoints for retrieving, discovering, and deleting subdomains.
    - Implemented a new `SubdomainList` component in the UI to manage discovered subdomains.
    - Improved WHOIS lookup robustness by supporting a wider range of registry field variations.
- **Advanced Monitoring UI**:
    - Introduced `GroupedMonitorsTable` to organize monitors by root domain and their respective subdomains.
    - Added visual uptime timelines (heartbeat dots) and response time statistics (Avg, Min, Max, P95, P99) to the monitor detail view.
    - Implemented "Uptime Pills" for high-visibility status indicators in the monitors table.
- **Status Page Management**: Replaced the static status pages table with a full `StatusPageManager` capable of managing status pages and incidents.
- **Refactoring & Cleanup**:
    - Cleaned up `.gitignore` and removed unused reference submodules.
    - Improved domain extraction and grouping logic in the frontend.
    - Enhanced the `SystemsTable` with better sorting and layout.
2026-05-05 16:14:45 +02:00
69 changed files with 255299 additions and 1843 deletions
+1
View File
@@ -14,6 +14,7 @@ MAX_STATUS_PAGES=10
# Optional Features
PAGESPEED_ENABLED=true
# PAGESPEED_API_KEY=your_google_api_key_here
SUBDOMAIN_DISCOVERY=true
STATUS_PAGES_ENABLED=true
BADGES_ENABLED=true
+7 -8
View File
@@ -38,16 +38,15 @@ __debug_*
*timestamp*
.playwright-cli/
# Reference code (external projects)
# Reference code (external projects) - cleaned up
reference/
# Graphify output - only keep json, md, html in root
graphify-out/*/
graphify-out/*.svg
!graphify-out/*.json
!graphify-out/*.md
!graphify-out/*.html
# Environment
.env
.env.local
graphify-out/*
!graphify-out/*.svg
!graphify-out/*.json
!graphify-out/*.md
!graphify-out/*.html
-7
View File
@@ -1,7 +0,0 @@
# Security Policy
## Reporting a Vulnerability
If you find a vulnerability in the latest version, please [submit a private advisory](https://github.com/henrygd/beszel/security/advisories/new).
If it's low severity (use best judgement) you may open an issue instead of an advisory.
+246046
View File
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 14 MiB

BIN
View File
Binary file not shown.
+100
View File
@@ -71,6 +71,33 @@ type Domain struct {
AbuseEmail string `json:"abuse_email" db:"abuse_email"`
AbusePhone string `json:"abuse_phone" db:"abuse_phone"`
// Provider Detection
DNSProvider string `json:"dns_provider" db:"dns_provider"`
HostingProvider string `json:"hosting_provider" db:"hosting_provider"`
EmailProvider string `json:"email_provider" db:"email_provider"`
CAProvider string `json:"ca_provider" db:"ca_provider"`
// HTTP Headers
Headers []Header `json:"headers" db:"headers"`
// Certificate Chain
Certificates []Certificate `json:"certificates" db:"certificates"`
// SEO Metadata
SEOMeta *SEOMeta `json:"seo_meta" db:"seo_meta"`
// Raw WHOIS Response
WHOISRaw string `json:"whois_raw" db:"whois_raw"`
// Registration Details
PrivacyEnabled bool `json:"privacy_enabled" db:"privacy_enabled"`
TransferLock bool `json:"transfer_lock" db:"transfer_lock"`
TLD string `json:"tld" db:"tld"`
DomainStatuses []string `json:"domain_statuses" db:"domain_statuses"`
// Enhanced Geo
HostCountryCode string `json:"host_country_code" db:"host_country_code"`
// Metadata
Tags []string `json:"tags" db:"tags"`
Notes string `json:"notes" db:"notes"`
@@ -176,6 +203,76 @@ type IPInfo struct {
IPv6 []string `json:"ipv6"`
}
// Header represents an HTTP response header
type Header struct {
Name string `json:"name"`
Value string `json:"value"`
}
// Certificate represents a TLS certificate in the chain
type Certificate struct {
Issuer string `json:"issuer"`
Subject string `json:"subject"`
AltNames []string `json:"alt_names"`
ValidFrom time.Time `json:"valid_from"`
ValidTo time.Time `json:"valid_to"`
CAProvider string `json:"ca_provider"`
}
// OpenGraphMeta represents Open Graph metadata
type OpenGraphMeta struct {
URL string `json:"url"`
Type string `json:"type"`
Title string `json:"title"`
Images []string `json:"images"`
Description string `json:"description"`
}
// TwitterMeta represents Twitter card metadata
type TwitterMeta struct {
Title string `json:"title"`
Description string `json:"description"`
Image string `json:"image"`
Card string `json:"card"`
}
// GeneralMeta represents general HTML meta tags
type GeneralMeta struct {
Title string `json:"title"`
Author string `json:"author"`
Robots string `json:"robots"`
Keywords string `json:"keywords"`
Canonical string `json:"canonical"`
Description string `json:"description"`
}
// RobotsTxt represents parsed robots.txt data
type RobotsTxt struct {
Fetched bool `json:"fetched"`
Groups []RobotsGroup `json:"groups"`
Sitemaps []string `json:"sitemaps"`
}
// RobotsGroup represents a user-agent group in robots.txt
type RobotsGroup struct {
UserAgents []string `json:"userAgents"`
Rules []RobotsRule `json:"rules"`
}
// RobotsRule represents a single rule in robots.txt
type RobotsRule struct {
Type string `json:"type"`
Value string `json:"value"`
}
// SEOMeta represents all SEO-related metadata
type SEOMeta struct {
OpenGraph OpenGraphMeta `json:"openGraph"`
Twitter TwitterMeta `json:"twitter"`
General GeneralMeta `json:"general"`
Robots RobotsTxt `json:"robots"`
}
// ChangeType constants for domain history
const (
ChangeTypeExpiry = "expiry"
@@ -185,6 +282,9 @@ const (
ChangeTypeIP = "ip"
ChangeTypeHost = "host"
ChangeTypeStatus = "status"
ChangeTypeProvider = "provider"
ChangeTypeSecurity = "security"
ChangeTypeSEO = "seo"
)
// Domain status constants
+168
View File
@@ -43,6 +43,11 @@ func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) {
api.GET("/{id}/stats", h.getDomainStats)
api.POST("/{id}/pause", h.pauseDomain)
api.POST("/{id}/resume", h.resumeDomain)
api.GET("/{id}/subdomains", h.getDomainSubdomains)
api.POST("/{id}/discover-subdomains", h.discoverSubdomains)
// Subdomain routes
api.DELETE("/subdomains/{subdomainId}", h.deleteSubdomain)
}
// listDomains lists all domains for the authenticated user
@@ -123,6 +128,9 @@ func (h *APIHandler) createDomain(e *core.RequestEvent) error {
NotifyOnSSL bool `json:"notify_on_ssl_expiry"`
NotifyOnDNS bool `json:"notify_on_dns_change"`
NotifyOnReg bool `json:"notify_on_registrar_change"`
// Manual expiry override when WHOIS fails
ExpiryDate string `json:"expiry_date,omitempty"`
CreationDate string `json:"creation_date,omitempty"`
}
if err := json.NewDecoder(e.Request.Body).Decode(&req); err != nil {
return e.BadRequestError("invalid request body", err)
@@ -174,6 +182,7 @@ func (h *APIHandler) createDomain(e *core.RequestEvent) error {
record.Set("user", authRecord.Id)
// Auto-lookup if requested
lookupHadExpiry := false
if req.AutoLookup {
lookupSvc := whois.NewLookupService("")
ctx := e.Request.Context()
@@ -183,6 +192,7 @@ func (h *APIHandler) createDomain(e *core.RequestEvent) error {
// Calculate status based on lookup results
status := domain.DomainStatusUnknown
if domainData.ExpiryDate != nil {
lookupHadExpiry = true
daysUntil := int(time.Until(*domainData.ExpiryDate).Hours() / 24)
if daysUntil < 0 {
status = domain.DomainStatusExpired
@@ -199,6 +209,29 @@ func (h *APIHandler) createDomain(e *core.RequestEvent) error {
}
}
// Apply manual expiry/creation dates if WHOIS didn't find them
if !lookupHadExpiry {
if req.ExpiryDate != "" {
if t, err := time.Parse("2006-01-02", req.ExpiryDate); err == nil {
record.Set("expiry_date", t)
// Recalculate status with manual expiry
daysUntil := int(time.Until(t).Hours() / 24)
status := domain.DomainStatusActive
if daysUntil < 0 {
status = domain.DomainStatusExpired
} else if daysUntil <= req.AlertDaysBefore {
status = domain.DomainStatusExpiring
}
record.Set("status", status)
}
}
if req.CreationDate != "" {
if t, err := time.Parse("2006-01-02", req.CreationDate); err == nil {
record.Set("creation_date", t)
}
}
}
if err := h.app.Save(record); err != nil {
return e.InternalServerError("failed to create domain", err)
}
@@ -602,6 +635,19 @@ func (h *APIHandler) recordToResponse(record *core.Record) map[string]interface{
"registrant_state": record.GetString("registrant_state"),
"abuse_email": record.GetString("abuse_email"),
"abuse_phone": record.GetString("abuse_phone"),
"dns_provider": record.GetString("dns_provider"),
"hosting_provider": record.GetString("hosting_provider"),
"email_provider": record.GetString("email_provider"),
"ca_provider": record.GetString("ca_provider"),
"headers": record.Get("headers"),
"certificates": record.Get("certificates"),
"seo_meta": record.Get("seo_meta"),
"whois_raw": record.GetString("whois_raw"),
"privacy_enabled": record.GetBool("privacy_enabled"),
"transfer_lock": record.GetBool("transfer_lock"),
"tld": record.GetString("tld"),
"domain_statuses": record.Get("domain_statuses"),
"host_country_code": record.GetString("host_country_code"),
"tags": record.Get("tags"),
"notes": record.GetString("notes"),
"favicon_url": record.GetString("favicon_url"),
@@ -684,6 +730,27 @@ func (h *APIHandler) applyLookupData(record *core.Record, domainData *domain.Dom
record.Set("registrant_postal", domainData.RegistrantPostal)
record.Set("abuse_email", domainData.AbuseEmail)
record.Set("abuse_phone", domainData.AbusePhone)
record.Set("dns_provider", domainData.DNSProvider)
record.Set("hosting_provider", domainData.HostingProvider)
record.Set("email_provider", domainData.EmailProvider)
record.Set("ca_provider", domainData.CAProvider)
if len(domainData.Headers) > 0 {
record.Set("headers", domainData.Headers)
}
if len(domainData.Certificates) > 0 {
record.Set("certificates", domainData.Certificates)
}
if domainData.SEOMeta != nil {
record.Set("seo_meta", domainData.SEOMeta)
}
record.Set("whois_raw", domainData.WHOISRaw)
record.Set("privacy_enabled", domainData.PrivacyEnabled)
record.Set("transfer_lock", domainData.TransferLock)
record.Set("tld", domainData.TLD)
if len(domainData.DomainStatuses) > 0 {
record.Set("domain_statuses", domainData.DomainStatuses)
}
record.Set("host_country_code", domainData.HostCountryCode)
record.Set("favicon_url", domainData.FaviconURL)
record.Set("last_checked", time.Now())
}
@@ -705,3 +772,104 @@ func cleanDomain(domain string) string {
}
return strings.ToLower(strings.TrimSpace(domain))
}
// getDomainSubdomains returns all subdomains for a domain
func (h *APIHandler) getDomainSubdomains(e *core.RequestEvent) error {
authRecord := e.Auth
if authRecord == nil {
return e.UnauthorizedError("unauthorized", nil)
}
domainID := e.Request.PathValue("id")
// Verify domain ownership
domain, err := h.app.FindRecordById("domains", domainID)
if err != nil {
return e.NotFoundError("domain not found", err)
}
if domain.GetString("user") != authRecord.Id {
return e.ForbiddenError("not authorized", nil)
}
records, err := h.app.FindAllRecords("subdomains",
dbx.NewExp("domain = {:domain}", dbx.Params{"domain": domainID}),
)
if err != nil {
return e.InternalServerError("failed to fetch subdomains", err)
}
subdomains := make([]map[string]interface{}, 0, len(records))
for _, record := range records {
subdomains = append(subdomains, map[string]interface{}{
"id": record.Id,
"domain": record.GetString("domain"),
"subdomain_name": record.GetString("subdomain_name"),
"full_domain": record.GetString("full_domain"),
"status": record.GetString("status"),
"ip_addresses": record.GetString("ip_addresses"),
"http_status": record.GetInt("http_status"),
"server_header": record.GetString("server_header"),
"discovery_source": record.GetString("discovery_source"),
"last_checked": record.GetDateTime("last_checked").Time(),
"created": record.GetDateTime("created").Time(),
"updated": record.GetDateTime("updated").Time(),
})
}
return e.JSON(http.StatusOK, subdomains)
}
// discoverSubdomains triggers subdomain discovery for a domain
func (h *APIHandler) discoverSubdomains(e *core.RequestEvent) error {
authRecord := e.Auth
if authRecord == nil {
return e.UnauthorizedError("unauthorized", nil)
}
domainID := e.Request.PathValue("id")
// Verify domain ownership
domain, err := h.app.FindRecordById("domains", domainID)
if err != nil {
return e.NotFoundError("domain not found", err)
}
if domain.GetString("user") != authRecord.Id {
return e.ForbiddenError("not authorized", nil)
}
// Trigger discovery asynchronously
go func() {
domainName := domain.GetString("domain_name")
userID := authRecord.Id
h.scheduler.discoverSubdomainsEnhanced(domain, domainName, userID)
}()
return e.JSON(http.StatusOK, map[string]string{"status": "discovery_started"})
}
// deleteSubdomain deletes a subdomain
func (h *APIHandler) deleteSubdomain(e *core.RequestEvent) error {
authRecord := e.Auth
if authRecord == nil {
return e.UnauthorizedError("unauthorized", nil)
}
subdomainID := e.Request.PathValue("subdomainId")
// Get subdomain
subdomain, err := h.app.FindRecordById("subdomains", subdomainID)
if err != nil {
return e.NotFoundError("subdomain not found", err)
}
// Verify ownership
if subdomain.GetString("user") != authRecord.Id {
return e.ForbiddenError("not authorized", nil)
}
if err := h.app.Delete(subdomain); err != nil {
return e.InternalServerError("failed to delete subdomain", err)
}
return e.NoContent(http.StatusNoContent)
}
+275
View File
@@ -0,0 +1,275 @@
package detect
import (
"net/http"
"strings"
)
// ProviderInfo holds detected provider name
type ProviderInfo struct {
Name string
}
// DetectDNSProvider detects the DNS provider from NS records
func DetectDNSProvider(nsRecords []string) string {
for _, ns := range nsRecords {
nsLower := strings.ToLower(ns)
switch {
case strings.Contains(nsLower, "cloudflare"):
return "Cloudflare"
case strings.Contains(nsLower, "awsdns"):
return "Amazon Route 53"
case strings.Contains(nsLower, "googledomains") || strings.Contains(nsLower, "google.com"):
return "Google Domains"
case strings.Contains(nsLower, "namecheap") || strings.Contains(nsLower, "namecheaphosting"):
return "Namecheap"
case strings.Contains(nsLower, "godaddy"):
return "GoDaddy"
case strings.Contains(nsLower, "domaincontrol"):
return "GoDaddy"
case strings.Contains(nsLower, "nsone.net"):
return "NS1"
case strings.Contains(nsLower, "digitalocean"):
return "DigitalOcean"
case strings.Contains(nsLower, "linode"):
return "Linode"
case strings.Contains(nsLower, "vultr"):
return "Vultr"
case strings.Contains(nsLower, "he.net"):
return "Hurricane Electric"
case strings.Contains(nsLower, "dyn.com") || strings.Contains(nsLower, "dynect"):
return "Dyn (Oracle)"
case strings.Contains(nsLower, "ultradns"):
return "UltraDNS"
case strings.Contains(nsLower, "dnsimple"):
return "DNSimple"
case strings.Contains(nsLower, "hover"):
return "Hover"
case strings.Contains(nsLower, "register.com"):
return "Register.com"
case strings.Contains(nsLower, "enom"):
return "eNom"
case strings.Contains(nsLower, "worldnic"):
return "Network Solutions"
case strings.Contains(nsLower, "zoneedit"):
return "ZoneEdit"
case strings.Contains(nsLower, "easydns"):
return "EasyDNS"
case strings.Contains(nsLower, "gandi"):
return "Gandi"
case strings.Contains(nsLower, "ovh"):
return "OVH"
case strings.Contains(nsLower, "hetzner"):
return "Hetzner"
case strings.Contains(nsLower, "azure-dns"):
return "Microsoft Azure"
}
}
return ""
}
// DetectEmailProvider detects the email provider from MX records
func DetectEmailProvider(mxRecords []string) string {
for _, mx := range mxRecords {
mxLower := strings.ToLower(mx)
// Extract just the hostname part if it has priority prefix
host := mxLower
if idx := strings.Index(mxLower, " "); idx > 0 {
host = strings.TrimSpace(mxLower[idx+1:])
}
host = strings.TrimSuffix(host, ".")
switch {
case strings.Contains(host, "google") || strings.Contains(host, "gmail"):
return "Google Workspace"
case strings.Contains(host, "outlook") || strings.Contains(host, "microsoft") || strings.Contains(host, "protection.outlook"):
return "Microsoft 365"
case strings.Contains(host, "purelymail"):
return "Purelymail"
case strings.Contains(host, "zoho"):
return "Zoho Mail"
case strings.Contains(host, "protonmail") || strings.Contains(host, "pm.me"):
return "ProtonMail"
case strings.Contains(host, "fastmail"):
return "Fastmail"
case strings.Contains(host, "tutanota"):
return "Tutanota"
case strings.Contains(host, "mxroute"):
return "MXroute"
case strings.Contains(host, "namecheap"):
return "Namecheap"
case strings.Contains(host, "icloud") || strings.Contains(host, "me.com"):
return "iCloud Mail"
case strings.Contains(host, "yahoo"):
return "Yahoo"
case strings.Contains(host, "qq.com"):
return "QQ Mail"
case strings.Contains(host, "mail.ru"):
return "Mail.ru"
case strings.Contains(host, "yandex"):
return "Yandex"
case strings.Contains(host, "hover"):
return "Hover"
case strings.Contains(host, "godaddy") || strings.Contains(host, "domaincontrol"):
return "GoDaddy"
case strings.Contains(host, "pobox"):
return "Pobox"
case strings.Contains(host, "runbox"):
return "Runbox"
case strings.Contains(host, "posteo"):
return "Posteo"
case strings.Contains(host, "mailbox.org"):
return "Mailbox.org"
case strings.Contains(host, "forwardemail"):
return "Forward Email"
case strings.Contains(host, "improvmx"):
return "ImprovMX"
case strings.Contains(host, "cloudflare"):
return "Cloudflare Email Routing"
case strings.Contains(host, "amazonaws") || strings.Contains(host, "aws"):
return "Amazon SES"
case strings.Contains(host, "sendgrid") || strings.Contains(host, "twilio"):
return "SendGrid"
case strings.Contains(host, "mailgun"):
return "Mailgun"
case strings.Contains(host, "postmark"):
return "Postmark"
}
}
return ""
}
// DetectHostingProvider detects the hosting provider from HTTP headers
func DetectHostingProvider(headers http.Header) string {
server := strings.ToLower(headers.Get("Server"))
poweredBy := strings.ToLower(headers.Get("X-Powered-By"))
cfRay := headers.Get("CF-Ray")
vercelCache := headers.Get("X-Vercel-Cache")
vercelID := headers.Get("X-Vercel-Id")
netlifyID := headers.Get("X-NF-Request-Id")
githubRequest := headers.Get("X-GitHub-Request-Id")
switch {
case vercelCache != "" || vercelID != "":
return "Vercel"
case netlifyID != "":
return "Netlify"
case githubRequest != "":
return "GitHub Pages"
case cfRay != "":
return "Cloudflare"
case strings.Contains(server, "cloudflare"):
return "Cloudflare"
case strings.Contains(server, "nginx") && vercelCache != "":
return "Vercel"
case strings.Contains(server, "awselb") || strings.Contains(server, "elb"):
return "AWS"
case strings.Contains(server, "amazon"):
return "AWS"
case strings.Contains(server, "microsoft-iis"):
return "Microsoft Azure"
case strings.Contains(server, "google") || strings.Contains(server, "gws"):
return "Google Cloud"
case strings.Contains(server, "heroku"):
return "Heroku"
case strings.Contains(server, "digitalocean"):
return "DigitalOcean"
case strings.Contains(server, "linode"):
return "Linode"
case strings.Contains(server, "ovh"):
return "OVH"
case strings.Contains(server, "hetzner"):
return "Hetzner"
case strings.Contains(server, "fastly"):
return "Fastly"
case strings.Contains(server, "bunnycdn"):
return "BunnyCDN"
case strings.Contains(server, "keycdn"):
return "KeyCDN"
case strings.Contains(server, "stackpath"):
return "StackPath"
case strings.Contains(server, "sucuri"):
return "Sucuri"
case strings.Contains(poweredBy, "next.js") || strings.Contains(poweredBy, "nextjs"):
return "Vercel"
case strings.Contains(poweredBy, "php"):
return "PHP"
case strings.Contains(server, "apache"):
return "Apache"
case strings.Contains(server, "nginx"):
return "nginx"
case strings.Contains(server, "caddy"):
return "Caddy"
case strings.Contains(server, "lighttpd"):
return "Lighttpd"
case strings.Contains(server, "litespeed"):
return "LiteSpeed"
case strings.Contains(server, "openresty"):
return "OpenResty"
case strings.Contains(server, "jetty"):
return "Jetty"
case strings.Contains(server, "tomcat"):
return "Tomcat"
case strings.Contains(server, "iis"):
return "IIS"
}
return ""
}
// DetectCertificateAuthority detects the CA from an issuer string
func DetectCertificateAuthority(issuer string) string {
issuerLower := strings.ToLower(issuer)
switch {
case strings.Contains(issuerLower, "let's encrypt"):
return "Let's Encrypt"
case strings.Contains(issuerLower, "digicert"):
return "DigiCert"
case strings.Contains(issuerLower, "sectigo") || strings.Contains(issuerLower, "comodoca"):
return "Sectigo"
case strings.Contains(issuerLower, "globalsign"):
return "GlobalSign"
case strings.Contains(issuerLower, "geotrust"):
return "GeoTrust"
case strings.Contains(issuerLower, "thawte"):
return "Thawte"
case strings.Contains(issuerLower, "rapidssl"):
return "RapidSSL"
case strings.Contains(issuerLower, "symantec"):
return "Symantec"
case strings.Contains(issuerLower, "entrust"):
return "Entrust"
case strings.Contains(issuerLower, "certum"):
return "Certum"
case strings.Contains(issuerLower, "go daddy") || strings.Contains(issuerLower, "godaddy"):
return "GoDaddy"
case strings.Contains(issuerLower, "amazon"):
return "Amazon"
case strings.Contains(issuerLower, "google") && strings.Contains(issuerLower, "trust"):
return "Google Trust Services"
case strings.Contains(issuerLower, "cloudflare"):
return "Cloudflare"
case strings.Contains(issuerLower, "zero ssl") || strings.Contains(issuerLower, "zerossl"):
return "ZeroSSL"
case strings.Contains(issuerLower, "ssl.com"):
return "SSL.com"
case strings.Contains(issuerLower, "buypass"):
return "Buypass"
case strings.Contains(issuerLower, "harica"):
return "HARICA"
case strings.Contains(issuerLower, " Actalis "):
return "Actalis"
case strings.Contains(issuerLower, "swisssign"):
return "SwissSign"
case strings.Contains(issuerLower, "telekom"):
return "Telekom"
case strings.Contains(issuerLower, "trustwave"):
return "Trustwave"
case strings.Contains(issuerLower, "identrust"):
return "IdenTrust"
case strings.Contains(issuerLower, "usertrust"):
return "UserTrust"
case strings.Contains(issuerLower, "isrg") || strings.Contains(issuerLower, "internet security research"):
return "Let's Encrypt"
}
return issuer
}
+98 -52
View File
@@ -151,13 +151,13 @@ func (s *Scheduler) checkDomain(record *core.Record) error {
oldRecord := record.Fresh()
// Update record (only overwrite if new data is present to preserve valid data on partial lookups)
if newData.ExpiryDate != nil {
if newData.ExpiryDate != nil && newData.ExpiryDate.After(time.Time{}) {
record.Set("expiry_date", *newData.ExpiryDate)
}
if newData.CreationDate != nil {
if newData.CreationDate != nil && newData.CreationDate.After(time.Time{}) {
record.Set("creation_date", *newData.CreationDate)
}
if newData.UpdatedDate != nil {
if newData.UpdatedDate != nil && newData.UpdatedDate.After(time.Time{}) {
record.Set("updated_date", *newData.UpdatedDate)
}
if newData.RegistrarName != "" {
@@ -262,9 +262,28 @@ func (s *Scheduler) checkDomain(record *core.Record) error {
if newData.AbuseEmail != "" {
record.Set("abuse_email", newData.AbuseEmail)
}
if newData.AbusePhone != "" {
record.Set("abuse_phone", newData.AbusePhone)
record.Set("abuse_phone", newData.AbusePhone)
record.Set("dns_provider", newData.DNSProvider)
record.Set("hosting_provider", newData.HostingProvider)
record.Set("email_provider", newData.EmailProvider)
record.Set("ca_provider", newData.CAProvider)
if len(newData.Headers) > 0 {
record.Set("headers", newData.Headers)
}
if len(newData.Certificates) > 0 {
record.Set("certificates", newData.Certificates)
}
if newData.SEOMeta != nil {
record.Set("seo_meta", newData.SEOMeta)
}
record.Set("whois_raw", newData.WHOISRaw)
record.Set("privacy_enabled", newData.PrivacyEnabled)
record.Set("transfer_lock", newData.TransferLock)
record.Set("tld", newData.TLD)
if len(newData.DomainStatuses) > 0 {
record.Set("domain_statuses", newData.DomainStatuses)
}
record.Set("host_country_code", newData.HostCountryCode)
record.Set("last_checked", time.Now())
// Update status - fallback to existing record expiry if new lookup didn't return one
@@ -334,66 +353,38 @@ func (s *Scheduler) checkDomain(record *core.Record) error {
}
}
// Discover and save subdomains
s.discoverSubdomains(record, domainName, userID)
// Discover and save subdomains using enhanced discovery
s.discoverSubdomainsEnhanced(record, domainName, userID)
log.Printf("[domain-scheduler] Updated domain: %s (status: %s)", domainName, status)
return nil
}
// discoverSubdomains discovers and saves subdomains for a domain
// discoverSubdomains discovers and saves subdomains for a domain (legacy method)
func (s *Scheduler) discoverSubdomains(record *core.Record, domainName, userID string) {
// Common subdomains to check
commonSubdomains := []string{
"www", "mail", "ftp", "api", "blog", "shop", "admin", "app", "cdn",
"static", "dev", "staging", "test", "demo", "docs", "support", "help",
"status", "monitor", "grafana", "prometheus", "db", "cache", "redis",
"queue", "worker", "backup", "media", "assets", "download", "upload",
"git", "gitlab", "github", "jenkins", "ci", "cd", "vpn", "ssh",
"smtp", "imap", "mx", "webmail", "email", "analytics", "stats",
"search", "login", "auth", "sso", "oauth", "account", "user",
}
// Deprecated: Use discoverSubdomainsEnhanced instead
s.discoverSubdomainsEnhanced(record, domainName, userID)
}
// Get existing subdomains to avoid duplicates
existing, _ := s.app.FindAllRecords("subdomains",
dbx.NewExp("domain = {:domain}", dbx.Params{"domain": record.Id}),
)
existingMap := make(map[string]bool)
for _, sub := range existing {
existingMap[sub.GetString("subdomain_name")] = true
}
// discoverSubdomainsEnhanced performs enhanced subdomain discovery
func (s *Scheduler) discoverSubdomainsEnhanced(record *core.Record, domainName, userID string) {
discovery := NewSubdomainDiscovery(s.app)
collection, err := s.app.FindCollectionByNameOrId("subdomains")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
results, err := discovery.Discover(ctx, domainName)
if err != nil {
log.Printf("[domain-scheduler] Subdomain discovery failed for %s: %v", domainName, err)
return
}
for _, sub := range commonSubdomains {
if existingMap[sub] {
continue
}
fullDomain := sub + "." + domainName
ips, err := net.LookupHost(fullDomain)
if err != nil || len(ips) == 0 {
continue
}
// Found a valid subdomain
subRecord := core.NewRecord(collection)
subRecord.Set("domain", record.Id)
subRecord.Set("subdomain_name", sub)
subRecord.Set("status", "active")
subRecord.Set("ip_addresses", strings.Join(ips, ","))
subRecord.Set("last_checked", time.Now())
subRecord.Set("user", userID)
if err := s.app.Save(subRecord); err != nil {
log.Printf("[domain-scheduler] Failed to save subdomain %s: %v", fullDomain, err)
} else {
log.Printf("[domain-scheduler] Discovered subdomain: %s", fullDomain)
}
if err := discovery.SaveSubdomains(record, results, userID); err != nil {
log.Printf("[domain-scheduler] Failed to save subdomains for %s: %v", domainName, err)
return
}
log.Printf("[domain-scheduler] Discovered %d subdomains for %s", len(results), domainName)
}
// trackChanges compares old and new data and returns history entries
@@ -450,6 +441,61 @@ func (s *Scheduler) trackChanges(oldRecord *core.Record, newData *domain.Domain,
})
}
// Check provider changes
providers := []struct {
field string
value string
}{
{"dns_provider", newData.DNSProvider},
{"hosting_provider", newData.HostingProvider},
{"email_provider", newData.EmailProvider},
{"ca_provider", newData.CAProvider},
}
for _, p := range providers {
oldVal := oldRecord.GetString(p.field)
if p.value != "" && p.value != oldVal && oldVal != "" {
history = append(history, domain.DomainHistory{
ChangeType: domain.ChangeTypeProvider,
FieldName: p.field,
OldValue: oldVal,
NewValue: p.value,
CreatedAt: now,
})
}
}
// Check security changes
if newData.PrivacyEnabled != oldRecord.GetBool("privacy_enabled") {
history = append(history, domain.DomainHistory{
ChangeType: domain.ChangeTypeSecurity,
FieldName: "privacy_enabled",
OldValue: fmt.Sprintf("%t", oldRecord.GetBool("privacy_enabled")),
NewValue: fmt.Sprintf("%t", newData.PrivacyEnabled),
CreatedAt: now,
})
}
if newData.TransferLock != oldRecord.GetBool("transfer_lock") {
history = append(history, domain.DomainHistory{
ChangeType: domain.ChangeTypeSecurity,
FieldName: "transfer_lock",
OldValue: fmt.Sprintf("%t", oldRecord.GetBool("transfer_lock")),
NewValue: fmt.Sprintf("%t", newData.TransferLock),
CreatedAt: now,
})
}
// Check host country code change
oldCountryCode := oldRecord.GetString("host_country_code")
if newData.HostCountryCode != "" && newData.HostCountryCode != oldCountryCode && oldCountryCode != "" {
history = append(history, domain.DomainHistory{
ChangeType: domain.ChangeTypeHost,
FieldName: "host_country_code",
OldValue: oldCountryCode,
NewValue: newData.HostCountryCode,
CreatedAt: now,
})
}
return history
}
+477
View File
@@ -0,0 +1,477 @@
package domains
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
// SubdomainDiscovery handles advanced subdomain discovery
type SubdomainDiscovery struct {
app core.App
client *http.Client
wordlist []string
}
// NewSubdomainDiscovery creates a new subdomain discovery service
func NewSubdomainDiscovery(app core.App) *SubdomainDiscovery {
return &SubdomainDiscovery{
app: app,
client: &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
wordlist: getEnhancedWordlist(),
}
}
// DiscoveryResult represents a discovered subdomain
type DiscoveryResult struct {
Subdomain string `json:"subdomain"`
FullDomain string `json:"full_domain"`
IPAddresses []string `json:"ip_addresses"`
StatusCode int `json:"status_code,omitempty"`
Server string `json:"server,omitempty"`
Source string `json:"source"` // dns, certificate, http, etc.
FoundAt time.Time `json:"found_at"`
}
// Discover performs comprehensive subdomain discovery
func (sd *SubdomainDiscovery) Discover(ctx context.Context, domainName string) ([]DiscoveryResult, error) {
var results []DiscoveryResult
var mu sync.Mutex
var wg sync.WaitGroup
// Create a channel for results
resultChan := make(chan DiscoveryResult, 100)
// Start multiple discovery methods concurrently
wg.Add(4)
// 1. DNS brute force with enhanced wordlist
go func() {
defer wg.Done()
sd.dnsBruteForce(ctx, domainName, resultChan)
}()
// 2. Certificate transparency log search
go func() {
defer wg.Done()
sd.ctLogSearch(ctx, domainName, resultChan)
}()
// 3. DNS enumeration via common patterns
go func() {
defer wg.Done()
sd.patternEnumeration(ctx, domainName, resultChan)
}()
// 4. HTTP probe for common subdomains
go func() {
defer wg.Done()
sd.httpProbe(ctx, domainName, resultChan)
}()
// Collect results in a separate goroutine
go func() {
wg.Wait()
close(resultChan)
}()
// Process results
seen := make(map[string]bool)
for result := range resultChan {
if seen[result.Subdomain] {
continue
}
seen[result.Subdomain] = true
mu.Lock()
results = append(results, result)
mu.Unlock()
}
return results, nil
}
// dnsBruteForce performs DNS brute forcing with wordlist
func (sd *SubdomainDiscovery) dnsBruteForce(ctx context.Context, domainName string, results chan<- DiscoveryResult) {
semaphore := make(chan struct{}, 20) // Limit concurrency
var wg sync.WaitGroup
for _, word := range sd.wordlist {
select {
case <-ctx.Done():
return
default:
}
wg.Add(1)
semaphore <- struct{}{}
go func(word string) {
defer wg.Done()
defer func() { <-semaphore }()
subdomain := word + "." + domainName
ips, err := net.LookupHost(subdomain)
if err != nil || len(ips) == 0 {
return
}
results <- DiscoveryResult{
Subdomain: word,
FullDomain: subdomain,
IPAddresses: ips,
Source: "dns",
FoundAt: time.Now(),
}
}(word)
}
wg.Wait()
}
// ctLogSearch searches certificate transparency logs via crt.sh
func (sd *SubdomainDiscovery) ctLogSearch(ctx context.Context, domainName string, results chan<- DiscoveryResult) {
// Query crt.sh for certificates
url := fmt.Sprintf("https://crt.sh/?q=%%.%s&output=json", domainName)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
log.Printf("[subdomain-discovery] CT log search failed for %s: %v", domainName, err)
return
}
resp, err := sd.client.Do(req)
if err != nil {
log.Printf("[subdomain-discovery] CT log search failed for %s: %v", domainName, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("[subdomain-discovery] CT log search returned status %d for %s", resp.StatusCode, domainName)
return
}
// Parse crt.sh JSON response
var entries []struct {
NameValue string `json:"name_value"`
}
if err := json.NewDecoder(resp.Body).Decode(&entries); err != nil {
log.Printf("[subdomain-discovery] Failed to parse CT log response for %s: %v", domainName, err)
return
}
seen := make(map[string]bool)
for _, entry := range entries {
// crt.sh returns one name_value per line, may contain wildcards or multiple names
names := strings.Split(entry.NameValue, "\n")
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" || name == domainName {
continue
}
// Remove wildcard prefix
name = strings.TrimPrefix(name, "*.")
// Only include subdomains of the target domain
if !strings.HasSuffix(name, "."+domainName) {
continue
}
subdomain := strings.TrimSuffix(name, "."+domainName)
if subdomain == "" || seen[subdomain] {
continue
}
seen[subdomain] = true
// Try to resolve IPs
ips, _ := net.LookupHost(name)
results <- DiscoveryResult{
Subdomain: subdomain,
FullDomain: name,
IPAddresses: ips,
Source: "certificate",
FoundAt: time.Now(),
}
}
}
log.Printf("[subdomain-discovery] CT log search found %d unique subdomains for %s", len(seen), domainName)
}
// patternEnumeration enumerates common subdomain patterns
func (sd *SubdomainDiscovery) patternEnumeration(ctx context.Context, domainName string, results chan<- DiscoveryResult) {
patterns := []string{
"api-v1", "api-v2", "api-v3", "api-dev", "api-staging", "api-prod",
"app-dev", "app-staging", "app-prod", "web-dev", "web-staging",
"admin-dev", "admin-staging", "admin-prod",
"portal-dev", "portal-staging", "portal-prod",
"dashboard-dev", "dashboard-staging", "dashboard-prod",
"service-1", "service-2", "service-3",
"node-1", "node-2", "node-3",
"server-1", "server-2", "server-3",
"web-01", "web-02", "web-03",
"app-01", "app-02", "app-03",
"us-east", "us-west", "eu-west", "eu-central", "ap-south", "ap-northeast",
}
semaphore := make(chan struct{}, 10)
var wg sync.WaitGroup
for _, pattern := range patterns {
select {
case <-ctx.Done():
return
default:
}
wg.Add(1)
semaphore <- struct{}{}
go func(pattern string) {
defer wg.Done()
defer func() { <-semaphore }()
subdomain := pattern + "." + domainName
ips, err := net.LookupHost(subdomain)
if err != nil || len(ips) == 0 {
return
}
results <- DiscoveryResult{
Subdomain: pattern,
FullDomain: subdomain,
IPAddresses: ips,
Source: "pattern",
FoundAt: time.Now(),
}
}(pattern)
}
wg.Wait()
}
// httpProbe probes common subdomains via HTTP
func (sd *SubdomainDiscovery) httpProbe(ctx context.Context, domainName string, results chan<- DiscoveryResult) {
commonWebSubdomains := []string{"www", "app", "api", "admin", "dashboard", "portal", "web"}
semaphore := make(chan struct{}, 5)
var wg sync.WaitGroup
for _, sub := range commonWebSubdomains {
select {
case <-ctx.Done():
return
default:
}
wg.Add(1)
semaphore <- struct{}{}
go func(sub string) {
defer wg.Done()
defer func() { <-semaphore }()
// Try HTTPS first
url := fmt.Sprintf("https://%s.%s", sub, domainName)
resp, err := sd.client.Get(url)
if err != nil {
// Try HTTP
url = fmt.Sprintf("http://%s.%s", sub, domainName)
resp, err = sd.client.Get(url)
if err != nil {
return
}
}
defer resp.Body.Close()
// Get IP addresses
subdomain := sub + "." + domainName
ips, _ := net.LookupHost(subdomain)
results <- DiscoveryResult{
Subdomain: sub,
FullDomain: subdomain,
IPAddresses: ips,
StatusCode: resp.StatusCode,
Server: resp.Header.Get("Server"),
Source: "http",
FoundAt: time.Now(),
}
}(sub)
}
wg.Wait()
}
// SaveSubdomains saves discovered subdomains to the database
func (sd *SubdomainDiscovery) SaveSubdomains(domainRecord *core.Record, results []DiscoveryResult, userID string) error {
collection, err := sd.app.FindCollectionByNameOrId("subdomains")
if err != nil {
return err
}
// Get existing subdomains to avoid duplicates
existing, _ := sd.app.FindAllRecords("subdomains",
dbx.NewExp("domain = {:domain}", dbx.Params{"domain": domainRecord.Id}),
)
existingMap := make(map[string]bool)
for _, sub := range existing {
existingMap[sub.GetString("subdomain_name")] = true
}
for _, result := range results {
if existingMap[result.Subdomain] {
continue
}
record := core.NewRecord(collection)
record.Set("domain", domainRecord.Id)
record.Set("subdomain_name", result.Subdomain)
record.Set("full_domain", result.FullDomain)
record.Set("status", "active")
record.Set("ip_addresses", strings.Join(result.IPAddresses, ","))
record.Set("discovery_source", result.Source)
record.Set("last_checked", time.Now())
record.Set("user", userID)
if result.StatusCode > 0 {
record.Set("http_status", result.StatusCode)
}
if result.Server != "" {
record.Set("server_header", result.Server)
}
if err := sd.app.Save(record); err != nil {
log.Printf("[subdomain-discovery] Failed to save subdomain %s: %v", result.FullDomain, err)
} else {
log.Printf("[subdomain-discovery] Saved subdomain: %s (source: %s)", result.FullDomain, result.Source)
}
}
return nil
}
// getEnhancedWordlist returns an enhanced wordlist for subdomain discovery
func getEnhancedWordlist() []string {
// Comprehensive wordlist including common subdomains
wordlist := []string{
// Common web
"www", "mail", "ftp", "localhost", "admin", "dashboard", "portal",
"api", "app", "mobile", "dev", "test", "staging", "demo", "beta",
"prod", "production", "live", "www2", "www1", "blog", "shop", "store",
"support", "help", "docs", "documentation", "wiki", "forum", "community",
"news", "media", "cdn", "static", "assets", "files", "download", "upload",
"images", "img", "css", "js", "scripts", "resources", "public", "private",
// Services
"api-v1", "api-v2", "api-v3", "api-dev", "api-staging", "api-prod",
"graphql", "rest", "grpc", "websocket", "socket", "ws", "wss",
"oauth", "auth", "login", "signin", "signup", "register", "sso",
"saml", "oidc", "openid", "keycloak", "auth0", "cognito", "okta",
// Infrastructure
"server", "server1", "server2", "server3", "srv", "srv1", "srv2",
"node", "node1", "node2", "node3", "worker", "worker1", "worker2",
"web", "web1", "web2", "web3", "app1", "app2", "app3",
"db", "database", "mysql", "postgres", "mongodb", "redis", "elasticsearch",
"cache", "queue", "rabbitmq", "kafka", "zookeeper", "etcd",
// Cloud & DevOps
"k8s", "kubernetes", "docker", "swarm", "nomad", "consul", "vault",
"terraform", "ansible", "puppet", "chef", "salt", "jenkins", "gitlab",
"github", "bitbucket", "travis", "circleci", "drone", "argo", "spinnaker",
"prometheus", "grafana", "alertmanager", "thanos", "loki", "tempo",
"jaeger", "zipkin", "kibana", "elk", "efk", "splunk", "datadog",
// Security
"vpn", "ipsec", "openvpn", "wireguard", "fortinet", "paloalto",
"firewall", "waf", "ids", "ips", "siem", "soc", "nids",
"scan", "scanner", "security", "sec", "pentest", "vulnerability",
// Monitoring & Logging
"monitor", "monitoring", "status", "health", "healthcheck", "ping",
"uptime", "metrics", "logs", "logging", "audit", "trace", "apm",
"sentry", "bugsnag", "raygun", "rollbar", "airbrake", "honeybadger",
"newrelic", "appdynamics", "dynatrace", "instana", "scout", "skylight",
// Communication
"mail", "email", "smtp", "pop", "imap", "exchange", "webmail",
"mailgun", "sendgrid", "mailchimp", " SES", "postmark", "sparkpost",
"chat", "slack", "teams", "discord", "zoom", "meet", "webex",
"jitsi", "mattermost", "rocket", "element", "matrix",
// Regions
"us", "us-east", "us-west", "us-central", "us-south",
"eu", "eu-west", "eu-central", "eu-east", "eu-north", "eu-south",
"ap", "ap-south", "ap-northeast", "ap-southeast", "ap-east",
"sa", "sa-east", "af", "af-south", "me", "me-south",
// Environments
"sandbox", "playground", "lab", "labs", "experiment", "canary",
"blue", "green", "a", "b", "alpha", "beta", "gamma", "delta",
"rc", "release", "nightly", "stable", "latest", "edge", "lts",
// Corporate
"corp", "corporate", "enterprise", "business", "company", "org",
"intranet", "extranet", "partners", "vendors", "suppliers",
"hr", "finance", "accounting", "legal", "compliance", "it",
"sales", "marketing", "crm", "erp", "scm", "plm", "hrm",
// Development tools
"git", "svn", "cvs", "mercurial", "hg", "bitkeeper",
"repo", "repository", "repos", "source", "code", "src",
"build", "ci", "cd", "deploy", "deployment", "release",
"artifact", "artifacts", "package", "packages", "registry",
// Testing
"qa", "qc", "test", "testing", "tests", "spec", "specs",
"unit", "integration", "e2e", "acceptance", "regression",
"mock", "stub", "fake", "dummy", "sandbox", "fixture",
// Archive & Backup
"archive", "archives", "backup", "backups", "snapshot", "snapshots",
"old", "legacy", "deprecated", "retired", "historical",
// Miscellaneous
"search", "find", "lookup", "query", "browse", "explorer",
"api-docs", "swagger", "openapi", "redoc", "postman", "graphql-playground",
"status-page", "statuspage", "trust", "security", "compliance",
"terms", "privacy", "legal", "about", "contact", "feedback",
"careers", "jobs", "team", "people", "staff", "employees",
"press", "media-kit", "brand", "assets", "styleguide", "design",
"sitemap", "robots", "humans", "security", "pgp", "key",
"invite", "join", "apply", "subscribe", "newsletter", "rss", "atom",
"mobile", "m", "touch", "app", "ios", "android", "windows", "mac",
"download", "downloads", "dl", "installer", "setup", "update", "upgrade",
"payment", "pay", "billing", "invoice", "subscription", "checkout",
"cart", "basket", "shop", "store", "buy", "purchase", "order",
"account", "my", "profile", "user", "users", "member", "members",
"session", "token", "auth", "verify", "validation", "confirm",
"reset", "recovery", "forgot", "password", "pass", "pwd",
"2fa", "mfa", "totp", "otp", "authenticator", "security-key",
"webhook", "callback", "hook", "integration", "connector", "adapter",
"plugin", "extension", "addon", "module", "component", "widget",
"embed", "iframe", "frame", "proxy", "gateway", "ingress", "egress",
"loadbalancer", "lb", "vip", "floating", "virtual", "ha", "failover",
"primary", "secondary", "master", "slave", "leader", "follower",
"active", "standby", "hot", "warm", "cold", "mirror", "replica",
"shard", "shards", "partition", "partitions", "segment", "segments",
}
return wordlist
}
File diff suppressed because it is too large Load Diff
+116 -10
View File
@@ -7,6 +7,8 @@ import (
"time"
"github.com/henrygd/beszel/internal/entities/monitor"
"github.com/henrygd/beszel/internal/hub/pagespeed"
"github.com/henrygd/beszel/internal/hub/utils"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
)
@@ -40,16 +42,24 @@ func (h *APIHandler) RegisterRoutes(se *core.ServeEvent) {
// CRUD endpoints
api.GET("", h.listMonitors)
api.POST("", h.createMonitor)
api.GET("/:id", h.getMonitor)
api.PATCH("/:id", h.updateMonitor)
api.DELETE("/:id", h.deleteMonitor)
api.GET("/{id}", h.getMonitor)
api.PATCH("/{id}", h.updateMonitor)
api.DELETE("/{id}", h.deleteMonitor)
// Action endpoints
api.POST("/:id/check", h.manualCheck)
api.POST("/:id/pause", h.pauseMonitor)
api.POST("/:id/resume", h.resumeMonitor)
api.GET("/:id/stats", h.getStats)
api.GET("/:id/heartbeats", h.getHeartbeats)
api.POST("/{id}/check", h.manualCheck)
api.POST("/{id}/pause", h.pauseMonitor)
api.POST("/{id}/resume", h.resumeMonitor)
api.GET("/{id}/stats", h.getStats)
api.GET("/{id}/heartbeats", h.getHeartbeats)
api.POST("/{id}/pagespeed", h.runPageSpeedCheck)
}
// HeartbeatSummary represents a minimal heartbeat for the monitor list
type HeartbeatSummary struct {
Status string `json:"status"`
Ping int `json:"ping"`
Time time.Time `json:"time"`
}
// MonitorResponse represents a monitor in API responses
@@ -69,6 +79,7 @@ type MonitorResponse struct {
Description string `json:"description,omitempty"`
LastCheck *time.Time `json:"last_check,omitempty"`
UptimeStats map[string]float64 `json:"uptime_stats,omitempty"`
RecentHeartbeats []HeartbeatSummary `json:"recent_heartbeats,omitempty"`
Tags []string `json:"tags,omitempty"`
Keyword string `json:"keyword,omitempty"`
JSONQuery string `json:"json_query,omitempty"`
@@ -165,6 +176,35 @@ func (h *APIHandler) listMonitors(e *core.RequestEvent) error {
})
}
// getRecentHeartbeats fetches the last N heartbeats for a monitor
func (h *APIHandler) getRecentHeartbeats(monitorID string, limit int) []HeartbeatSummary {
records, err := h.app.FindRecordsByFilter(
"monitor_heartbeats",
"monitor = {:monitorId}",
"-time",
limit,
0,
map[string]any{"monitorId": monitorID},
)
if err != nil {
return nil
}
heartbeats := make([]HeartbeatSummary, 0, len(records))
for _, hb := range records {
var t time.Time
if ts := hb.GetDateTime("time"); !ts.IsZero() {
t = ts.Time()
}
heartbeats = append(heartbeats, HeartbeatSummary{
Status: hb.GetString("status"),
Ping: hb.GetInt("ping"),
Time: t,
})
}
return heartbeats
}
// getMonitor returns a single monitor by ID
func (h *APIHandler) getMonitor(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
@@ -182,7 +222,10 @@ func (h *APIHandler) getMonitor(e *core.RequestEvent) error {
return e.ForbiddenError("Access denied", nil)
}
return e.JSON(http.StatusOK, recordToResponse(record))
resp := recordToResponse(record)
resp.RecentHeartbeats = h.getRecentHeartbeats(id, 12)
return e.JSON(http.StatusOK, resp)
}
// createMonitor creates a new monitor
@@ -253,8 +296,12 @@ func (h *APIHandler) createMonitor(e *core.RequestEvent) error {
h.scheduler.AddMonitor(record)
// Run initial check synchronously so the monitor shows real status immediately
if _, err := h.scheduler.RunManualCheck(record.Id); err != nil {
result, err := h.scheduler.RunManualCheck(record.Id)
if err != nil {
log.Printf("[monitor-api] Initial check failed for %s: %v", record.Id, err)
// Note: The monitor will remain in pending status and the scheduler will retry
} else {
log.Printf("[monitor-api] Initial check completed for %s: status=%s, ping=%v", record.Id, result.Status, result.Ping)
}
// Re-fetch the updated record to get the new status
@@ -565,6 +612,65 @@ func (h *APIHandler) getStats(e *core.RequestEvent) error {
})
}
// runPageSpeedCheck runs a PageSpeed Insights check for a monitor
func (h *APIHandler) runPageSpeedCheck(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
if id == "" {
return e.BadRequestError("Monitor ID is required", nil)
}
record, err := h.app.FindRecordById("monitors", id)
if err != nil {
return e.NotFoundError("Monitor not found", err)
}
if record.GetString("user") != e.Auth.Id {
return e.ForbiddenError("Access denied", nil)
}
url := record.GetString("url")
if url == "" {
return e.BadRequestError("Monitor does not have a URL", nil)
}
// Get strategy from query param, default to mobile
strategy := e.Request.URL.Query().Get("strategy")
if strategy == "" {
strategy = "mobile"
}
if strategy != "mobile" && strategy != "desktop" {
return e.BadRequestError("strategy must be 'mobile' or 'desktop'", nil)
}
apiKey, _ := utils.GetEnv("PAGESPEED_API_KEY")
checker := pagespeed.NewChecker(apiKey)
metrics, err := checker.CheckURL(url, strategy)
if err != nil {
return e.BadRequestError("PageSpeed check failed: "+err.Error(), err)
}
vitals := pagespeed.GetCoreWebVitalsStatus(metrics)
return e.JSON(http.StatusOK, map[string]interface{}{
"performance": metrics.Performance,
"accessibility": metrics.Accessibility,
"bestPractices": metrics.BestPractices,
"seo": metrics.SEO,
"pwa": metrics.PWA,
"fcp": metrics.FCP,
"lcp": metrics.LCP,
"ttfb": metrics.TTFB,
"cls": metrics.CLS,
"tbt": metrics.TBT,
"speedIndex": metrics.SpeedIndex,
"tti": metrics.TTI,
"strategy": metrics.Strategy,
"checkedAt": metrics.CheckedAt,
"url": metrics.URL,
"vitals": vitals,
})
}
// getHeartbeats returns recent heartbeats for a monitor
func (h *APIHandler) getHeartbeats(e *core.RequestEvent) error {
id := e.Request.PathValue("id")
+29 -18
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"time"
)
@@ -65,21 +66,21 @@ type PageSpeedResponse struct {
// Metrics represents the extracted performance metrics
type Metrics struct {
Performance float64 `json:"performance"`
Accessibility float64 `json:"accessibility"`
BestPractices float64 `json:"bestPractices"`
SEO float64 `json:"seo"`
PWA float64 `json:"pwa"`
FCP float64 `json:"fcp"` // First Contentful Paint (ms)
LCP float64 `json:"lcp"` // Largest Contentful Paint (ms)
TTFB float64 `json:"ttfb"` // Time to First Byte (ms)
CLS float64 `json:"cls"` // Cumulative Layout Shift
TBT float64 `json:"tbt"` // Total Blocking Time (ms)
SpeedIndex float64 `json:"speedIndex"` // Speed Index (ms)
TTI float64 `json:"tti"` // Time to Interactive (ms)
CheckedAt time.Time `json:"checkedAt"`
URL string `json:"url"`
Strategy string `json:"strategy"` // mobile or desktop
Performance float64 `json:"performance"`
Accessibility float64 `json:"accessibility"`
BestPractices float64 `json:"bestPractices"`
SEO float64 `json:"seo"`
PWA float64 `json:"pwa"`
FCP float64 `json:"fcp"` // First Contentful Paint (ms)
LCP float64 `json:"lcp"` // Largest Contentful Paint (ms)
TTFB float64 `json:"ttfb"` // Time to First Byte (ms)
CLS float64 `json:"cls"` // Cumulative Layout Shift
TBT float64 `json:"tbt"` // Total Blocking Time (ms)
SpeedIndex float64 `json:"speedIndex"` // Speed Index (ms)
TTI float64 `json:"tti"` // Time to Interactive (ms)
CheckedAt time.Time `json:"checkedAt"`
URL string `json:"url"`
Strategy string `json:"strategy"` // mobile or desktop
}
// Checker handles PageSpeed checks
@@ -99,7 +100,7 @@ func NewChecker(apiKey string) *Checker {
}
// CheckURL runs a PageSpeed check on a URL
func (c *Checker) CheckURL(url string, strategy string) (*Metrics, error) {
func (c *Checker) CheckURL(pageURL string, strategy string) (*Metrics, error) {
if strategy == "" {
strategy = "mobile"
}
@@ -107,7 +108,7 @@ func (c *Checker) CheckURL(url string, strategy string) (*Metrics, error) {
// Build PageSpeed API URL
apiURL := fmt.Sprintf(
"https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=%s&strategy=%s&category=PERFORMANCE&category=ACCESSIBILITY&category=BEST_PRACTICES&category=SEO&category=PWA",
url,
url.QueryEscape(pageURL),
strategy,
)
@@ -132,7 +133,7 @@ func (c *Checker) CheckURL(url string, strategy string) (*Metrics, error) {
}
metrics := &Metrics{
URL: url,
URL: pageURL,
Strategy: strategy,
CheckedAt: time.Now(),
Performance: result.LighthouseResult.Categories.Performance.Score * 100,
@@ -192,6 +193,7 @@ func GetCoreWebVitalsStatus(metrics *Metrics) map[string]string {
"cls": getCLSStatus(metrics.CLS),
"fcp": getFCPStatus(metrics.FCP),
"ttfb": getTTFBStatus(metrics.TTFB),
"tti": getTTIStatus(metrics.TTI),
}
}
@@ -240,6 +242,15 @@ func getTTFBStatus(value float64) string {
return "poor"
}
func getTTIStatus(value float64) string {
if value <= 3800 {
return "good"
} else if value <= 7300 {
return "needs-improvement"
}
return "poor"
}
// FormatDuration formats milliseconds to readable string
func FormatDuration(ms float64) string {
if ms < 1000 {
@@ -0,0 +1,65 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
if err := enhanceDomainCollection(app); err != nil {
return err
}
return nil
}, func(app core.App) error {
return nil
})
}
func enhanceDomainCollection(app core.App) error {
collection, err := app.FindCollectionByNameOrId("domains")
if err != nil {
return err
}
// Provider detection fields
addTextField3(collection, "dns_provider")
addTextField3(collection, "hosting_provider")
addTextField3(collection, "email_provider")
addTextField3(collection, "ca_provider")
// JSON fields for complex data
addJSONField3(collection, "headers")
addJSONField3(collection, "certificates")
addJSONField3(collection, "seo_meta")
addJSONField3(collection, "domain_statuses")
// WHOIS and registration fields
addTextField3(collection, "whois_raw")
addBoolField3(collection, "privacy_enabled")
addBoolField3(collection, "transfer_lock")
addTextField3(collection, "tld")
// Enhanced geo
addTextField3(collection, "host_country_code")
return app.Save(collection)
}
func addTextField3(collection *core.Collection, name string) {
if collection.Fields.GetByName(name) == nil {
collection.Fields.Add(&core.TextField{Name: name})
}
}
func addBoolField3(collection *core.Collection, name string) {
if collection.Fields.GetByName(name) == nil {
collection.Fields.Add(&core.BoolField{Name: name})
}
}
func addJSONField3(collection *core.Collection, name string) {
if collection.Fields.GetByName(name) == nil {
collection.Fields.Add(&core.JSONField{Name: name})
}
}
@@ -5,11 +5,25 @@ import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Link } from "@/components/router"
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, AlertCircle, Globe, Shield } from "lucide-react"
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, AlertCircle, Globe, Shield, Filter, X } from "lucide-react"
import { getCalendarEvents, type CalendarEvent } from "@/lib/incidents"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function CalendarView() {
const [currentDate, setCurrentDate] = useState(new Date())
const [eventFilters, setEventFilters] = useState({
domain_expiry: true,
ssl_expiry: true,
incident: true,
})
const year = currentDate.getFullYear()
const month = currentDate.getMonth()
@@ -46,20 +60,22 @@ export function CalendarView() {
// Days of month
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`
const dayEvents = events?.filter((e) => e.date === dateStr) || []
const dayEvents = events?.filter((e) =>
e.date === dateStr && eventFilters[e.type as keyof typeof eventFilters]
) || []
d.push({ day, events: dayEvents })
}
return d
}, [year, month, daysInMonth, firstDayOfMonth, events])
}, [year, month, daysInMonth, firstDayOfMonth, events, eventFilters])
const upcomingEvents = useMemo(() => {
const today = toDateString(new Date())
return (events || [])
.filter((event) => event.date >= today)
.filter((event) => event.date >= today && eventFilters[event.type as keyof typeof eventFilters])
.sort((a, b) => a.date.localeCompare(b.date))
.slice(0, 8)
}, [events])
}, [events, eventFilters])
const prevMonth = () => {
setCurrentDate(new Date(year, month - 1, 1))
@@ -123,26 +139,116 @@ export function CalendarView() {
return (
<Card className="w-full">
<CardHeader className="pb-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="flex items-center gap-2 text-lg sm:text-xl">
<div className="p-2 bg-primary/10 rounded-lg">
<CalendarIcon className="h-5 w-5 text-primary" />
<div className="flex flex-col gap-4">
{/* Title Row */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<CardTitle className="flex items-center gap-2 text-lg sm:text-xl">
<div className="p-2 bg-primary/10 rounded-lg">
<CalendarIcon className="h-5 w-5 text-primary" />
</div>
<span>Calendar View</span>
</CardTitle>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setCurrentDate(new Date())} className="h-8 text-xs">
Today
</Button>
<Button variant="outline" size="icon" onClick={prevMonth} className="h-8 w-8">
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="font-semibold min-w-[120px] sm:min-w-[160px] text-center text-sm sm:text-base px-2">
{monthNames[month]} {year}
</span>
<Button variant="outline" size="icon" onClick={nextMonth} className="h-8 w-8">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
{/* Filter Controls Row */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-muted-foreground">Show:</span>
<div className="flex flex-wrap gap-1">
<Button
variant={eventFilters.domain_expiry ? "default" : "outline"}
size="sm"
onClick={() => setEventFilters(prev => ({ ...prev, domain_expiry: !prev.domain_expiry }))}
className="h-7 text-xs gap-1"
>
<Globe className="h-3 w-3" />
Domain
</Button>
<Button
variant={eventFilters.ssl_expiry ? "default" : "outline"}
size="sm"
onClick={() => setEventFilters(prev => ({ ...prev, ssl_expiry: !prev.ssl_expiry }))}
className="h-7 text-xs gap-1"
>
<Shield className="h-3 w-3" />
SSL
</Button>
<Button
variant={eventFilters.incident ? "default" : "outline"}
size="sm"
onClick={() => setEventFilters(prev => ({ ...prev, incident: !prev.incident }))}
className="h-7 text-xs gap-1"
>
<AlertCircle className="h-3 w-3" />
Incidents
</Button>
</div>
</div>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-7 text-xs">
<Filter className="h-3 w-3 mr-1" />
Quick Filters
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Event Types</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={eventFilters.domain_expiry}
onCheckedChange={(checked) => setEventFilters(prev => ({ ...prev, domain_expiry: checked }))}
>
<div className="flex items-center gap-2">
<Globe className="h-3 w-3" />
Domain Expiry
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={eventFilters.ssl_expiry}
onCheckedChange={(checked) => setEventFilters(prev => ({ ...prev, ssl_expiry: checked }))}
>
<div className="flex items-center gap-2">
<Shield className="h-3 w-3" />
SSL Expiry
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={eventFilters.incident}
onCheckedChange={(checked) => setEventFilters(prev => ({ ...prev, incident: checked }))}
>
<div className="flex items-center gap-2">
<AlertCircle className="h-3 w-3" />
Incidents
</div>
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setEventFilters({ domain_expiry: true, ssl_expiry: true, incident: true })}>
Show All
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEventFilters({ domain_expiry: true, ssl_expiry: false, incident: false })}>
Domain Only
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setEventFilters({ domain_expiry: false, ssl_expiry: true, incident: false })}>
SSL Only
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<span>Calendar View</span>
</CardTitle>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setCurrentDate(new Date())} className="h-8 text-xs">
Today
</Button>
<Button variant="outline" size="icon" onClick={prevMonth} className="h-8 w-8">
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="font-semibold min-w-[120px] sm:min-w-[160px] text-center text-sm sm:text-base px-2">
{monthNames[month]} {year}
</span>
<Button variant="outline" size="icon" onClick={nextMonth} className="h-8 w-8">
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
@@ -36,7 +36,7 @@ import {
type UpdateDomainRequest,
type DomainLookupResult,
} from "@/lib/domains"
import { Loader2, Search } from "lucide-react"
import { Loader2, Search, AlertTriangle, Calendar } from "lucide-react"
const formSchema = z.object({
domain_name: z.string().min(1, "Domain name is required"),
@@ -79,6 +79,13 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
const [activeTab, setActiveTab] = useState("basic")
const [lookupData, setLookupData] = useState<DomainLookupResult | null>(null)
const [isLookingUp, setIsLookingUp] = useState(false)
// Manual expiry inputs when WHOIS fails
const [manualRegDate, setManualRegDate] = useState(() => {
const today = new Date()
return today.toISOString().split("T")[0]
})
const [manualRegPeriod, setManualRegPeriod] = useState<number>(1)
const [manualPurchasePrice, setManualPurchasePrice] = useState<number>(0)
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
@@ -163,6 +170,10 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
quiet_hours_end: "08:00",
})
setLookupData(null)
const today = new Date().toISOString().split("T")[0]
setManualRegDate(today)
setManualRegPeriod(1)
setManualPurchasePrice(0)
}
}, [open, isEdit, domain, form])
@@ -207,6 +218,11 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
try {
const data = await lookupDomain(domainName)
setLookupData(data)
// Reset manual inputs on new lookup
const today = new Date().toISOString().split("T")[0]
setManualRegDate(today)
setManualRegPeriod(1)
setManualPurchasePrice(0)
toast({ title: "Domain info retrieved successfully" })
} catch (error) {
toast({
@@ -219,6 +235,17 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
}
}
// Calculate expiry date from registration date + period in years
const calculateExpiryDate = (regDateStr: string, years: number): string | null => {
const regDate = new Date(regDateStr)
if (isNaN(regDate.getTime())) return null
const expiry = new Date(regDate)
expiry.setFullYear(expiry.getFullYear() + years)
// Subtract 1 day (expiry is typically the day before the anniversary)
expiry.setDate(expiry.getDate() - 1)
return expiry.toISOString().split("T")[0]
}
const onSubmit = (data: FormData) => {
const payload: CreateDomainRequest = {
domain_name: cleanDomain(data.domain_name),
@@ -247,6 +274,19 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
quiet_hours_end: data.quiet_hours_enabled ? data.quiet_hours_end : undefined,
}
// If lookup returned no expiry, attach manual dates
if (!isEdit && lookupData && !lookupData.expiry_date) {
const calculatedExpiry = calculateExpiryDate(manualRegDate, manualRegPeriod)
if (calculatedExpiry) {
payload.expiry_date = calculatedExpiry
payload.creation_date = manualRegDate
}
// Use the manual purchase price if set (overrides form value when WHOIS fails)
if (manualPurchasePrice > 0) {
payload.purchase_price = manualPurchasePrice
}
}
if (isEdit && domain) {
updateMutation.mutate({
id: domain.id,
@@ -384,19 +424,103 @@ export function DomainDialog({ open, onOpenChange, domain, isEdit = false }: Dom
/>
{lookupData && !isEdit && (
<div className="rounded-lg border p-4 space-y-2">
<h4 className="font-medium">Lookup Results</h4>
{lookupData.registrar_name && (
<p className="text-sm">Registrar: {lookupData.registrar_name}</p>
)}
{lookupData.expiry_date && (
<p className="text-sm">Expires: {lookupData.expiry_date}</p>
)}
{lookupData.ssl_valid_to && (
<p className="text-sm">SSL Expires: {lookupData.ssl_valid_to}</p>
)}
{lookupData.host_country && (
<p className="text-sm">Location: {lookupData.host_country}</p>
<div className="space-y-3">
{/* Lookup Results */}
<div className="rounded-lg border p-4 space-y-2">
<h4 className="font-medium">Lookup Results</h4>
{lookupData.registrar_name && (
<p className="text-sm">Registrar: {lookupData.registrar_name}</p>
)}
{lookupData.expiry_date && (
<p className="text-sm">Expires: {lookupData.expiry_date}</p>
)}
{lookupData.ssl_valid_to && (
<p className="text-sm">SSL Expires: {lookupData.ssl_valid_to}</p>
)}
{lookupData.host_country && (
<p className="text-sm">Location: {lookupData.host_country}</p>
)}
{lookupData.dns_provider && (
<p className="text-sm">DNS: {lookupData.dns_provider}</p>
)}
{lookupData.hosting_provider && (
<p className="text-sm">Hosting: {lookupData.hosting_provider}</p>
)}
{lookupData.email_provider && (
<p className="text-sm">Email: {lookupData.email_provider}</p>
)}
{lookupData.ca_provider && (
<p className="text-sm">CA: {lookupData.ca_provider}</p>
)}
</div>
{/* Manual expiry fallback when WHOIS doesn't return expiry */}
{!lookupData.expiry_date && (
<div className="rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-4 space-y-3">
<div className="flex items-center gap-2 text-yellow-700">
<AlertTriangle className="h-4 w-4" />
<h4 className="font-medium text-sm">Expiry date not found in WHOIS</h4>
</div>
<p className="text-xs text-muted-foreground">
Enter your registration details below and we&apos;ll calculate the expiry date.
</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs font-medium">Registration Date</label>
<div className="flex items-center gap-2">
<Calendar className="h-3.5 w-3.5 text-muted-foreground" />
<Input
type="date"
value={manualRegDate}
onChange={(e) => setManualRegDate(e.target.value)}
className="h-8 text-sm"
/>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">Registration Period</label>
<select
value={manualRegPeriod}
onChange={(e) => setManualRegPeriod(Number(e.target.value))}
className="w-full h-8 px-2 rounded-md border border-input bg-background text-sm"
>
<option value={1}>1 year</option>
<option value={2}>2 years</option>
<option value={3}>3 years</option>
<option value={5}>5 years</option>
<option value={10}>10 years</option>
</select>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-medium">Purchase Price (total for selected period)</label>
<Input
type="number"
min={0}
step="0.01"
value={manualPurchasePrice || ""}
placeholder="e.g. 29.99"
onChange={(e) => setManualPurchasePrice(Number(e.target.value))}
className="h-8 text-sm"
/>
{manualPurchasePrice > 0 && manualRegPeriod > 1 && (
<p className="text-[10px] text-muted-foreground">
~{(manualPurchasePrice / manualRegPeriod).toFixed(2)} per year
</p>
)}
</div>
{manualRegDate && manualRegPeriod > 0 && (
<div className="rounded-md bg-muted p-2 text-xs">
<p className="font-medium">Calculated Expiry:</p>
<p className="text-muted-foreground">
{calculateExpiryDate(manualRegDate, manualRegPeriod)}
</p>
</div>
)}
</div>
)}
</div>
)}
@@ -39,17 +39,15 @@ import {
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
getDomains,
deleteDomain,
refreshDomain,
getStatusBadgeColor,
getStatusLabel,
getDomainSubdomains,
formatDate,
formatDays,
type Domain,
} from "@/lib/domains"
import {
@@ -67,11 +65,78 @@ import {
} from "lucide-react"
import { DomainDialog } from "./domain-dialog"
import { Link } from "@/components/router"
import { useBrowserStorage } from "@/lib/utils"
import { cn, useBrowserStorage } from "@/lib/utils"
type ViewMode = "table" | "grid"
type StatusFilter = "all" | "active" | "expiring" | "expired" | "unknown" | "paused"
type DisplayOptions = {
showSSL: boolean
showRegistrar: boolean
showExpiryDate: boolean
showTags: boolean
showProviders: boolean
}
// Days left badge component - big and visible
function DaysLeftBadge({ days, label = "days" }: { days: number | undefined; label?: string }) {
if (days === undefined || days === null) return <span className="text-muted-foreground">-</span>
const isCritical = days >= 0 && days <= 7
const isWarning = days >= 0 && days <= 30
const isExpired = days < 0
const colorClass = isExpired
? "bg-red-500/15 text-red-600 border-red-500/30"
: isCritical
? "bg-red-500/15 text-red-600 border-red-500/30"
: isWarning
? "bg-yellow-500/15 text-yellow-600 border-yellow-500/30"
: "bg-green-500/15 text-green-600 border-green-500/30"
return (
<div className={`inline-flex flex-col items-center justify-center px-3 py-1.5 rounded-lg border-2 ${colorClass} min-w-[70px]`}>
<span className="text-lg font-bold leading-none">{isExpired ? Math.abs(days) : days}</span>
<span className="text-[10px] font-medium uppercase tracking-wide opacity-80">{isExpired ? "EXPIRED" : days === 1 ? "DAY" : label.toUpperCase()}</span>
</div>
)
}
// Subdomain indicator component
function SubdomainIndicator({ domainId }: { domainId: string }) {
const { data: subdomains, isLoading } = useQuery({
queryKey: ["domain-subdomains", domainId],
queryFn: () => getDomainSubdomains(domainId),
enabled: !!domainId,
staleTime: 5 * 60 * 1000, // 5 minutes
})
if (isLoading || !subdomains || subdomains.length === 0) {
return null
}
const activeCount = subdomains.filter(s => s.status === "active").length
const totalCount = subdomains.length
const hasIssues = subdomains.some(s => s.status === "error")
return (
<div className="flex items-center gap-1">
<div className={cn(
"inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium border",
hasIssues
? "bg-orange-500/15 text-orange-600 border-orange-500/30"
: "bg-blue-500/15 text-blue-600 border-blue-500/30"
)}>
<Globe className="h-3 w-3" />
<span>{activeCount}/{totalCount}</span>
</div>
{hasIssues && (
<AlertTriangle className="h-3 w-3 text-orange-500" />
)}
</div>
)
}
export default function DomainsTable() {
const { t } = useLingui()
const { toast } = useToast()
@@ -88,6 +153,11 @@ export default function DomainsTable() {
window.innerWidth < 1024 ? "grid" : "table"
)
const [displayOptions, setDisplayOptions] = useBrowserStorage<DisplayOptions>(
"domainsDisplayOptions",
{ showSSL: true, showRegistrar: true, showExpiryDate: true, showTags: true, showProviders: false }
)
const { data: domains = [], isLoading } = useQuery({
queryKey: ["domains"],
queryFn: getDomains,
@@ -167,19 +237,35 @@ export default function DomainsTable() {
refreshMutation.mutate(id)
}
const getStatusIcon = (status: string) => {
switch (status) {
case "active":
return <CheckCircle2 className="h-4 w-4 text-green-500" />
case "expiring":
return <Clock className="h-4 w-4 text-yellow-500" />
case "expired":
return <AlertTriangle className="h-4 w-4 text-red-500" />
default:
return <Globe className="h-4 w-4 text-gray-500" />
}
// Status indicator component matching monitors table style
function StatusIndicator({ status }: { status: string }) {
const colors = {
active: "bg-green-500",
expiring: "bg-yellow-500",
expired: "bg-red-500",
unknown: "bg-gray-500",
paused: "bg-blue-500",
}
const icons = {
active: CheckCircle2,
expiring: Clock,
expired: AlertTriangle,
unknown: AlertTriangle,
paused: Clock,
}
const Icon = icons[status as keyof typeof icons] || AlertTriangle
return (
<div className="flex items-center gap-2">
<div className={cn("h-2.5 w-2.5 rounded-full", colors[status as keyof typeof colors] || "bg-gray-500")} />
<Icon className="h-4 w-4 text-muted-foreground" />
<span className="capitalize text-sm">{status === "active" ? "Active" : status === "expiring" ? "Expiring Soon" : status === "expired" ? "Expired" : status}</span>
</div>
)
}
if (isLoading) {
return (
<Card className="w-full px-3 py-5 sm:py-6 sm:px-6">
@@ -346,6 +432,43 @@ export default function DomainsTable() {
</DropdownMenuRadioItem>
)}
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
{/* Display Options */}
<DropdownMenuLabel className="flex items-center gap-2">
<FilterIcon className="size-4" />
<Trans>Display Columns</Trans>
</DropdownMenuLabel>
<DropdownMenuCheckboxItem
checked={displayOptions.showSSL}
onCheckedChange={(checked: boolean) => setDisplayOptions({ ...displayOptions, showSSL: checked })}
>
SSL Info
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={displayOptions.showRegistrar}
onCheckedChange={(checked: boolean) => setDisplayOptions({ ...displayOptions, showRegistrar: checked })}
>
Registrar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={displayOptions.showExpiryDate}
onCheckedChange={(checked: boolean) => setDisplayOptions({ ...displayOptions, showExpiryDate: checked })}
>
Expiry Date
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={displayOptions.showTags}
onCheckedChange={(checked: boolean) => setDisplayOptions({ ...displayOptions, showTags: checked })}
>
Tags
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={displayOptions.showProviders}
onCheckedChange={(checked: boolean) => setDisplayOptions({ ...displayOptions, showProviders: checked })}
>
Providers
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -374,11 +497,12 @@ export default function DomainsTable() {
<TableRow>
<TableHead>Domain</TableHead>
<TableHead>Status</TableHead>
<TableHead>Expiry</TableHead>
{displayOptions.showExpiryDate && <TableHead>Expiry</TableHead>}
<TableHead>Days Left</TableHead>
<TableHead>Registrar</TableHead>
<TableHead>SSL Expiry</TableHead>
<TableHead>Tags</TableHead>
{displayOptions.showRegistrar && <TableHead>Registrar</TableHead>}
{displayOptions.showSSL && <TableHead>SSL Expiry</TableHead>}
{displayOptions.showTags && <TableHead>Tags</TableHead>}
{displayOptions.showProviders && <TableHead>Providers</TableHead>}
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
@@ -391,64 +515,62 @@ export default function DomainsTable() {
<img
src={domain.favicon_url}
alt=""
className="h-4 w-4"
className="h-4 w-4 rounded-sm"
onError={(e) => (e.currentTarget.style.display = "none")}
/>
)}
<span className="hover:underline">{domain.domain_name}</span>
<SubdomainIndicator domainId={domain.id} />
</Link>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getStatusIcon(domain.status)}
<Badge className={getStatusBadgeColor(domain.status)}>
{getStatusLabel(domain.status)}
</Badge>
</div>
<StatusIndicator status={domain.status} />
</TableCell>
{displayOptions.showExpiryDate && (
<TableCell>
{domain.expiry_date ? formatDate(domain.expiry_date) : "Unknown"}
</TableCell>
)}
<TableCell>
{domain.expiry_date ? formatDate(domain.expiry_date) : "Unknown"}
</TableCell>
<TableCell>
<span className={
domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 && domain.days_until_expiry <= 30
? domain.days_until_expiry <= 7
? "text-red-600 font-semibold"
: "text-yellow-600"
: ""
}>
{formatDays(domain.days_until_expiry)}
</span>
</TableCell>
<TableCell>{domain.registrar_name || "Unknown"}</TableCell>
<TableCell>
{domain.ssl_valid_to ? (
<span
className={
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 14
? "text-red-600"
: ""
}
>
{formatDays(domain.ssl_days_until)}
</span>
) : (
"N/A"
)}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{domain.tags?.map((tag: string) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-md bg-muted px-1.5 py-0.5 text-[10px] font-medium"
>
<Tag className="h-3 w-3" />
{tag}
</span>
))}
</div>
<DaysLeftBadge days={domain.days_until_expiry} />
</TableCell>
{displayOptions.showRegistrar && (
<TableCell>{domain.registrar_name || "Unknown"}</TableCell>
)}
{displayOptions.showSSL && (
<TableCell>
{domain.ssl_valid_to ? (
<DaysLeftBadge days={domain.ssl_days_until} label="ssl" />
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
)}
{displayOptions.showTags && (
<TableCell>
<div className="flex flex-wrap gap-1">
{domain.tags?.map((tag: string) => (
<span
key={tag}
className="inline-flex items-center gap-1 rounded-md bg-muted px-1.5 py-0.5 text-[10px] font-medium"
>
<Tag className="h-3 w-3" />
{tag}
</span>
))}
</div>
</TableCell>
)}
{displayOptions.showProviders && (
<TableCell>
<div className="flex flex-col gap-0.5 text-xs">
{domain.dns_provider && <span className="text-muted-foreground">DNS: <span className="text-foreground">{domain.dns_provider}</span></span>}
{domain.hosting_provider && <span className="text-muted-foreground">Host: <span className="text-foreground">{domain.hosting_provider}</span></span>}
{domain.email_provider && <span className="text-muted-foreground">Email: <span className="text-foreground">{domain.email_provider}</span></span>}
{domain.ca_provider && <span className="text-muted-foreground">CA: <span className="text-foreground">{domain.ca_provider}</span></span>}
</div>
</TableCell>
)}
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -507,6 +629,7 @@ export default function DomainsTable() {
)}
<div className="min-w-0">
<div className="font-medium truncate hover:underline">{domain.domain_name}</div>
<SubdomainIndicator domainId={domain.id} />
</div>
</Link>
<DropdownMenu>
@@ -528,14 +651,9 @@ export default function DomainsTable() {
</DropdownMenu>
</div>
<div className="flex items-center gap-2">
{getStatusIcon(domain.status)}
<Badge className={getStatusBadgeColor(domain.status)}>
{getStatusLabel(domain.status)}
</Badge>
</div>
<StatusIndicator status={domain.status} />
{domain.tags && domain.tags.length > 0 && (
{displayOptions.showTags && domain.tags && domain.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{domain.tags.map((tag: string) => (
<span
@@ -549,30 +667,28 @@ export default function DomainsTable() {
</div>
)}
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<div className="text-xs text-muted-foreground">Days Left</div>
<span className={
domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 && domain.days_until_expiry <= 30
? domain.days_until_expiry <= 7
? "text-red-600 font-semibold"
: "text-yellow-600"
: ""
}>
{formatDays(domain.days_until_expiry)}
</span>
<div className="grid gap-2 text-sm">
<div className="flex items-center justify-between">
{displayOptions.showExpiryDate && (
<span className="text-xs text-muted-foreground">{domain.expiry_date ? formatDate(domain.expiry_date) : "Unknown"}</span>
)}
{displayOptions.showRegistrar && (
<span className="text-xs text-muted-foreground truncate max-w-[120px]">{domain.registrar_name || "Unknown"}</span>
)}
</div>
<div>
<div className="text-xs text-muted-foreground">SSL</div>
<span
className={
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 14
? "text-red-600"
: ""
}
>
{formatDays(domain.ssl_days_until)}
</span>
{displayOptions.showProviders && (
<div className="flex flex-wrap gap-1 text-[10px]">
{domain.dns_provider && <span className="text-muted-foreground">DNS: <span className="text-foreground">{domain.dns_provider}</span></span>}
{domain.hosting_provider && <span className="text-muted-foreground">Host: <span className="text-foreground">{domain.hosting_provider}</span></span>}
{domain.email_provider && <span className="text-muted-foreground">Email: <span className="text-foreground">{domain.email_provider}</span></span>}
{domain.ca_provider && <span className="text-muted-foreground">CA: <span className="text-foreground">{domain.ca_provider}</span></span>}
</div>
)}
<div className="flex gap-2">
<DaysLeftBadge days={domain.days_until_expiry} />
{displayOptions.showSSL && domain.ssl_valid_to && (
<DaysLeftBadge days={domain.ssl_days_until} label="ssl" />
)}
</div>
</div>
</div>
@@ -0,0 +1,297 @@
"use client"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Skeleton } from "@/components/ui/skeleton"
import {
Globe,
RefreshCw,
Trash2,
Search,
Server,
ExternalLink,
CheckCircle2,
XCircle,
Shield,
} from "lucide-react"
import {
getDomainSubdomains,
refreshSubdomainDiscovery,
deleteSubdomain,
type Subdomain,
} from "@/lib/domains"
import { useState } from "react"
interface SubdomainListProps {
domainId: string
}
function SubdomainStatusBadge({ status }: { status: string }) {
const configs = {
active: { color: "bg-green-500", icon: CheckCircle2, text: "Active" },
inactive: { color: "bg-gray-500", icon: XCircle, text: "Inactive" },
error: { color: "bg-red-500", icon: XCircle, text: "Error" },
}
const config = configs[status as keyof typeof configs] || configs.inactive
const Icon = config.icon
return (
<div className="flex items-center gap-1.5">
<div className={`h-2 w-2 rounded-full ${config.color}`} />
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs capitalize">{config.text}</span>
</div>
)
}
function SourceBadge({ source }: { source: string }) {
const sourceConfig: Record<string, { label: string; variant: "default" | "secondary" | "outline" }> = {
dns: { label: "DNS", variant: "default" },
http: { label: "HTTP", variant: "secondary" },
pattern: { label: "Pattern", variant: "outline" },
certificate: { label: "Cert", variant: "secondary" },
}
const config = sourceConfig[source] || { label: source, variant: "outline" }
return (
<Badge variant={config.variant} className="text-xs">
{config.label}
</Badge>
)
}
export function SubdomainList({ domainId }: SubdomainListProps) {
const { toast } = useToast()
const queryClient = useQueryClient()
const [isDiscovering, setIsDiscovering] = useState(false)
const { data: subdomains, isLoading } = useQuery({
queryKey: ["domain-subdomains", domainId],
queryFn: () => getDomainSubdomains(domainId),
})
const refreshMutation = useMutation({
mutationFn: async () => {
setIsDiscovering(true)
await refreshSubdomainDiscovery(domainId)
// Wait a bit for discovery to start
await new Promise((resolve) => setTimeout(resolve, 2000))
setIsDiscovering(false)
},
onSuccess: () => {
toast({ title: "Subdomain discovery started" })
queryClient.invalidateQueries({ queryKey: ["domain-subdomains", domainId] })
},
onError: (error: Error) => {
setIsDiscovering(false)
toast({
title: "Discovery failed",
description: error.message,
variant: "destructive",
})
},
})
const deleteMutation = useMutation({
mutationFn: deleteSubdomain,
onSuccess: () => {
toast({ title: "Subdomain deleted" })
queryClient.invalidateQueries({ queryKey: ["domain-subdomains", domainId] })
},
onError: (error: Error) => {
toast({
title: "Failed to delete",
description: error.message,
variant: "destructive",
})
},
})
if (isLoading) {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-32" />
<Skeleton className="h-4 w-48 mt-2" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
</div>
</CardContent>
</Card>
)
}
const activeCount = subdomains?.filter((s) => s.status === "active").length || 0
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Search className="h-5 w-5" />
Discovered Subdomains
{subdomains && subdomains.length > 0 && (
<Badge variant="secondary" className="ml-2">
{activeCount}/{subdomains.length} active
</Badge>
)}
</CardTitle>
<CardDescription>
Subdomains discovered through DNS, HTTP, and pattern enumeration
</CardDescription>
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={() => refreshMutation.mutate()}
disabled={isDiscovering || refreshMutation.isPending}
>
<RefreshCw className={`mr-2 h-4 w-4 ${isDiscovering ? "animate-spin" : ""}`} />
{isDiscovering ? "Discovering..." : "Discover"}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Run enhanced subdomain discovery</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</CardHeader>
<CardContent>
{!subdomains || subdomains.length === 0 ? (
<div className="text-center py-8">
<Search className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-lg font-medium">No subdomains discovered yet</p>
<p className="text-muted-foreground mb-4">
Run discovery to find subdomains via DNS, HTTP, and certificate transparency logs
</p>
<Button
onClick={() => refreshMutation.mutate()}
disabled={isDiscovering || refreshMutation.isPending}
>
<RefreshCw className={`mr-2 h-4 w-4 ${isDiscovering ? "animate-spin" : ""}`} />
{isDiscovering ? "Discovering..." : "Start Discovery"}
</Button>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Subdomain</TableHead>
<TableHead>Status</TableHead>
<TableHead>Source</TableHead>
<TableHead>IP Addresses</TableHead>
<TableHead>HTTP</TableHead>
<TableHead className="w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{subdomains.map((subdomain) => (
<TableRow key={subdomain.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="font-mono text-sm">
{subdomain.subdomain_name}
</span>
</div>
</TableCell>
<TableCell>
<SubdomainStatusBadge status={subdomain.status} />
</TableCell>
<TableCell>
<SourceBadge source={subdomain.discovery_source} />
</TableCell>
<TableCell>
{subdomain.ip_addresses ? (
<div className="flex items-center gap-1.5">
<Server className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-xs text-muted-foreground truncate max-w-[150px]">
{subdomain.ip_addresses.split(",").length} IP(s)
</span>
</div>
) : (
<span className="text-xs text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{subdomain.http_status ? (
<div className="flex items-center gap-2">
<Badge
variant={subdomain.http_status < 400 ? "default" : "destructive"}
className="text-xs"
>
{subdomain.http_status}
</Badge>
{subdomain.server_header && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Shield className="h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{subdomain.server_header}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<a
href={`https://${subdomain.full_domain}`}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground"
>
<ExternalLink className="h-3.5 w-3.5" />
</a>
</div>
) : (
<span className="text-xs text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
onClick={() => deleteMutation.mutate(subdomain.id)}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
)
}
@@ -0,0 +1,255 @@
"use client"
import { useMemo } from "react"
import { useQuery } from "@tanstack/react-query"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Collapsible } from "@/components/ui/collapsible"
import {
Globe,
Server,
Activity,
ArrowUpRight,
ArrowDownRight,
Minus,
ExternalLink,
} from "lucide-react"
import {
listMonitors,
groupMonitorsByDomain,
getMonitorStatusColor,
formatUptime,
type Monitor,
type GroupedMonitors,
} from "@/lib/monitors"
import { Link } from "@/components/router"
import { cn } from "@/lib/utils"
interface GroupedMonitorsTableProps {
view?: "grid" | "list"
}
function DomainGroupHeader({
domain,
group,
monitorCount,
}: {
domain: string
group: GroupedMonitors
monitorCount: number
}) {
// Calculate aggregate status for the domain
const allMonitors = [...group.monitors, ...Array.from(group.subdomains.values()).flat()]
const upCount = allMonitors.filter((m) => m.status === "up").length
const downCount = allMonitors.filter((m) => m.status === "down").length
const pausedCount = allMonitors.filter((m) => m.status === "paused").length
const statusColor =
downCount > 0 ? "bg-red-500" : upCount > 0 ? "bg-green-500" : pausedCount > 0 ? "bg-yellow-500" : "bg-gray-400"
return (
<div className="flex items-center gap-3 py-2">
<div className={cn("h-3 w-3 rounded-full", statusColor)} />
<div className="flex-1">
<div className="flex items-center gap-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="font-semibold">{domain}</span>
<Badge variant="secondary" className="text-xs">
{monitorCount} monitor{monitorCount !== 1 ? "s" : ""}
</Badge>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
{upCount > 0 && (
<span className="flex items-center gap-1 text-green-600">
<ArrowUpRight className="h-3.5 w-3.5" />
{upCount}
</span>
)}
{downCount > 0 && (
<span className="flex items-center gap-1 text-red-600">
<ArrowDownRight className="h-3.5 w-3.5" />
{downCount}
</span>
)}
{pausedCount > 0 && (
<span className="flex items-center gap-1 text-yellow-600">
<Minus className="h-3.5 w-3.5" />
{pausedCount}
</span>
)}
</div>
</div>
)
}
function MonitorCard({ monitor }: { monitor: Monitor }) {
const uptime24h = monitor.uptime_stats?.["24h"] ?? 100
const statusColor = getMonitorStatusColor(monitor.status)
return (
<Link href={`/monitor/${monitor.id}`}>
<div className="group relative rounded-lg border p-3 hover:bg-muted/50 transition-colors cursor-pointer">
<div className="flex items-start gap-3">
<div className={cn("mt-1 h-2.5 w-2.5 rounded-full", statusColor)} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="font-medium text-sm truncate">{monitor.name}</span>
{monitor.active === false && (
<Badge variant="secondary" className="text-[10px]">
Paused
</Badge>
)}
</div>
<div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
<Activity className="h-3 w-3" />
<span>24h: {formatUptime(uptime24h)}</span>
</div>
</div>
<ExternalLink className="h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</div>
</Link>
)
}
function SubdomainSection({
subdomain,
monitors,
}: {
subdomain: string
monitors: Monitor[]
}) {
const downCount = monitors.filter((m) => m.status === "down").length
const header = (
<div className="flex items-center gap-2 py-1.5">
<Server className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-sm font-medium">{subdomain}</span>
<Badge variant="outline" className="text-[10px] h-5 px-1">
{monitors.length}
</Badge>
{downCount > 0 && (
<Badge variant="destructive" className="text-[10px] h-5 px-1">
{downCount} down
</Badge>
)}
</div>
)
return (
<Collapsible
title={`${subdomain} (${monitors.length})`}
icon={<Server className="h-4 w-4" />}
defaultOpen={true}
className="ml-4 border-l-2 border-muted rounded-none border-t-0 border-r-0 border-b-0"
>
<div className="pl-6 py-1 space-y-1">
{monitors.map((monitor) => (
<MonitorCard key={monitor.id} monitor={monitor} />
))}
</div>
</Collapsible>
)
}
function DomainGroup({ domain, group }: { domain: string; group: GroupedMonitors }) {
const monitorCount = group.monitors.length + Array.from(group.subdomains.values()).flat().length
const content = (
<CardContent className="pt-0 pb-4 px-4">
{/* Root domain monitors */}
{group.monitors.length > 0 && (
<div className="mb-3">
<div className="flex items-center gap-2 py-1.5 text-sm text-muted-foreground">
<Globe className="h-3.5 w-3.5" />
<span>Root Domain</span>
</div>
<div className="pl-6 space-y-1">
{group.monitors.map((monitor) => (
<MonitorCard key={monitor.id} monitor={monitor} />
))}
</div>
</div>
)}
{/* Subdomain sections */}
{Array.from(group.subdomains.entries()).map(([subdomain, monitors]) => (
<SubdomainSection key={subdomain} subdomain={subdomain} monitors={monitors} />
))}
</CardContent>
)
return (
<Collapsible
title={domain}
icon={<Globe className="h-4 w-4" />}
defaultOpen={true}
description={<DomainGroupHeader domain={domain} group={group} monitorCount={monitorCount} />}
>
{content}
</Collapsible>
)
}
export function GroupedMonitorsTable() {
const { data: monitors, isLoading } = useQuery({
queryKey: ["monitors"],
queryFn: listMonitors,
})
const groupedMonitors = useMemo(() => {
if (!monitors) return new Map()
return groupMonitorsByDomain(monitors)
}, [monitors])
const ungroupedMonitors = useMemo(() => {
if (!monitors) return []
return monitors.filter((m) => !m.url && !m.hostname)
}, [monitors])
if (isLoading) {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-32 bg-muted rounded-lg animate-pulse" />
))}
</div>
)
}
const domainGroups = Array.from(groupedMonitors.entries()).sort((a, b) => a[0].localeCompare(b[0]))
return (
<div className="space-y-4">
{/* Domain groups */}
{domainGroups.map(([domain, group]) => (
<DomainGroup key={domain} domain={domain} group={group} />
))}
{/* Ungrouped monitors (no URL/hostname) */}
{ungroupedMonitors.length > 0 && (
<Collapsible
title="Other Monitors"
icon={<Server className="h-4 w-4" />}
defaultOpen={true}
>
<div className="space-y-1">
{ungroupedMonitors.map((monitor) => (
<MonitorCard key={monitor.id} monitor={monitor} />
))}
</div>
</Collapsible>
)}
{/* Empty state */}
{domainGroups.length === 0 && ungroupedMonitors.length === 0 && (
<div className="text-center py-12">
<Activity className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-lg font-medium">No monitors yet</p>
<p className="text-muted-foreground">Create your first monitor to get started</p>
</div>
)}
</div>
)
}
@@ -1,6 +1,7 @@
import { Trans, useLingui } from "@lingui/react/macro"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import {
AlertTriangle,
ArrowDownIcon,
ArrowUpIcon,
CheckCircleIcon,
@@ -16,17 +17,12 @@ import {
Settings2Icon,
TagIcon,
Trash2Icon,
XCircle,
XCircleIcon,
} from "lucide-react"
import { memo, useMemo, useState } from "react"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import {
DropdownMenu,
DropdownMenuContent,
@@ -38,24 +34,13 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
import { useToast } from "@/components/ui/use-toast"
import {
deleteMonitor,
getMonitorTypeLabel,
getMonitorFaviconUrl,
listMonitors,
manualCheck,
pauseMonitor,
@@ -99,13 +84,35 @@ function StatusIndicator({ status }: { status: MonitorStatus }) {
)
}
// Favicon component
function MonitorFavicon({ monitor, className }: { monitor: Monitor; className?: string }) {
const [error, setError] = useState(false)
const faviconUrl = getMonitorFaviconUrl(monitor)
if (!faviconUrl || error) {
return <GlobeIcon className={cn("h-4 w-4 text-muted-foreground", className)} />
}
return (
<img
src={faviconUrl}
alt=""
className={cn("h-4 w-4 object-contain", className)}
onError={() => setError(true)}
loading="lazy"
/>
)
}
// Monitor Card component for grid view
function MonitorCard({
monitor,
onEdit,
displayOptions,
}: {
monitor: Monitor
onEdit: (m: Monitor) => void
displayOptions: DisplayOptions
}) {
const { toast } = useToast()
const queryClient = useQueryClient()
@@ -144,7 +151,10 @@ function MonitorCard({
<div className="flex items-start justify-between">
<Link href={`/monitor/${monitor.id}`} className="flex items-center gap-3 cursor-pointer min-w-0">
<div className="shrink-0">
<StatusIndicator status={monitor.status} />
<div className="flex items-center gap-2">
<MonitorFavicon monitor={monitor} className="h-5 w-5" />
<StatusIndicator status={monitor.status} />
</div>
</div>
<div className="min-w-0">
<div className="font-medium truncate hover:underline">{monitor.name}</div>
@@ -165,10 +175,7 @@ function MonitorCard({
<Edit3Icon className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => deleteMutation.mutate(monitor.id)}
className="text-destructive"
>
<DropdownMenuItem onClick={() => deleteMutation.mutate(monitor.id)} className="text-destructive">
<Trash2Icon className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
@@ -176,14 +183,32 @@ function MonitorCard({
</DropdownMenu>
</div>
<div className="grid grid-cols-2 gap-3 text-sm">
<div className="space-y-1">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="text-xs text-muted-foreground">Type</div>
<div className="inline-flex items-center rounded-md bg-muted px-2 py-1 text-xs font-medium">
{getMonitorTypeLabel(monitor.type)}
</div>
</div>
<div className="space-y-1">
{/* Uptime - Prominent pill display */}
{displayOptions.showUptimePills && (
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 flex-wrap">
{displayOptions.showUptimePercentage && (
<UptimePill uptime={monitor.uptime_stats?.uptime_24h ?? 100} label="24h" />
)}
{displayOptions.showUptimePercentage &&
monitor.uptime_stats?.uptime_7d !== undefined &&
monitor.uptime_stats.uptime_7d !== monitor.uptime_stats?.uptime_24h && (
<UptimePill uptime={monitor.uptime_stats.uptime_7d} label="7d" />
)}
</div>
{displayOptions.showHeartbeatDots && <UptimeDots heartbeats={monitor.recent_heartbeats} />}
</div>
)}
<div className="flex items-center justify-between text-sm">
<div className="text-xs text-muted-foreground">Response</div>
<div>
{monitor.last_check ? (
@@ -193,10 +218,6 @@ function MonitorCard({
)}
</div>
</div>
<div className="col-span-2 space-y-1">
<div className="text-xs text-muted-foreground">Uptime (24h)</div>
<UptimeBar stats={monitor.uptime_stats} />
</div>
</div>
{monitor.tags && monitor.tags.length > 0 && (
@@ -224,12 +245,7 @@ function MonitorCard({
onClick={() => checkMutation.mutate(monitor.id)}
disabled={checkMutation.isPending}
>
<RefreshCwIcon
className={cn(
"h-4 w-4 mr-1",
checkMutation.isPending && "animate-spin"
)}
/>
<RefreshCwIcon className={cn("h-4 w-4 mr-1", checkMutation.isPending && "animate-spin")} />
Check
</Button>
</TooltipTrigger>
@@ -249,9 +265,13 @@ function MonitorCard({
disabled={pauseMutation.isPending}
>
{monitor.status === "paused" ? (
<><PlayIcon className="h-4 w-4 mr-1" /> Resume</>
<>
<PlayIcon className="h-4 w-4 mr-1" /> Resume
</>
) : (
<><PauseIcon className="h-4 w-4 mr-1" /> Pause</>
<>
<PauseIcon className="h-4 w-4 mr-1" /> Pause
</>
)}
</Button>
</TooltipTrigger>
@@ -265,36 +285,123 @@ function MonitorCard({
)
}
// Uptime bar component
function UptimeBar({ stats }: { stats?: Record<string, number> }) {
const uptime24h = stats?.uptime_24h ?? 100
// Uptime pill badge component - styled like domainstack.io status badges
function UptimePill({ uptime, label = "24h" }: { uptime: number; label?: string }) {
let colorClass = "bg-green-500/10 text-green-700 border-green-500/20 dark:text-green-400"
let icon = <CheckCircleIcon className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />
let ringClass = "ring-green-500/20"
let color = "bg-green-500"
if (uptime24h < 95) color = "bg-yellow-500"
if (uptime24h < 90) color = "bg-red-500"
if (uptime < 99.9) {
colorClass = "bg-green-500/10 text-green-700 border-green-500/20 dark:text-green-400"
ringClass = "ring-green-500/20"
}
if (uptime < 95) {
colorClass = "bg-yellow-500/10 text-yellow-700 border-yellow-500/20 dark:text-yellow-400"
icon = <AlertTriangle className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400" />
ringClass = "ring-yellow-500/20"
}
if (uptime < 90) {
colorClass = "bg-red-500/10 text-red-700 border-red-500/20 dark:text-red-400"
icon = <XCircle className="h-3.5 w-3.5 text-red-600 dark:text-red-400" />
ringClass = "ring-red-500/20"
}
return (
<div className="flex items-center gap-2">
<div className="h-2 w-16 rounded-full bg-muted overflow-hidden">
<div
className={cn("h-full transition-all", color)}
style={{ width: `${uptime24h}%` }}
/>
</div>
<span className="text-xs text-muted-foreground w-14">
{formatUptime(uptime24h)}
<div
className={cn(
"inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full border text-xs font-semibold shadow-sm",
"transition-all hover:scale-105",
colorClass,
ringClass,
"ring-1"
)}
>
{icon}
<span>{formatUptime(uptime)}</span>
<span className="text-[10px] font-medium uppercase opacity-60 border-l border-current/20 pl-1.5 ml-0.5">
{label}
</span>
</div>
)
}
// Uptime bar component with pill style
function UptimeBar({ stats }: { stats?: Record<string, number> }) {
const uptime24h = stats?.uptime_24h ?? 100
const uptime7d = stats?.uptime_7d ?? 100
const uptime30d = stats?.uptime_30d ?? 100
return (
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2">
<UptimePill uptime={uptime24h} label="24h" />
{uptime7d !== 100 && uptime7d !== uptime24h && <UptimePill uptime={uptime7d} label="7d" />}
{uptime30d !== 100 && uptime30d !== uptime24h && uptime30d !== uptime7d && (
<UptimePill uptime={uptime30d} label="30d" />
)}
</div>
</div>
)
}
// Mini uptime dots visualization - styled as a clean status bar
function UptimeDots({ heartbeats }: { heartbeats?: Array<{ status: string; time: string }> }) {
const totalSlots = 16
const recent = heartbeats?.slice(-totalSlots) || []
const emptySlots = totalSlots - recent.length
const getStatusColor = (status: string) => {
switch (status) {
case "up":
return "bg-green-500 shadow-[0_0_6px_rgba(34,197,94,0.4)]"
case "down":
return "bg-red-500 shadow-[0_0_6px_rgba(239,68,68,0.4)]"
case "paused":
return "bg-gray-400"
default:
return "bg-yellow-500"
}
}
return (
<TooltipProvider>
<div className="flex items-center gap-1">
<div className="flex gap-[3px] p-1 rounded-md bg-muted/50">
{recent.map((hb, i) => (
<Tooltip key={i}>
<TooltipTrigger asChild>
<div
className={cn(
"h-4 w-[6px] rounded-full transition-all cursor-pointer hover:scale-125",
getStatusColor(hb.status)
)}
/>
</TooltipTrigger>
<TooltipContent side="bottom" className="text-xs">
<p className="capitalize font-medium">{hb.status}</p>
<p className="text-muted-foreground">{new Date(hb.time).toLocaleString()}</p>
</TooltipContent>
</Tooltip>
))}
{emptySlots > 0 &&
Array.from({ length: emptySlots }).map((_, i) => (
<div key={`empty-${i}`} className="h-4 w-[6px] rounded-full bg-muted" />
))}
</div>
</div>
</TooltipProvider>
)
}
// Monitor row component
function MonitorRow({
monitor,
onEdit,
displayOptions,
}: {
monitor: Monitor
onEdit: (m: Monitor) => void
displayOptions: DisplayOptions
}) {
const { toast } = useToast()
const queryClient = useQueryClient()
@@ -338,7 +445,7 @@ function MonitorRow({
<TableRow>
<TableCell>
<Link href={`/monitor/${monitor.id}`} className="flex items-center gap-3 cursor-pointer">
<GlobeIcon className="h-4 w-4 text-muted-foreground" />
<MonitorFavicon monitor={monitor} className="h-5 w-5" />
<div>
<div className="font-medium hover:underline">{monitor.name}</div>
<div className="text-xs text-muted-foreground">
@@ -358,15 +465,17 @@ function MonitorRow({
</TableCell>
<TableCell>
{monitor.last_check ? (
<div className="text-sm">
{formatPing(monitor.uptime_stats?.last_ping || 0)}
</div>
<div className="text-sm">{formatPing(monitor.uptime_stats?.last_ping || 0)}</div>
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
<UptimeBar stats={monitor.uptime_stats} />
{displayOptions.showUptimePills ? (
<UptimeBar stats={monitor.uptime_stats} />
) : (
<span className="text-sm text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
@@ -393,12 +502,7 @@ function MonitorRow({
onClick={() => checkMutation.mutate(monitor.id)}
disabled={checkMutation.isPending}
>
<RefreshCwIcon
className={cn(
"h-4 w-4",
checkMutation.isPending && "animate-spin"
)}
/>
<RefreshCwIcon className={cn("h-4 w-4", checkMutation.isPending && "animate-spin")} />
</Button>
</TooltipTrigger>
<TooltipContent>
@@ -417,11 +521,7 @@ function MonitorRow({
onClick={() => pauseMutation.mutate(monitor.id)}
disabled={pauseMutation.isPending}
>
{monitor.status === "paused" ? (
<PlayIcon className="h-4 w-4" />
) : (
<PauseIcon className="h-4 w-4" />
)}
{monitor.status === "paused" ? <PlayIcon className="h-4 w-4" /> : <PauseIcon className="h-4 w-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>
@@ -441,10 +541,7 @@ function MonitorRow({
<Edit3Icon className="mr-2 h-4 w-4" />
Edit
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onClick={() => deleteMutation.mutate(monitor.id)}
>
<DropdownMenuItem className="text-destructive" onClick={() => deleteMutation.mutate(monitor.id)}>
<Trash2Icon className="mr-2 h-4 w-4" />
Delete
</DropdownMenuItem>
@@ -460,6 +557,12 @@ type ViewMode = "table" | "grid"
type StatusFilter = "all" | MonitorStatus
type TypeFilter = "all" | MonitorType
interface DisplayOptions {
showUptimePills: boolean
showUptimePercentage: boolean
showHeartbeatDots: boolean
}
// Main component
export default memo(function MonitorsTable() {
const { t } = useLingui()
@@ -475,6 +578,12 @@ export default memo(function MonitorsTable() {
window.innerWidth < 1024 ? "grid" : "table"
)
const [displayOptions, setDisplayOptions] = useBrowserStorage<DisplayOptions>("monitorsDisplayOptions", {
showUptimePills: true,
showUptimePercentage: true,
showHeartbeatDots: true,
})
const { data: monitors = [], isLoading } = useQuery({
queryKey: ["monitors"],
queryFn: listMonitors,
@@ -550,8 +659,7 @@ export default memo(function MonitorsTable() {
{stats.down > 0 && (
<>
{" "}
{stats.down}{" "}
<ArrowDownIcon className="inline h-3 w-3 text-red-500" />
{stats.down} <ArrowDownIcon className="inline h-3 w-3 text-red-500" />
</>
)}
{stats.paused > 0 && (
@@ -672,6 +780,42 @@ export default memo(function MonitorsTable() {
</DropdownMenuRadioGroup>
<DropdownMenuSeparator />
{/* Display Options */}
<DropdownMenuLabel className="flex items-center gap-2">
<Settings2Icon className="size-4" />
<Trans>Display</Trans>
</DropdownMenuLabel>
<div className="px-2 py-1 space-y-1">
<label className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5">
<input
type="checkbox"
checked={displayOptions.showUptimePills}
onChange={(e) => setDisplayOptions({ ...displayOptions, showUptimePills: e.target.checked })}
className="rounded border-gray-300"
/>
<span>Show uptime pills</span>
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5">
<input
type="checkbox"
checked={displayOptions.showUptimePercentage}
onChange={(e) => setDisplayOptions({ ...displayOptions, showUptimePercentage: e.target.checked })}
className="rounded border-gray-300"
/>
<span>Show percentage</span>
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer hover:bg-muted/50 rounded px-1 py-0.5">
<input
type="checkbox"
checked={displayOptions.showHeartbeatDots}
onChange={(e) => setDisplayOptions({ ...displayOptions, showHeartbeatDots: e.target.checked })}
className="rounded border-gray-300"
/>
<span>Show heartbeat dots</span>
</label>
</div>
<DropdownMenuSeparator />
{/* Status Filter */}
<DropdownMenuLabel className="flex items-center gap-2">
<FilterIcon className="size-4" />
@@ -760,6 +904,7 @@ export default memo(function MonitorsTable() {
key={monitor.id}
monitor={monitor}
onEdit={setEditingMonitor}
displayOptions={displayOptions}
/>
))}
</TableBody>
@@ -771,6 +916,7 @@ export default memo(function MonitorsTable() {
key={monitor.id}
monitor={monitor}
onEdit={setEditingMonitor}
displayOptions={displayOptions}
/>
))}
</div>
@@ -778,10 +924,7 @@ export default memo(function MonitorsTable() {
</CardContent>
{/* Add Monitor Dialog */}
<AddMonitorDialog
open={isAddDialogOpen}
onOpenChange={setIsAddDialogOpen}
/>
<AddMonitorDialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen} />
{/* Edit Monitor Dialog */}
{editingMonitor && (
@@ -0,0 +1,875 @@
import type { Domain } from "@/lib/domains"
import { formatDate, formatDays } from "@/lib/domains"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { cn } from "@/lib/utils"
import {
Globe,
Shield,
Server,
MapPin,
FileText,
Building2,
User,
Eye,
EyeOff,
Mail,
Network,
Search,
Code2,
AlertTriangle,
CheckCircle2,
Clock,
ExternalLink,
Info,
} from "lucide-react"
// --- Reusable section wrapper inspired by domainstack.io ---
function SectionCard({
title,
description,
icon: Icon,
children,
accent = "slate",
}: {
title: string
description?: string
icon: React.ElementType
children: React.ReactNode
accent?: "blue" | "green" | "orange" | "purple" | "slate" | "red" | "yellow"
}) {
const accentBorder = {
blue: "border-t-blue-500/40",
green: "border-t-green-500/40",
orange: "border-t-orange-500/40",
purple: "border-t-purple-500/40",
slate: "border-t-slate-500/30",
red: "border-t-red-500/40",
yellow: "border-t-yellow-500/40",
}
const accentIcon = {
blue: "text-blue-500",
green: "text-green-500",
orange: "text-orange-500",
purple: "text-purple-500",
slate: "text-slate-500",
red: "text-red-500",
yellow: "text-yellow-500",
}
return (
<Card className={cn("overflow-hidden rounded-xl border-t-2", accentBorder[accent])}>
<CardHeader className="pb-3">
<div className="flex items-center gap-2.5">
<Icon className={cn("h-5 w-5", accentIcon[accent])} />
<div>
<CardTitle className="text-base">{title}</CardTitle>
{description && <CardDescription className="text-xs">{description}</CardDescription>}
</div>
</div>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
)
}
function KV({
label,
value,
suffix,
leading,
}: {
label: string
value?: string | null
suffix?: React.ReactNode
leading?: React.ReactNode
}) {
if (!value && value !== "0") return null
return (
<div className="flex h-14 min-w-0 items-center justify-between gap-3 rounded-lg border bg-background/60 px-3 py-2">
<div className="flex min-w-0 flex-col">
<div className="text-[10px] leading-none tracking-wider uppercase text-foreground/60">{label}</div>
<div className="inline-flex min-w-0 items-center gap-1.5 text-[13px] text-foreground/90 mt-1">
{leading ? <span className="shrink-0">{leading}</span> : null}
<span className="truncate">{value}</span>
{suffix ? <span className="shrink-0">{suffix}</span> : null}
</div>
</div>
</div>
)
}
function KVGrid({ children, cols = 2 }: { children: React.ReactNode; cols?: 1 | 2 | 3 | 4 }) {
const colClass =
cols === 4
? "lg:grid-cols-4 md:grid-cols-2"
: cols === 3
? "sm:grid-cols-3"
: cols === 1
? "grid-cols-1"
: "sm:grid-cols-2"
return <div className={cn("grid grid-cols-1 gap-2", colClass)}>{children}</div>
}
function DnsGroup({
type,
records,
ttl,
}: {
type: string
records: Array<{ value: string; ttl?: string | number }>
ttl?: string | number
}) {
if (!records?.length) return null
return (
<div className="space-y-1.5">
<div className="flex items-center gap-2 mb-2">
<Badge variant="outline" className="font-mono text-[10px] px-1.5 py-0.5">
{type}
</Badge>
<span className="text-xs text-muted-foreground">
{records.length} record{records.length !== 1 ? "s" : ""}
</span>
</div>
<div className="space-y-1">
{records.map((rec, i) => (
<div key={i} className="flex items-center gap-2 rounded-md border bg-background/40 px-2.5 py-1.5 text-sm">
<code className="text-xs font-mono text-foreground/80 truncate flex-1">{rec.value}</code>
{(rec.ttl || ttl) && (
<span className="text-[10px] text-muted-foreground shrink-0">TTL {rec.ttl || ttl}</span>
)}
</div>
))}
</div>
</div>
)
}
function MapEmbed({ lat, lon, title }: { lat: number; lon: number; title?: string }) {
const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.8}%2C${lat - 0.8}%2C${lon + 0.8}%2C${lat + 0.8}&layer=mapnik&marker=${lat}%2C${lon}`
return (
<div className="relative h-[200px] w-full overflow-hidden rounded-lg border mt-2">
<iframe title={title || "Location map"} src={mapUrl} className="h-full w-full border-0" loading="lazy" />
<a
href={`https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=12/${lat}/${lon}`}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-2 right-2 rounded-md bg-white/90 px-2 py-1 text-[10px] font-medium text-foreground shadow-sm hover:bg-white border"
>
Open Map
</a>
</div>
)
}
function StatusDot({ status }: { status: string }) {
const colors: Record<string, string> = {
up: "bg-green-500",
down: "bg-red-500",
paused: "bg-gray-400",
active: "bg-green-500",
expiring: "bg-yellow-500",
expired: "bg-red-500",
unknown: "bg-gray-400",
}
return <div className={cn("h-2.5 w-2.5 rounded-full", colors[status] || "bg-yellow-500")} />
}
function CertificateCard({
cert,
index,
total,
}: {
cert: NonNullable<Domain["certificates"]> extends Array<infer T> ? T : never
index: number
total: number
}) {
const label = index === 0 ? "Leaf" : index === total - 1 ? "Root" : "Intermediate"
return (
<div className="rounded-lg border p-3 space-y-2">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant={index === 0 ? "default" : "secondary"} className="text-[10px]">
{label}
</Badge>
{cert.ca_provider && (
<Badge variant="outline" className="text-[10px]">
{cert.ca_provider}
</Badge>
)}
</div>
<div className="text-sm space-y-1">
<p>
<span className="text-muted-foreground text-xs">Subject:</span>{" "}
<span className="font-medium">{cert.subject}</span>
</p>
<p>
<span className="text-muted-foreground text-xs">Issuer:</span> {cert.issuer}
</p>
</div>
{cert.alt_names && cert.alt_names.length > 0 && (
<div>
<p className="text-[10px] text-muted-foreground mb-1">SANs ({cert.alt_names.length})</p>
<div className="flex flex-wrap gap-1">
{cert.alt_names.slice(0, 6).map((name, j) => (
<code key={j} className="text-[10px] bg-muted px-1.5 py-0.5 rounded">
{name}
</code>
))}
{cert.alt_names.length > 6 && (
<span className="text-[10px] text-muted-foreground">+{cert.alt_names.length - 6}</span>
)}
</div>
</div>
)}
<div className="text-[10px] text-muted-foreground pt-1 border-t">
{cert.valid_from} {cert.valid_to}
</div>
</div>
)
}
// --- Main exported sections ---
export function RegistrationSection({ domain }: { domain: Domain }) {
return (
<SectionCard title="Registration" description="Registrar and registrant details" icon={Building2} accent="blue">
<KVGrid>
<KV
label="Registrar"
value={domain.registrar_name || "Unknown"}
leading={<Building2 className="h-3.5 w-3.5 text-muted-foreground" />}
/>
<KV
label="Registrant"
value={
domain.privacy_enabled
? "Hidden (Privacy Protected)"
: domain.registrant_name || domain.registrant_org || "Unknown"
}
leading={
domain.privacy_enabled ? (
<EyeOff className="h-3.5 w-3.5 text-muted-foreground" />
) : (
<User className="h-3.5 w-3.5 text-muted-foreground" />
)
}
suffix={
domain.privacy_enabled !== undefined && (
<Badge variant={domain.privacy_enabled ? "default" : "outline"} className="text-[10px]">
{domain.privacy_enabled ? "Privacy On" : "Privacy Off"}
</Badge>
)
}
/>
<KV label="Created" value={formatDate(domain.creation_date) || "Unknown"} />
<KV
label="Expires"
value={formatDate(domain.expiry_date) || "Unknown"}
suffix={
domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 ? (
<Badge
variant={
domain.days_until_expiry <= 7
? "destructive"
: domain.days_until_expiry <= 30
? "outline"
: "secondary"
}
className="text-[10px]"
>
{formatDays(domain.days_until_expiry)}
</Badge>
) : null
}
/>
{domain.registrar_id && <KV label="Registrar IANA ID" value={domain.registrar_id} />}
{domain.whois_server && <KV label="WHOIS Server" value={domain.whois_server} />}
</KVGrid>
{domain.whois_status && (
<div className="mt-3 pt-3 border-t">
<p className="text-[10px] uppercase tracking-wider text-foreground/60 mb-2">EPP Status Codes</p>
<div className="flex flex-wrap gap-1.5">
{domain.whois_status.split(", ").map((s, i) => (
<Badge key={i} variant="secondary" className="text-[10px]">
{s}
</Badge>
))}
</div>
</div>
)}
</SectionCard>
)
}
export function HostingSection({ domain }: { domain: Domain }) {
const location = [domain.host_city, domain.host_region, domain.host_country].filter(Boolean).join(", ") || null
const hasCoords = domain.host_lat !== undefined && domain.host_lon !== undefined
return (
<SectionCard title="Hosting & Email" description="Providers and IP geolocation" icon={Server} accent="green">
<KVGrid>
{domain.dns_provider && (
<KV
label="DNS"
value={domain.dns_provider}
leading={<Network className="h-3.5 w-3.5 text-muted-foreground" />}
/>
)}
{domain.hosting_provider && (
<KV
label="Hosting"
value={domain.hosting_provider}
leading={<Server className="h-3.5 w-3.5 text-muted-foreground" />}
/>
)}
{domain.email_provider && (
<KV
label="Email"
value={domain.email_provider}
leading={<Mail className="h-3.5 w-3.5 text-muted-foreground" />}
/>
)}
{domain.ca_provider && (
<KV
label="Certificate Authority"
value={domain.ca_provider}
leading={<Shield className="h-3.5 w-3.5 text-muted-foreground" />}
/>
)}
{location && (
<KV
label="Location"
value={location}
leading={
<span className="text-sm">
{domain.host_country_code ? (
<span title={domain.host_country_code}>
{String.fromCodePoint(
...domain.host_country_code
.toUpperCase()
.split("")
.map((c) => 127397 + c.charCodeAt(0))
)}
</span>
) : (
<MapPin className="h-3.5 w-3.5 text-muted-foreground" />
)}
</span>
}
/>
)}
</KVGrid>
{hasCoords && domain.host_lat && domain.host_lon && (
<MapEmbed lat={domain.host_lat} lon={domain.host_lon} title={`Map for ${domain.domain_name}`} />
)}
{/* IP Addresses */}
<div className="mt-3 pt-3 border-t">
<p className="text-[10px] uppercase tracking-wider text-foreground/60 mb-2">IP Addresses</p>
<div className="space-y-1">
{domain.ipv4_addresses?.map((ip) => (
<div key={ip} className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] font-mono">
IPv4
</Badge>
<code className="text-sm font-mono">{ip}</code>
</div>
))}
{domain.ipv6_addresses?.map((ip) => (
<div key={ip} className="flex items-center gap-2">
<Badge variant="outline" className="text-[10px] font-mono">
IPv6
</Badge>
<code className="text-sm font-mono break-all">{ip}</code>
</div>
))}
{!domain.ipv4_addresses?.length && !domain.ipv6_addresses?.length && (
<p className="text-sm text-muted-foreground">No IP addresses found</p>
)}
</div>
</div>
</SectionCard>
)
}
export function DnsSection({ domain }: { domain: Domain }) {
const aRecords =
domain.dns_a_records?.map((v) => ({ value: v })) || domain.ipv4_addresses?.map((v) => ({ value: v })) || []
const aaaaRecords =
domain.dns_aaaa_records?.map((v) => ({ value: v })) || domain.ipv6_addresses?.map((v) => ({ value: v })) || []
const mxRecords =
domain.dns_mx_records?.map((v) => ({ value: v })) || domain.mx_records?.map((v) => ({ value: v })) || []
const nsRecords =
domain.dns_ns_records?.map((v) => ({ value: v })) || domain.name_servers?.map((v) => ({ value: v })) || []
const txtRecords =
domain.dns_txt_records?.map((v) => ({ value: v })) || domain.txt_records?.map((v) => ({ value: v })) || []
if (
!aRecords.length &&
!aaaaRecords.length &&
!mxRecords.length &&
!nsRecords.length &&
!txtRecords.length &&
!domain.cname_record &&
!domain.srv_records?.length
) {
return (
<SectionCard title="DNS Records" description="A, AAAA, MX, CNAME, TXT, NS" icon={Network} accent="orange">
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No DNS records available</p>
</div>
</SectionCard>
)
}
return (
<SectionCard title="DNS Records" description="A, AAAA, MX, CNAME, TXT, NS" icon={Network} accent="orange">
<div className="space-y-4">
<DnsGroup type="A" records={aRecords} />
<DnsGroup type="AAAA" records={aaaaRecords} />
{domain.cname_record && <DnsGroup type="CNAME" records={[{ value: domain.cname_record }]} />}
<DnsGroup type="MX" records={mxRecords} />
<DnsGroup type="TXT" records={txtRecords} />
<DnsGroup type="NS" records={nsRecords} />
{domain.srv_records && domain.srv_records.length > 0 && (
<DnsGroup type="SRV" records={domain.srv_records.map((v) => ({ value: v }))} />
)}
{domain.dnssec && (
<div className="flex items-center gap-2 pt-2 border-t">
<span className="text-xs text-muted-foreground">DNSSEC</span>
<Badge variant={domain.dnssec === "signed" ? "default" : "secondary"} className="text-[10px]">
{domain.dnssec}
</Badge>
</div>
)}
</div>
</SectionCard>
)
}
export function SslSection({ domain }: { domain: Domain }) {
if (!domain.ssl_valid_to) {
return (
<SectionCard title="SSL Certificates" description="Issuer and validity" icon={Shield} accent="purple">
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No SSL certificate information available</p>
</div>
</SectionCard>
)
}
return (
<SectionCard title="SSL Certificates" description="Issuer and validity" icon={Shield} accent="purple">
<div className="space-y-3">
<KVGrid>
<KV
label="Status"
value={domain.ssl_days_until && domain.ssl_days_until > 0 ? "Valid" : "Expired"}
leading={<StatusDot status={domain.ssl_days_until && domain.ssl_days_until > 0 ? "up" : "down"} />}
suffix={
domain.ssl_days_until !== undefined && (
<Badge
variant={
domain.ssl_days_until <= 7 ? "destructive" : domain.ssl_days_until <= 30 ? "outline" : "secondary"
}
className="text-[10px]"
>
{formatDays(domain.ssl_days_until)}
</Badge>
)
}
/>
<KV label="Subject" value={domain.ssl_subject || domain.domain_name} />
<KV label="Issuer" value={domain.ssl_issuer || "Unknown"} />
<KV label="Valid From" value={formatDate(domain.ssl_valid_from) || "Unknown"} />
<KV label="Valid To" value={formatDate(domain.ssl_valid_to) || "Unknown"} />
{domain.ssl_key_size && <KV label="Key Size" value={`${domain.ssl_key_size} bits`} />}
{domain.ssl_signature_algo && <KV label="Algorithm" value={domain.ssl_signature_algo} />}
</KVGrid>
{domain.certificates && domain.certificates.length > 0 && (
<div className="space-y-2 pt-2 border-t">
<p className="text-[10px] uppercase tracking-wider text-foreground/60">
Certificate Chain ({domain.certificates.length})
</p>
{domain.certificates.map((cert, i) => (
<CertificateCard key={i} cert={cert} index={i} total={domain.certificates?.length ?? 0} />
))}
</div>
)}
</div>
</SectionCard>
)
}
export function SeoSection({ domain }: { domain: Domain }) {
const seo = domain.seo_meta
if (!seo) {
return (
<SectionCard title="SEO & Social" description="Meta tags, previews, robots.txt" icon={Search} accent="slate">
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<p className="text-sm text-muted-foreground">No SEO data available</p>
</div>
</SectionCard>
)
}
const metaTags = seo.general
const og = seo.openGraph
const twitter = seo.twitter
const robots = seo.robots
return (
<SectionCard title="SEO & Social" description="Meta tags, previews, robots.txt" icon={Search} accent="slate">
<div className="space-y-4">
{/* Meta Tags */}
{metaTags && (
<div>
<div className="flex items-center gap-2 mb-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium">Meta Tags</span>
{Object.values(metaTags).filter(Boolean).length > 0 && (
<Badge variant="secondary" className="text-[10px]">
{Object.values(metaTags).filter(Boolean).length}
</Badge>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{metaTags.title && <KV label="Title" value={metaTags.title} />}
{metaTags.description && <KV label="Description" value={metaTags.description} />}
{metaTags.canonical && <KV label="Canonical" value={metaTags.canonical} />}
{metaTags.robots && <KV label="Robots" value={metaTags.robots} />}
{metaTags.author && <KV label="Author" value={metaTags.author} />}
{metaTags.keywords && (
<div className="sm:col-span-2">
<KV
label="Keywords"
value={metaTags.keywords.substring(0, 120) + (metaTags.keywords.length > 120 ? "..." : "")}
/>
</div>
)}
</div>
</div>
)}
{/* Open Graph Preview */}
{og && (og.title || og.description) && (
<div className="pt-3 border-t">
<div className="flex items-center gap-2 mb-2">
<Globe className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium">Open Graph</span>
</div>
<div className="rounded-lg border bg-background/40 p-3 space-y-1">
{og.images && og.images.length > 0 && (
<div className="mb-2">
<img
src={og.images[0]}
alt="OG preview"
className="max-h-32 rounded-md object-cover w-full"
loading="lazy"
onError={(e) => ((e.target as HTMLImageElement).style.display = "none")}
/>
</div>
)}
<p className="text-sm font-medium text-foreground/90">{og.title}</p>
<p className="text-xs text-muted-foreground line-clamp-2">{og.description}</p>
{og.url && (
<a
href={og.url}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] text-primary hover:underline truncate block"
>
{og.url}
</a>
)}
</div>
</div>
)}
{/* Twitter Card Preview */}
{twitter && (twitter.title || twitter.description) && (
<div className="pt-3 border-t">
<div className="flex items-center gap-2 mb-2">
<ExternalLink className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium">Twitter/X Card</span>
{twitter.card && (
<Badge variant="outline" className="text-[10px]">
{twitter.card}
</Badge>
)}
</div>
<div className="rounded-lg border bg-background/40 p-3 space-y-1">
{twitter.image && (
<img
src={twitter.image}
alt="Twitter preview"
className="max-h-32 rounded-md object-cover w-full"
loading="lazy"
onError={(e) => ((e.target as HTMLImageElement).style.display = "none")}
/>
)}
<p className="text-sm font-medium text-foreground/90">{twitter.title}</p>
<p className="text-xs text-muted-foreground line-clamp-2">{twitter.description}</p>
</div>
</div>
)}
{/* robots.txt */}
{robots?.fetched && (
<div className="pt-3 border-t">
<div className="flex items-center gap-2 mb-2">
<Code2 className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-medium">robots.txt</span>
</div>
{robots.sitemaps && robots.sitemaps.length > 0 && (
<div className="mb-2">
<p className="text-[10px] text-muted-foreground mb-1">Sitemaps</p>
<div className="flex flex-wrap gap-1">
{robots.sitemaps.map((s, i) => (
<a
key={i}
href={s}
target="_blank"
rel="noopener noreferrer"
className="text-[10px] text-primary hover:underline truncate max-w-[300px]"
>
{s}
</a>
))}
</div>
</div>
)}
{robots.groups && robots.groups.length > 0 && (
<div className="space-y-2">
{robots.groups.map((group, i) => (
<div key={i} className="rounded-md bg-muted/50 p-2 text-xs space-y-1">
<p className="text-muted-foreground font-medium">User-agent: {group.userAgents.join(", ")}</p>
{group.rules.map((rule, j) => (
<div key={j} className="flex items-center gap-1.5">
{rule.type === "Allow" ? (
<CheckCircle2 className="h-3 w-3 text-green-500" />
) : (
<AlertTriangle className="h-3 w-3 text-yellow-500" />
)}
<span className={rule.type === "Allow" ? "text-green-600" : "text-yellow-600"}>
{rule.type}: {rule.value}
</span>
</div>
))}
</div>
))}
</div>
)}
</div>
)}
</div>
</SectionCard>
)
}
export function DomainTypeBadge({ type }: { type?: string }) {
if (!type) return null
const configs: Record<string, { color: string; icon: React.ElementType; label: string }> = {
expiry: { color: "bg-blue-500/10 text-blue-600 border-blue-500/20", icon: Clock, label: "Expiry Monitor" },
watchlist: { color: "bg-purple-500/10 text-purple-600 border-purple-500/20", icon: Eye, label: "Watchlist" },
portfolio: { color: "bg-green-500/10 text-green-600 border-green-500/20", icon: Globe, label: "Portfolio" },
}
const config = configs[type] || configs.expiry
const Icon = config.icon
return (
<Badge variant="outline" className={cn("gap-1 text-[10px]", config.color)}>
<Icon className="h-3 w-3" />
{config.label}
</Badge>
)
}
export function ValuationSection({ domain }: { domain: Domain }) {
const hasData = (domain.purchase_price ?? 0) > 0 || (domain.current_value ?? 0) > 0 || (domain.renewal_cost ?? 0) > 0
if (!hasData) return null
return (
<SectionCard
title="Valuation & Costs"
description="Financial information and renewal settings"
icon={FileText}
accent="yellow"
>
<KVGrid>
{(domain.purchase_price ?? 0) > 0 && <KV label="Purchase Price" value={`$${domain.purchase_price}`} />}
{(domain.current_value ?? 0) > 0 && <KV label="Current Value" value={`$${domain.current_value}`} />}
{(domain.renewal_cost ?? 0) > 0 && <KV label="Renewal Cost" value={`$${domain.renewal_cost}`} />}
<KV
label="Auto-renew"
value={domain.auto_renew ? "Enabled" : "Disabled"}
leading={
domain.auto_renew ? (
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
) : (
<AlertTriangle className="h-3.5 w-3.5 text-yellow-500" />
)
}
/>
</KVGrid>
</SectionCard>
)
}
export function DomainExpiryOverview({ domain }: { domain: Domain }) {
return (
<div className="grid sm:grid-cols-2 gap-4">
{/* Domain Expiry */}
<Card
className={cn(
"overflow-hidden",
domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 && domain.days_until_expiry <= 7
? "border-red-500/30"
: domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 && domain.days_until_expiry <= 30
? "border-yellow-500/30"
: ""
)}
>
<div
className={cn(
"h-1",
domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 && domain.days_until_expiry <= 7
? "bg-red-500"
: domain.days_until_expiry !== undefined &&
domain.days_until_expiry >= 0 &&
domain.days_until_expiry <= 30
? "bg-yellow-500"
: "bg-green-500"
)}
/>
<CardContent className="p-5">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div
className={cn(
"p-2.5 rounded-xl",
domain.days_until_expiry !== undefined &&
domain.days_until_expiry >= 0 &&
domain.days_until_expiry <= 7
? "bg-red-500/10"
: domain.days_until_expiry !== undefined &&
domain.days_until_expiry >= 0 &&
domain.days_until_expiry <= 30
? "bg-yellow-500/10"
: "bg-green-500/10"
)}
>
<Globe
className={cn(
"h-5 w-5",
domain.days_until_expiry !== undefined &&
domain.days_until_expiry >= 0 &&
domain.days_until_expiry <= 7
? "text-red-500"
: domain.days_until_expiry !== undefined &&
domain.days_until_expiry >= 0 &&
domain.days_until_expiry <= 30
? "text-yellow-500"
: "text-green-500"
)}
/>
</div>
<div>
<p className="text-sm text-muted-foreground">Domain Expires</p>
<p className="font-semibold">{formatDate(domain.expiry_date) || "N/A"}</p>
</div>
</div>
<div
className={cn(
"text-xl font-bold",
domain.days_until_expiry !== undefined && domain.days_until_expiry >= 0 && domain.days_until_expiry <= 7
? "text-red-500"
: domain.days_until_expiry !== undefined &&
domain.days_until_expiry >= 0 &&
domain.days_until_expiry <= 30
? "text-yellow-500"
: "text-green-500"
)}
>
{typeof domain.days_until_expiry === "number" && domain.days_until_expiry >= 0
? formatDays(domain.days_until_expiry)
: domain.days_until_expiry === -1
? "No data"
: "N/A"}
</div>
</div>
</CardContent>
</Card>
{/* SSL Expiry */}
<Card
className={cn(
"overflow-hidden",
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 7
? "border-red-500/30"
: domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 30
? "border-yellow-500/30"
: ""
)}
>
<div
className={cn(
"h-1",
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 7
? "bg-red-500"
: domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 30
? "bg-yellow-500"
: "bg-green-500"
)}
/>
<CardContent className="p-5">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div
className={cn(
"p-2.5 rounded-xl",
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 7
? "bg-red-500/10"
: domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 30
? "bg-yellow-500/10"
: "bg-green-500/10"
)}
>
<Shield
className={cn(
"h-5 w-5",
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 7
? "text-red-500"
: domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 30
? "text-yellow-500"
: "text-green-500"
)}
/>
</div>
<div>
<p className="text-sm text-muted-foreground">SSL Expires</p>
<p className="font-semibold">{formatDate(domain.ssl_valid_to) || "No SSL"}</p>
</div>
</div>
<div
className={cn(
"text-xl font-bold",
domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 7
? "text-red-500"
: domain.ssl_days_until !== undefined && domain.ssl_days_until >= 0 && domain.ssl_days_until <= 30
? "text-yellow-500"
: "text-green-500"
)}
>
{typeof domain.ssl_days_until === "number" && domain.ssl_days_until >= 0
? formatDays(domain.ssl_days_until)
: "N/A"}
</div>
</div>
</CardContent>
</Card>
</div>
)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,403 @@
import { useEffect, useMemo, useState } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Globe, Shield, Server, MapPin, FileText, Info, AlertTriangle } from "lucide-react"
import { cn } from "@/lib/utils"
import { formatDate } from "@/lib/domains"
import type { Monitor, Heartbeat } from "@/lib/monitors"
// --- Styled components inspired by domainstack.io ---
function InfoSection({
title,
icon: Icon,
children,
accent = "slate",
}: {
title: string
icon: React.ElementType
children: React.ReactNode
accent?: "blue" | "green" | "orange" | "purple" | "slate"
}) {
const accentColors = {
blue: "border-blue-500/10 bg-blue-500/5",
green: "border-green-500/10 bg-green-500/5",
orange: "border-orange-500/10 bg-orange-500/5",
purple: "border-purple-500/10 bg-purple-500/5",
slate: "border-border bg-background/60",
}
return (
<Card className={cn("relative overflow-hidden rounded-xl border", accentColors[accent])}>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-base">{title}</CardTitle>
</div>
</CardHeader>
<CardContent>{children}</CardContent>
</Card>
)
}
function KV({
label,
value,
suffix,
leading,
}: {
label: string
value: string
suffix?: React.ReactNode
leading?: React.ReactNode
}) {
return (
<div className="flex h-14 min-w-0 items-center justify-between gap-3 rounded-lg border bg-background/60 px-3 py-2">
<div className="flex min-w-0 flex-col">
<div className="text-[10px] leading-none tracking-wider uppercase text-foreground/70">{label}</div>
<div className="inline-flex min-w-0 items-center gap-1.5 text-[13px] text-foreground/95 mt-1">
{leading ? <span className="shrink-0">{leading}</span> : null}
<span className="truncate">{value}</span>
{suffix ? <span className="shrink-0">{suffix}</span> : null}
</div>
</div>
</div>
)
}
function KVGrid({ children, cols = 2 }: { children: React.ReactNode; cols?: 1 | 2 | 3 }) {
const colClass = cols === 3 ? "sm:grid-cols-3" : cols === 1 ? "grid-cols-1" : "sm:grid-cols-2"
return <div className={cn("grid grid-cols-1 gap-2", colClass)}>{children}</div>
}
// --- Data fetching hooks ---
interface DnsRecord {
type: string
value: string
ttl?: number
}
interface DomainInfo {
hostname: string | null
rootDomain: string | null
dnsRecords: DnsRecord[]
geo: { city?: string; region?: string; country?: string; lat?: number; lon?: number } | null
ssl: { valid: boolean; expiry?: string; daysLeft?: number } | null
seo: {
title?: string
description?: string
canonical?: string
robots?: string
generator?: string
} | null
loading: boolean
}
function useDomainInfo(monitor: Monitor | undefined, heartbeats: Heartbeat[] | undefined) {
const [info, setInfo] = useState<DomainInfo>({
hostname: null,
rootDomain: null,
dnsRecords: [],
geo: null,
ssl: null,
seo: null,
loading: true,
})
const hostname = useMemo(() => {
if (!monitor) return null
if (monitor.hostname) return monitor.hostname.toLowerCase()
if (monitor.url) {
try {
const url = new URL(monitor.url.startsWith("http") ? monitor.url : `https://${monitor.url}`)
return url.hostname.toLowerCase()
} catch {
return monitor.url.toLowerCase()
}
}
return null
}, [monitor])
const rootDomain = useMemo(() => {
if (!hostname) return null
const clean = hostname.replace(/^www\./, "")
const parts = clean.split(".")
if (parts.length <= 2) return clean
const specialTLDs = ["co.uk", "com.au", "co.jp", "com.br", "co.nz", "co.za", "co.in", "com.cn"]
const lastTwo = parts.slice(-2).join(".")
const lastThree = parts.slice(-3).join(".")
if (specialTLDs.includes(lastThree)) return lastThree
return lastTwo
}, [hostname])
// SSL from latest heartbeat
useEffect(() => {
if (!heartbeats?.length) {
setInfo((prev) => ({ ...prev, ssl: null }))
return
}
const latest = heartbeats[0]
if (latest.cert_expiry) {
const expiry = new Date(latest.cert_expiry * 1000)
const daysLeft = Math.ceil((expiry.getTime() - Date.now()) / (1000 * 60 * 60 * 24))
setInfo((prev) => ({
...prev,
ssl: { valid: latest.cert_valid ?? true, expiry: expiry.toISOString(), daysLeft },
}))
}
}, [heartbeats])
// Fetch DNS, geo, SEO
useEffect(() => {
if (!hostname) {
setInfo((prev) => ({ ...prev, loading: false }))
return
}
let cancelled = false
async function fetchData() {
// DNS via Cloudflare DoH
const dnsPromise = fetch(`https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(hostname)}&type=A`, {
headers: { Accept: "application/dns-json" },
})
.then((r) => r.json())
.then((data) => {
const records: DnsRecord[] = []
if (data.Answer) {
for (const ans of data.Answer) {
records.push({ type: "A", value: ans.data, ttl: ans.TTL })
}
}
return records
})
.catch(() => [] as DnsRecord[])
// Geolocation via ipapi.co (free, no key needed for basic)
const geoPromise = fetch(`https://ipapi.co/${encodeURIComponent(hostname)}/json/`, {
headers: { Accept: "application/json" },
})
.then((r) => {
if (!r.ok) throw new Error("geo failed")
return r.json()
})
.then((data) => ({
city: data.city,
region: data.region,
country: data.country_name,
lat: data.latitude,
lon: data.longitude,
}))
.catch(() => null)
// SEO meta tags
const seoPromise = monitor?.url
? fetch(monitor.url, { method: "GET", mode: "cors" })
.then((r) => r.text())
.then((html) => {
const parser = new DOMParser()
const doc = parser.parseFromString(html, "text/html")
const getMeta = (name: string) =>
doc.querySelector(`meta[name="${name}"]`)?.getAttribute("content") ||
doc.querySelector(`meta[property="og:${name}"]`)?.getAttribute("content") ||
undefined
return {
title: doc.querySelector("title")?.textContent || undefined,
description: getMeta("description"),
canonical: doc.querySelector('link[rel="canonical"]')?.getAttribute("href") || undefined,
robots: getMeta("robots"),
generator: getMeta("generator"),
}
})
.catch(() => null)
: Promise.resolve(null)
const [dnsRecords, geo, seo] = await Promise.all([dnsPromise, geoPromise, seoPromise])
if (!cancelled) {
setInfo((prev) => ({
...prev,
hostname,
rootDomain,
dnsRecords,
geo,
seo,
loading: false,
}))
}
}
fetchData()
return () => {
cancelled = true
}
}, [hostname, monitor?.url, rootDomain])
return { ...info, hostname, rootDomain }
}
// --- Map component ---
function MapEmbed({ lat, lon, hostname }: { lat: number; lon: number; hostname?: string | null }) {
const mapUrl = `https://www.openstreetmap.org/export/embed.html?bbox=${lon - 0.5}%2C${lat - 0.5}%2C${lon + 0.5}%2C${lat + 0.5}&layer=mapnik&marker=${lat}%2C${lon}`
return (
<div className="relative h-[220px] w-full overflow-hidden rounded-lg border">
<iframe
title={`Map for ${hostname || "location"}`}
src={mapUrl}
className="h-full w-full border-0"
loading="lazy"
/>
<a
href={`https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=12/${lat}/${lon}`}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-2 right-2 rounded bg-white/90 px-2 py-1 text-[10px] font-medium text-foreground shadow hover:bg-white"
>
View Larger Map
</a>
</div>
)
}
// --- Main composite component ---
export function MonitorInfoSections({
monitor,
heartbeats,
}: {
monitor: Monitor | undefined
heartbeats: Heartbeat[] | undefined
}) {
const { hostname, rootDomain, dnsRecords, geo, ssl, seo, loading } = useDomainInfo(monitor, heartbeats)
if (!monitor) return null
const showSeo = seo && (seo.title || seo.description)
return (
<div className="grid gap-4">
{/* Registration / Domain Overview */}
<InfoSection title="Domain Overview" icon={Globe} accent="blue">
<KVGrid>
<KV
label="Hostname"
value={hostname || "N/A"}
leading={<Globe className="h-3.5 w-3.5 text-muted-foreground" />}
/>
<KV
label="Root Domain"
value={rootDomain || "N/A"}
leading={<Server className="h-3.5 w-3.5 text-muted-foreground" />}
/>
<KV label="Type" value={monitor.type.toUpperCase()} />
<KV label="Created" value={formatDate(monitor.created)} />
</KVGrid>
</InfoSection>
<div className="grid sm:grid-cols-2 gap-4">
{/* Hosting & Geolocation */}
<InfoSection title="Hosting & Location" icon={MapPin} accent="green">
{geo ? (
<div className="space-y-3">
<KVGrid cols={1}>
<KV
label="Location"
value={[geo.city, geo.region, geo.country].filter(Boolean).join(", ") || "Unknown"}
leading={<MapPin className="h-3.5 w-3.5 text-muted-foreground" />}
/>
</KVGrid>
{geo.lat && geo.lon ? <MapEmbed lat={geo.lat} lon={geo.lon} hostname={hostname} /> : null}
</div>
) : loading ? (
<div className="flex items-center gap-2 py-4 text-sm text-muted-foreground">
<span className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-foreground" />
Looking up location...
</div>
) : (
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div className="text-sm text-muted-foreground">No geolocation data available.</div>
</div>
)}
</InfoSection>
{/* SSL Certificate */}
<InfoSection title="SSL Certificate" icon={Shield} accent="purple">
{ssl ? (
<div className="space-y-2">
<KVGrid cols={1}>
<KV
label="Status"
value={ssl.valid ? "Valid" : "Invalid"}
leading={
<div className={cn("h-2.5 w-2.5 rounded-full", ssl.valid ? "bg-green-500" : "bg-red-500")} />
}
/>
<KV label="Expires" value={ssl.expiry ? formatDate(ssl.expiry) : "Unknown"} />
{ssl.daysLeft !== undefined && (
<KV
label="Days Left"
value={`${ssl.daysLeft} days`}
suffix={
ssl.daysLeft <= 7 ? (
<Badge variant="destructive" className="text-[10px]">
Expiring Soon
</Badge>
) : ssl.daysLeft <= 30 ? (
<Badge variant="outline" className="text-[10px] border-yellow-500/50 text-yellow-600">
Warning
</Badge>
) : null
}
/>
)}
</KVGrid>
</div>
) : monitor.type === "https" || monitor.url?.startsWith("https") ? (
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div className="text-sm text-muted-foreground">No SSL data yet. It will appear after the next check.</div>
</div>
) : (
<div className="flex items-start gap-3 rounded-lg border border-dashed p-4">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div className="text-sm text-muted-foreground">SSL not applicable for this monitor type.</div>
</div>
)}
</InfoSection>
</div>
{/* DNS Records */}
{dnsRecords.length > 0 && (
<InfoSection title="DNS Records" icon={Server} accent="orange">
<div className="space-y-2">
{dnsRecords.map((rec, i) => (
<div key={i} className="flex items-center gap-3 rounded-lg border bg-background/60 px-3 py-2">
<Badge variant="outline" className="shrink-0 font-mono text-[10px]">
{rec.type}
</Badge>
<span className="text-[13px] text-foreground/90 truncate">{rec.value}</span>
{rec.ttl && <span className="ml-auto text-[10px] text-muted-foreground">TTL {rec.ttl}</span>}
</div>
))}
</div>
</InfoSection>
)}
{/* SEO & Meta */}
{showSeo && (
<InfoSection title="SEO & Social" icon={FileText} accent="slate">
<KVGrid>
{seo?.title && <KV label="Title" value={seo.title} />}
{seo?.description && <KV label="Description" value={seo.description} />}
{seo?.canonical && <KV label="Canonical" value={seo.canonical} />}
{seo?.robots && <KV label="Robots" value={seo.robots} />}
{seo?.generator && <KV label="Generator" value={seo.generator} />}
</KVGrid>
</InfoSection>
)}
</div>
)
}
+467 -150
View File
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import {
AlertDialog,
AlertDialogAction,
@@ -17,7 +18,6 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import {
Globe,
Clock,
@@ -33,10 +33,17 @@ import {
TrendingUp,
TrendingDown,
Plus,
Zap,
Gauge,
Smartphone,
Lock,
Eye,
LayoutDashboard,
type LucideIcon,
} from "lucide-react"
import {
type Heartbeat,
type Monitor,
getMonitor,
getMonitorStats,
getMonitorHeartbeats,
@@ -45,8 +52,10 @@ import {
resumeMonitor,
deleteMonitor,
getMonitorTypeLabel,
getMonitorFaviconUrl,
formatUptime,
formatPing,
runPageSpeedCheck,
} from "@/lib/monitors"
import { formatDate } from "@/lib/domains"
import {
@@ -57,23 +66,324 @@ import {
getStatusPageUrl,
removeMonitorFromStatusPage,
} from "@/lib/statuspages"
import {
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Area,
Cell,
ComposedChart,
Legend,
} from "recharts"
import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Line, LineChart } from "recharts"
import { Link, navigate } from "@/components/router"
import { AddMonitorDialog } from "@/components/monitors-table/add-monitor-dialog"
import { cn } from "@/lib/utils"
import { MonitorInfoSections } from "./monitor-info-sections"
type HeartbeatRow = Heartbeat & { timestamp?: string }
type HeartbeatRow = Heartbeat
// Uptime Bar Component - Visual timeline of recent checks
function UptimeBarVisualization({ heartbeats }: { heartbeats?: HeartbeatRow[] }) {
const recent = useMemo(() => {
if (!heartbeats?.length) return []
return heartbeats.slice(0, 30).reverse()
}, [heartbeats])
if (!recent.length) {
return (
<div className="flex gap-0.5 h-8 items-center">
{Array.from({ length: 12 }).map((_, i) => (
<div key={i} className="flex-1 h-6 rounded-sm bg-muted/50" />
))}
</div>
)
}
return (
<div className="space-y-2">
<div className="flex gap-0.5 h-8">
{recent.map((hb, i) => (
<div
key={i}
className={cn(
"flex-1 rounded-sm transition-all hover:opacity-80 cursor-pointer",
hb.status === "up"
? "bg-green-500"
: hb.status === "down"
? "bg-red-500"
: hb.status === "paused"
? "bg-gray-400"
: "bg-yellow-500"
)}
title={`${hb.status} • ${formatPing(hb.ping)} • ${formatDate(hb.time || "")}`}
/>
))}
</div>
<div className="flex justify-between text-xs text-muted-foreground">
<span>{recent.length} recent checks</span>
<span>
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-green-500" />
{recent.filter((h) => h.status === "up").length} up
</span>
<span className="inline-flex items-center gap-1 ml-3">
<span className="w-2 h-2 rounded-full bg-red-500" />
{recent.filter((h) => h.status === "down").length} down
</span>
</span>
</div>
</div>
)
}
// Response time statistics component
function ResponseTimeStats({ heartbeats }: { heartbeats?: HeartbeatRow[] }) {
const stats = useMemo(() => {
if (!heartbeats?.length) return null
const pings = heartbeats.filter((h) => h.ping && h.ping > 0).map((h) => h.ping)
if (!pings.length) return null
const sorted = [...pings].sort((a, b) => a - b)
const avg = Math.round(pings.reduce((a, b) => a + b, 0) / pings.length)
const min = sorted[0]
const max = sorted[sorted.length - 1]
const p95 = sorted[Math.floor(sorted.length * 0.95)]
const p99 = sorted[Math.floor(sorted.length * 0.99)]
return { avg, min, max, p95, p99, count: pings.length }
}, [heartbeats])
if (!stats) return null
return (
<div className="grid grid-cols-5 gap-2 text-center">
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">Avg</div>
<div className="text-lg font-semibold">{formatPing(stats.avg)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">Min</div>
<div className="text-lg font-semibold text-green-600">{formatPing(stats.min)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">Max</div>
<div className="text-lg font-semibold text-red-600">{formatPing(stats.max)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">P95</div>
<div className="text-lg font-semibold">{formatPing(stats.p95)}</div>
</div>
<div className="p-2 bg-muted/50 rounded-lg">
<div className="text-xs text-muted-foreground">P99</div>
<div className="text-lg font-semibold">{formatPing(stats.p99)}</div>
</div>
</div>
)
}
function getVitalColor(status: string): string {
switch (status) {
case "good":
return "text-green-500"
case "needs-improvement":
return "text-yellow-500"
default:
return "text-red-500"
}
}
function getVitalBg(status: string): string {
switch (status) {
case "good":
return "bg-green-500/10 border-green-500/20"
case "needs-improvement":
return "bg-yellow-500/10 border-yellow-500/20"
default:
return "bg-red-500/10 border-red-500/20"
}
}
function ScoreRing({ score, label }: { score: number; label: string }) {
const color = score >= 90 ? "text-green-500" : score >= 70 ? "text-yellow-500" : "text-red-500"
const bg = score >= 90 ? "stroke-green-500" : score >= 70 ? "stroke-yellow-500" : "stroke-red-500"
const circumference = 2 * Math.PI * 18
const offset = circumference - (score / 100) * circumference
return (
<div className="flex flex-col items-center gap-1">
<div className="relative w-12 h-12">
<svg
className="w-12 h-12 -rotate-90"
viewBox="0 0 44 44"
role="img"
aria-label={`Score ${Math.round(score)} for ${label}`}
>
<title>
Score {Math.round(score)} for {label}
</title>
<circle cx="22" cy="22" r="18" fill="none" stroke="currentColor" strokeWidth="4" className="text-muted/30" />
<circle
cx="22"
cy="22"
r="18"
fill="none"
strokeWidth="4"
strokeLinecap="round"
className={bg}
strokeDasharray={circumference}
strokeDashoffset={offset}
/>
</svg>
<span className={cn("absolute inset-0 flex items-center justify-center text-xs font-bold", color)}>
{Math.round(score)}
</span>
</div>
<span className="text-[10px] text-muted-foreground uppercase tracking-wider">{label}</span>
</div>
)
}
function VitalCard({ label, value, status, detail }: { label: string; value: string; status: string; detail: string }) {
return (
<div className={cn("p-3 rounded-lg border", getVitalBg(status))}>
<div className="flex items-center justify-between mb-1">
<span className="text-xs text-muted-foreground">{label}</span>
<div
className={cn(
"w-2 h-2 rounded-full",
status === "good" ? "bg-green-500" : status === "needs-improvement" ? "bg-yellow-500" : "bg-red-500"
)}
/>
</div>
<div className={cn("text-lg font-bold", getVitalColor(status))}>{value}</div>
<div className="text-[10px] text-muted-foreground">{detail}</div>
</div>
)
}
function formatMs(ms: number): string {
if (ms < 1000) return `${Math.round(ms)}ms`
return `${(ms / 1000).toFixed(1)}s`
}
function CoreWebVitalsCard({ monitorId, url }: { monitorId: string; url?: string }) {
const [strategy, setStrategy] = useState<"mobile" | "desktop">("mobile")
const { toast } = useToast()
const {
data,
isPending: isPageSpeedLoading,
mutate,
} = useMutation({
mutationFn: () => runPageSpeedCheck(monitorId, strategy),
onSuccess: () => {
toast({ title: "Lighthouse check complete" })
},
onError: (err: Error) => {
toast({ title: "Check failed", description: err.message, variant: "destructive" })
},
})
if (!url) return null
return (
<Card>
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<CardTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-yellow-500" />
Core Web Vitals
</CardTitle>
<CardDescription>
{data
? `Checked ${new Date(data.checkedAt).toLocaleString()}`
: "Run a Lighthouse check to get performance metrics"}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<div className="flex rounded-lg border overflow-hidden">
<button
onClick={() => setStrategy("mobile")}
className={cn(
"px-3 py-1.5 text-xs font-medium transition-colors",
strategy === "mobile" ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
)}
>
<Smartphone className="h-3 w-3 inline mr-1" />
Mobile
</button>
<button
onClick={() => setStrategy("desktop")}
className={cn(
"px-3 py-1.5 text-xs font-medium transition-colors",
strategy === "desktop" ? "bg-primary text-primary-foreground" : "bg-muted hover:bg-muted/80"
)}
>
<Gauge className="h-3 w-3 inline mr-1" />
Desktop
</button>
</div>
<Button size="sm" onClick={() => mutate()} disabled={isPageSpeedLoading}>
<RefreshCw className={cn("mr-2 h-4 w-4", isPageSpeedLoading && "animate-spin")} />
{isPageSpeedLoading ? "Running..." : "Run Check"}
</Button>
</div>
</CardHeader>
<CardContent>
{data ? (
<div className="space-y-4">
{/* Lighthouse Scores */}
<div className="flex items-center gap-4 justify-center sm:justify-start">
<ScoreRing score={data.performance} label="Perf" />
<ScoreRing score={data.accessibility} label="A11y" />
<ScoreRing score={data.bestPractices} label="BP" />
<ScoreRing score={data.seo} label="SEO" />
</div>
{/* Core Web Vitals */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
<VitalCard
label="LCP"
value={formatMs(data.lcp)}
status={data.vitals.lcp || "poor"}
detail="Largest Contentful Paint"
/>
<VitalCard
label="FID"
value={formatMs(data.tbt)}
status={data.vitals.fid || "poor"}
detail="Total Blocking Time (proxy)"
/>
<VitalCard
label="CLS"
value={data.cls.toFixed(3)}
status={data.vitals.cls || "poor"}
detail="Cumulative Layout Shift"
/>
<VitalCard
label="FCP"
value={formatMs(data.fcp)}
status={data.vitals.fcp || "poor"}
detail="First Contentful Paint"
/>
<VitalCard
label="TTFB"
value={formatMs(data.ttfb)}
status={data.vitals.ttfb || "poor"}
detail="Time to First Byte"
/>
<VitalCard
label="TTI"
value={formatMs(data.tti)}
status={data.vitals.tti || "poor"}
detail="Time to Interactive"
/>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-8 gap-3 text-muted-foreground">
<div className="p-3 bg-muted/50 rounded-full">
<Gauge className="h-6 w-6 opacity-50" />
</div>
<p className="text-sm">No Lighthouse data yet. Click "Run Check" to analyze performance.</p>
<p className="text-xs text-muted-foreground">Powered by Google PageSpeed Insights</p>
</div>
)}
</CardContent>
</Card>
)
}
// Status badge component
function StatusBadge({ status }: { status: string }) {
@@ -136,6 +446,15 @@ function StatCard({
)
}
function MonitorFaviconImage({ monitor, iconColor }: { monitor: Monitor; iconColor: string }) {
const [error, setError] = useState(false)
const faviconUrl = getMonitorFaviconUrl(monitor)
if (!faviconUrl || error) {
return <Globe className={cn("h-6 w-6", iconColor)} />
}
return <img src={faviconUrl} alt="" className="h-6 w-6 object-contain" onError={() => setError(true)} />
}
export default memo(function MonitorDetail({ id }: { id: string }) {
const { toast } = useToast()
const queryClient = useQueryClient()
@@ -197,6 +516,7 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
const [isCreateStatusPageOpen, setIsCreateStatusPageOpen] = useState(false)
const [statusPageName, setStatusPageName] = useState("")
const [statusPageSlug, setStatusPageSlug] = useState("")
const [statusPagePublic, setStatusPagePublic] = useState(true)
const { data: statusPages } = useQuery({
queryKey: ["status-pages"],
@@ -233,19 +553,29 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
})
const createStatusPageMutation = useMutation({
mutationFn: () =>
createStatusPage({
mutationFn: async () => {
const page = await createStatusPage({
name: statusPageName || `${monitor?.name} Status`,
slug: statusPageSlug || monitor?.name?.toLowerCase().replace(/\s+/g, "-") || "status",
title: statusPageName || `${monitor?.name} Status Page`,
public: true,
}),
onSuccess: () => {
public: statusPagePublic,
})
// Auto-link this monitor to the newly created status page
await addMonitorToStatusPage(page.id, { monitor: id })
return page
},
onSuccess: (page) => {
queryClient.invalidateQueries({ queryKey: ["status-pages"] })
toast({ title: "Status page created" })
queryClient.invalidateQueries({ queryKey: ["monitor-status-page-links", id] })
toast({ title: "Status page created and monitor linked" })
setIsCreateStatusPageOpen(false)
setStatusPageName("")
setStatusPageSlug("")
setStatusPagePublic(true)
// Open private pages in new tab since user is authenticated
if (!page.public) {
window.open(getStatusPageUrl(page.slug), "_blank", "noopener,noreferrer")
}
},
})
@@ -264,7 +594,7 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
}
const cutoff = now - (ranges[timeRange] || ranges["24h"])
return heartbeats.filter((h: HeartbeatRow) => {
const t = new Date(h.time || h.timestamp || "").getTime()
const t = new Date(h.time || "").getTime()
return t >= cutoff
})
}, [heartbeats, timeRange])
@@ -276,7 +606,7 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
.slice()
.reverse()
.map((h: HeartbeatRow) => ({
time: new Date(h.time || h.timestamp || "").toLocaleTimeString(),
time: new Date(h.time || "").toLocaleTimeString(),
responseTime: h.ping || 0,
status: h.status === "up" ? 1 : 0,
}))
@@ -320,8 +650,20 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
const isPaused = monitor.status === "paused"
const isPending = monitor.status === "pending"
const headerIconColor = isUp ? "text-green-500" : isPaused ? "text-gray-500" : isPending ? "text-yellow-500" : "text-red-500"
const headerBgColor = isUp ? "bg-green-500/10" : isPaused ? "bg-gray-500/10" : isPending ? "bg-yellow-500/10" : "bg-red-500/10"
const headerIconColor = isUp
? "text-green-500"
: isPaused
? "text-gray-500"
: isPending
? "text-yellow-500"
: "text-red-500"
const headerBgColor = isUp
? "bg-green-500/10"
: isPaused
? "bg-gray-500/10"
: isPending
? "bg-yellow-500/10"
: "bg-red-500/10"
return (
<div className="grid gap-4 mb-14">
@@ -330,13 +672,8 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
<CardContent className="p-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<div
className={cn(
"h-12 w-12 rounded-full flex items-center justify-center",
headerBgColor
)}
>
<Globe className={cn("h-6 w-6", headerIconColor)} />
<div className={cn("h-12 w-12 rounded-full flex items-center justify-center", headerBgColor)}>
<MonitorFaviconImage monitor={monitor} iconColor={headerIconColor} />
</div>
<div>
<h1 className="text-2xl font-bold">{monitor.name}</h1>
@@ -421,13 +758,41 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
/>
</div>
{/* Combined Uptime & Response Chart */}
{/* Uptime Bar & Response Stats */}
<div className="grid sm:grid-cols-2 gap-4">
<Card>
<CardHeader>
<CardTitle>Recent Uptime</CardTitle>
<CardDescription>Visual timeline of the last 30 checks</CardDescription>
</CardHeader>
<CardContent>
<UptimeBarVisualization heartbeats={heartbeats} />
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Response Time Statistics</CardTitle>
<CardDescription>Distribution of response times</CardDescription>
</CardHeader>
<CardContent>
<ResponseTimeStats heartbeats={heartbeats} />
</CardContent>
</Card>
</div>
{/* Core Web Vitals */}
<CoreWebVitalsCard monitorId={id} url={monitor.url} />
{/* Domain Info Sections */}
<MonitorInfoSections monitor={monitor} heartbeats={heartbeats} />
{/* Response Time Chart */}
<Card>
<CardHeader className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<CardTitle>Uptime & Response Time</CardTitle>
<CardTitle>Response Time</CardTitle>
<CardDescription>
<Trans>Status and response time over the selected period</Trans>
<Trans>Response time over the selected period</Trans>
</CardDescription>
</div>
<div className="flex items-center gap-2">
@@ -447,45 +812,36 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
<div className="h-[300px]">
{chartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData}>
<LineChart data={chartData}>
<defs>
<linearGradient id="colorResponse" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.3} />
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
<stop offset="5%" stopColor="hsl(var(--chart-1))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--chart-1))" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="time" tick={{ fontSize: 12 }} />
<YAxis yAxisId="left" tick={{ fontSize: 12 }} unit="ms" />
<YAxis
yAxisId="right"
orientation="right"
tick={{ fontSize: 12 }}
domain={[0, 1]}
tickFormatter={(v) => (v === 1 ? "Up" : "Down")}
/>
<CartesianGrid vertical={false} strokeDasharray="3 3" opacity={0.3} />
<XAxis dataKey="time" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
<YAxis tick={{ fontSize: 11 }} tickLine={false} axisLine={false} unit="ms" />
<Tooltip
contentStyle={{
backgroundColor: "hsl(var(--card))",
border: "1px solid hsl(var(--border))",
borderRadius: "8px",
}}
formatter={(value: number) => [`${value}ms`, "Response Time"]}
/>
<Legend />
<Area
yAxisId="left"
<Line
type="monotone"
dataKey="responseTime"
stroke="#3b82f6"
stroke="hsl(var(--chart-1))"
strokeWidth={1.5}
fillOpacity={1}
fill="url(#colorResponse)"
name="Response Time (ms)"
name="Response Time"
isAnimationActive={false}
dot={false}
/>
<Bar yAxisId="right" dataKey="status" barSize={4} name="Status">
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.status === 1 ? "#22c55e" : "#ef4444"} />
))}
</Bar>
</ComposedChart>
</LineChart>
</ResponsiveContainer>
) : (
<div className="h-full flex flex-col items-center justify-center gap-3 text-muted-foreground">
@@ -507,7 +863,7 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
<RefreshCw className={cn("mr-2 h-4 w-4", checkMutation.isPending && "animate-spin")} />
Run First Check
</Button>
)}
)}
</div>
)}
</div>
@@ -547,12 +903,20 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
<Card>
<CardHeader>
<CardTitle>Status Page</CardTitle>
<CardDescription>Link this monitor to public status pages</CardDescription>
<div className="flex items-center justify-between">
<div>
<CardTitle>Status Page</CardTitle>
<CardDescription>Create or link to status pages</CardDescription>
</div>
<Button variant="outline" size="sm" onClick={() => setIsCreateStatusPageOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create New
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3">
{statusPages && statusPages.length > 0 ? (
<div className="space-y-3">
<div className="space-y-2">
{statusPages.map((page) => {
const isLinked = linkedStatusPageMonitors?.some((link) => link.status_page_id === page.id) || false
const linkInfo = linkedStatusPageMonitors?.find((link) => link.status_page_id === page.id)
@@ -560,15 +924,25 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
return (
<div
key={page.id}
className={`flex items-center justify-between p-3 rounded-lg border ${
isLinked ? 'bg-primary/5 border-primary/20' : 'bg-muted/30'
}`}
className={cn(
"flex items-center justify-between p-3 rounded-lg border transition-colors",
isLinked ? "bg-primary/5 border-primary/20" : "bg-muted/30 border-border"
)}
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<LayoutDashboard className="h-3.5 w-3.5 text-muted-foreground" />
<span className="font-medium text-sm truncate">{page.name}</span>
{page.public && (
<Globe className="h-3 w-3 text-muted-foreground flex-shrink-0" />
{page.public ? (
<Badge variant="outline" className="text-[10px] gap-1">
<Eye className="h-2.5 w-2.5" />
Public
</Badge>
) : (
<Badge variant="outline" className="text-[10px] gap-1">
<Lock className="h-2.5 w-2.5" />
Private
</Badge>
)}
</div>
{isLinked && linkInfo && (
@@ -577,30 +951,24 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
{linkInfo.group && ` • Group: ${linkInfo.group}`}
</p>
)}
{!isLinked && page.public && (
{!isLinked && (
<p className="text-xs text-muted-foreground mt-1">
{page.monitor_count} monitor{page.monitor_count !== 1 ? 's' : ''} linked
{page.monitor_count} monitor{page.monitor_count !== 1 ? "s" : ""} linked
</p>
)}
</div>
<div className="flex items-center gap-2 ml-2">
{isLinked && page.public && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
asChild
>
<a
href={getStatusPageUrl(page.slug)}
target="_blank"
rel="noopener noreferrer"
title="View public status page"
>
<ExternalLink className="h-4 w-4" />
</a>
</Button>
)}
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
asChild
title={page.public ? "View public status page" : "View private status page"}
>
<a href={getStatusPageUrl(page.slug)} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-4 w-4" />
</a>
</Button>
<Button
variant={isLinked ? "default" : "outline"}
size="sm"
@@ -629,82 +997,22 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
) : (
<div className="text-center py-4">
<p className="text-sm text-muted-foreground">No status pages yet.</p>
<p className="text-xs text-muted-foreground mt-1">Create one to share your service status publicly.</p>
<p className="text-xs text-muted-foreground mt-1">
Create one to share your service status or keep it private for internal use.
</p>
</div>
)}
<Button variant="outline" size="sm" className="w-full" onClick={() => setIsCreateStatusPageOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create Status Page
</Button>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Recent Checks</CardTitle>
<CardDescription>Last 50 monitor checks</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Time</TableHead>
<TableHead>Status</TableHead>
<TableHead>Response Time</TableHead>
<TableHead>Message</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{heartbeats?.slice(0, 50).map((hb: HeartbeatRow) => (
<TableRow key={hb.id}>
<TableCell>{formatDate(hb.time || hb.timestamp)}</TableCell>
<TableCell>
<Badge variant={hb.status === "up" ? "default" : "destructive"}>{hb.status}</Badge>
</TableCell>
<TableCell>{formatPing(hb.ping)}</TableCell>
<TableCell className="max-w-xs truncate">{hb.msg || "-"}</TableCell>
</TableRow>
))}
{!heartbeats?.length && (
<TableRow>
<TableCell colSpan={4}>
<div className="flex flex-col items-center justify-center py-8 gap-3 text-muted-foreground">
<div className="p-2 bg-muted/50 rounded-full">
<Clock className="h-5 w-5 opacity-50" />
</div>
<p className="text-sm">
{isPending
? "No checks have been run yet."
: "No check history available for the selected period."}
</p>
{isPending && (
<Button
variant="outline"
size="sm"
onClick={() => checkMutation.mutate()}
disabled={checkMutation.isPending}
>
<RefreshCw className={cn("mr-2 h-4 w-4", checkMutation.isPending && "animate-spin")} />
Run First Check
</Button>
)}
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Create Status Page Dialog */}
{isCreateStatusPageOpen && (
<AlertDialog open={isCreateStatusPageOpen} onOpenChange={setIsCreateStatusPageOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Create Status Page</AlertDialogTitle>
<AlertDialogDescription>Create a public status page for this monitor.</AlertDialogDescription>
<AlertDialogDescription>Create a status page and optionally link this monitor.</AlertDialogDescription>
</AlertDialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
@@ -725,6 +1033,15 @@ export default memo(function MonitorDetail({ id }: { id: string }) {
placeholder={monitor.name?.toLowerCase().replace(/\s+/g, "-")}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label htmlFor="sp-public" className="text-sm font-medium">
Public Status Page
</Label>
<p className="text-xs text-muted-foreground">Anyone can view this page without authentication.</p>
</div>
<Switch id="sp-public" checked={statusPagePublic} onCheckedChange={setStatusPagePublic} />
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setIsCreateStatusPageOpen(false)}>Cancel</AlertDialogCancel>
@@ -1,17 +1,17 @@
import { memo, useEffect } from "react"
import { useLingui } from "@lingui/react/macro"
import { StatusPagesTable } from "@/components/status-pages/status-pages-table"
import { StatusPageManager } from "@/components/status-pages/status-page-manager"
export default memo(() => {
const { t } = useLingui()
useEffect(() => {
document.title = `${t`Status Pages`} / Beszel`
document.title = `${t`Status Page Manager`} / Beszel`
}, [t])
return (
<div className="flex flex-col gap-8">
<StatusPagesTable />
<div className="container mx-auto py-6">
<StatusPageManager />
</div>
)
})
@@ -0,0 +1,853 @@
"use client"
import { useState, useMemo } from "react"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { useToast } from "@/components/ui/use-toast"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import {
Plus,
ExternalLink,
Globe,
Lock,
AlertTriangle,
CheckCircle2,
Clock,
XCircle,
LayoutTemplate,
Activity,
TrendingUp,
Filter,
Search,
Wrench,
Trash2,
} from "lucide-react"
import {
getStatusPages,
deleteStatusPage,
getStatusPageUrl,
type StatusPage,
} from "@/lib/statuspages"
import {
getIncidents,
createIncident,
acknowledgeIncident,
resolveIncident,
closeIncident,
getIncidentStats,
type Incident,
type CreateIncidentRequest,
getSeverityColor,
getStatusColor,
formatDuration,
} from "@/lib/incidents"
import { StatusPageDialog } from "./status-page-dialog"
import { cn } from "@/lib/utils"
// Quick Stats Card Component
function QuickStatCard({
title,
value,
subtitle,
icon: Icon,
trend,
color = "blue",
}: {
title: string
value: string | number
subtitle?: string
icon: React.ElementType
trend?: { value: number; positive: boolean }
color?: "blue" | "green" | "yellow" | "red" | "purple"
}) {
const colorClasses = {
blue: "bg-blue-500/10 text-blue-600 border-blue-500/20",
green: "bg-green-500/10 text-green-600 border-green-500/20",
yellow: "bg-yellow-500/10 text-yellow-600 border-yellow-500/20",
red: "bg-red-500/10 text-red-600 border-red-500/20",
purple: "bg-purple-500/10 text-purple-600 border-purple-500/20",
}
return (
<Card className="relative overflow-hidden">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
{title}
</CardTitle>
<div className={cn("p-2 rounded-lg border", colorClasses[color])}>
<Icon className="h-4 w-4" />
</div>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
{subtitle && (
<p className="text-xs text-muted-foreground mt-1">{subtitle}</p>
)}
{trend && (
<div className={cn(
"flex items-center gap-1 text-xs mt-2",
trend.positive ? "text-green-600" : "text-red-600"
)}>
<TrendingUp className={cn("h-3 w-3", !trend.positive && "rotate-180")} />
<span>{trend.value}%</span>
</div>
)}
</CardContent>
</Card>
)
}
// Incident Quick Actions Menu
function IncidentQuickActions({
incident,
onAcknowledge,
onResolve,
onClose,
}: {
incident: Incident
onAcknowledge: (id: string) => void
onResolve: (id: string) => void
onClose: (id: string) => void
}) {
const [showResolveDialog, setShowResolveDialog] = useState(false)
const [resolution, setResolution] = useState("")
return (
<div className="flex items-center gap-2">
{incident.status === "open" && (
<Button
variant="outline"
size="sm"
className="h-8 text-yellow-600 border-yellow-200 hover:bg-yellow-50"
onClick={() => onAcknowledge(incident.id)}
>
<Clock className="mr-1 h-3.5 w-3.5" />
Ack
</Button>
)}
{(incident.status === "open" || incident.status === "acknowledged") && (
<>
<Button
variant="outline"
size="sm"
className="h-8 text-green-600 border-green-200 hover:bg-green-50"
onClick={() => setShowResolveDialog(true)}
>
<CheckCircle2 className="mr-1 h-3.5 w-3.5" />
Resolve
</Button>
<Dialog open={showResolveDialog} onOpenChange={setShowResolveDialog}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Resolve Incident</DialogTitle>
<DialogDescription>
Add resolution details for this incident.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Resolution</label>
<Textarea
placeholder="How was this incident resolved?"
value={resolution}
onChange={(e) => setResolution(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowResolveDialog(false)}>
Cancel
</Button>
<Button
onClick={() => {
onResolve(incident.id)
setShowResolveDialog(false)
setResolution("")
}}
>
Resolve Incident
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)}
{incident.status === "resolved" && (
<Button
variant="outline"
size="sm"
className="h-8 text-gray-600 border-gray-200 hover:bg-gray-50"
onClick={() => onClose(incident.id)}
>
<XCircle className="mr-1 h-3.5 w-3.5" />
Close
</Button>
)}
</div>
)
}
// Create Incident Dialog
function CreateIncidentDialog({
open,
onOpenChange,
onCreate,
}: {
open: boolean
onOpenChange: (open: boolean) => void
onCreate: (data: CreateIncidentRequest) => void
}) {
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [severity, setSeverity] = useState<"critical" | "high" | "medium" | "low">("high")
const [type, setType] = useState("monitor_down")
const handleSubmit = () => {
onCreate({
title,
description,
severity,
type,
})
onOpenChange(false)
setTitle("")
setDescription("")
setSeverity("high")
setType("monitor_down")
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>Create New Incident</DialogTitle>
<DialogDescription>
Report a new incident or maintenance event.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Title</label>
<Input
placeholder="Incident title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description</label>
<Textarea
placeholder="Describe the incident..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">Severity</label>
<Select value={severity} onValueChange={(v) => setSeverity(v as typeof severity)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="critical">Critical</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Type</label>
<Select value={type} onValueChange={setType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monitor_down">Monitor Down</SelectItem>
<SelectItem value="domain_expiring">Domain Expiring</SelectItem>
<SelectItem value="ssl_expiring">SSL Expiring</SelectItem>
<SelectItem value="system_offline">System Offline</SelectItem>
<SelectItem value="maintenance">Maintenance</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={!title}>
Create Incident
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
// Main Status Page Manager Component
export function StatusPageManager() {
const { toast } = useToast()
const queryClient = useQueryClient()
const [activeTab, setActiveTab] = useState("overview")
const [statusPageDialogOpen, setStatusPageDialogOpen] = useState(false)
const [editingPage, setEditingPage] = useState<StatusPage | null>(null)
const [createIncidentOpen, setCreateIncidentOpen] = useState(false)
const [incidentFilter, setIncidentFilter] = useState<string>("all")
const [searchQuery, setSearchQuery] = useState("")
// Fetch data
const { data: pages, isLoading: pagesLoading } = useQuery({
queryKey: ["status-pages"],
queryFn: getStatusPages,
})
const { data: incidents, isLoading: incidentsLoading } = useQuery({
queryKey: ["incidents", incidentFilter],
queryFn: () => getIncidents(incidentFilter === "all" ? {} : { status: incidentFilter }),
})
const { data: stats } = useQuery({
queryKey: ["incident-stats"],
queryFn: getIncidentStats,
})
// Mutations
const deletePageMutation = useMutation({
mutationFn: deleteStatusPage,
onSuccess: () => {
toast({ title: "Status page deleted" })
queryClient.invalidateQueries({ queryKey: ["status-pages"] })
},
onError: (error: Error) => {
toast({ title: "Failed to delete", description: error.message, variant: "destructive" })
},
})
const createIncidentMutation = useMutation({
mutationFn: createIncident,
onSuccess: () => {
toast({ title: "Incident created" })
queryClient.invalidateQueries({ queryKey: ["incidents"] })
queryClient.invalidateQueries({ queryKey: ["incident-stats"] })
},
onError: (error: Error) => {
toast({ title: "Failed to create incident", description: error.message, variant: "destructive" })
},
})
const acknowledgeMutation = useMutation({
mutationFn: acknowledgeIncident,
onSuccess: () => {
toast({ title: "Incident acknowledged" })
queryClient.invalidateQueries({ queryKey: ["incidents"] })
},
})
const resolveMutation = useMutation({
mutationFn: (id: string) => resolveIncident(id),
onSuccess: () => {
toast({ title: "Incident resolved" })
queryClient.invalidateQueries({ queryKey: ["incidents"] })
queryClient.invalidateQueries({ queryKey: ["incident-stats"] })
},
})
const closeMutation = useMutation({
mutationFn: closeIncident,
onSuccess: () => {
toast({ title: "Incident closed" })
queryClient.invalidateQueries({ queryKey: ["incidents"] })
queryClient.invalidateQueries({ queryKey: ["incident-stats"] })
},
})
// Filtered incidents
const filteredIncidents = useMemo(() => {
if (!incidents) return []
if (!searchQuery) return incidents
return incidents.filter(
(i) =>
i.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
i.description?.toLowerCase().includes(searchQuery.toLowerCase())
)
}, [incidents, searchQuery])
// Active incidents count
const activeIncidents = useMemo(
() => incidents?.filter((i) => i.status === "open" || i.status === "acknowledged").length || 0,
[incidents]
)
const handleEdit = (page: StatusPage) => {
setEditingPage(page)
setStatusPageDialogOpen(true)
}
const handleAdd = () => {
setEditingPage(null)
setStatusPageDialogOpen(true)
}
const handleDelete = (page: StatusPage) => {
if (confirm(`Delete "${page.name}"? This will unlink all ${page.monitor_count} monitor(s).`)) {
deletePageMutation.mutate(page.id)
}
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">Status Page Manager</h1>
<p className="text-muted-foreground">
Manage status pages, incidents, and public communications
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => setCreateIncidentOpen(true)}>
<AlertTriangle className="mr-2 h-4 w-4" />
New Incident
</Button>
<Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" />
New Status Page
</Button>
</div>
</div>
{/* Quick Stats */}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<QuickStatCard
title="Status Pages"
value={pages?.length || 0}
subtitle={`${pages?.filter((p) => p.public).length || 0} public`}
icon={LayoutTemplate}
color="blue"
/>
<QuickStatCard
title="Active Incidents"
value={activeIncidents}
subtitle={`${incidents?.filter((i) => i.severity === "critical").length || 0} critical`}
icon={AlertTriangle}
color={activeIncidents > 0 ? "red" : "green"}
/>
<QuickStatCard
title="Total Monitors"
value={pages?.reduce((acc, p) => acc + p.monitor_count, 0) || 0}
subtitle="Across all pages"
icon={Activity}
color="purple"
/>
<QuickStatCard
title="MTTR (Hours)"
value={stats?.mttr_hours?.toFixed(1) || "-"}
subtitle="Mean time to resolution"
icon={Clock}
color="yellow"
/>
</div>
{/* Main Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="pages">
Status Pages
{pages && pages.length > 0 && (
<Badge variant="secondary" className="ml-2">
{pages.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="incidents">
Incidents
{activeIncidents > 0 && (
<Badge variant="destructive" className="ml-2">
{activeIncidents}
</Badge>
)}
</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
{/* Recent Status Pages */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<LayoutTemplate className="h-5 w-5" />
Recent Status Pages
</CardTitle>
<CardDescription>
Your public and private status pages
</CardDescription>
</CardHeader>
<CardContent>
{pagesLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-12 bg-muted rounded animate-pulse" />
))}
</div>
) : pages?.length === 0 ? (
<div className="text-center py-8">
<LayoutTemplate className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground">No status pages yet</p>
<Button variant="outline" size="sm" className="mt-2" onClick={handleAdd}>
Create one
</Button>
</div>
) : (
<div className="space-y-2">
{pages?.slice(0, 5).map((page) => (
<div
key={page.id}
className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3">
{page.public ? (
<Globe className="h-4 w-4 text-green-500" />
) : (
<Lock className="h-4 w-4 text-muted-foreground" />
)}
<div>
<p className="font-medium text-sm">{page.name}</p>
<p className="text-xs text-muted-foreground">
{page.monitor_count} monitors
</p>
</div>
</div>
<div className="flex items-center gap-2">
{page.public && (
<a
href={getStatusPageUrl(page.slug)}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-foreground"
>
<ExternalLink className="h-4 w-4" />
</a>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(page)}
>
Edit
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
{/* Recent Incidents */}
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Active Incidents
</CardTitle>
<CardDescription>
Incidents requiring attention
</CardDescription>
</CardHeader>
<CardContent>
{incidentsLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-12 bg-muted rounded animate-pulse" />
))}
</div>
) : filteredIncidents.filter((i) => i.status !== "closed").length === 0 ? (
<div className="text-center py-8">
<CheckCircle2 className="h-8 w-8 text-green-500 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">All clear! No active incidents.</p>
</div>
) : (
<div className="space-y-2">
{filteredIncidents
.filter((i) => i.status !== "closed")
.slice(0, 5)
.map((incident) => (
<div
key={incident.id}
className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3">
<Badge className={getSeverityColor(incident.severity)}>
{incident.severity}
</Badge>
<div>
<p className="font-medium text-sm">{incident.title}</p>
<p className="text-xs text-muted-foreground">
{formatDuration(incident.started_at)}
</p>
</div>
</div>
<IncidentQuickActions
incident={incident}
onAcknowledge={acknowledgeMutation.mutate}
onResolve={resolveMutation.mutate}
onClose={closeMutation.mutate}
/>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
{/* Status Pages Tab */}
<TabsContent value="pages">
<Card>
<CardHeader>
<CardTitle>All Status Pages</CardTitle>
<CardDescription>
Manage your public and private status pages
</CardDescription>
</CardHeader>
<CardContent>
{pagesLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-16 bg-muted rounded animate-pulse" />
))}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Slug</TableHead>
<TableHead>Monitors</TableHead>
<TableHead>Visibility</TableHead>
<TableHead>Updated</TableHead>
<TableHead className="w-[150px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pages?.map((page) => (
<TableRow key={page.id}>
<TableCell className="font-medium">{page.name}</TableCell>
<TableCell>{page.slug}</TableCell>
<TableCell>{page.monitor_count}</TableCell>
<TableCell>
{page.public ? (
<Badge variant="default" className="bg-green-500">
<Globe className="mr-1 h-3 w-3" />
Public
</Badge>
) : (
<Badge variant="secondary">
<Lock className="mr-1 h-3 w-3" />
Private
</Badge>
)}
</TableCell>
<TableCell>
{new Date(page.updated).toLocaleDateString()}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{page.public && (
<Button
variant="ghost"
size="icon"
asChild
>
<a
href={getStatusPageUrl(page.slug)}
target="_blank"
rel="noopener noreferrer"
>
<ExternalLink className="h-4 w-4" />
</a>
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(page)}
>
<Wrench className="mr-1 h-3.5 w-3.5" />
Edit
</Button>
<Button
variant="ghost"
size="icon"
className="text-destructive"
onClick={() => handleDelete(page)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
{/* Incidents Tab */}
<TabsContent value="incidents">
<Card>
<CardHeader>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<CardTitle>All Incidents</CardTitle>
<CardDescription>
Manage and track all incidents
</CardDescription>
</div>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search incidents..."
className="pl-8 w-[200px]"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<Select value={incidentFilter} onValueChange={setIncidentFilter}>
<SelectTrigger className="w-[130px]">
<Filter className="mr-2 h-4 w-4" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="open">Open</SelectItem>
<SelectItem value="acknowledged">Acknowledged</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="closed">Closed</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent>
{incidentsLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-16 bg-muted rounded animate-pulse" />
))}
</div>
) : filteredIncidents.length === 0 ? (
<div className="text-center py-12">
<CheckCircle2 className="h-12 w-12 text-green-500 mx-auto mb-4" />
<p className="text-lg font-medium">No incidents found</p>
<p className="text-muted-foreground">
{searchQuery
? "Try adjusting your search or filters"
: "All systems are running smoothly"}
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Severity</TableHead>
<TableHead>Status</TableHead>
<TableHead>Duration</TableHead>
<TableHead>Started</TableHead>
<TableHead className="w-[250px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredIncidents.map((incident) => (
<TableRow key={incident.id}>
<TableCell className="font-medium max-w-[300px] truncate">
{incident.title}
</TableCell>
<TableCell>
<Badge className={getSeverityColor(incident.severity)}>
{incident.severity}
</Badge>
</TableCell>
<TableCell>
<Badge
variant="outline"
className={getStatusColor(incident.status)}
>
{incident.status}
</Badge>
</TableCell>
<TableCell>{formatDuration(incident.started_at)}</TableCell>
<TableCell>
{new Date(incident.started_at).toLocaleDateString()}
</TableCell>
<TableCell>
<IncidentQuickActions
incident={incident}
onAcknowledge={acknowledgeMutation.mutate}
onResolve={resolveMutation.mutate}
onClose={closeMutation.mutate}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Dialogs */}
<StatusPageDialog
open={statusPageDialogOpen}
onOpenChange={setStatusPageDialogOpen}
page={editingPage}
isEdit={!!editingPage}
/>
<CreateIncidentDialog
open={createIncidentOpen}
onOpenChange={setCreateIncidentOpen}
onCreate={createIncidentMutation.mutate}
/>
</div>
)
}
@@ -6,7 +6,6 @@ import { getPagePath } from "@nanostores/router"
import type { CellContext, ColumnDef, HeaderContext } from "@tanstack/react-table"
import type { ClassValue } from "clsx"
import {
ArrowUpDownIcon,
ChevronRightSquareIcon,
ClockArrowUp,
CopyIcon,
@@ -265,7 +264,6 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
id: "temp",
name: () => t({ message: "Temp", comment: "Temperature label in systems table" }),
size: 50,
hideSort: true,
Icon: ThermometerIcon,
header: sortableHeader,
cell(info) {
@@ -289,7 +287,6 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
size: 70,
Icon: BatteryMediumIcon,
header: sortableHeader,
hideSort: true,
cell(info) {
const [pct, state] = info.row.original.info.bat ?? []
if (pct === undefined) {
@@ -335,7 +332,6 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
size: 50,
Icon: TerminalSquareIcon,
header: sortableHeader,
hideSort: true,
sortingFn: (a, b) => {
// sort priorities: 1) failed services, 2) total services
const [totalCountA, numFailedA] = a.original.info.sv ?? [0, 0]
@@ -374,7 +370,6 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
size: 50,
Icon: ClockArrowUp,
header: sortableHeader,
hideSort: true,
cell(info) {
const uptime = info.getValue() as number
if (!uptime) {
@@ -389,7 +384,6 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
name: () => t`Agent`,
size: 50,
Icon: WifiIcon,
hideSort: true,
header: sortableHeader,
cell(info) {
const version = info.getValue() as string
@@ -443,17 +437,16 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<Syste
function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
const { column } = context
// @ts-expect-error
const { Icon, hideSort, name }: { Icon: React.ElementType; name: () => string; hideSort: boolean } = column.columnDef
const { Icon, name }: { Icon: React.ElementType; name: () => string } = column.columnDef
const isSorted = column.getIsSorted()
return (
<Button
variant="ghost"
className={cn("h-9 px-3 flex duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
className={cn("h-9 px-3 flex items-center gap-2 duration-50", isSorted && "bg-accent/70 light:bg-accent text-accent-foreground/90")}
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
{Icon && <Icon className="me-2 size-4" />}
{Icon && <Icon className="size-4" />}
{name()}
{hideSort || <ArrowUpDownIcon className="ms-2 size-4" />}
</Button>
)
}
@@ -21,6 +21,7 @@ import {
ArrowUpIcon,
EyeIcon,
FilterIcon,
GripVertical,
LayoutGridIcon,
LayoutListIcon,
PlusIcon,
@@ -96,6 +97,58 @@ export default function SystemsTable() {
window.innerWidth < 1024 && filteredData.length < 200 ? "grid" : "table"
)
// Drag and drop state
const [draggedItem, setDraggedItem] = useState<SystemRecord | null>(null)
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
// Handle drag start
const handleDragStart = (e: React.DragEvent, item: SystemRecord) => {
setDraggedItem(item)
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/html', e.currentTarget.outerHTML)
}
// Handle drag over
const handleDragOver = (e: React.DragEvent, index: number) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
setDragOverIndex(index)
}
// Handle drag leave
const handleDragLeave = () => {
setDragOverIndex(null)
}
// Handle drop
const handleDrop = (e: React.DragEvent, dropIndex: number) => {
e.preventDefault()
setDragOverIndex(null)
if (!draggedItem) return
// Find the dragged item's current index
const draggedIndex = filteredData.findIndex(item => item.id === draggedItem.id)
if (draggedIndex === dropIndex) return
// Reorder the data
const reorderedData = [...filteredData]
reorderedData.splice(draggedIndex, 1)
reorderedData.splice(dropIndex, 0, draggedItem)
// Update the systems store with new order
// This would require backend support to persist the order
console.log('Reordered systems:', reorderedData.map(item => ({ id: item.id, name: item.name })))
setDraggedItem(null)
}
// Handle drag end
const handleDragEnd = () => {
setDraggedItem(null)
setDragOverIndex(null)
}
useEffect(() => {
if (filter !== undefined) {
table.getColumn("system")?.setFilterValue(filter)
@@ -138,21 +191,25 @@ export default function SystemsTable() {
const CardHead = useMemo(() => {
return (
<CardHeader className="p-0 mb-3 sm:mb-4">
<div className="grid md:flex gap-x-5 gap-y-3 w-full items-end">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2">
<Trans>All Systems</Trans>
</CardTitle>
<CardDescription className="flex">
<Trans>Click on a system to view more information.</Trans>
</CardDescription>
</div>
<div className="flex gap-2 ms-auto w-full md:w-80">
<div className="flex flex-col gap-4">
{/* Title and Add Button Row */}
<div className="flex items-center justify-between">
<div className="px-2 sm:px-1">
<CardTitle className="mb-2">
<Trans>All Systems</Trans>
</CardTitle>
<CardDescription className="flex">
<Trans>Click on a system to view more information.</Trans>
</CardDescription>
</div>
<Button onClick={() => setIsAddDialogOpen(true)} className="shrink-0">
<PlusIcon className="mr-2 h-4 w-4" />
<Trans>Add System</Trans>
</Button>
</div>
{/* Filter and View Controls Row */}
<div className="flex gap-2 w-full md:w-96">
<div className="relative flex-1">
<Input
placeholder={t`Filter...`}
@@ -250,11 +307,12 @@ export default function SystemsTable() {
}
return (
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault()
setSorting([{ id: column.id, desc: sorting[0]?.id === column.id && !sorting[0]?.desc }])
}}
key={column.id}
onClick={() => {
const isDesc = sorting[0]?.id === column.id && !sorting[0]?.desc
setSorting([{ id: column.id, desc: isDesc }])
}}
className="gap-2"
>
{Icon}
{/* @ts-ignore */}
@@ -268,25 +326,24 @@ export default function SystemsTable() {
<div>
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<EyeIcon className="size-4" />
<Trans>Visible Fields</Trans>
<Trans>Columns</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="px-1.5 pb-1">
{columns
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
onSelect={(e) => e.preventDefault()}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{/* @ts-ignore */}
{column.columnDef.name()}
</DropdownMenuCheckboxItem>
)
})}
<div className="px-1 pb-1">
{columns.map((column) => {
if (column.id === "select") return null
return (
<DropdownMenuCheckboxItem
key={column.id}
onSelect={(e) => e.preventDefault()}
checked={column.getIsVisible()}
onCheckedChange={(value) => column.toggleVisibility(!!value)}
>
{/* @ts-ignore */}
{column.columnDef.name()}
</DropdownMenuCheckboxItem>
)
})}
</div>
</div>
</div>
@@ -315,7 +372,18 @@ export default function SystemsTable() {
{viewMode === "table" ? (
// table layout
<div className="rounded-md">
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
<AllSystemsTable
table={table}
rows={rows}
colLength={visibleColumns.length}
draggedItem={draggedItem}
dragOverIndex={dragOverIndex}
handleDragStart={handleDragStart}
handleDragOver={handleDragOver}
handleDragLeave={handleDragLeave}
handleDrop={handleDrop}
handleDragEnd={handleDragEnd}
/>
</div>
) : (
// grid layout
@@ -338,7 +406,29 @@ export default function SystemsTable() {
}
const AllSystemsTable = memo(
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
({
table,
rows,
colLength,
draggedItem,
dragOverIndex,
handleDragStart,
handleDragOver,
handleDragLeave,
handleDrop,
handleDragEnd
}: {
table: TableType<SystemRecord>;
rows: Row<SystemRecord>[];
colLength: number
draggedItem: SystemRecord | null
dragOverIndex: number | null
handleDragStart: (e: React.DragEvent, item: SystemRecord) => void
handleDragOver: (e: React.DragEvent, index: number) => void
handleDragLeave: () => void
handleDrop: (e: React.DragEvent, index: number) => void
handleDragEnd: () => void
}) => {
// The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
@@ -377,6 +467,13 @@ const AllSystemsTable = memo(
virtualRow={virtualRow}
length={rows.length}
colLength={colLength}
draggedItem={draggedItem}
dragOverIndex={dragOverIndex}
handleDragStart={handleDragStart}
handleDragOver={handleDragOver}
handleDragLeave={handleDragLeave}
handleDrop={handleDrop}
handleDragEnd={handleDragEnd}
/>
)
})
@@ -396,7 +493,6 @@ const AllSystemsTable = memo(
)
function SystemsTableHead({ table }: { table: TableType<SystemRecord> }) {
const { t } = useLingui()
return (
<TableHeader className="sticky top-0 z-50 w-full border-b-2">
{table.getHeaderGroups().map((headerGroup) => (
@@ -419,32 +515,73 @@ const SystemTableRow = memo(
row,
virtualRow,
colLength,
draggedItem,
dragOverIndex,
handleDragStart,
handleDragOver,
handleDragLeave,
handleDrop,
handleDragEnd,
}: {
row: Row<SystemRecord>
virtualRow: VirtualItem
length: number
colLength: number
draggedItem: SystemRecord | null
dragOverIndex: number | null
handleDragStart: (e: React.DragEvent, item: SystemRecord) => void
handleDragOver: (e: React.DragEvent, index: number) => void
handleDragLeave: () => void
handleDrop: (e: React.DragEvent, index: number) => void
handleDragEnd: () => void
}) => {
const system = row.original
const { t } = useLingui()
const isDragged = draggedItem?.id === system.id
const isDragOver = dragOverIndex === virtualRow.index
return useMemo(() => {
return (
<TableRow
draggable
onDragStart={(e) => handleDragStart(e, system)}
onDragOver={(e) => handleDragOver(e, virtualRow.index)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, virtualRow.index)}
onDragEnd={handleDragEnd}
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
"opacity-50": system.status === SystemStatus.Paused,
"opacity-30": isDragged,
"border-t-2 border-b-2 border-blue-500 bg-blue-50": isDragOver,
})}
>
{row.getVisibleCells().map((cell) => (
{row.getVisibleCells().map((cell, index) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
height: virtualRow.size,
}}
className="py-0 ps-4.5"
className={cn("py-0", index === 0 ? "ps-2" : "ps-4.5")}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
{index === 0 ? (
<div className="flex items-center gap-2">
<div
className="cursor-grab active:cursor-grabbing p-1 hover:bg-muted rounded"
onDragStart={(e) => handleDragStart(e, system)}
onDragOver={(e) => handleDragOver(e, virtualRow.index)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, virtualRow.index)}
onDragEnd={handleDragEnd}
>
<GripVertical className="h-4 w-4 text-muted-foreground" />
</div>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
) : (
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
</TableCell>
))}
</TableRow>
+200
View File
@@ -1,5 +1,20 @@
import { pb } from "./api"
export interface Subdomain {
id: string
domain: string
subdomain_name: string
full_domain: string
status: "active" | "inactive" | "error"
ip_addresses?: string
http_status?: number
server_header?: string
discovery_source: string
last_checked?: string
created: string
updated: string
}
export interface Domain {
id: string
domain_name: string
@@ -106,6 +121,66 @@ export interface Domain {
dns_spf_records?: string[]
dns_dkim_records?: string[]
dns_dmarc_records?: string[]
// Provider Detection
dns_provider?: string
hosting_provider?: string
email_provider?: string
ca_provider?: string
// HTTP Headers
headers?: { name: string; value: string }[]
// Certificate Chain
certificates?: {
issuer: string
subject: string
alt_names: string[]
valid_from: string
valid_to: string
ca_provider: string
}[]
// SEO Metadata
seo_meta?: {
openGraph: {
url: string
type: string
title: string
images: string[]
description: string
}
twitter: {
title: string
description: string
image: string
card: string
}
general: {
title: string
author: string
robots: string
keywords: string
canonical: string
description: string
}
robots: {
fetched: boolean
groups: {
userAgents: string[]
rules: { type: string; value: string }[]
}[]
sitemaps: string[]
}
}
// Raw WHOIS & Registration Details
whois_raw?: string
privacy_enabled?: boolean
transfer_lock?: boolean
tld?: string
domain_statuses?: string[]
host_country_code?: string
}
export interface DomainHistory {
@@ -139,6 +214,9 @@ export interface CreateDomainRequest {
quiet_hours_enabled?: boolean
quiet_hours_start?: string
quiet_hours_end?: string
// Manual expiry override when WHOIS fails
expiry_date?: string
creation_date?: string
}
export interface UpdateDomainRequest {
@@ -186,6 +264,66 @@ export interface DomainLookupResult {
host_isp?: string
favicon_url?: string
last_checked?: string
// Provider Detection
dns_provider?: string
hosting_provider?: string
email_provider?: string
ca_provider?: string
// HTTP Headers
headers?: { name: string; value: string }[]
// Certificate Chain
certificates?: {
issuer: string
subject: string
alt_names: string[]
valid_from: string
valid_to: string
ca_provider: string
}[]
// SEO Metadata
seo_meta?: {
openGraph: {
url: string
type: string
title: string
images: string[]
description: string
}
twitter: {
title: string
description: string
image: string
card: string
}
general: {
title: string
author: string
robots: string
keywords: string
canonical: string
description: string
}
robots: {
fetched: boolean
groups: {
userAgents: string[]
rules: { type: string; value: string }[]
}[]
sitemaps: string[]
}
}
// Raw WHOIS & Registration Details
whois_raw?: string
privacy_enabled?: boolean
transfer_lock?: boolean
tld?: string
domain_statuses?: string[]
host_country_code?: string
}
const API_BASE = "/api/beszel/domains"
@@ -377,3 +515,65 @@ export function cleanDomain(domain: string): string {
.toLowerCase()
.trim()
}
// Subdomain API functions
export async function getDomainSubdomains(domainId: string): Promise<Subdomain[]> {
const response = await fetch(`/api/beszel/domains/${domainId}/subdomains`, {
headers: {
Authorization: `Bearer ${pb.authStore.token}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch subdomains: ${response.statusText}`)
}
return response.json()
}
export async function refreshSubdomainDiscovery(domainId: string): Promise<void> {
const response = await fetch(`/api/beszel/domains/${domainId}/discover-subdomains`, {
method: "POST",
headers: {
Authorization: `Bearer ${pb.authStore.token}`,
},
})
if (!response.ok) {
throw new Error(`Failed to start subdomain discovery: ${response.statusText}`)
}
}
export async function deleteSubdomain(subdomainId: string): Promise<void> {
const response = await fetch(`/api/beszel/subdomains/${subdomainId}`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${pb.authStore.token}`,
},
})
if (!response.ok) {
throw new Error(`Failed to delete subdomain: ${response.statusText}`)
}
}
export function extractDomainFromUrl(url: string): string {
try {
const urlObj = new URL(url.startsWith("http") ? url : `https://${url}`)
return urlObj.hostname.toLowerCase()
} catch {
return cleanDomain(url)
}
}
export function isSubdomain(fullDomain: string, parentDomain: string): boolean {
const cleanFull = cleanDomain(fullDomain)
const cleanParent = cleanDomain(parentDomain)
return cleanFull.endsWith(`.${cleanParent}`) || cleanFull === cleanParent
}
export function getSubdomainName(fullDomain: string, parentDomain: string): string {
const cleanFull = cleanDomain(fullDomain)
const cleanParent = cleanDomain(parentDomain)
if (cleanFull === cleanParent) return "@"
if (cleanFull.endsWith(`.${cleanParent}`)) {
return cleanFull.slice(0, -cleanParent.length - 1)
}
return cleanFull
}
+128
View File
@@ -51,6 +51,7 @@ export interface Monitor {
description?: string
last_check?: string
uptime_stats?: Record<string, number>
recent_heartbeats?: Array<{ status: string; time: string; ping?: number }>
tags?: string[]
keyword?: string
json_query?: string
@@ -196,6 +197,25 @@ export interface CheckResult {
time?: string
}
export interface PageSpeedMetrics {
performance: number
accessibility: number
bestPractices: number
seo: number
pwa: number
fcp: number
lcp: number
ttfb: number
cls: number
tbt: number
speedIndex: number
tti: number
strategy: string
checkedAt: string
url: string
vitals: Record<string, string>
}
// API Functions
export async function listMonitors(): Promise<Monitor[]> {
const response = await pb.send<{ monitors: Monitor[] }>("/api/beszel/monitors", {})
@@ -260,6 +280,12 @@ export function getMonitorHeartbeats(id: string): Promise<{ heartbeats: Heartbea
return pb.send(`/api/beszel/monitors/${id}/heartbeats`, {})
}
export function runPageSpeedCheck(id: string, strategy: string = "mobile"): Promise<PageSpeedMetrics> {
return pb.send(`/api/beszel/monitors/${id}/pagespeed?strategy=${strategy}`, {
method: "POST",
})
}
// Helper functions
export function getMonitorTypeLabel(type: MonitorType): string {
const labels: Record<MonitorType, string> = {
@@ -338,3 +364,105 @@ export function formatPing(ping: number): string {
if (ping < 1000) return `${ping}ms`
return `${(ping / 1000).toFixed(2)}s`
}
// Favicon URL helper - uses Google's favicon service as fallback
export function getMonitorFaviconUrl(monitor: Monitor): string | null {
const hostname = extractHostnameFromMonitor(monitor)
if (!hostname) return null
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(hostname)}&sz=32`
}
// Domain extraction and grouping utilities
export function extractHostnameFromMonitor(monitor: Monitor): string | null {
if (monitor.hostname) {
return monitor.hostname.toLowerCase()
}
if (monitor.url) {
try {
const url = new URL(monitor.url.startsWith("http") ? monitor.url : `https://${monitor.url}`)
return url.hostname.toLowerCase()
} catch {
return monitor.url.toLowerCase()
}
}
return null
}
export function getDomainFromHostname(hostname: string): string {
// Remove www prefix
const clean = hostname.replace(/^www\./, "")
// Extract root domain (last 2 parts for most domains, last 3 for co.uk etc)
const parts = clean.split(".")
if (parts.length <= 2) {
return clean
}
// Handle special TLDs
const specialTLDs = ["co.uk", "com.au", "co.jp", "com.br", "co.nz", "co.za", "co.in", "com.cn"]
const lastTwo = parts.slice(-2).join(".")
const lastThree = parts.slice(-3).join(".")
if (specialTLDs.includes(lastThree)) {
return lastThree
}
return lastTwo
}
export function isSubdomain(hostname: string, domain: string): boolean {
const cleanHostname = hostname.toLowerCase().replace(/^www\./, "")
const cleanDomain = domain.toLowerCase().replace(/^www\./, "")
return cleanHostname.endsWith(`.${cleanDomain}`) || cleanHostname === cleanDomain
}
export function getSubdomainPart(hostname: string, domain: string): string | null {
const cleanHostname = hostname.toLowerCase().replace(/^www\./, "")
const cleanDomain = domain.toLowerCase().replace(/^www\./, "")
if (cleanHostname === cleanDomain) {
return "@" // Root domain
}
if (cleanHostname.endsWith(`.${cleanDomain}`)) {
return cleanHostname.slice(0, -cleanDomain.length - 1)
}
return null
}
export interface GroupedMonitors {
domain: string
isRootDomain: boolean
monitors: Monitor[]
subdomains: Map<string, Monitor[]>
}
export function groupMonitorsByDomain(monitors: Monitor[]): Map<string, GroupedMonitors> {
const groups = new Map<string, GroupedMonitors>()
for (const monitor of monitors) {
const hostname = extractHostnameFromMonitor(monitor)
if (!hostname) continue
const rootDomain = getDomainFromHostname(hostname)
const subdomain = getSubdomainPart(hostname, rootDomain)
if (!groups.has(rootDomain)) {
groups.set(rootDomain, {
domain: rootDomain,
isRootDomain: true,
monitors: [],
subdomains: new Map(),
})
}
const group = groups.get(rootDomain)!
if (subdomain === "@" || subdomain === null) {
// Root domain monitor
group.monitors.push(monitor)
} else {
// Subdomain monitor
if (!group.subdomains.has(subdomain)) {
group.subdomains.set(subdomain, [])
}
group.subdomains.get(subdomain)!.push(monitor)
}
}
return groups
}
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "جميع الحاويات"
msgid "All Systems"
msgstr "جميع الأنظمة"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "عرض"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "الشبكة"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "حركة مرور الشبكة لحاويات الدوكر"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "لا"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "فتح القائمة"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "أو المتابعة باستخدام"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "متوقف مؤقتا"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "تم حلها"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "الحالة"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "الحالة"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "متوسط تحميل النظام مع مرور الوقت"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "تبويبات"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "الإجمالي: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "يتم التفعيل عندما يتغير الحالة بين التش
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص عتبة معينة"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "الاستخدام"
msgid "Value"
msgstr "القيمة"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "عرض"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "عرض أحدث 200 تنبيه."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "الأعمدة الظاهرة"
#~ msgid "Visible Fields"
#~ msgstr "الأعمدة الظاهرة"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "عتبات التحذير"
msgid "Webhook / Push notifications"
msgstr "إشعارات Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Всички контейнери"
msgid "All Systems"
msgstr "Всички системи"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Показване"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Мрежа"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Мрежов трафик на docker контейнери"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Не"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Отвори менюто"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Или продължи с"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "На пауза"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Решен"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Състояние"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Статус"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Средно натоварване на системата във времето"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Табове"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Общо: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Задейства се, когато статуса превключв
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Задейства се, когато употребата на някой диск надивши зададен праг"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Натоварване"
msgid "Value"
msgstr "Стойност"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Изглед"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Прегледайте последните си 200 сигнала."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Видими полета"
#~ msgid "Visible Fields"
#~ msgstr "Видими полета"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Прагове за предупреждение"
msgid "Webhook / Push notifications"
msgstr "Webhook / Пуш нотификации"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Všechny kontejnery"
msgid "All Systems"
msgstr "Všechny systémy"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Zobrazení"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Síť"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Síťový provoz kontejnerů docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Ne"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Otevřít menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Nebo pokračujte s"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Pozastaveno"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Vyřešeno"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Stav"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Stav"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Průměry zatížení systému v průběhu času"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Karty"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Celkem: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Spouští se, když se změní dostupnost"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Využití"
msgid "Value"
msgstr "Hodnota"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Zobrazení"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Zobrazit vašich 200 nejnovějších upozornění."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Viditelné sloupce"
#~ msgid "Visible Fields"
#~ msgstr "Viditelné sloupce"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Prahové hodnoty pro varování"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push oznámení"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Alle containere"
msgid "All Systems"
msgstr "Alle systemer"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Visning"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Netværkstrafik af dockercontainere"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nej"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Åbn menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Eller fortsæt med"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Sat på pause"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Løst"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Tilstand"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Gennemsnitlig system belastning over tid"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Faner"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "I alt: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Udløser når status skifter mellem op og ned"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Udløser når brugen af en disk overstiger en tærskel"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Udnyttelse"
msgid "Value"
msgstr "Værdi"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Vis"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Se dine 200 nyeste alarmer."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Synlige felter"
#~ msgid "Visible Fields"
#~ msgstr "Synlige felter"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Advarselstærskler"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push notifikationer"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Alle Container"
msgid "All Systems"
msgstr "Alle Systeme"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Anzeige"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Netzwerk"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Netzwerkverkehr der Docker-Container"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nein"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Menü öffnen"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Oder fortfahren mit"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Pausiert"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Gelöst"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Status"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Systemlastdurchschnitt im Zeitverlauf"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr "Gesamtzeit für Lese-/Schreibvorgänge (kann 100% überschreiten)"
msgid "Total: {0}"
msgstr "Gesamt: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Löst aus, wenn der Status zwischen online und offline wechselt"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert überschreitet"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Auslastung"
msgid "Value"
msgstr "Wert"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Ansicht"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Sieh dir die neusten 200 Alarme an."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Sichtbare Spalten"
#~ msgid "Visible Fields"
#~ msgstr "Sichtbare Spalten"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Warnschwellen"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push-Benachrichtigungen"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+53 -21
View File
@@ -225,6 +225,15 @@ msgstr "All Containers"
msgid "All Systems"
msgstr "All Systems"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr "All Tags"
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr "All Types"
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr "App update"
@@ -460,8 +469,8 @@ msgstr "Check now"
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr "Check Now"
#~ msgid "Check Now"
#~ msgstr "Check Now"
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -498,8 +507,8 @@ msgid "Close"
msgstr "Close"
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr "Columns"
msgid "Columns"
msgstr "Columns"
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -779,6 +788,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Display"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr "Display"
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr "Display Columns"
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr "DNS Records"
@@ -809,7 +826,6 @@ msgid "Domain and SSL expiry calendar"
msgstr "Domain and SSL expiry calendar"
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr "Domain Monitoring"
@@ -1402,7 +1418,6 @@ msgid "Monitor updated successfully"
msgstr "Monitor updated successfully"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr "Monitor websites, APIs, and services"
@@ -1443,6 +1458,10 @@ msgstr "Name is required"
msgid "Net"
msgstr "Net"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr "Network (Grouped)"
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Network traffic of docker containers"
@@ -1466,8 +1485,8 @@ msgid "No"
msgstr "No"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr "No data available for selected time range"
#~ msgid "No data available for selected time range"
#~ msgstr "No data available for selected time range"
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1548,6 +1567,11 @@ msgstr "Open menu"
msgid "Optional description for this monitor"
msgstr "Optional description for this monitor"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr "Options"
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Or continue with"
@@ -1621,6 +1645,8 @@ msgid "Paused"
msgstr "Paused"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr "Paused ({0})"
@@ -1799,6 +1825,10 @@ msgstr "Resolved"
msgid "Response"
msgstr "Response"
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr "Response time over the selected period"
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr "Response Times"
@@ -1999,7 +2029,6 @@ msgstr "State"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2011,12 +2040,15 @@ msgid "Status"
msgstr "Status"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
msgstr "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr "Status and response time over the selected period"
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr "Status Page Manager"
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr "Status Pages"
@@ -2060,8 +2092,8 @@ msgid "System load averages over time"
msgstr "System load averages over time"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr "System Monitoring"
#~ msgid "System Monitoring"
#~ msgstr "System Monitoring"
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2086,6 +2118,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Tabs"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr "Tags"
@@ -2208,7 +2242,6 @@ msgstr "Total time spent on read/write (can exceed 100%)"
msgid "Total: {0}"
msgstr "Total: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr "Track domain expiry dates and DNS status"
@@ -2218,8 +2251,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr "Track domain expiry dates and watch domains for purchase"
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr "Track system resources, containers, and health"
#~ msgid "Track system resources, containers, and health"
#~ msgstr "Track system resources, containers, and health"
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2277,6 +2310,7 @@ msgstr "Triggers when status switches between up and down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Triggers when usage of any disk exceeds a threshold"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2411,7 +2445,6 @@ msgstr "Utilization"
msgid "Value"
msgstr "Value"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "View"
@@ -2431,8 +2464,8 @@ msgid "View your 200 most recent alerts."
msgstr "View your 200 most recent alerts."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Visible Fields"
#~ msgid "Visible Fields"
#~ msgstr "Visible Fields"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2463,7 +2496,6 @@ msgstr "Warning thresholds"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push notifications"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr "Website & Service Monitoring"
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Todos los contenedores"
msgid "All Systems"
msgstr "Todos los sistemas"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Pantalla"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Red"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Tráfico de red de los contenedores Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Abrir menú"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "O continuar con"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Pausado"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Resuelto"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Estado"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Estado"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Promedios de carga del sistema a lo largo del tiempo"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Pestañas"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr ""
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Se activa cuando el estado cambia entre activo e inactivo"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Se activa cuando el uso de cualquier disco supera un umbral"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Utilización"
msgid "Value"
msgstr "Valor"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Vista"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Ver tus 200 alertas más recientes."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Columnas visibles"
#~ msgid "Visible Fields"
#~ msgstr "Columnas visibles"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Umbrales de advertencia"
msgid "Webhook / Push notifications"
msgstr "Notificaciones Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "همه کانتینرها"
msgid "All Systems"
msgstr "همه سیستم‌ها"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "نمایش"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "شبکه"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "ترافیک شبکه کانتینرهای داکر"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "خیر"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "باز کردن منو"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "یا ادامه با"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "مکث شده"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "حل شده"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "وضعیت"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "وضعیت"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "میانگین بار سیستم در طول زمان"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "تب‌ها"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "کل: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "هنگامی که وضعیت بین بالا و پایین تغییر م
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "هنگامی که استفاده از هر دیسکی از یک آستانه فراتر رود، فعال می‌شود"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "بهره‌وری"
msgid "Value"
msgstr "مقدار"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "مشاهده"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "۲۰۰ هشدار اخیر خود را مشاهده کنید."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "فیلدهای قابل مشاهده"
#~ msgid "Visible Fields"
#~ msgstr "فیلدهای قابل مشاهده"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "آستانه های هشدار"
msgid "Webhook / Push notifications"
msgstr "اعلان‌های Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Tous les conteneurs"
msgid "All Systems"
msgstr "Tous les systèmes"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Affichage"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Rés"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Trafic réseau des conteneurs Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Non"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Ouvrir le menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Ou continuer avec"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "En pause"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Résolu"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "État"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Statut"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Charges moyennes du système dans le temps"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Onglets"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Total : {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Se déclenche lorsque le statut passe de \"Joignable\" à \"Injoignable\
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Utilisation"
msgid "Value"
msgstr "Valeur"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Vue"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Voir vos 200 dernières alertes."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Colonnes visibles"
#~ msgid "Visible Fields"
#~ msgstr "Colonnes visibles"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Seuils d'avertissement"
msgid "Webhook / Push notifications"
msgstr "Notifications Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "כל הקונטיינרים"
msgid "All Systems"
msgstr "כל המערכות"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "תצוגה"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "רשת"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "תעבורת רשת של קונטיינרים של Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "לא"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "פתח תפריט"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "או המשך עם"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "מושהה"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "נפתר"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "מצב"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "סטטוס"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "ממוצעי עומס מערכת לאורך זמן"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "לשוניות"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "סה\"כ: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "מופעל כאשר הסטטוס מתחלף בין למעלה ולמטה
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "מופעל כאשר שימוש בכל דיסק עולה על סף"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "ניצולת"
msgid "Value"
msgstr "ערך"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "צפה"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "צפה ב-200 ההתראות האחרונות שלך."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "שדות גלויים"
#~ msgid "Visible Fields"
#~ msgstr "שדות גלויים"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "ספי אזהרה"
msgid "Webhook / Push notifications"
msgstr "Webhook / התראות דחיפה"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Svi spremnici"
msgid "All Systems"
msgstr "Svi Sustavi"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Prikaz"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Mreža"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Mrežni promet Docker spremnika"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Ne"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Otvori meni"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Ili nastavi s"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Pauzirano"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Razrješeno"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Stanje"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Prosječno opterećenje sustava kroz vrijeme"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Kartice"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Ukupno: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Pokreće se kada se status sustava promijeni"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Pokreće se kada iskorištenost bilo kojeg diska premaši prag"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Iskorištenost"
msgid "Value"
msgstr "Vrijednost"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Prikaz"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Pogledajte posljednjih 200 upozorenja."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Vidljiva polja"
#~ msgid "Visible Fields"
#~ msgstr "Vidljiva polja"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Pragovi upozorenja"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push obavijest"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Minden konténer"
msgid "All Systems"
msgstr "Minden rendszer"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Megjelenítés"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Hálózat"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Docker konténerek hálózati forgalma"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nem"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Menü megnyitása"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Vagy folytasd ezzel"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Szüneteltetve"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Megoldva"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Állapot"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Állapot"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Rendszer terhelési átlaga"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Lapok"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Összesen: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Riaszt, amikor a rendszer online állapota változik"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Riaszt, ha a lemezhasználat túllép egy küszöbértéket"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Kihasználtság"
msgid "Value"
msgstr "Érték"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Nézet"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Legfrissebb 200 riasztásod áttekintése."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Látható mezők"
#~ msgid "Visible Fields"
#~ msgstr "Látható mezők"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Figyelmeztetési küszöbértékek"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push értesítések"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Semua Container"
msgid "All Systems"
msgstr "Semua Sistem"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Tampilan"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Jaringan"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Trafik jaringan kontainer docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Tidak"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Buka menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Atau lanjutkan dengan"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Dijeda"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Diselesaikan"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Status"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Rata-rata beban sistem dari waktu ke waktu"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Tab"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr ""
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Dipicu ketika status beralih antara up dan down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Dipicu ketika penggunaan disk apa pun melebihi ambang batas"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Utilisasi"
msgid "Value"
msgstr "Nilai"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Lihat"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Lihat 200 peringatan terbaru anda."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Metrik yang Terlihat"
#~ msgid "Visible Fields"
#~ msgstr "Metrik yang Terlihat"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Ambang peringatan"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push notifikasi"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Tutti i contenitori"
msgid "All Systems"
msgstr "Tutti i Sistemi"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Rete"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Traffico di rete dei container Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Apri menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Oppure continua con"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "In pausa"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Risolto"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Stato"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Stato"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Medie di carico del sistema nel tempo"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Schede"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Totale: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Attiva quando lo stato passa tra up e down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Attiva quando l'utilizzo di un disco supera una soglia"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Utilizzo"
msgid "Value"
msgstr "Valore"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Vista"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Visualizza i tuoi 200 avvisi più recenti."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Colonne visibili"
#~ msgid "Visible Fields"
#~ msgstr "Colonne visibili"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Soglie di avviso"
msgid "Webhook / Push notifications"
msgstr "Notifiche Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "すべてのコンテナ"
msgid "All Systems"
msgstr "すべてのシステム"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "表示"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "帯域"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Dockerコンテナのネットワークトラフィック"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "いいえ"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "メニューを開く"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "または、以下の方法でログイン"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "一時停止中"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "解決済み"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "状態"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "ステータス"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "システムの負荷平均の推移"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "タブ"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "合計: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "ステータスが上から下に切り替わるときにトリガーさ
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "ディスクの使用量がしきい値を超えたときにトリガーされます"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "利用率"
msgid "Value"
msgstr "値"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "表示"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "直近200件のアラートを表示します。"
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "表示列"
#~ msgid "Visible Fields"
#~ msgstr "表示列"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "警告のしきい値"
msgid "Webhook / Push notifications"
msgstr "Webhook / プッシュ通知"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "모든 컨테이너"
msgid "All Systems"
msgstr "모든 시스템"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "표시"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "네트워크"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Docker 컨테이너의 네트워크 트래픽"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "아니오"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "메뉴 열기"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "또는 아래 항목으로 진행하기"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "일시 정지됨"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "해결됨"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "상태"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "상태"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "시간에 따른 시스템 부하 평균"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "탭"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "총: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "시스템의 전원이 켜지거나 꺼질때 트리거됩니다."
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "디스크 사용량이 임계값을 초과할 때 트리거됩니다."
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "사용률"
msgid "Value"
msgstr "값"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "보기"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "최근 200개의 알림을 봅니다."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "표시할 열"
#~ msgid "Visible Fields"
#~ msgstr "표시할 열"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "경고 임계값"
msgid "Webhook / Push notifications"
msgstr "Webhook / 푸시 알림"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Alle containers"
msgid "All Systems"
msgstr "Alle systemen"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Weergave"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Netwerk"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Netwerkverkeer van docker containers"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nee"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Menu openen"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Of ga verder met"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Gepauzeerd"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Opgelost"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Status"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Gemiddelde systeembelasting na verloop van tijd"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Tabbladen"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Totaal: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Triggert wanneer de status schakelt tussen up en down"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrijdt"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Gebruik"
msgid "Value"
msgstr "Waarde"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Weergave"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Bekijk je 200 meest recente meldingen."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Zichtbare kolommen"
#~ msgid "Visible Fields"
#~ msgstr "Zichtbare kolommen"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Waarschuwingsdrempels"
msgid "Webhook / Push notifications"
msgstr "Webhook / Pushmeldingen"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Alle containere"
msgid "All Systems"
msgstr "Alle Systemer"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Vis"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Nett"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Nettverkstrafikk av docker-konteinere"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nei"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Åpne meny"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Eller fortsett med"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Satt på Pause"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Løst"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Tilstand"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Systembelastning gjennomsnitt over tid"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Faner"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Totalt: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Slår inn når statusen veksler mellom oppe og nede"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grenseverdi"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Utnyttelse"
msgid "Value"
msgstr "Verdi"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Visning"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Vis de 200 siste varslene."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Synlige Felter"
#~ msgid "Visible Fields"
#~ msgstr "Synlige Felter"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Advarselsterskler"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push-varslinger"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Wszystkie kontenery"
msgid "All Systems"
msgstr "Wszystkie systemy"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Widok"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Sieć"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Ruch sieciowy kontenerów Docker."
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nie"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Otwórz menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Lub kontynuuj z"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Wstrzymane"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Rozwiązany"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Stan"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Średnie obciążenie systemu w czasie"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Karty"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Łącznie: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Wyzwalane, gdy status przełącza się między stanem aktywnym a nieakty
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Wyzwalane, gdy wykorzystanie któregokolwiek dysku przekroczy ustalony próg"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Użycie"
msgid "Value"
msgstr "Wartość"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Widok"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Wyświetl 200 ostatnich alertów."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Widoczne kolumny"
#~ msgid "Visible Fields"
#~ msgstr "Widoczne kolumny"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Progi ostrzegawcze"
msgid "Webhook / Push notifications"
msgstr "Webhook / Powiadomienia push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Todos os Contêineres"
msgid "All Systems"
msgstr "Todos os Sistemas"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Ecrã"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Rede"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Tráfego de rede dos contêineres Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Não"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Abrir menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Ou continue com"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Pausado"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Resolvido"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Estado"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Estado"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Médias de carga do sistema ao longo do tempo"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Separadores"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr ""
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Dispara quando o status alterna entre ativo e inativo"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Dispara quando o uso de qualquer disco excede um limite"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Utilização"
msgid "Value"
msgstr "Valor"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Visual"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Veja os seus 200 alertas mais recentes."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Campos Visíveis"
#~ msgid "Visible Fields"
#~ msgstr "Campos Visíveis"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Limites de aviso"
msgid "Webhook / Push notifications"
msgstr "Notificações Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Все контейнеры"
msgid "All Systems"
msgstr "Все системы"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Отображение"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Сеть"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Сетевой трафик контейнеров Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Нет"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Открыть меню"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Или продолжить с"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Пауза"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Завершено"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Состояние"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Статус"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Средняя загрузка системы за время"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Вкладки"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Всего: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Срабатывает, когда статус переключаетс
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Срабатывает, когда использование любого диска превышает порог"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Использование"
msgid "Value"
msgstr "Значение"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Вид"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Просмотреть 200 последних оповещений."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Видимые столбцы"
#~ msgid "Visible Fields"
#~ msgstr "Видимые столбцы"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Пороги предупреждения"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push уведомления"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Vsi kontejnerji"
msgid "All Systems"
msgstr "Vsi sistemi"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Zaslon"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Mreža"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Omrežni promet docker kontejnerjev"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Ne"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Odpri menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Ali nadaljuj z"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Zaustavljeno"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Rešeno"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Stanje"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Stanje"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Sistemske povprečne obremenitve skozi čas"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Zavihki"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Skupaj: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Sproži se, ko se stanje preklaplja med gor in dol"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Sproži se, ko uporaba katerega koli diska preseže prag"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Izkoriščenost"
msgid "Value"
msgstr "Vrednost"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Pogled"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Oglejte si svojih 200 najnovejših opozoril."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Vidna polja"
#~ msgid "Visible Fields"
#~ msgstr "Vidna polja"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Pragovi za opozorila"
msgid "Webhook / Push notifications"
msgstr "Webhook / potisna obvestila"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Сви контејнери"
msgid "All Systems"
msgstr "Сви системи"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Приказ"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Мрежа"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Мрежни саобраћај docker контејнера"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Не"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Отвори мени"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Или наставите са"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Паузирано"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Решено"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Стање"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Статус"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Просечна оптерећења система током времена"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Картице"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Укупно: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Окида се када статус прелази између укљ
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Окида се када употреба било ког диска премаши праг"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Искоришћеност"
msgid "Value"
msgstr "Вредност"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Погледај"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Погледајте ваших 200 најновијих упозорења."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Видљива поља"
#~ msgid "Visible Fields"
#~ msgstr "Видљива поља"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Прагове упозорења"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push обавештења"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Alla behållare"
msgid "All Systems"
msgstr "Alla system"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Visa"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Nät"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Nätverkstrafik för dockercontainrar"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Nej"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Öppna menyn"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Eller fortsätt med"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Pausad"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Löst"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Tillstånd"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Systembelastning över tid"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Flikar"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Totalt: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Utlöses när status växlar mellan upp och ner"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Utlöses när användningen av någon disk överskrider ett tröskelvärde"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Användning"
msgid "Value"
msgstr "Värde"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Visa"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Visa dina 200 senaste larm."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Synliga fält"
#~ msgid "Visible Fields"
#~ msgstr "Synliga fält"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Varningströsklar"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push-aviseringar"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Tüm Konteynerler"
msgid "All Systems"
msgstr "Tüm Sistemler"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Görünüm"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Ağ"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Docker konteynerlerinin ağ trafiği"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Hayır"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Menüyü aç"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Veya devam et"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Duraklatıldı"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Çözüldü"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Durum"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Durum"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Zaman içindeki sistem yükü ortalamaları"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Sekmeler"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Toplam: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Durum yukarı ve aşağı arasında değiştiğinde tetiklenir"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Herhangi bir diskin kullanımı bir eşiği aştığında tetiklenir"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Kullanım"
msgid "Value"
msgstr "Değer"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Görüntüle"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "En son 200 uyarınızı görüntüleyin."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Görünür Alanlar"
#~ msgid "Visible Fields"
#~ msgstr "Görünür Alanlar"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Uyarı eşikleri"
msgid "Webhook / Push notifications"
msgstr "Webhook / Anlık bildirimler"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Всі контейнери"
msgid "All Systems"
msgstr "Всі системи"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Відображення"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Мережа"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Мережевий трафік контейнерів Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Ні"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Відкрити меню"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Або продовжити з"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Призупинено"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Вирішено"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Стан"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Статус"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Середнє навантаження системи з часом"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Вкладки"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Всього: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Спрацьовує, коли статус перемикається
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Спрацьовує, коли використання будь-якого диска перевищує поріг"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Використання"
msgid "Value"
msgstr "Значення"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Вигляд"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Переглянути 200 останніх сповіщень."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Видимі стовпці"
#~ msgid "Visible Fields"
#~ msgstr "Видимі стовпці"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Пороги попередження"
msgid "Webhook / Push notifications"
msgstr "Webhook / Push сповіщення"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "Tất cả container"
msgid "All Systems"
msgstr "Tất cả Hệ thống"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "Hiển thị"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "Mạng"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Lưu lượng mạng của các container Docker"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "Không"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "Mở menu"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "Hoặc tiếp tục với"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "Đã tạm dừng"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "Đã giải quyết"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "Trạng thái"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "Trạng thái"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "Tải trung bình của hệ thống theo thời gian"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "Tab"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "Tổng: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "Kích hoạt khi trạng thái chuyển đổi giữa lên và xuống"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "Kích hoạt khi sử dụng bất kỳ đĩa nào vượt quá ngưỡng"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "Sử dụng"
msgid "Value"
msgstr "Giá trị"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "Xem"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "Xem 200 cảnh báo gần đây nhất của bạn."
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "Các cột hiển thị"
#~ msgid "Visible Fields"
#~ msgstr "Các cột hiển thị"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "Ngưỡng cảnh báo"
msgid "Webhook / Push notifications"
msgstr "Thông báo Webhook / Push"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "所有容器"
msgid "All Systems"
msgstr "所有客户端"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "显示"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "网络"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Docker 容器的网络流量"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "否"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "打开菜单"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "或使用以下方式登录"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "已暂停"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "已解决"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "状态"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "状态"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "系统负载平均值随时间变化"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "标签页"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "总计: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "当状态在上线与掉线之间切换时触发"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "当任何磁盘的使用率超过阈值时触发"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "利用率"
msgid "Value"
msgstr "值"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "视图"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "查看您最近的200个警报。"
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "可见列"
#~ msgid "Visible Fields"
#~ msgstr "可见列"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "警告阈值"
msgid "Webhook / Push notifications"
msgstr "Webhook / 推送通知"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "所有容器"
msgid "All Systems"
msgstr "所有系統"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "顯示"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "網絡"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Docker 容器的網絡流量"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "否"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "開啟選單"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "或繼續使用"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "已暫停"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "已解決"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "狀態"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "狀態"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "系統平均負載隨時間變化"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "分頁"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "總計: {0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "當狀態在上和下之間切換時觸發"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "當任何磁碟的使用超過閾值時觸發"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "利用率"
msgid "Value"
msgstr "值"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "檢視"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "檢視最近 200 則警報。"
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "可見欄位"
#~ msgid "Visible Fields"
#~ msgstr "可見欄位"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "警告閾值"
msgid "Webhook / Push notifications"
msgstr "Webhook / 推送通知"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
+52 -20
View File
@@ -230,6 +230,15 @@ msgstr "所有容器"
msgid "All Systems"
msgstr "所有系統"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "All Tags"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
msgid "All Types"
msgstr ""
#: src/components/routes/settings/general.tsx
msgid "App update"
msgstr ""
@@ -465,8 +474,8 @@ msgstr ""
#: src/components/routes/monitor.tsx
#: src/components/routes/monitor.tsx
msgid "Check Now"
msgstr ""
#~ msgid "Check Now"
#~ msgstr ""
#: src/components/routes/settings/heartbeat.tsx
msgid "Check your monitoring service"
@@ -503,8 +512,8 @@ msgid "Close"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#~ msgid "Columns"
#~ msgstr ""
msgid "Columns"
msgstr ""
#: src/components/login/forgot-pass-form.tsx
#: src/components/login/forgot-pass-form.tsx
@@ -784,6 +793,14 @@ msgctxt "Layout display options"
msgid "Display"
msgstr "顯示"
#: src/components/monitors-table/monitors-table.tsx
msgid "Display"
msgstr ""
#: src/components/domains-table/domains-table.tsx
msgid "Display Columns"
msgstr ""
#: src/components/routes/domain.tsx
#~ msgid "DNS Records"
#~ msgstr ""
@@ -814,7 +831,6 @@ msgid "Domain and SSL expiry calendar"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Domain Monitoring"
msgstr ""
@@ -1407,7 +1423,6 @@ msgid "Monitor updated successfully"
msgstr ""
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/home.tsx
msgid "Monitor websites, APIs, and services"
msgstr ""
@@ -1448,6 +1463,10 @@ msgstr ""
msgid "Net"
msgstr "網路"
#: src/components/monitors-table/monitors-table.tsx
#~ msgid "Network (Grouped)"
#~ msgstr ""
#: src/components/routes/system/charts/network-charts.tsx
msgid "Network traffic of docker containers"
msgstr "Docker 容器的網路流量"
@@ -1471,8 +1490,8 @@ msgid "No"
msgstr "否"
#: src/components/routes/monitor.tsx
msgid "No data available for selected time range"
msgstr ""
#~ msgid "No data available for selected time range"
#~ msgstr ""
#: src/components/routes/incidents.tsx
msgid "No incidents found."
@@ -1553,6 +1572,11 @@ msgstr "開啟選單"
msgid "Optional description for this monitor"
msgstr ""
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Options"
msgstr ""
#: src/components/login/auth-form.tsx
msgid "Or continue with"
msgstr "或繼續使用"
@@ -1626,6 +1650,8 @@ msgid "Paused"
msgstr "已暫停"
#. placeholder {0}: stats.paused
#. placeholder {0}: statusCounts.paused
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Paused ({0})"
msgstr ""
@@ -1804,6 +1830,10 @@ msgstr "已解決"
msgid "Response"
msgstr ""
#: src/components/routes/monitor.tsx
msgid "Response time over the selected period"
msgstr ""
#: src/components/routes/monitor.tsx
#~ msgid "Response Times"
#~ msgstr ""
@@ -2004,7 +2034,6 @@ msgstr "狀態"
#: src/components/containers-table/containers-table-columns.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
@@ -2016,12 +2045,15 @@ msgid "Status"
msgstr "狀態"
#: src/components/routes/monitor.tsx
msgid "Status and response time over the selected period"
#~ msgid "Status and response time over the selected period"
#~ msgstr ""
#: src/components/routes/status-pages.tsx
msgid "Status Page Manager"
msgstr ""
#: src/components/command-palette.tsx
#: src/components/routes/home.tsx
#: src/components/routes/status-pages.tsx
msgid "Status Pages"
msgstr ""
@@ -2065,8 +2097,8 @@ msgid "System load averages over time"
msgstr "系統平均負載隨時間變化"
#: src/components/routes/home.tsx
msgid "System Monitoring"
msgstr ""
#~ msgid "System Monitoring"
#~ msgstr ""
#: src/components/systemd-table/systemd-table.tsx
msgid "Systemd Services"
@@ -2091,6 +2123,8 @@ msgctxt "Tabs system layout option"
msgid "Tabs"
msgstr "分頁"
#: src/components/domains-table/domains-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
msgid "Tags"
msgstr ""
@@ -2213,7 +2247,6 @@ msgstr ""
msgid "Total: {0}"
msgstr "總計:{0}"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Track domain expiry dates and DNS status"
msgstr ""
@@ -2223,8 +2256,8 @@ msgid "Track domain expiry dates and watch domains for purchase"
msgstr ""
#: src/components/routes/home.tsx
msgid "Track system resources, containers, and health"
msgstr ""
#~ msgid "Track system resources, containers, and health"
#~ msgstr ""
#: src/components/routes/monitoring.tsx
msgid "Track uptime, response times, and service health"
@@ -2282,6 +2315,7 @@ msgstr "當連線和離線時觸發"
msgid "Triggers when usage of any disk exceeds a threshold"
msgstr "當任何磁碟使用率超過閾值時觸發"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/monitors-table/monitors-table.tsx
#: src/components/routes/settings/quiet-hours.tsx
#: src/components/routes/settings/quiet-hours.tsx
@@ -2416,7 +2450,6 @@ msgstr "利用率"
msgid "Value"
msgstr "值"
#: src/components/monitors-table/monitors-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "View"
msgstr "檢視"
@@ -2436,8 +2469,8 @@ msgid "View your 200 most recent alerts."
msgstr "檢視最近 200 則警報。"
#: src/components/systems-table/systems-table.tsx
msgid "Visible Fields"
msgstr "顯示欄位"
#~ msgid "Visible Fields"
#~ msgstr "顯示欄位"
#: src/components/routes/domain.tsx
#: src/components/routes/monitor.tsx
@@ -2468,7 +2501,6 @@ msgstr "警告閾值"
msgid "Webhook / Push notifications"
msgstr "Webhook / 推送通知"
#: src/components/routes/home.tsx
#: src/components/routes/monitoring.tsx
msgid "Website & Service Monitoring"
msgstr ""
Submodule reference/domain-locker deleted from 9c2962de33
Submodule reference/uptime-kuma deleted from 2f45b46315
+160
View File
@@ -0,0 +1,160 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
func tryAlternativeWHOISService(ctx context.Context, serviceName, url, domainName string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create request for %s: %w", serviceName, err)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to fetch %s: %w", serviceName, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("%s returned status %d", serviceName, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read %s response: %w", serviceName, err)
}
return string(body), nil
}
func parseAlternativeWHOISHTML(html, domainName, serviceName string) {
fmt.Printf("HTML length from %s: %d characters\n", serviceName, len(html))
// Look for expiry date patterns (common across WHOIS services)
expiryPatterns := []string{
`Expiry Date:\s*</[^>]*>\s*([^<\n]+)`,
`Expiry Date:</[^>]*>\s*([^<\n]+)`,
`Expires on:\s*</[^>]*>\s*([^<\n]+)`,
`Expires:\s*</[^>]*>\s*([^<\n]+)`,
`"expiry":"([^"]+)"`,
`"expires":"([^"]+)"`,
`data-expiry="([^"]+)"`,
`expiry_date["\s]*:\s*"([^"]+)"`,
`expires["\s]*:\s*"([^"]+)"`,
`\d{4}-\d{2}-\d{2}`, // ISO date pattern
`\d{2}/\d{2}/\d{4}`, // DD/MM/YYYY pattern
`\d{2}-\d{2}-\d{4}`, // MM-DD-YYYY pattern
`202\d-\d{2}-\d{2}`, // 202x-xx-xx pattern
}
fmt.Printf("\n=== Searching for expiry dates on %s ===\n", serviceName)
for _, pattern := range expiryPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, strings.TrimSpace(match[1]))
}
}
}
}
// Look for registrar name
registrarPatterns := []string{
`Registrar:\s*</[^>]*>\s*([^<\n]+)`,
`Registrar:</[^>]*>\s*([^<\n]+)`,
`Registered through:\s*</[^>]*>\s*([^<\n]+)`,
`"registrar":"([^"]+)"`,
`data-registrar="([^"]+)"`,
}
fmt.Printf("\n=== Searching for registrar on %s ===\n", serviceName)
for _, pattern := range registrarPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, strings.TrimSpace(match[1]))
}
}
}
}
// Look for status
statusPatterns := []string{
`Status:\s*</[^>]*>\s*([^<\n]+)`,
`Status:</[^>]*>\s*([^<\n]+)`,
`"status":"([^"]+)"`,
`data-status="([^"]+)"`,
}
fmt.Printf("\n=== Searching for status on %s ===\n", serviceName)
for _, pattern := range statusPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, strings.TrimSpace(match[1]))
}
}
}
}
// Show sample HTML
fmt.Printf("\n=== Sample HTML from %s (first 1500 chars) ===\n", serviceName)
if len(html) > 1500 {
fmt.Printf("%s...\n", html[:1500])
} else {
fmt.Printf("%s\n", html)
}
}
func main() {
testDomains := []string{"bookra.eu", "sportcreative.eu"}
for _, domain := range testDomains {
fmt.Printf("Testing alternative WHOIS services for: %s\n", domain)
fmt.Println("========================================")
// Try multiple alternative WHOIS services
services := []struct {
name string
url string
}{
{"whois.com", fmt.Sprintf("https://www.whois.com/whois/%s", domain)},
{"who.is", fmt.Sprintf("https://who.is/whois/%s", domain)},
{"ip2location.com", fmt.Sprintf("https://www.ip2location.com/whois/%s", domain)},
}
for _, service := range services {
fmt.Printf("\n--- Testing %s ---\n", service.name)
html, err := tryAlternativeWHOISService(context.Background(), service.name, service.url, domain)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
parseAlternativeWHOISHTML(html, domain, service.name)
}
fmt.Println("\n========================================\n")
}
}
+94
View File
@@ -0,0 +1,94 @@
package main
import (
"fmt"
"regexp"
"strconv"
)
// parseFlexibleDate parses dates in various formats
func parseFlexibleDate(dateString string) string {
if dateString == "" {
return ""
}
// Remove common separators and normalize
normalized := regexp.MustCompile(`[./-]`).ReplaceAllString(dateString, "-")
normalized = regexp.MustCompile(`\s+`).ReplaceAllString(normalized, "")
// Try different date formats
formats := []string{
// DD.MM.YYYY, DD/MM/YYYY, DD-MM-YYYY
`^(\d{2})[-/.](\d{2})[-/.](\d{4})$`,
// YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD
`^(\d{4})[-/.](\d{2})[-/.](\d{2})$`,
// MM-DD-YYYY, MM/DD/YYYY, MM.DD.YYYY
`^(\d{2})[-/.](\d{2})[-/.](\d{4})$`,
}
for _, format := range formats {
re := regexp.MustCompile(format)
match := re.FindStringSubmatch(normalized)
if match != nil {
part1, part2, part3 := match[1], match[2], match[3]
// Determine if it's DD.MM.YYYY or YYYY.MM.DD format
var year, month, day string
if len(part1) == 4 {
// YYYY.MM.DD format
year = part1
month = part2
day = part3
} else {
// DD.MM.YYYY format (most common)
day = part1
month = part2
year = part3
}
// Validate and format
yearNum, _ := strconv.Atoi(year)
monthNum, _ := strconv.Atoi(month)
dayNum, _ := strconv.Atoi(day)
if yearNum >= 2000 && yearNum <= 2100 && monthNum >= 1 && monthNum <= 12 && dayNum >= 1 && dayNum <= 31 {
return fmt.Sprintf("%s-%s-%s", year, fmt.Sprintf("%02s", month), fmt.Sprintf("%02s", day))
}
}
}
return ""
}
func main() {
testDates := []string{
"15.06.2026",
"13.11.2029",
"2026-06-15",
"15/06/2026",
"13-11-2029",
"2026.06.15",
"15 06 2026",
"invalid-date",
"32.13.2026", // Invalid day/month
"1999.12.31", // Too old
}
fmt.Println("Testing Flexible Date Parsing")
fmt.Println("=============================")
for _, date := range testDates {
result := parseFlexibleDate(date)
status := "✅"
if result == "" {
status = "❌"
}
fmt.Printf("%s '%s' -> '%s'\n", status, date, result)
}
fmt.Println("\nExample Usage:")
fmt.Println("=============")
fmt.Println("User can paste: '15.06.2026, 13.11.2029'")
fmt.Println("System will parse: '2026-06-15', '2029-11-13'")
}
+112
View File
@@ -0,0 +1,112 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
func main() {
testDomains := []string{"bookra.eu", "sportcreative.eu"}
for _, domain := range testDomains {
fmt.Printf("Testing EURid web scraping for: %s\n", domain)
fmt.Println("----------------------------------------")
// EURid web WHOIS URL
url := fmt.Sprintf("https://www.eurid.eu/en/registrations/search/?domain=%s", domain)
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
if err != nil {
fmt.Printf("Failed to create request: %v\n", err)
continue
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Failed to fetch EURid web page: %v\n", err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("EURid web page returned status %d\n", resp.StatusCode)
continue
}
// Read the HTML response
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Failed to read EURid response: %v\n", err)
continue
}
html := string(body)
fmt.Printf("HTML length: %d characters\n", len(html))
// Look for expiry date patterns
expiryPatterns := []string{
`Expiry date:\s*</strong>\s*(\d{2}/\d{2}/\d{4})`,
`Expiry date:</strong>\s*(\d{2}/\d{2}/\d{4})`,
`Expiry date</strong>\s*(\d{2}/\d{2}/\d{4})`,
`"expiryDate":"([^"]+)"`,
`data-expiry="([^"]+)"`,
`\d{2}/\d{2}/\d{4}`, // Generic date pattern
}
fmt.Println("\n=== Searching for expiry dates ===")
for _, pattern := range expiryPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, match[1])
}
}
}
}
// Look for registrar patterns
registrarPatterns := []string{
`Registrar:\s*</strong>\s*([^<\n]+)`,
`Registrar:</strong>\s*([^<\n]+)`,
`Registrar</strong>\s*([^<\n]+)`,
`"registrar":"([^"]+)"`,
`data-registrar="([^"]+)"`,
}
fmt.Println("\n=== Searching for registrar ===")
for _, pattern := range registrarPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, strings.TrimSpace(match[1]))
}
}
}
}
// Show some sample HTML to understand the structure
fmt.Println("\n=== Sample HTML (first 1000 chars) ===")
if len(html) > 1000 {
fmt.Printf("%s...\n", html[:1000])
} else {
fmt.Printf("%s\n", html)
}
fmt.Println("\n========================================\n")
}
}
+163
View File
@@ -0,0 +1,163 @@
package main
import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
func tryEURidEndpoint(ctx context.Context, url, domainName string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}
// Use more realistic browser headers
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
req.Header.Set("Cache-Control", "no-cache")
req.Header.Set("Pragma", "no-cache")
req.Header.Set("Sec-Ch-Ua", "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"")
req.Header.Set("Sec-Ch-Ua-Mobile", "?0")
req.Header.Set("Sec-Ch-Ua-Platform", "\"Windows\"")
req.Header.Set("Sec-Fetch-Dest", "document")
req.Header.Set("Sec-Fetch-Mode", "navigate")
req.Header.Set("Sec-Fetch-Site", "none")
req.Header.Set("Sec-Fetch-User", "?1")
req.Header.Set("Upgrade-Insecure-Requests", "1")
client := &http.Client{Timeout: 20 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to fetch EURid web page: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return "", fmt.Errorf("EURid web page returned status %d", resp.StatusCode)
}
// Read the HTML response
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("failed to read EURid response: %w", err)
}
return string(body), nil
}
func parseEURidWebHTML(html, domainName string) {
fmt.Printf("HTML length: %d characters\n", len(html))
// Look for expiry date patterns
expiryPatterns := []string{
`Expiry date:\s*</strong>\s*(\d{2}/\d{2}/\d{4})`,
`Expiry date:</strong>\s*(\d{2}/\d{2}/\d{4})`,
`Expiry date</strong>\s*(\d{2}/\d{2}/\d{4})`,
`"expiryDate":"([^"]+)"`,
`data-expiry="([^"]+)"`,
`\d{2}/\d{2}/\d{4}`, // Generic date pattern
`\d{4}-\d{2}-\d{2}`, // ISO date pattern
}
fmt.Println("\n=== Searching for expiry dates ===")
for _, pattern := range expiryPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, match[1])
}
}
}
}
// Look for registrar patterns
registrarPatterns := []string{
`Registrar:\s*</strong>\s*([^<\n]+)`,
`Registrar:</strong>\s*([^<\n]+)`,
`Registrar</strong>\s*([^<\n]+)`,
`"registrar":"([^"]+)"`,
`data-registrar="([^"]+)"`,
}
fmt.Println("\n=== Searching for registrar ===")
for _, pattern := range registrarPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
if len(match) > 1 {
fmt.Printf(" Match %d: %s\n", i+1, strings.TrimSpace(match[1]))
}
}
}
}
// Look for any JSON data
jsonPatterns := []string{
`\{[^}]*"expiry[^}]*\}`,
`\{[^}]*"registrar[^}]*\}`,
`\{[^}]*"domain"[^}]*\}`,
}
fmt.Println("\n=== Searching for JSON data ===")
for _, pattern := range jsonPatterns {
re := regexp.MustCompile(pattern)
matches := re.FindAllString(html, -1)
if len(matches) > 0 {
fmt.Printf("Pattern '%s' found %d matches:\n", pattern, len(matches))
for i, match := range matches {
fmt.Printf(" Match %d: %s\n", i+1, match)
}
}
}
// Show sample HTML
fmt.Println("\n=== Sample HTML (first 2000 chars) ===")
if len(html) > 2000 {
fmt.Printf("%s...\n", html[:2000])
} else {
fmt.Printf("%s\n", html)
}
}
func main() {
testDomains := []string{"bookra.eu", "sportcreative.eu"}
for _, domain := range testDomains {
fmt.Printf("Testing EURid web scraping for: %s\n", domain)
fmt.Println("========================================")
// Try multiple EURid endpoints
endpoints := []string{
fmt.Sprintf("https://whois.eurid.eu/en/?q=%s", domain),
fmt.Sprintf("https://whois.eurid.eu/en/search?q=%s", domain),
fmt.Sprintf("https://www.eurid.eu/en/whois/?domain=%s", domain),
}
for i, url := range endpoints {
fmt.Printf("\n--- Attempt %d: %s ---\n", i+1, url)
html, err := tryEURidEndpoint(context.Background(), url, domain)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
parseEURidWebHTML(html, domain)
break // If we got HTML, don't try other endpoints
}
fmt.Println("\n========================================\n")
}
}
+112
View File
@@ -0,0 +1,112 @@
package main
import (
"fmt"
"net"
"strings"
"time"
)
func main() {
// Test TCP WHOIS for .eu domains
testDomains := []string{"bookra.eu", "sportcreative.eu"}
for _, domainName := range testDomains {
fmt.Printf("Testing TCP WHOIS for: %s\n", domainName)
fmt.Println("----------------------------------------")
// Extract TLD
parts := strings.Split(domainName, ".")
if len(parts) < 2 {
fmt.Printf("Invalid domain format: %s\n", domainName)
continue
}
tld := strings.ToLower(parts[len(parts)-1])
// WHOIS server for .eu
server := "whois.eu"
if tld != "eu" {
fmt.Printf("Skipping non-eu domain: %s\n", domainName)
continue
}
addr := net.JoinHostPort(server, "43")
fmt.Printf("Connecting to: %s\n", addr)
// Use longer timeout for .eu domains
timeout := 20 * time.Second
dialer := &net.Dialer{Timeout: timeout}
start := time.Now()
conn, err := dialer.Dial("tcp", addr)
if err != nil {
fmt.Printf("TCP WHOIS dial failed: %v\n", err)
continue
}
defer conn.Close()
fmt.Printf("Connected in: %v\n", time.Since(start))
// Send query
query := domainName + "\r\n"
fmt.Printf("Sending query: %q\n", query)
writeStart := time.Now()
if _, err := conn.Write([]byte(query)); err != nil {
fmt.Printf("TCP WHOIS write failed: %v\n", err)
continue
}
fmt.Printf("Write completed in: %v\n", time.Since(writeStart))
// Set read deadline
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
fmt.Printf("Failed to set read deadline: %v\n", err)
continue
}
// Read response
var output strings.Builder
buf := make([]byte, 4096)
totalRead := 0
readStart := time.Now()
for {
n, err := conn.Read(buf)
if n > 0 {
output.Write(buf[:n])
totalRead += n
fmt.Printf("Read %d bytes (total: %d)\n", n, totalRead)
}
if err != nil {
if err.Error() != "EOF" {
fmt.Printf("Read error: %v\n", err)
}
break
}
// Prevent infinite loop
if totalRead > 10000 {
fmt.Printf("Stopping read after 10KB\n")
break
}
}
fmt.Printf("Read completed in: %v\n", time.Since(readStart))
fmt.Printf("Total bytes read: %d\n", totalRead)
response := output.String()
if len(response) > 0 {
fmt.Printf("✅ WHOIS Response (first 500 chars):\n")
if len(response) > 500 {
fmt.Printf("%s...\n", response[:500])
} else {
fmt.Printf("%s\n", response)
}
} else {
fmt.Printf("❌ No response received\n")
}
fmt.Println()
fmt.Println("========================================")
fmt.Println()
}
}
+119
View File
@@ -0,0 +1,119 @@
package main
import (
"context"
"fmt"
"time"
"github.com/henrygd/beszel/internal/hub/domains/whois"
)
func main() {
// Create a new lookup service with a dummy API key
lookupService := whois.NewLookupService("dummy-api-key")
// Test domains including .eu domain
testDomains := []string{
"google.com",
"bookra.eu", // Your .eu domain
"sportcreative.eu", // Another .eu domain
"github.com",
}
ctx := context.Background()
fmt.Println("=== WHOIS Lookup Test ===")
fmt.Println()
for _, domainName := range testDomains {
fmt.Printf("Testing domain: %s\n", domainName)
fmt.Println("----------------------------------------")
start := time.Now()
// Test WHOIS lookup
whoisData, _, err := lookupService.LookupWHOIS(ctx, domainName)
duration := time.Since(start)
fmt.Printf("Lookup duration: %v\n", duration)
if err != nil {
fmt.Printf("ERROR: %v\n", err)
} else if whoisData != nil {
fmt.Printf("✅ WHOIS Data Found:\n")
fmt.Printf(" Domain: %s\n", whoisData.DomainName)
fmt.Printf(" Status: %v\n", whoisData.Status)
fmt.Printf(" DNSSEC: %s\n", whoisData.DNSSEC)
if whoisData.Dates.ExpiryDate != nil && !whoisData.Dates.ExpiryDate.IsZero() {
fmt.Printf(" Expiry Date: %s\n", whoisData.Dates.ExpiryDate.Format("2006-01-02"))
}
if whoisData.Dates.CreationDate != nil && !whoisData.Dates.CreationDate.IsZero() {
fmt.Printf(" Creation Date: %s\n", whoisData.Dates.CreationDate.Format("2006-01-02"))
}
if whoisData.Dates.UpdatedDate != nil && !whoisData.Dates.UpdatedDate.IsZero() {
fmt.Printf(" Updated Date: %s\n", whoisData.Dates.UpdatedDate.Format("2006-01-02"))
}
if whoisData.Registrar.Name != "" {
fmt.Printf(" Registrar: %s\n", whoisData.Registrar.Name)
}
if whoisData.Registrar.ID != "" {
fmt.Printf(" Registrar ID: %s\n", whoisData.Registrar.ID)
}
if whoisData.Registrant.Name != "" || whoisData.Registrant.Organization != "" {
fmt.Printf(" Registrant: %s (%s)\n", whoisData.Registrant.Name, whoisData.Registrant.Organization)
}
if whoisData.Registrant.Country != "" {
fmt.Printf(" Registrant Country: %s\n", whoisData.Registrant.Country)
}
} else {
fmt.Printf("❌ No WHOIS data returned\n")
}
fmt.Println()
fmt.Println("=== Full Domain Lookup Test ===")
// Test full domain lookup
fullDomain, err := lookupService.LookupDomain(ctx, domainName)
if err != nil {
fmt.Printf("Full lookup ERROR: %v\n", err)
} else if fullDomain != nil {
fmt.Printf("✅ Full Domain Data:\n")
fmt.Printf(" Domain: %s\n", fullDomain.DomainName)
fmt.Printf(" Status: %s\n", fullDomain.Status)
fmt.Printf(" Active: %t\n", fullDomain.Active)
if fullDomain.ExpiryDate != nil {
fmt.Printf(" Expiry: %s\n", fullDomain.ExpiryDate.Format("2006-01-02"))
daysUntil := int(fullDomain.ExpiryDate.Sub(time.Now()).Hours() / 24)
fmt.Printf(" Days Until Expiry: %d\n", daysUntil)
}
fmt.Printf(" Registrar: %s\n", fullDomain.RegistrarName)
fmt.Printf(" DNSSEC: %s\n", fullDomain.DNSSEC)
if len(fullDomain.IPv4Addresses) > 0 {
fmt.Printf(" IPv4 Addresses: %v\n", fullDomain.IPv4Addresses)
}
if len(fullDomain.IPv6Addresses) > 0 {
fmt.Printf(" IPv6 Addresses: %v\n", fullDomain.IPv6Addresses)
}
if len(fullDomain.NameServers) > 0 {
fmt.Printf(" Name Servers: %v\n", fullDomain.NameServers)
}
if len(fullDomain.MXRecords) > 0 {
fmt.Printf(" MX Records: %v\n", fullDomain.MXRecords)
}
if fullDomain.SSLValidTo != nil {
fmt.Printf(" SSL Valid Until: %s\n", fullDomain.SSLValidTo.Format("2006-01-02"))
sslDaysUntil := int(fullDomain.SSLValidTo.Sub(time.Now()).Hours() / 24)
fmt.Printf(" SSL Days Until: %d\n", sslDaysUntil)
}
}
fmt.Println()
fmt.Println("========================================")
fmt.Println()
}
}
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env node
// Test WhoisXML API for .eu domains
const testWhoisXML = async (domain) => {
try {
// Note: This would require a real API key to test
console.log(`Testing WhoisXML API for: ${domain}`);
console.log('Note: WhoisXML API requires an API key to test properly');
// URL structure they use
const url = `https://www.whoisxmlapi.com/whoisserver/WhoisService?apiKey=YOUR_API_KEY&outputFormat=json&domainName=${domain}`;
console.log(`WhoisXML URL: ${url}`);
// Based on their code, WhoisXML should return expiry dates for .eu domains
console.log('WhoisXML API typically provides expiry dates for .eu domains that TCP WHOIS does not');
} catch (error) {
console.error(`Error: ${error.message}`);
}
};
testWhoisXML('bookra.eu');
testWhoisXML('sportcreative.eu');