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
+194
View File
@@ -0,0 +1,194 @@
// Sets up the PocketBase collections the API Server expects.
//
// Usage (PowerShell):
// $env:POCKETBASE_URL="http://10.2.1.10:8028"
// $env:PB_ADMIN_EMAIL="you@example.com"
// $env:PB_ADMIN_PASSWORD="your-password"
// node scripts/setup-pocketbase.mjs
//
// It is idempotent: existing collections are updated, missing ones created.
// Requires Node 18+ (uses global fetch). No npm install needed.
const BASE = (process.env.POCKETBASE_URL || "http://10.2.1.10:8028").replace(/\/$/, "");
const EMAIL = process.env.PB_ADMIN_EMAIL;
const PASSWORD = process.env.PB_ADMIN_PASSWORD;
if (!EMAIL || !PASSWORD) {
console.error("Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD environment variables.");
process.exit(1);
}
let token = "";
async function api(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: token } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let json = null;
try { json = text ? JSON.parse(text) : null; } catch { /* non-JSON */ }
if (!res.ok) {
const msg = json?.message || text || res.statusText;
throw new Error(`${method} ${path} -> ${res.status}: ${msg}`);
}
return json;
}
async function authenticate() {
const res = await api("POST", "/api/collections/_superusers/auth-with-password", {
identity: EMAIL,
password: PASSWORD,
});
token = res.token;
console.log("Authenticated as superuser.");
}
async function getCollections() {
const res = await api("GET", "/api/collections?perPage=200");
const byName = {};
for (const c of res.items) byName[c.name] = c;
return byName;
}
// Field builders for PocketBase v0.23+ (collections use a `fields` array).
const f = {
text: (name, opts = {}) => ({ name, type: "text", required: false, ...opts }),
number: (name, opts = {}) => ({ name, type: "number", required: false, ...opts }),
date: (name, opts = {}) => ({ name, type: "date", required: false, ...opts }),
json: (name, opts = {}) => ({ name, type: "json", required: false, maxSize: 2000000, ...opts }),
url: (name, opts = {}) => ({ name, type: "url", required: false, ...opts }),
select: (name, values, opts = {}) => ({
name, type: "select", required: false, maxSelect: 1, values, ...opts,
}),
relation: (name, collectionId, opts = {}) => ({
name, type: "relation", required: false, collectionId,
cascadeDelete: false, maxSelect: 1, minSelect: 0, ...opts,
}),
autodate: (name, opts) => ({ name, type: "autodate", onCreate: true, onUpdate: false, ...opts }),
};
// Superuser-only API rules: only the API Server (acting as superuser) reads or
// writes these collections. Clients never touch PocketBase directly.
const LOCKED = { listRule: null, viewRule: null, createRule: null, updateRule: null, deleteRule: null };
function definitions(ids) {
return [
{
name: "devices",
type: "base",
...LOCKED,
fields: [
f.text("device_id", { required: true }),
f.text("name"),
f.text("platform"),
f.text("app_version"),
f.text("push_token"),
f.text("auth_token", { required: true }),
f.select("status", ["online", "offline"]),
f.date("last_seen_at"),
f.relation("owner", ids.users, { required: true, cascadeDelete: true }),
f.autodate("created", { onCreate: true, onUpdate: false }),
f.autodate("updated", { onCreate: true, onUpdate: true }),
],
indexes: [
"CREATE UNIQUE INDEX idx_devices_auth_token ON devices (auth_token)",
"CREATE UNIQUE INDEX idx_devices_owner_device ON devices (owner, device_id)",
],
},
{
name: "messages",
type: "base",
...LOCKED,
fields: [
f.json("phone_numbers", { required: true }),
f.text("text_message"),
f.select("type", ["sms", "call"]),
f.number("sim_number"),
f.select("status", ["Pending", "Processed", "Sent", "Delivered", "Failed"]),
f.text("error"),
f.date("schedule_at"),
f.date("sent_at"),
f.date("delivered_at"),
f.relation("device", ids.devices, { cascadeDelete: false }),
f.relation("owner", ids.users, { required: true, cascadeDelete: true }),
f.autodate("created", { onCreate: true, onUpdate: false }),
f.autodate("updated", { onCreate: true, onUpdate: true }),
],
indexes: [
"CREATE INDEX idx_messages_device_status ON messages (device, status)",
"CREATE INDEX idx_messages_owner ON messages (owner)",
],
},
{
name: "inbox",
type: "base",
...LOCKED,
fields: [
f.text("phone_number", { required: true }),
f.text("message"),
f.date("received_at"),
f.relation("device", ids.devices, { cascadeDelete: false }),
f.relation("owner", ids.users, { required: true, cascadeDelete: true }),
f.autodate("created", { onCreate: true, onUpdate: false }),
],
indexes: ["CREATE INDEX idx_inbox_owner ON inbox (owner)"],
},
{
name: "webhooks",
type: "base",
...LOCKED,
fields: [
f.select("event", ["sms:received", "sms:sent", "sms:delivered", "sms:failed"]),
f.url("url", { required: true }),
f.relation("device", ids.devices, { cascadeDelete: false }),
f.relation("owner", ids.users, { required: true, cascadeDelete: true }),
f.autodate("created", { onCreate: true, onUpdate: false }),
],
indexes: ["CREATE INDEX idx_webhooks_owner_event ON webhooks (owner, event)"],
},
];
}
async function main() {
await authenticate();
let collections = await getCollections();
if (!collections.users) {
throw new Error('The default "users" auth collection was not found in PocketBase.');
}
// Resolve collection ids needed for relation fields. devices must exist before
// messages/inbox/webhooks reference it, so create in order, refreshing ids.
const order = ["devices", "messages", "inbox", "webhooks"];
for (const name of order) {
const ids = {
users: collections.users.id,
devices: collections.devices?.id,
};
const def = definitions(ids).find((d) => d.name === name);
if (collections[name]) {
await api("PATCH", `/api/collections/${collections[name].id}`, def);
console.log(`Updated collection: ${name}`);
} else {
await api("POST", "/api/collections", def);
console.log(`Created collection: ${name}`);
}
collections = await getCollections(); // refresh so later relations resolve
}
console.log("\nPocketBase setup complete.");
console.log("Collections: users (existing), devices, messages, inbox, webhooks.");
console.log("\nNext: create a user to log in with, e.g. via the PocketBase admin UI,");
console.log("or run scripts/create-user.mjs.");
}
main().catch((err) => {
console.error("\nSetup failed:", err.message);
process.exit(1);
});