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