Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
38cadb8
fix: overhaul duplicate detection scoring, add address matching, trig…
bashar-qassis Apr 4, 2026
38cba05
fix: paginate duplicates page to prevent timeout on large result sets
bashar-qassis Apr 4, 2026
ab349c6
fix: add pagination to duplicates panel in contacts index page
bashar-qassis Apr 4, 2026
af046bc
fix: handle duplicate photos during merge, reduce merge to 3 steps
bashar-qassis Apr 4, 2026
7cdcfb0
feat: mount official Oban Web dashboard for admins
bashar-qassis May 15, 2026
edbaa2d
chore: parameterize Docker host ports via .env
bashar-qassis May 15, 2026
1219489
fix: extract Monica photo import to async worker with live sync_summary
bashar-qassis May 15, 2026
77496f9
fix: queue MonicaPhotoSyncWorker on :imports instead of :photo_sync
bashar-qassis May 15, 2026
34cdb80
docs: drop stale photo-sync references after Monica import refactor
bashar-qassis May 15, 2026
d5f5fd9
docs: add design spec for account reset completeness fix
bashar-qassis May 15, 2026
e7bcea2
docs: add implementation plan for account-reset-completeness + spec r…
bashar-qassis May 15, 2026
9f3022a
feat: add Kith.Imports.Cleanup for account-scoped import wipe
bashar-qassis May 15, 2026
07a3d55
feat: add Kith.Imports.JobCancellation for account-scoped Oban cancel
bashar-qassis May 15, 2026
d7e9aa3
feat: add Kith.Storage.AccountCleanup for account-scoped file wipe
bashar-qassis May 15, 2026
7ac3a8b
feat: add Kith.Contacts.Cleanup for account-scoped contacts+tags wipe
bashar-qassis May 15, 2026
7d1e666
feat: add Kith.Conversations.Cleanup for account-scoped conversation …
bashar-qassis May 15, 2026
eb72c11
feat: add Kith.Journal.Cleanup for account-scoped journal wipe
bashar-qassis May 15, 2026
55e48c4
feat: add Kith.Tasks.Cleanup for account-scoped task wipe
bashar-qassis May 15, 2026
c6b7035
feat: add Kith.Reminders.Cleanup for account-scoped reminder wipe
bashar-qassis May 15, 2026
63e0bbf
feat: add Kith.Activities.Cleanup for account-scoped activity wipe
bashar-qassis May 15, 2026
ab41cf7
feat: add Kith.AuditLogs.Cleanup for account-scoped audit-log wipe
bashar-qassis May 15, 2026
cdd46d4
refactor: AccountResetWorker becomes orchestrator over per-domain Cle…
bashar-qassis May 15, 2026
28516fb
test: add regression + cross-account isolation tests for AccountReset…
bashar-qassis May 15, 2026
7b3a355
docs: clarify Reminders.Cleanup moduledoc on reminder_rules preservation
bashar-qassis May 15, 2026
6af91bf
fix: Monica import duplicate handling — auto-merge contract, cartesia…
bashar-qassis May 15, 2026
3952938
docs: design spec for Monica import performance fix
bashar-qassis May 15, 2026
fe213fd
docs: implementation plan for Monica import performance fix
bashar-qassis May 16, 2026
81ba714
feat: add Monica API per-host rate limiter (55/min)
bashar-qassis May 16, 2026
4daf1ea
chore: configure Monica API rate limit (55/min prod, unlimited test)
bashar-qassis May 16, 2026
23169f9
refactor: collapse Monica double-retry to Req's built-in + RateLimiter
bashar-qassis May 16, 2026
c10df75
feat: Contacts.create_contact_field/3 supports normalize: false opt
bashar-qassis May 16, 2026
4220e93
perf: skip redundant normalization in Monica contact_field writes
bashar-qassis May 16, 2026
90f3ee8
perf: replace :persistent_term phone-cft cache with ref_data map
bashar-qassis May 16, 2026
56d5911
feat: collect misc-data plan during Monica crawl
bashar-qassis May 16, 2026
913e4d0
feat: add MonicaMiscDataWorker (per-contact extra data, plan-driven)
bashar-qassis May 16, 2026
07158ed
refactor: extract Phase 4 to MonicaMiscDataWorker; enqueue from crawl…
bashar-qassis May 16, 2026
7f11d94
docs: update MonicaApi moduledoc for Phase 4 extraction
bashar-qassis May 16, 2026
c4f3283
docs: spec for Monica import deployment fixes (PubSub + Oban + cluste…
bashar-qassis May 16, 2026
25ea120
docs: implementation plan for Monica import deployment fixes
bashar-qassis May 16, 2026
147d6f0
fix: start PubSub + DNSCluster in base_children for worker mode
bashar-qassis May 16, 2026
f38aac7
fix: gate Oban queues by KITH_MODE in :prod (web=insert-only)
bashar-qassis May 16, 2026
6e9eb95
infra: cluster app + worker containers via shared cookie + DNS alias
bashar-qassis May 16, 2026
d39b5a6
docs: document RELEASE_COOKIE in .env.example
bashar-qassis May 16, 2026
0312a33
fix: replace DNSCluster with libcluster Epmd strategy for cross-conta…
bashar-qassis May 16, 2026
785f2c7
fix: use sname distribution for libcluster (bare hostnames are illega…
bashar-qassis May 16, 2026
f4dfb8d
docs(specs): design for phone display format fix
bashar-qassis May 16, 2026
67f3688
docs(plans): implementation plan for phone display format fix
bashar-qassis May 16, 2026
e39a6d5
fix(phone): replace NANP-only renderer with ExPhoneNumber library calls
bashar-qassis May 16, 2026
cd74b85
docs(phone): update PhoneFormatter moduledoc + rename render arg
bashar-qassis May 16, 2026
375c98e
test(phone): add non-NANP Playwright coverage for display format
bashar-qassis May 16, 2026
4919369
docs(specs): design for Monica import coverage backfill
bashar-qassis May 16, 2026
f928885
docs(plans): implementation plan for Monica coverage backfill
bashar-qassis May 16, 2026
16596c9
refactor(monica): thread ref_data through crawl_all_contacts return
bashar-qassis May 16, 2026
4be5337
feat(monica): add backfill helpers (fetch_single_contact, accept_back…
bashar-qassis May 16, 2026
4ce8a3e
feat(monica): coverage_check_and_backfill/3 core algorithm
bashar-qassis May 16, 2026
4744984
feat(monica): wire coverage_check_and_backfill into crawl/5
bashar-qassis May 16, 2026
3039247
test(monica): edge-case coverage for coverage_check_and_backfill
bashar-qassis May 16, 2026
b96f08c
refactor(monica): flatten coverage_check_and_backfill helper nesting
bashar-qassis May 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Suppress dialyzer warnings produced by generated code in third-party
# libraries. Re-evaluate this list whenever we upgrade deps.

[
# ex_cldr_territories generates type specs that are slightly broader than
# the success typing for these zero-arg accessors on Kith.Cldr.Territory.
# Reported as `:contract_supertype` against lib/kith/cldr.ex (the backend
# module that injects the provider). Not actionable from our code.
{"lib/kith/cldr.ex", :contract_supertype}
]
32 changes: 28 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
# Core — REQUIRED (no defaults)
# ============================================================
SECRET_KEY_BASE=generate-with-mix-phx-gen-secret
# Erlang BEAM distribution cookie. Shared between the app and worker
# containers so they can cluster for cross-container PubSub broadcasts
# (LiveView import progress). Generate with one of:
# mix phx.gen.secret 32
# openssl rand -base64 32
RELEASE_COOKIE=generate-with-mix-phx-gen-secret

DATABASE_URL=ecto://kith:change_me@postgres:5432/kith_prod
AUTH_TOKEN_SALT=generate-with-mix-phx-gen-secret
CLOAK_KEY=generate-32-byte-base64-key
Expand All @@ -30,10 +37,7 @@ KITH_HOSTNAME=localhost
POSTGRES_USER=kith
POSTGRES_PASSWORD=change_me
POSTGRES_DB=kith_prod
# PostgreSQL port — used by Elixir app (dev/test) AND as Docker host port
# Default 5434 avoids conflicts with standard postgres (5432)
# Inside Docker, the app container overrides this to 5432 (internal network)
DB_PORT=5434

POOL_SIZE=10
# DATABASE_SSL=false

Expand Down Expand Up @@ -121,3 +125,23 @@ SENTRY_DSN=
SENTRY_ENVIRONMENT=production
# Required in production for /metrics endpoint access
METRICS_TOKEN=generate-a-random-token

# ============================================================
# Docker Host Ports
# ============================================================
# These configure which HOST ports Compose publishes for each service.
# Defaults match docker-compose.{dev,prod}.yml; override only on conflict.
# Internal container ports are NOT configurable (services talk to each other
# on standard ports over the Docker network).
#
# Dev stack (docker-compose.dev.yml):
DB_PORT=5434 # postgres -> host (default 5434, avoids local 5432)
MAILPIT_SMTP_PORT=1025 # mailpit SMTP listener
MAILPIT_WEB_PORT=8025 # mailpit web UI -> http://localhost:8025
MINIO_PORT=9000 # MinIO S3 API
MINIO_CONSOLE_PORT=9001 # MinIO web console
APP_PORT=4000 # Phoenix app (only when running via Compose)
#
# Prod stack (docker-compose.prod.yml):
HTTP_PORT=80 # Caddy HTTP (redirects to HTTPS)
HTTPS_PORT=443 # Caddy HTTPS
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ lib/kith/ # Domain layer (contexts + schemas)
storage/ # File storage abstraction (local disk / S3)
tasks/ # Personal tasks
vcard/ # vCard parser + serializer
workers/ # 16 Oban workers across 9 queues
workers/ # 16 Oban workers across 7 queues

lib/kith_web/ # Web layer
controllers/api/ # REST API controllers (bearer token auth, cursor pagination)
Expand Down Expand Up @@ -106,7 +106,7 @@ default queries. 30-day trash before permanent purge via `ContactPurgeWorker`.

### Oban background jobs
Workers live in `lib/kith/workers/`. Queues: default, mailers, reminders, exports,
imports, immich, purge, photo_sync, api_supplement. Four cron jobs run nightly/weekly.
imports, immich, purge. Four cron jobs run nightly/weekly.
Tests use `Oban.Testing` — Oban is disabled in test env.

### REST API conventions
Expand Down
14 changes: 11 additions & 3 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ import Config
# Register .vcf (vCard) MIME type for LiveView uploads
config :mime, :types, %{"text/vcard" => ["vcf"], "application/json" => ["json"]}

# Default CLDR backend — required so ex_cldr_territories can resolve
# locale-aware territory data without an explicit per-call backend argument.
config :ex_cldr, default_backend: Kith.Cldr

# Outbound rate limit for Monica API calls. One below the documented
# default of 60 req/min leaves a one-call safety margin.
config :kith, :monica_rate_limit, 55

config :kith, :scopes,
user: [
default: true,
Expand Down Expand Up @@ -40,8 +48,7 @@ config :kith, Oban,
exports: 2,
imports: 2,
immich: 3,
purge: 1,
photo_sync: 5
purge: 1
],
plugins: [
Oban.Plugins.Pruner,
Expand Down Expand Up @@ -118,7 +125,8 @@ config :logger, :default_formatter,
:attempt,
:max_attempts,
:state,
:source
:source,
:import_id
]

# Cloak encryption vault — key set per-environment
Expand Down
48 changes: 48 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,54 @@ if config_env() == :prod do
backend: {Hammer.Backend.Redis, [expiry_ms: 60_000 * 60, redis_url: redis_url]}
end

# Oban — only the worker container processes jobs in production.
# The web container can call `Oban.insert/1` to enqueue jobs, but
# runs no queues or plugins (no cron, no pruner) — so it never claims
# rows from `oban_jobs`. The worker container keeps the full config
# from `config.exs`.
#
# Dev (`config_env() == :dev`) is unaffected: this block only runs in
# `:prod`. Test env is pinned to `testing: :manual` in `config/test.exs`.
case System.get_env("KITH_MODE", "web") do
"worker" ->
:ok

_web ->
config :kith, Oban, queues: false, plugins: false
end

# libcluster — connect this BEAM node to its peer(s) so Phoenix.PubSub
# broadcasts span containers (web ↔ worker). Configure via
# `KITH_CLUSTER_HOSTS` env var: comma-separated long node names, e.g.
# `kith@app,kith@worker`. Leave unset to disable clustering (single-node).
#
# Each container must also set `RELEASE_DISTRIBUTION=name` and
# `RELEASE_NODE=kith@<hostname>` so its actual node name matches the
# name listed in `KITH_CLUSTER_HOSTS`. `RELEASE_COOKIE` must be shared.
cluster_hosts =
case System.get_env("KITH_CLUSTER_HOSTS") do
nil ->
[]

"" ->
[]

str ->
str
|> String.split(",", trim: true)
|> Enum.map(&(&1 |> String.trim() |> String.to_atom()))
end

if cluster_hosts != [] do
config :libcluster,
topologies: [
kith: [
strategy: Cluster.Strategy.Epmd,
config: [hosts: cluster_hosts]
]
]
end

# Sentry error tracking (optional — only when SENTRY_DSN is set)
if sentry_dsn = System.get_env("SENTRY_DSN") do
config :sentry,
Expand Down
8 changes: 8 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ config :kith, KithWeb.Endpoint,
# Disable Oban in tests (use Oban.Testing)
config :kith, Oban, testing: :manual

# Use the production libphonenumber metadata in tests so test-only validation
# rules (NANP "555" prefixes, etc.) don't diverge from real behavior.
config :ex_phone_number, metadata_file: Path.join("resources", "PhoneNumberMetadata.xml")

# Effectively unthrottled in tests — throttle logic is exercised in
# isolation in rate_limiter_test.exs, not via the full crawl integration.
config :kith, :monica_rate_limit, 1_000_000

# Disable PromEx in tests (its Ecto poller conflicts with sandbox ownership)
config :kith, Kith.PromEx, disabled: true

Expand Down
17 changes: 14 additions & 3 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Kith Development Docker Compose
#
# Host ports are configurable via a `.env` file in the project root (auto-loaded
# by Compose). All variables have defaults — `.env` is only needed to override.
# See `.env.example` ("Docker Host Ports" section) for the full list.
#
# Usage:
# docker compose -f docker-compose.dev.yml up -d # infra only
# docker compose -f docker-compose.dev.yml up -d postgres mailpit # subset
# docker compose -f docker-compose.dev.yml --profile app up -d # also run app

services:
postgres:
image: postgres:15-alpine
Expand All @@ -16,8 +27,8 @@ services:
mailpit:
image: axllent/mailpit:latest
ports:
- "1025:1025"
- "8025:8025"
- "${MAILPIT_SMTP_PORT:-1025}:1025"
- "${MAILPIT_WEB_PORT:-8025}:8025"

minio:
image: minio/minio:latest
Expand Down Expand Up @@ -52,7 +63,7 @@ services:
context: .
dockerfile: Dockerfile.dev
ports:
- "4000:4000"
- "${APP_PORT:-4000}:4000"
environment:
DB_HOST: postgres
DB_PORT: "5432"
Expand Down
18 changes: 16 additions & 2 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ services:
app:
image: kith:latest
command: ["start"]
hostname: app
depends_on:
migrate:
condition: service_completed_successfully
Expand All @@ -72,6 +73,12 @@ services:
tmpfs:
- /tmp:size=64M
environment:
# ── BEAM distribution / clustering (libcluster Epmd strategy) ──
RELEASE_COOKIE: ${RELEASE_COOKIE}
RELEASE_DISTRIBUTION: sname
RELEASE_NODE: kith@app
KITH_CLUSTER_HOSTS: kith@app,kith@worker
# ── existing env vars unchanged ──
DATABASE_URL: ${DATABASE_URL}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
KITH_HOSTNAME: ${KITH_HOSTNAME:-localhost}
Expand Down Expand Up @@ -135,6 +142,7 @@ services:
worker:
image: kith:latest
command: ["start"]
hostname: worker
security_opt:
- no-new-privileges:true
cap_drop:
Expand All @@ -148,6 +156,12 @@ services:
migrate:
condition: service_completed_successfully
environment:
# ── BEAM distribution / clustering (libcluster Epmd strategy) ──
RELEASE_COOKIE: ${RELEASE_COOKIE}
RELEASE_DISTRIBUTION: sname
RELEASE_NODE: kith@worker
KITH_CLUSTER_HOSTS: kith@app,kith@worker
# ── existing env vars unchanged ──
DATABASE_URL: ${DATABASE_URL}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
KITH_HOSTNAME: ${KITH_HOSTNAME:-localhost}
Expand Down Expand Up @@ -202,8 +216,8 @@ services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
Expand Down
Loading
Loading