Commit Graph
137 Commits
Author SHA1 Message Date
iamdoubz 6f87d6f663 api: spam check settings, lookups and cache
SD1 (14 §9): the spam/lookups tables and calls columns, the
IPQualityScore and FTC lookups with the cache, the spam settings with
write-only keys, the provider Test routes and Settings → Spam check.
2026-10-27 15:32:31 -05:00
iamdoubz a3dfcb6b14 docs: spam check spec
SD1 (spec 14, decision 0006): IPQualityScore first, then the FTC Do Not
Call complaint list, for outside US callers who aren't in anyone's
contacts. Information only, off by default; credits running out pause
IPQualityScore until its credit usage API says they're back.
2026-10-27 13:51:08 -05:00
iamdoubz dc5965617e release: version 0.0.9
ci / check (push) Successful in 1m4s
ci / images (push) Successful in 5s
release / images (release) Successful in 1m33s
v0.0.9
2026-10-27 12:49:05 -05:00
iamdoubz a614319990 Merge pull request 'Feat/mobile' (#4) from feat/mobile into main
ci / check (push) Successful in 1m3s
ci / images (push) Successful in 4s
Reviewed-on: #4
2026-10-27 12:42:08 -05:00
iamdoubz 33d5f1a5f5 web: answer incoming calls from the browser notification
ci / check (push) Successful in 1m34s
ci / images (push) Successful in 27s
ci / check (pull_request) Successful in 1m18s
ci / images (pull_request) Successful in 5s
Owner follow-up to the softphone (spec 05 §5).

- the incoming call notification comes from a service worker
  (public/sw.js) so it carries Answer and Decline buttons (Chromium);
  Answer and a plain click focus the softphone tab, Decline doesn't
- shown whenever the tab isn't focused, not only when it's hidden; kept
  up while it rings and closed once it stops
- the softphone panel says how to allow notifications when they're blocked
2026-10-27 12:35:43 -05:00
iamdoubz 431ada51aa docs: confirm the mobile app id
ci / check (push) Successful in 1m30s
ci / images (push) Successful in 22s
MB3
2026-10-26 20:54:54 -05:00
iamdoubz 7da0611a98 docs: mobile app MB3 as built
MB3: the Expo skeleton and pairing are in the mobile repo (6e4bc2b);
the device test with a real Android phone and iPhone is still open.
2026-10-26 17:45:28 -05:00
iamdoubz 0cdac0bcdc api: pair the mobile app with device tokens
MB1 (spec 13). Decision 0005: our own Android/iPhone app, Expo in the
mobile repo, user portal only, no push in v1.

- mobile_devices (migration 0013): one-time pairing codes from My settings
  (QR or server + code, 10 minutes), traded by the app for an spm_ device
  token that acts as its user and never as an admin
- POST /api/me/mobile-devices/pair (web session only), POST /api/mobile/pair,
  GET/DELETE /api/me/mobile-devices
- signing out everywhere (password change or reset, disabling) removes
  paired phones too
- My settings: Mobile app section with the QR code and paired phones
- mobile/ (its own repo) ignored here
2026-10-26 17:12:53 -05:00
iamdoubz adb6d52ddf release: version 0.0.8
ci / check (push) Successful in 1m3s
ci / images (push) Successful in 19s
release / images (release) Successful in 59s
v0.0.8
2026-10-26 09:55:54 -05:00
iamdoubz 7722f33639 Merge pull request 'Feat/sso' (#3) from feat/sso into main
ci / check (push) Successful in 1m20s
ci / images (push) Successful in 4s
Reviewed-on: #3
2026-10-26 09:52:49 -05:00
iamdoubz 97e67e347d auth: security audit fixes, rate limits on every route
ci / check (push) Successful in 1m3s
ci / images (push) Successful in 19s
ci / check (pull_request) Successful in 1m5s
ci / images (pull_request) Successful in 5s
Two separate reviews of the SSO branch and all 207 API routes (spec 12 §8.6). Every route was
guarded as meant. Fixed, each with a test:
- login: a directory 503 counts as a failed sign-in; a locked directory account is a refused
  password, not "unavailable"
- open redirect after OpenID sign-in (a tab in next)
- directory email matching only for non-admins without their own password; others connect on
  My settings (POST /api/me/identities/ldap); only admins disconnect a directory account
- OpenID session check reads the refreshed ID token's claims too
- connect is POST /api/auth/oidc/connect (CSRF header)
- stored bind password, client secret and SMTP password tied to their server
- 409 role_managed when a provider sets the role; empty Contrack PATCH is a 400
- @fastify/rate-limit on every route (RATE_LIMIT_PER_MINUTE), 10/min on Test buttons; a
  password asked again shares the login limiter
Live tests pass against lldap 0.6.3, Samba 4 AD and Keycloak 26.8.1.
2026-10-26 09:46:23 -05:00
iamdoubz 19610777a4 auth: directory leavers check, dashboard warning, Samba AD check (S5)
ci / check (push) Successful in 1m22s
ci / images (push) Successful in 22s
Spec 12 S5: job directoryCheck (hourly) looks up every active user with a directory account;
one deleted, disabled there or no longer matched by the user filter is disabled here, sessions
ended. Nothing changes when the directory is unreachable or a run would disable most people;
the last admin with a password is kept; nobody is re-enabled. Dashboard warning
directory_check (system.directoryWarning). The by-id search is a filter object: ldapts string
filters send bytes >= 0x80 as UTF-8, so AD's objectGUID never matched. Live test against a
Samba 4 AD DC: nested-group admin, the disabled bit, objectGUID, a deleted user disabled.
2026-10-26 07:47:12 -05:00
iamdoubz b06335586b auth: sign in with LDAP/LDAPS, directory settings and Test (S4)
Spec 12 S4: the login form checks the SelfPhoneNG password first, then the directory (ldapts,
service-account search, the user's own bind on a second connection). Presets for lldap, AD,
FreeIPA and OpenLDAP; Test with an optional test login. A directory account connects by email,
never makes a user; adminFilter sets the role (applyRole shared with OpenID). One Disconnect
route per kind (oidc, ldap). Live test against lldap 0.6.3 over LDAPS.
2026-10-25 18:39:52 -05:00
iamdoubz 73963ee3a3 auth: OpenID session check, live Keycloak test, sign-in guide (S3)
Spec 12 S3: job oidcCheck refreshes every OpenID session at the provider every 15 minutes and
applies groups and role again; invalid_grant ends the session, an outage doesn't. A live test
makes a throwaway Keycloak realm and signs in through its login form (passed against 26.8.1).
NODE_EXTRA_CA_CERTS for a private CA. Admin guide: docs/admin-guide/sign-in.md.
2026-10-25 15:14:59 -05:00
iamdoubz 06b3a73edc auth: sign in with OpenID Connect, connect and disconnect, sign-in modes (S2)
Spec 12 S2: openid-client 6.8 (decision 0004). Discovery, PKCE + state + nonce, ID token
signatures checked, userinfo always read. Users connect their OpenID account from My account
and sign in either way; no accounts are made; a user without a password is matched once by
verified email. auth.mode mixed and oidc (admins keep a password way in), role from adminGroups,
allowedGroups, signing out at the provider. Settings -> Sign-in with Test. Tested against a fake
provider with RS256 and ES256 keys.
2026-10-25 14:13:27 -05:00
iamdoubz 3a9423af1f auth: sign-in ways, sign-in mode, and the admin who keeps a password (S1)
Spec 12 S1: migration 0012 (password_hash nullable, user_identities, sessions.method and the
OpenID token columns), auth.mode (only "password" until S2), GET /api/auth/methods, login body
{login, password} (email still accepted), signIn on users and /api/auth/me, a Signs in with
column on Users. The last active admin with a password can't be deleted, disabled or demoted.
2026-10-25 13:30:58 -05:00
iamdoubz ba293f8703 spec: single sign-on (12), OpenID Connect and LDAP
Owner decisions 2026-10-25: a user connects their OpenID account and signs in with either it or
their password; the admin picks passwords only, OpenID only (admins keep a password way in), or
mixed. No automatic accounts. OIDC sessions re-checked every 15 min with the refresh token;
signing out also signs out at the provider; password sign-ins keep two-step. LDAP is one more way
to check a password. Decision 0004: openid-client and ldapts. Tasks S1-S5.
2026-10-25 13:09:45 -05:00
iamdoubz 95561f651f release: version 0.0.7
ci / check (push) Successful in 1m4s
ci / images (push) Successful in 5s
release / images (release) Successful in 31s
v0.0.7
2026-10-23 16:16:37 -05:00
iamdoubz 53aefaa5d7 Merge pull request 'Feat/contacts' (#2) from feat/contacts into main
ci / check (push) Successful in 1m1s
ci / images (push) Successful in 5s
Reviewed-on: #2
2026-10-23 16:14:19 -05:00
iamdoubz 604e40adf1 contacts: owners' contacts on their desk phones, copy builtin contacts to Contrack (C6)
ci / check (push) Successful in 1m2s
ci / images (push) Successful in 20s
ci / check (pull_request) Successful in 1m4s
ci / images (pull_request) Successful in 5s
Spec 11 §6.2: a phone's directory is the extensions, its owner's own contacts (accepted shares
included), then the company's, in both modes. In Contrack mode, contacts from before Contrack
are offered for copying (contrack.toCopy, POST …/copy-to-contrack: one Contrack bulk import,
skipping what's already there; the builtin rows stay). Tested against a fake and a real
Contrack 2.0.2.
2026-10-23 16:07:19 -05:00
iamdoubz e6a1373f36 contacts: calls on the Contrack timeline, summaries opt-in (C5)
ci / check (push) Successful in 1m7s
ci / images (push) Successful in 20s
Spec 11 §4.4: the contrackLog job puts each connected person's calls with their Contrack
contacts on that contact's timeline once (contrack_call_log, migration 0011), from when they
connected. With summaries on (My settings → Contacts, needs call summaries), the escaped summary
and action items go in, and the first action item becomes a follow-up due the next day. Waits up
to 30 min for a summary; Contrack down is retried, refusals noted once. Tested against a fake and
a real Contrack 2.0.2.
2026-10-23 15:59:53 -05:00
iamdoubz b05361e840 contacts: caller names on desk phones while they ring (C4)
ci / check (push) Successful in 1m2s
ci / images (push) Successful in 22s
Spec 11 §6.1: sp-sub-dial-user asks /api/internal/caller-name (func_curl, 2 s at most; this
machine only, with an APP_SECRET-derived key, not logged) for the ringing user's name for the
caller, and sets it on each ringing leg (pre-dial handler, CONNECTEDLINE), never on the call, so
forwards and other members don't see someone else's contacts. Ring group/queue/number prefixes
kept. Contrack lookups never wait (cache, contactsWarm every 5 min). Proven on real Asterisk
with baresip: apps/api/test/asterisk/cnam-test.ts.
2026-10-23 15:45:36 -05:00
iamdoubz 692dd59552 contacts: Contrack as the robust contacts (C3), picked in Settings or the setup wizard
ci / check (push) Successful in 1m0s
ci / images (push) Successful in 32s
Spec 11 §4: contacts.provider and contacts.contrack (local address only, company token
write-only), each person's own token (users.contrack_token_enc, migration 0010) in My settings,
books read from Contrack with a 5-minute cache, Add to contacts and accepted shares created in
Contrack, the company account on desk phones. Setup wizard step 8 Contacts. deploy/contrack
(2.0.2, pinned), Caddy/nginx blocks, decision 0003, admin guide Contacts. Tested against a fake
and a real Contrack 2.0.2 (contrack-live.test.ts).
2026-10-23 15:23:49 -05:00
iamdoubz a754974a90 contacts: my book, the company book, vCard/CSV import, sharing with Accept/Deny
C1 (spec 11): /api/me/contacts and /api/contacts (admin), our own vCard 2.1/3.0/4.0 and CSV
readers with a preview, contact_shares (migration 0009) for offers, and contact names in call
history, voicemail, Home, the softphone and the call report email. Admin → Contacts.
2026-10-23 14:53:21 -05:00
iamdoubz 32f4590369 spec: contacts (11), Simple or Contrack, picked at setup
Owner decisions 2026-10-23: vCard import for admins and users; desk-phone caller name while
ringing is its own task (C4); the company is one Contrack account shown to everyone, each user
has their own; sharing offers a copy the colleague accepts or denies; imports skip duplicates.
2026-10-23 14:53:21 -05:00
iamdoubz 20d27232ac Merge pull request 'Feat/call report' (#1) from feat/call-report into main
ci / check (push) Successful in 1m11s
ci / images (push) Successful in 12s
Reviewed-on: #1
2026-10-23 13:59:42 -05:00
iamdoubz 4c006f2388 me: call report email (hourly, daily, weekly), with call summaries
ci / check (push) Successful in 1m14s
ci / images (push) Successful in 40s
ci / check (pull_request) Successful in 59s
ci / images (pull_request) Successful in 4s
My settings → Call report: each user's calls of the last hour, day (at a time) or week (on a
day at a time), company time zone: numbers, status, talk time, recorded, and optionally each
recorded call's summary (10 §3). No calls, no email. Job callReports every minute, 5 min after
the period ends; periods computed in PostgreSQL (DST-safe). Migration 0008_call_reports.
2026-10-23 13:55:47 -05:00
iamdoubz 46cc8c2002 voicemail: search messages by transcript or caller
Filters the already-loaded mailbox in the browser (GET /api/me/voicemail isn't paged).
2026-10-23 13:55:46 -05:00
iamdoubz aaa294ae6a release: version 0.0.6
ci / check (push) Successful in 1m17s
ci / images (push) Successful in 11s
release / images (release) Successful in 54s
v0.0.6
2026-10-21 20:24:46 -05:00
iamdoubz 4d331fb70e ai: call transcripts (each side recorded, merged by time) and summaries; search on Recordings
ci / check (push) Successful in 1m22s
ci / images (push) Successful in 27s
2026-10-21 19:10:02 -05:00
iamdoubz d9f9c6e444 release: version 0.0.5
ci / check (push) Successful in 1m27s
ci / images (push) Successful in 26s
2026-10-21 18:46:04 -05:00
iamdoubz a26a5ca740 ai: Settings → AI (local endpoints only, with tests), the AI stack, voicemail transcripts
ci / check (push) Successful in 1m0s
ci / images (push) Successful in 26s
2026-10-21 18:37:13 -05:00
iamdoubz 274242ce32 roadmap: a backlog, the HT802 V2 setup first
ci / check (push) Successful in 1m0s
ci / images (push) Successful in 26s
2026-10-21 17:57:32 -05:00
iamdoubz 0deff2a637 dial plan: made from routes and number lengths; Yealink dial-now, the ATA plan on the Credentials tab 2026-10-21 17:14:13 -05:00
iamdoubz eacc571093 docs: analog phones (ATAs) set up by hand; the ATA plan in the roadmap
ci / check (push) Successful in 57s
ci / images (push) Successful in 30s
2026-10-21 17:01:42 -05:00
iamdoubz 4a175a0d77 release: version 0.0.4
ci / check (push) Successful in 58s
ci / images (push) Successful in 4s
release / images (release) Successful in 38s
v0.0.4
2026-10-21 16:22:54 -05:00
iamdoubz f2868cf5d3 settings: the SIP port, with a check against other listeners
ci / check (push) Successful in 58s
ci / images (push) Successful in 30s
2026-10-21 16:12:12 -05:00
iamdoubz 6893c55e1b settings: a switch for SIP over TCP
ci / check (push) Successful in 1m8s
ci / images (push) Successful in 36s
2026-10-21 15:22:39 -05:00
iamdoubz e54e8a1576 release: version 0.0.3
ci / check (push) Successful in 1m16s
ci / images (push) Successful in 12s
release / images (release) Successful in 2m22s
v0.0.3
2026-10-21 13:44:40 -05:00
iamdoubz 338ab8d7f1 reports: total minutes, talk time, answered; outside minutes per day; voip.ms balance
Owner asked for total minutes, call time and missed calls in one place, and the money left on
voip.ms. Reports now totals answered calls, talk time and outside minutes (in from arrival, out
from the answer: roughly what the provider bills), charts outside minutes per day, and shows each
voip.ms trunk's balance (getBalance; verify with a real account).
2026-10-21 13:38:23 -05:00
iamdoubz 175d5a2c73 music on hold: play order set in the UI, and shuffle
Owner: the first upload always played first. A class is now an Asterisk playlist in the order
set on Sounds & music (move up/down), new uploads last, with a Shuffle option (sort = random).
moh-test.ts plays both in a real Asterisk and reads the file order from its debug log.
2026-10-21 13:27:02 -05:00
iamdoubz aa6681da03 numbers: any free internal number of 1 to 6 digits; ranges only suggest
Owner wanted the first user on 1. Numbering ranges now choose the Next free suggestion
(Settings → Numbering), the setup wizard takes a number per person, and a one-digit number
can't also be a menu key of a digital receptionist with direct dialing. Dialplan patterns that
take an internal number match one digit (_X!); golden case short-numbers proves it in Asterisk.
2026-10-21 13:17:25 -05:00
iamdoubz 4143f6d064 probe: downloadable executables for machines without Docker
ci / check (push) Successful in 59s
ci / images (push) Successful in 49s
Each release attaches selfphoneng-probe-linux-x64 and -arm64 (Node's single executable
applications: esbuild bundle + postject into the official Node binary, checked against
nodejs.org's SHASUMS256.txt), the bundle as selfphoneng-probe.cjs (Node 22+) and SHA256SUMS.
Built in binary.Dockerfile with BuildKit's local output, so Gitea's runner can build it too.
deploy/probe/selfphoneng-probe.service runs it under systemd, sandboxed.
2026-10-21 12:57:04 -05:00
iamdoubz 65a0438dde diagnostics: System → Diagnostics with a network test probe; RTP_PORT_START/END
The server checks itself (public IP, DNS, local networks, phone ACL, SIP transports, free audio
ports) and says how to fix each problem. A quick or complete network test asks the probe
(apps/probe, own image) to test this server from outside: HTTP/HTTPS, SIP over UDP/TCP/TLS, the
browser phone's /ws, sampled audio ports. The probe only tests the address that asks.
The audio port range comes from RTP_PORT_START/END (default 10000-20000).
2026-10-21 12:37:16 -05:00
iamdoubz 90b3081368 docs: update.sh tested on a throwaway compose install
ci / check (push) Successful in 57s
ci / images (push) Successful in 1m11s
2026-10-21 11:53:25 -05:00
iamdoubz 22b3ef9a8c backup before migrating: once per set of pending changes, not on every restart of a crash-looping api 2026-10-21 11:43:24 -05:00
iamdoubz e2518b03d8 backups: seconds in the name, so two in one minute don't overwrite; update.sh prints the api's log on a failed check 2026-10-21 11:34:42 -05:00
iamdoubz ff59bc7799 update.sh: executable 2026-10-21 11:26:45 -05:00
iamdoubz 4d9b5dab8a update: go back to the running release; docs for the first update from v0.0.2 2026-10-21 11:25:46 -05:00
iamdoubz 54e1e201a1 update: the api backs up before migrating; scripts/update.sh for compose installs
Docker only, never bare metal (owner, 2026-10-21). A new version with pending migrations backs
up first (database, voicemail, sounds, MOH) and stops if it can't (SKIP_BACKUP_BEFORE_MIGRATE
to override). update.sh: checks, changelog, backup (node dist/backup-cli.js), log compaction,
build while the old version runs, restart, self-check, and rollback with restore. Container
logs capped in compose. Admin guide: Updating, for compose and Portainer.
2026-10-21 11:15:40 -05:00