Rebrand CommGate to gsmnode and adopt new design system

Rename the whole project from CommGate to gsmnode and re-skin every
surface onto the new signal-green + ink design system (Space Grotesk /
IBM Plex Sans / JetBrains Mono, flat surfaces, two-arrow routing mark,
lowercase gsm+node wordmark).

- Web App + API panel: rewrite token layer, gn-* utilities, data-gsm-theme,
  new logo/favicon assets; both builds verified.
- Phone App (Flutter): green theme, new mark/wordmark widgets, adaptive
  launcher icons; rename Android applicationId/package to app.gsmnode.phone;
  bump AGP to 8.6.0 and google_fonts to 8.1.0; debug APK build verified.
- API Server (Go): panel routes, User-Agent, health service id, branding.
- Home Assistant Plugin: rename integration domain sms_gateway to gsmnode
  (folder, DOMAIN, services, entity ids, classes); py_compile verified.
- Update all READMEs; add .gitignore (excludes Design/, build artifacts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-07-06 08:53:14 +02:00
commit d6956395c8
166 changed files with 12605 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
# Web App (BFF) configuration
# Address the Web App listens on
WEB_ADDR=:8090
# Base URL of the API Server. The BFF reverse-proxies /api/* to this host, so
# the browser only ever talks same-origin to the Web App.
API_BASE=http://localhost:8080
+29
View File
@@ -0,0 +1,29 @@
# Builds the Vue frontend (if needed) and runs the Web App BFF.
#
# ./Run-WebApp.ps1 # build frontend then serve
# ./Run-WebApp.ps1 -SkipBuild # serve existing dist only
param([switch]$SkipBuild)
$ErrorActionPreference = "Stop"
$serverDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$root = Split-Path -Parent $serverDir # the "Web App" folder
$webDir = Join-Path $root "web"
$goBin = "C:\Program Files\Go\bin"
if (Test-Path $goBin) { $env:Path = "$goBin;$env:Path" }
if (-not $SkipBuild) {
Write-Host "Building frontend..." -ForegroundColor Cyan
Push-Location $webDir
try {
if (-not (Test-Path "node_modules")) { npm install }
npm run build
} finally { Pop-Location }
}
Push-Location $serverDir
try {
Write-Host "Starting Web App on :8090 (Ctrl+C to stop)..." -ForegroundColor Cyan
go run .
} finally { Pop-Location }
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="7" fill="#2E9E6B"></rect>
<g transform="translate(6.5 6.5) scale(0.475)">
<line x1="9" y1="15" x2="28" y2="15" stroke="#fff" stroke-width="3.4" stroke-linecap="round"></line>
<path d="M25 10.5 L31 15 L25 19.5" stroke="#fff" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"></path>
<line x1="12" y1="25" x2="31" y2="25" stroke="#fff" stroke-width="3.4" stroke-linecap="round"></line>
<path d="M15 20.5 L9 25 L15 29.5" stroke="#fff" stroke-width="3.4" stroke-linecap="round" stroke-linejoin="round"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
<line x1="7" y1="15" x2="31" y2="15" stroke="#12161C" stroke-width="2.6" stroke-linecap="round"></line>
<path d="M26 10 L32 15 L26 20" stroke="#12161C" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path>
<line x1="9" y1="25" x2="33" y2="25" stroke="#2E9E6B" stroke-width="2.6" stroke-linecap="round"></line>
<path d="M14 20 L8 25 L14 30" stroke="#2E9E6B" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#2E9E6B" />
<title>gsmnode</title>
<script type="module" crossorigin src="/assets/index-B7DZHD5s.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Ab7NGFI_.css">
</head>
<body>
<div id="app"></div>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
module smsgateway/webapp
go 1.26
+113
View File
@@ -0,0 +1,113 @@
// Command webapp is the Web App backend-for-frontend. It serves the embedded
// Vue single-page app and reverse-proxies /api/* to the API Server, so the
// browser only ever talks to this server (same-origin) and all data access
// still flows through the API Server.
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"time"
)
//go:embed all:dist
var distFS embed.FS
func getenv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func main() {
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
log.SetPrefix("[web] ")
loadDotEnv(".env")
addr := getenv("WEB_ADDR", ":8090")
apiBase := strings.TrimRight(getenv("API_BASE", "http://localhost:8080"), "/")
apiURL, err := url.Parse(apiBase)
if err != nil {
log.Fatalf("invalid API_BASE %q: %v", apiBase, err)
}
// Reverse proxy: /api/* -> API Server (path preserved).
proxy := httputil.NewSingleHostReverseProxy(apiURL)
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, e error) {
log.Printf("proxy error for %s: %v", r.URL.Path, e)
http.Error(w, `{"error":"api server unavailable"}`, http.StatusBadGateway)
}
// Embedded SPA file server.
sub, err := fs.Sub(distFS, "dist")
if err != nil {
log.Fatalf("embed dist: %v", err)
}
spa := http.FileServer(http.FS(sub))
mux := http.NewServeMux()
mux.Handle("/api/", proxy)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Serve static assets when they exist; otherwise fall back to index.html
// so client-side routing works on deep links.
if r.URL.Path != "/" {
if f, err := sub.Open(strings.TrimPrefix(r.URL.Path, "/")); err == nil {
f.Close()
spa.ServeHTTP(w, r)
return
}
}
r2 := r.Clone(r.Context())
r2.URL.Path = "/"
spa.ServeHTTP(w, r2)
})
srv := &http.Server{
Addr: addr,
Handler: logRequests(mux),
ReadHeaderTimeout: 10 * time.Second,
}
log.Printf("listening on %s (proxying /api -> %s)", addr, apiBase)
if err := srv.ListenAndServe(); err != nil {
log.Fatalf("server error: %v", err)
}
}
func logRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start).Round(time.Millisecond))
})
}
// loadDotEnv loads KEY=VALUE pairs from a .env file if present.
func loadDotEnv(path string) {
data, err := os.ReadFile(path)
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
k, v, ok := strings.Cut(line, "=")
if !ok {
continue
}
k = strings.TrimSpace(k)
v = strings.Trim(strings.TrimSpace(v), `"'`)
if _, exists := os.LookupEnv(k); !exists {
_ = os.Setenv(k, v)
}
}
}