diff --git a/.gitignore b/.gitignore
index 0aa9dfff..f43a4040 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,4 @@ SECURITY_AUDIT_REPORT.md
*.exe
docs/
+.output
diff --git a/api/.env.docker b/api/.env.docker
index cf8f8cda..1d1e16ee 100644
--- a/api/.env.docker
+++ b/api/.env.docker
@@ -55,10 +55,17 @@ REDIS_URL=redis://@redis:6379
# Google Cloud Storage bucket for MMS attachments. Leave empty to use in-memory storage.
GCS_BUCKET_NAME=
-# [optional] If you would like to use uptrace.dev for distributed tracing, you can set the DSN here.
-# This is optional and you can leave it empty if you don't want to use uptrace
+# [deprecated] Uptrace DSN - kept for rollback. Use AXIOM_TOKEN/AXIOM_DATASET instead.
UPTRACE_DSN=
+# Axiom observability configuration
+# API token for Axiom (required for logging, traces, and metrics in production)
+AXIOM_TOKEN=
+# Dataset for logs and traces (e.g. "events")
+AXIOM_DATASET_EVENTS=
+# Dataset for metrics (e.g. "metrics")
+AXIOM_DATASET_METRICS=
+
# [optional] Websocket configuration for https://pusher.com if you will like to frontend to update in real time
PUSHER_APP_ID=
diff --git a/api/go.mod b/api/go.mod
index 1fe7dca1..657a3f86 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -12,6 +12,7 @@ require (
github.com/NdoleStudio/lemonsqueezy-go v1.3.1
github.com/NdoleStudio/plunk-go v0.0.2
github.com/avast/retry-go/v5 v5.0.0
+ github.com/axiomhq/axiom-go v0.32.0
github.com/carlmjohnson/requests v0.25.1
github.com/cloudevents/sdk-go/v2 v2.16.2
github.com/cockroachdb/cockroach-go/v2 v2.4.3
@@ -48,11 +49,13 @@ require (
github.com/xuri/excelize/v2 v2.10.1
go.mongodb.org/mongo-driver/v2 v2.6.0
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/v2/mongo/otelmongo v0.0.0-20260513205827-ba143fc95a5e
- go.opentelemetry.io/otel v1.43.0
- go.opentelemetry.io/otel/metric v1.43.0
- go.opentelemetry.io/otel/sdk v1.43.0
- go.opentelemetry.io/otel/sdk/metric v1.43.0
- go.opentelemetry.io/otel/trace v1.43.0
+ go.opentelemetry.io/otel v1.44.0
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
+ go.opentelemetry.io/otel/metric v1.44.0
+ go.opentelemetry.io/otel/sdk v1.44.0
+ go.opentelemetry.io/otel/sdk/metric v1.44.0
+ go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/sync v0.20.0
google.golang.org/api v0.277.0
google.golang.org/protobuf v1.36.11
@@ -95,6 +98,8 @@ require (
github.com/PuerkitoBio/goquery v1.12.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
+ github.com/buger/jsonparser v1.1.2 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
@@ -122,6 +127,7 @@ require (
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/go-querystring v1.2.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
@@ -177,9 +183,7 @@ require (
go.opentelemetry.io/contrib/instrumentation/runtime v0.68.0 // indirect
go.opentelemetry.io/contrib/processors/minsev v0.16.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect
go.opentelemetry.io/otel/log v0.19.0 // indirect
go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect
@@ -190,17 +194,17 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.35.0 // indirect
- golang.org/x/net v0.53.0 // indirect
+ golang.org/x/net v0.55.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
- golang.org/x/sys v0.44.0 // indirect
+ golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 // indirect
- google.golang.org/grpc v1.81.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
+ google.golang.org/grpc v1.81.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/clickhouse v0.7.0 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
diff --git a/api/go.sum b/api/go.sum
index fa1163a2..3495f841 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -68,12 +68,18 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/avast/retry-go/v5 v5.0.0 h1:kf1Qc2UsTZ4qq8elDymqfbISvkyMuhgRxuJqX2NHP7k=
github.com/avast/retry-go/v5 v5.0.0/go.mod h1://d+usmKWio1agtZfS1H/ltTqwtIfBnRq9zEwjc3eH8=
+github.com/axiomhq/axiom-go v0.32.0 h1:aRpbqUAn01hY8aJXQftvWHyXfnrNB2KzN5ZquBWvFcE=
+github.com/axiomhq/axiom-go v0.32.0/go.mod h1:3Gmr5M4tINm7Ti00GVfzAduO92Uhd0pghr4ZehIhFxc=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
+github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
+github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/carlmjohnson/requests v0.25.1 h1:17zNRLecxtAjhtdEIV+F+wrYfe+AGZUjWJtpndcOUYA=
github.com/carlmjohnson/requests v0.25.1/go.mod h1:z3UEf8IE4sZxZ78spW6/tLdqBkfCu1Fn4RaYMnZ8SRM=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -164,8 +170,8 @@ github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJb
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA=
github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=
-github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
-github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
+github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
+github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -176,6 +182,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
+github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
@@ -318,6 +326,14 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/thedevsaddam/govalidator v1.9.10 h1:m3dLRbSZ5Hts3VUWYe+vxLMG+FdyQuWOjzTeQRiMCvU=
github.com/thedevsaddam/govalidator v1.9.10/go.mod h1:Ilx8u7cg5g3LXbSS943cx5kczyNuUn7LH/cK5MYuE90=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
+github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
+github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/uptrace/uptrace-go v1.43.0 h1:5QuCdyFJdWUEXx6Fr6sYfezdgO6n6lnkOvUTLlyQO7U=
@@ -371,36 +387,38 @@ go.opentelemetry.io/contrib/processors/minsev v0.16.0 h1:bjTZkvAKnG1mqWgCjU7RkOk
go.opentelemetry.io/contrib/processors/minsev v0.16.0/go.mod h1:R2mmaDsqsWb+Y0mQkPifiCwifdotrG4fFoD4z0tim+g=
go.opentelemetry.io/contrib/propagators/b3 v1.19.0 h1:ulz44cpm6V5oAeg5Aw9HyqGFMS6XM7untlMEhD7YzzA=
go.opentelemetry.io/contrib/propagators/b3 v1.19.0/go.mod h1:OzCmE2IVS+asTI+odXQstRGVfXQ4bXv9nMBRK0nNyqQ=
-go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
-go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
+go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
+go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
-go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0 h1:RuynHbfU8JUEw7DyONgkVYg2SVtsoF28y0LGIr69jgA=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.44.0/go.mod h1:qZF+/lBs71APw8mlnEZcqZHMzqrYrsFiJOv83lX1OGo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU=
go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4=
go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk=
-go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
-go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
+go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
+go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
+go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
+go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/oteltest v1.0.0-RC3 h1:MjaeegZTaX0Bv9uB9CrdVjOFM/8slRjReoWoV9xDCpY=
go.opentelemetry.io/otel/oteltest v1.0.0-RC3/go.mod h1:xpzajI9JBRr7gX63nO6kAmImmYIAtuQblZ36Z+LfCjE=
-go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
-go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
+go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
+go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko=
go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk=
go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk=
-go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
-go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
-go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
-go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
+go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
+go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
+go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
+go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
@@ -441,8 +459,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
-golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
-golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -466,8 +484,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
-golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -509,12 +527,12 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348 h1:JjVGDZYWkJWZcxveJGzfkXC5myDVWAd4dZdgbzrDUv8=
google.golang.org/genproto v0.0.0-20260504160031-60b97b32f348/go.mod h1:95PqD4xM+AdOcBGsmgfaofXsiA37uXDtDufVbntT3TU=
-google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348 h1:U8orV30l6KpDsi9dxU0CoJZGbjS8EEpw+6ba+XwGPQA=
-google.golang.org/genproto/googleapis/api v0.0.0-20260504160031-60b97b32f348/go.mod h1:Yzdzr5OOZFgSsEV2D/Xi9NL3bszpXFAg0hFJiRohcD8=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348 h1:pfIbyB44sWzHiCpRqIen67ZQnVXSfIxWrqUMk1qwODE=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260504160031-60b97b32f348/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
-google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
+google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
+google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
+google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
diff --git a/api/pkg/di/container.go b/api/pkg/di/container.go
index df2e5d6f..45cad0cc 100644
--- a/api/pkg/di/container.go
+++ b/api/pkg/di/container.go
@@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"fmt"
+ "log"
"net/http"
"os"
"strconv"
@@ -44,9 +45,13 @@ import (
cloudtasks "cloud.google.com/go/cloudtasks/apiv2"
"go.opentelemetry.io/otel"
+ "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
+ "go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
+ axiomzerolog "github.com/axiomhq/axiom-go/adapters/zerolog"
"github.com/hirosassa/zerodriver"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/sdk/trace"
@@ -1801,7 +1806,7 @@ func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entiti
// InitializeTraceProvider initializes the open telemetry trace provider
func (container *Container) InitializeTraceProvider() func() {
- return container.initializeUptraceProvider(container.version, container.projectID)
+ return container.initializeAxiomTraceProvider(container.version, container.projectID)
}
func (container *Container) initializeGoogleTraceProvider(version string, namespace string) func() {
@@ -1840,6 +1845,63 @@ func (container *Container) initializeGoogleTraceProvider(version string, namesp
}
}
+func (container *Container) initializeAxiomTraceProvider(version string, namespace string) func() {
+ container.logger.Debug("initializing axiom trace provider")
+
+ traceHeaders := map[string]string{
+ "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"),
+ "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET_EVENTS"),
+ }
+
+ traceExporter, err := otlptracehttp.New(context.Background(),
+ otlptracehttp.WithEndpoint("us-east-1.aws.edge.axiom.co"),
+ otlptracehttp.WithHeaders(traceHeaders),
+ )
+ if err != nil {
+ container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP trace exporter"))
+ }
+
+ tp := trace.NewTracerProvider(
+ trace.WithBatcher(traceExporter),
+ trace.WithSampler(trace.AlwaysSample()),
+ trace.WithResource(container.OtelResources(version, namespace)),
+ )
+ otel.SetTracerProvider(tp)
+
+ otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
+ propagation.TraceContext{},
+ propagation.Baggage{},
+ ))
+
+ metricHeaders := map[string]string{
+ "Authorization": "Bearer " + os.Getenv("AXIOM_TOKEN"),
+ "X-Axiom-Dataset": os.Getenv("AXIOM_DATASET_METRICS"),
+ }
+
+ metricExporter, err := otlpmetrichttp.New(context.Background(),
+ otlpmetrichttp.WithEndpoint("us-east-1.aws.edge.axiom.co"),
+ otlpmetrichttp.WithHeaders(metricHeaders),
+ )
+ if err != nil {
+ container.logger.Fatal(stacktrace.Propagate(err, "cannot create axiom OTLP metric exporter"))
+ }
+
+ meterProvider := metric.NewMeterProvider(
+ metric.WithReader(metric.NewPeriodicReader(metricExporter)),
+ metric.WithResource(container.OtelResources(version, namespace)),
+ )
+ otel.SetMeterProvider(meterProvider)
+
+ return func() {
+ if err := tp.Shutdown(context.Background()); err != nil {
+ container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom trace provider"))
+ }
+ if err := meterProvider.Shutdown(context.Background()); err != nil {
+ container.logger.Error(stacktrace.Propagate(err, "cannot shutdown axiom meter provider"))
+ }
+ }
+}
+
func (container *Container) initializeUptraceProvider(version string, namespace string) (flush func()) {
container.logger.Debug("initializing uptrace provider")
// Configure OpenTelemetry with sensible defaults.
@@ -1861,8 +1923,8 @@ func (container *Container) initializeUptraceProvider(version string, namespace
func logger(skipFrameCount int) telemetry.Logger {
fields := map[string]string{
- "pid": strconv.Itoa(os.Getpid()),
- "hostname": hostName(),
+ "hostname": hostName(),
+ string(semconv.DeploymentEnvironmentKey): os.Getenv("ENV"),
}
return telemetry.NewZerologLogger(
@@ -1877,7 +1939,7 @@ func logDriver(skipFrameCount int) *zerodriver.Logger {
if isLocal() {
return consoleLogger(skipFrameCount)
}
- return jsonLogger(skipFrameCount)
+ return axiomLogger(skipFrameCount)
}
func jsonLogger(skipFrameCount int) *zerodriver.Logger {
@@ -1906,6 +1968,19 @@ func jsonLogger(skipFrameCount int) *zerodriver.Logger {
return &zerodriver.Logger{Logger: &zl}
}
+func axiomLogger(skipFrameCount int) *zerodriver.Logger {
+ axiomWriter, err := axiomzerolog.New(
+ axiomzerolog.SetLevels([]zerolog.Level{zerolog.TraceLevel, zerolog.DebugLevel, zerolog.InfoLevel, zerolog.WarnLevel, zerolog.ErrorLevel, zerolog.PanicLevel, zerolog.FatalLevel, zerolog.NoLevel}),
+ axiomzerolog.SetDataset(os.Getenv("AXIOM_DATASET_EVENTS")),
+ )
+ if err != nil {
+ log.Fatal(stacktrace.Propagate(err, "cannot create axiom zerolog writer"))
+ }
+
+ zl := zerolog.New(axiomWriter).With().Timestamp().CallerWithSkipFrameCount(skipFrameCount).Logger()
+ return &zerodriver.Logger{Logger: &zl}
+}
+
func hostName() string {
h, err := os.Hostname()
if err != nil {
diff --git a/api/pkg/telemetry/gorm_logger.go b/api/pkg/telemetry/gorm_logger.go
index 10ed03c7..3cf02162 100644
--- a/api/pkg/telemetry/gorm_logger.go
+++ b/api/pkg/telemetry/gorm_logger.go
@@ -27,15 +27,15 @@ func (gorm *gormLogger) LogMode(_ logger.LogLevel) logger.Interface {
return gorm
}
-func (gorm *gormLogger) Info(ctx context.Context, s string, i ...interface{}) {
+func (gorm *gormLogger) Info(ctx context.Context, s string, i ...any) {
gorm.logger.WithSpan(gorm.tracer.Span(ctx).SpanContext()).Info(fmt.Sprintf(s, i...))
}
-func (gorm *gormLogger) Warn(ctx context.Context, s string, i ...interface{}) {
+func (gorm *gormLogger) Warn(ctx context.Context, s string, i ...any) {
gorm.logger.WithSpan(gorm.tracer.Span(ctx).SpanContext()).Warn(fmt.Errorf(s, i...))
}
-func (gorm *gormLogger) Error(ctx context.Context, s string, i ...interface{}) {
+func (gorm *gormLogger) Error(ctx context.Context, s string, i ...any) {
gorm.logger.WithSpan(gorm.tracer.Span(ctx).SpanContext()).Error(fmt.Errorf(s, i...))
}
diff --git a/api/pkg/telemetry/zerolog_logger.go b/api/pkg/telemetry/zerolog_logger.go
index c6cd8b68..c5230bc0 100644
--- a/api/pkg/telemetry/zerolog_logger.go
+++ b/api/pkg/telemetry/zerolog_logger.go
@@ -5,7 +5,7 @@ import (
"github.com/hirosassa/zerodriver"
"github.com/rs/zerolog"
- semconv "go.opentelemetry.io/otel/semconv/v1.10.0"
+ semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
"go.opentelemetry.io/otel/trace"
)
@@ -34,7 +34,7 @@ func NewZerologLogger(projectID string, fields map[string]string, driver *zerodr
func (logger *zerologLogger) WithService(service string) Logger {
return NewZerologLogger(
logger.projectID,
- logger.addField(string(semconv.ServiceNameKey), service),
+ logger.addField(string(semconv.ServiceNamespaceKey), service),
logger.zerolog,
logger.spanContext,
)
@@ -96,7 +96,9 @@ func (logger *zerologLogger) WithSpan(spanContext trace.SpanContext) Logger {
func (logger *zerologLogger) decorateEvent(event *zerodriver.Event) *zerolog.Event {
if logger.spanContext != nil {
- event.TraceContext(logger.spanContext.TraceID().String(), logger.spanContext.SpanID().String(), logger.spanContext.IsSampled(), logger.projectID)
+ event.Str("trace_id", logger.spanContext.TraceID().String())
+ event.Str("span_id", logger.spanContext.SpanID().String())
+ event.Bool("trace_sampled", logger.spanContext.IsSampled())
}
for key, value := range logger.fields {
event.Str(key, value)
diff --git a/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md b/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md
new file mode 100644
index 00000000..bc982b57
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-26-nuxt4-vuetify4-migration-design.md
@@ -0,0 +1,385 @@
+# httpSMS Frontend Migration: Nuxt 2 + Vuetify 2 → Nuxt 4 + Vuetify 4
+
+## Summary
+
+Migrate the `web/` frontend from Nuxt 2 (Vue 2, Vuetify 2, Vuex, class-based components) to Nuxt 4 (Vue 3, Vuetify 4, Pinia, `
+```
+
+**After (Vue 3):**
+```vue
+
+```
+
+#### Vuetify Breakpoints: `$vuetify.breakpoint` → `useDisplay()`
+
+**Before:**
+```vue
+
+```
+
+**After:**
+```vue
+
+
+
+
+```
+
+#### State: Vuex → Pinia
+
+**Before:**
+```ts
+this.$store.dispatch('loadPhones', true)
+this.$store.getters.getAuthUser
+```
+
+**After:**
+```ts
+const phonesStore = usePhonesStore()
+await phonesStore.loadPhones(true)
+phonesStore.authUser
+```
+
+#### Firebase: `this.$fire.auth` → VueFire composables
+
+**Before:**
+```ts
+await this.$fire.auth.currentUser?.getIdToken()
+```
+
+**After:**
+```ts
+import { useCurrentUser } from 'vuefire'
+const user = useCurrentUser()
+const token = await user.value?.getIdToken()
+```
+
+#### Dynamic Routes: `_id` → `[id]`
+
+- `pages/threads/_id/index.vue` → `pages/threads/[id]/index.vue`
+- `pages/heartbeats/_id.vue` → `pages/heartbeats/[id].vue`
+
+### Vuetify 4 Breaking Changes to Address
+
+Using the Vuetify MCP for each component, the key changes are:
+
+1. **CSS Layers** — mandatory in v4; adjust any custom style overrides
+2. **Theme** — default is now "system" (we want dark, configure explicitly)
+3. **Typography** — MD2 → MD3 type scale (text-h1 → text-display-large, etc.)
+4. **Breakpoints** — reduced default sizes (restore v3 values via config)
+5. **Elevation** — 25 levels → 6 levels (MD3)
+6. **VBtn** — no default uppercase, grid → flex layout
+7. **VSnackbar** — removed multi-line prop
+8. **VSelect** — "item" slot → "internalItem"
+9. **Grid** — v-row/v-col overhauled
+10. **CSS Reset** — mostly removed, add selective resets
+
+### Vuetify MCP Usage Per Component
+
+For EVERY component/page being migrated, the implementation must:
+1. Call `vuetify-mcp-get_component_api_by_version` for each Vuetify component used
+2. Call `vuetify-mcp-get_v4_breaking_changes` filtered by relevant category
+3. Apply the correct v4 API (props, slots, events) based on MCP output
+4. Verify no deprecated props/events remain
+
+### Pinia Store Design
+
+Split the monolithic Vuex store into domain stores:
+
+| Store | Responsibility |
+|-------|---------------|
+| `auth.ts` | Firebase auth state, user profile, onAuthStateChanged |
+| `messages.ts` | Messages CRUD, search |
+| `threads.ts` | Message threads, current thread |
+| `phones.ts` | Phone list, heartbeats, polling |
+| `billing.ts` | Usage, subscription, payments |
+| `notifications.ts` | Toast/snackbar queue |
+| `app.ts` | App metadata, polling state, runtime config |
+
+### Plugin Migrations
+
+| Old Plugin | New Approach |
+|-----------|-------------|
+| `plugins/axios.ts` | `composables/useApi.ts` using `$fetch` with auth header |
+| `plugins/filters.ts` | `utils/filters.ts` (import explicitly or app.config globalProperties) |
+| `plugins/vue-glow.ts` | `plugins/vue-glow.client.ts` (client-only plugin) |
+| `plugins/chart.ts` | `plugins/chart.client.ts` (client-only plugin) |
+| `plugins/errors.ts` | `utils/errors.ts` |
+| `plugins/bag.ts` | `utils/bag.ts` |
+| `plugins/capitalize.ts` | `utils/capitalize.ts` |
+| `plugins/veutify.ts` | `plugins/vuetify.ts` (createVuetify setup) |
+
+## Migration Order (Tasks)
+
+### Phase 1: Scaffold & Configuration
+1. Initialize fresh Nuxt 4 project in `web/` (backup old code)
+2. Install dependencies (vuetify, pinia, nuxt-vuefire, sass, @mdi/js, pusher-js, etc.)
+3. Configure `nuxt.config.ts` (SSG, runtime config, modules)
+4. Set up Vuetify plugin with dark theme, restored breakpoints, MDI SVG icons
+5. Set up nuxt-vuefire with Firebase config
+6. Configure TypeScript strictly
+
+### Phase 2: Foundation
+7. Port `shared/types/` (API models — mostly copy)
+8. Port `utils/` (errors, filters, bag, capitalize)
+9. Create `composables/useApi.ts` (replace Axios plugin)
+10. Create `composables/useAuth.ts` (Firebase auth helpers)
+
+### Phase 3: State Management
+11. Create Pinia store: `stores/auth.ts`
+12. Create Pinia store: `stores/notifications.ts`
+13. Create Pinia store: `stores/app.ts`
+14. Create Pinia store: `stores/phones.ts`
+15. Create Pinia store: `stores/messages.ts`
+16. Create Pinia store: `stores/threads.ts`
+17. Create Pinia store: `stores/billing.ts`
+
+### Phase 4: Layouts & Middleware
+18. Port `middleware/auth.ts`
+19. Port `middleware/guest.ts`
+20. Port `layouts/default.vue` (with Vuetify MCP)
+21. Port `layouts/website.vue` (with Vuetify MCP)
+22. Port `layouts/error.vue` (with Vuetify MCP)
+23. Create `app.vue`
+
+### Phase 5: Components (use Vuetify MCP for each)
+24. Port `components/Toast.vue`
+25. Port `components/LoadingDashboard.vue`
+26. Port `components/LoadingButton.vue`
+27. Port `components/BackButton.vue`
+28. Port `components/CopyButton.vue`
+29. Port `components/FixedHeader.vue`
+30. Port `components/BlogAuthorBio.vue`
+31. Port `components/BlogInfo.vue`
+32. Port `components/NuxtLogo.vue`
+33. Port `components/FirebaseAuth.vue`
+34. Port `components/MessageThread.vue`
+35. Port `components/MessageThreadHeader.vue`
+
+### Phase 6: Pages (use Vuetify MCP for each)
+36. Port `pages/index.vue` (homepage)
+37. Port `pages/login.vue`
+38. Port `pages/threads/index.vue`
+39. Port `pages/threads/[id]/index.vue`
+40. Port `pages/messages/index.vue`
+41. Port `pages/search-messages/index.vue`
+42. Port `pages/bulk-messages/index.vue`
+43. Port `pages/settings/index.vue`
+44. Port `pages/billing/index.vue`
+45. Port `pages/heartbeats/[id].vue`
+46. Port `pages/phone-api-keys/index.vue`
+47. Port `pages/privacy-policy/index.vue`
+48. Port `pages/terms-and-conditions/index.vue`
+49. Port `pages/blog/index.vue`
+50. Port `pages/blog/how-to-send-sms-messages-from-excel.vue`
+51. Port `pages/blog/grant-send-and-read-sms-permissions-on-android.vue`
+52. Port `pages/blog/forward-incoming-sms-from-phone-to-webhook.vue`
+53. Port `pages/blog/end-to-end-encryption-to-sms-messages.vue`
+54. Port `pages/blog/send-bulk-sms-from-csv-file-with-no-code.vue`
+55. Port `pages/blog/send-sms-from-android-phone-with-python.vue`
+56. Port `pages/blog/send-sms-when-new-row-is-added-to-google-sheets-using-zapier.vue`
+
+### Phase 7: Final Setup
+57. Port static assets (`public/`)
+58. Port environment files (`.env`, `.env.production`)
+59. Update Dockerfile and nginx.conf
+60. Update sitemap configuration
+61. Configure highlight.js (nuxt-highlightjs or manual)
+
+### Phase 8: Verification (EVERY component and page)
+62. Verify `app.vue` renders
+63. Verify `layouts/default.vue` renders correctly
+64. Verify `layouts/website.vue` renders correctly
+65. Verify `layouts/error.vue` renders correctly
+66. Verify `components/Toast.vue` renders correctly
+67. Verify `components/LoadingDashboard.vue` renders correctly
+68. Verify `components/LoadingButton.vue` renders correctly
+69. Verify `components/BackButton.vue` renders correctly
+70. Verify `components/CopyButton.vue` renders correctly
+71. Verify `components/FixedHeader.vue` renders correctly
+72. Verify `components/BlogAuthorBio.vue` renders correctly
+73. Verify `components/BlogInfo.vue` renders correctly
+74. Verify `components/NuxtLogo.vue` renders correctly
+75. Verify `components/FirebaseAuth.vue` renders correctly
+76. Verify `components/MessageThread.vue` renders correctly
+77. Verify `components/MessageThreadHeader.vue` renders correctly
+78. Verify `pages/index.vue` renders correctly
+79. Verify `pages/login.vue` renders correctly
+80. Verify `pages/threads/index.vue` renders correctly
+81. Verify `pages/threads/[id]/index.vue` renders correctly
+82. Verify `pages/messages/index.vue` renders correctly
+83. Verify `pages/search-messages/index.vue` renders correctly
+84. Verify `pages/bulk-messages/index.vue` renders correctly
+85. Verify `pages/settings/index.vue` renders correctly
+86. Verify `pages/billing/index.vue` renders correctly
+87. Verify `pages/heartbeats/[id].vue` renders correctly
+88. Verify `pages/phone-api-keys/index.vue` renders correctly
+89. Verify `pages/privacy-policy/index.vue` renders correctly
+90. Verify `pages/terms-and-conditions/index.vue` renders correctly
+91. Verify `pages/blog/index.vue` renders correctly
+92. Verify all blog subpages render correctly
+93. Run `pnpm build` (static generation) successfully
+94. Verify no TypeScript errors (`pnpm typecheck`)
+95. Verify lint passes (`pnpm lint`)
+
+## Verification Strategy
+
+Each verification task in Phase 8 means:
+1. Start the dev server (`pnpm dev`)
+2. Navigate to the page/route in question
+3. Confirm no console errors, no hydration mismatches
+4. Confirm visual layout matches intent (Vuetify components render, dark theme active, responsive breakpoints work)
+5. For interactive components (forms, modals, auth), confirm basic interactions work
+
+The build verification (`pnpm build`) confirms all pages can be statically generated without errors.
+
+## Risk Mitigations
+
+- **Backup old code**: Keep old `web/` contents in a branch before starting
+- **Incremental porting**: Each file is ported and verified before moving to the next
+- **Vuetify MCP**: Use for every Vuetify component to catch breaking changes
+- **Restored breakpoints**: Keep v2/v3 breakpoint values to minimize layout drift
+- **CSS Reset compatibility**: Add selective reset CSS to maintain existing spacing behavior