diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod index 72ee3fd4..fac26666 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.mod @@ -3,6 +3,7 @@ module github.com/carterperez-dev/monitor-the-situation/backend go 1.25.0 require ( + github.com/coder/websocket v1.8.14 github.com/go-chi/chi/v5 v5.2.3 github.com/go-playground/validator/v10 v10.23.0 github.com/go-redis/redis_rate/v10 v10.0.1 @@ -17,6 +18,7 @@ require ( github.com/redis/go-redis/v9 v9.7.3 github.com/sony/gobreaker/v2 v2.4.0 github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 go.opentelemetry.io/otel v1.41.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 go.opentelemetry.io/otel/sdk v1.35.0 @@ -27,15 +29,29 @@ require ( ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect @@ -44,6 +60,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect @@ -53,12 +70,34 @@ require ( github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect github.com/lestrrat-go/option v1.0.1 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mdelapenya/tlscert v0.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/segmentio/asm v1.2.1 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/testcontainers/testcontainers-go v0.42.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/valyala/fastjson v1.6.4 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/proto/otlp v1.4.0 // indirect diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum index 7d9a840f..6625ecd7 100644 --- a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/go.sum @@ -1,5 +1,13 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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= @@ -8,6 +16,20 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -15,6 +37,16 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvw github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -26,6 +58,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -44,6 +78,7 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -60,6 +95,8 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= @@ -94,33 +131,81 @@ github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLO github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg= github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/redis v0.42.0 h1:id/6LH8ZeDrtAUVSuNvZUAJ1kVpb82y1pr9yweAWsRg= +github.com/testcontainers/testcontainers-go/modules/redis v0.42.0/go.mod h1:uF0jI8FITagQpBNOgweGBmPf6rP4K0SeL1XFPbsZSSY= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= @@ -131,6 +216,8 @@ go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCn go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= @@ -145,12 +232,18 @@ golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= @@ -165,3 +258,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/heartbeat/collector.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/heartbeat/collector.go new file mode 100644 index 00000000..4c9c0333 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/heartbeat/collector.go @@ -0,0 +1,63 @@ +// ©AngelaMos | 2026 +// collector.go + +package heartbeat + +import ( + "context" + "time" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" +) + +const defaultInterval = 5 * time.Second + +type Emitter interface { + Emit(ev events.Event) +} + +type Config struct { + Interval time.Duration + Emitter Emitter +} + +type Collector struct { + cfg Config +} + +func New(cfg Config) *Collector { + if cfg.Interval <= 0 { + cfg.Interval = defaultInterval + } + return &Collector{cfg: cfg} +} + +func (c *Collector) Name() string { return "heartbeat" } + +func (c *Collector) Run(ctx context.Context) error { + ticker := time.NewTicker(c.cfg.Interval) + defer ticker.Stop() + + c.emitNow() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + c.emitNow() + } + } +} + +func (c *Collector) emitNow() { + now := time.Now().UTC() + c.cfg.Emitter.Emit(events.Event{ + Topic: events.TopicHeartbeat, + Timestamp: now, + Source: c.Name(), + Payload: map[string]string{ + "ts": now.Format(time.RFC3339Nano), + }, + }) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/heartbeat/collector_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/heartbeat/collector_test.go new file mode 100644 index 00000000..5be60a8b --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/collectors/heartbeat/collector_test.go @@ -0,0 +1,72 @@ +// ©AngelaMos | 2026 +// collector_test.go + +package heartbeat_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/collectors/heartbeat" + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" +) + +type collectingEmitter struct { + mu sync.Mutex + events []events.Event +} + +func (c *collectingEmitter) Emit(ev events.Event) { + c.mu.Lock() + defer c.mu.Unlock() + c.events = append(c.events, ev) +} + +func (c *collectingEmitter) Count() int { + c.mu.Lock() + defer c.mu.Unlock() + return len(c.events) +} + +func (c *collectingEmitter) Last() events.Event { + c.mu.Lock() + defer c.mu.Unlock() + return c.events[len(c.events)-1] +} + +func TestCollector_EmitsAtInterval(t *testing.T) { + emitter := &collectingEmitter{} + c := heartbeat.New(heartbeat.Config{ + Interval: 50 * time.Millisecond, + Emitter: emitter, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := c.Run(ctx) + require.ErrorIs(t, err, context.DeadlineExceeded) + + require.GreaterOrEqual(t, emitter.Count(), 3) + last := emitter.Last() + require.Equal(t, events.TopicHeartbeat, last.Topic) + require.Equal(t, "heartbeat", last.Source) +} + +func TestCollector_EmitsImmediatelyOnStart(t *testing.T) { + emitter := &collectingEmitter{} + c := heartbeat.New(heartbeat.Config{ + Interval: time.Hour, + Emitter: emitter, + }) + + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + _ = c.Run(ctx) + require.Equal(t, 1, emitter.Count(), "should emit one tick immediately on start") +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/handler.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/handler.go new file mode 100644 index 00000000..9e43a403 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/handler.go @@ -0,0 +1,29 @@ +// ©AngelaMos | 2026 +// handler.go + +package snapshot + +import ( + "encoding/json" + "net/http" +) + +type Handler struct { + store *Store +} + +func NewHandler(store *Store) *Handler { return &Handler{store: store} } + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + all, err := h.store.GetAll(r.Context()) + if err != nil { + http.Error(w, "snapshot read failed", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + if err := json.NewEncoder(w).Encode(all); err != nil { + http.Error(w, "encode failed", http.StatusInternalServerError) + return + } +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/persister.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/persister.go new file mode 100644 index 00000000..bd694aef --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/persister.go @@ -0,0 +1,24 @@ +// ©AngelaMos | 2026 +// persister.go + +package snapshot + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" +) + +type StorePersister struct { + Store *Store +} + +func (s StorePersister) Save(ctx context.Context, ev events.Event) error { + raw, err := json.Marshal(ev.Payload) + if err != nil { + return fmt.Errorf("marshal payload: %w", err) + } + return s.Store.PutLatest(ctx, ev.Topic, raw) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/snapshot_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/snapshot_test.go new file mode 100644 index 00000000..c70e9f74 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/snapshot_test.go @@ -0,0 +1,75 @@ +// ©AngelaMos | 2026 +// snapshot_test.go + +package snapshot_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/require" + tcredis "github.com/testcontainers/testcontainers-go/modules/redis" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" + "github.com/carterperez-dev/monitor-the-situation/backend/internal/snapshot" +) + +func startRedis(t *testing.T) *redis.Client { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + c, err := tcredis.Run(ctx, "redis:7-alpine") + require.NoError(t, err) + t.Cleanup(func() { + _ = c.Terminate(context.Background()) + }) + + endpoint, err := c.Endpoint(ctx, "") + require.NoError(t, err) + rdb := redis.NewClient(&redis.Options{Addr: endpoint}) + t.Cleanup(func() { _ = rdb.Close() }) + + return rdb +} + +func TestStore_PutLatestAndGetAll(t *testing.T) { + rdb := startRedis(t) + store := snapshot.NewStore(rdb) + + ctx := context.Background() + require.NoError(t, store.PutLatest(ctx, events.TopicHeartbeat, json.RawMessage(`{"ts":"2026-05-01T00:00:00Z"}`))) + require.NoError(t, store.PutLatest(ctx, events.TopicCVENew, json.RawMessage(`{"id":"CVE-2026-9999"}`))) + + all, err := store.GetAll(ctx) + require.NoError(t, err) + require.Contains(t, all, string(events.TopicHeartbeat)) + require.Contains(t, all, string(events.TopicCVENew)) + + var hb map[string]string + require.NoError(t, json.Unmarshal(all[string(events.TopicHeartbeat)], &hb)) + require.Equal(t, "2026-05-01T00:00:00Z", hb["ts"]) +} + +func TestStorePersister_SavesEventPayload(t *testing.T) { + rdb := startRedis(t) + store := snapshot.NewStore(rdb) + persister := snapshot.StorePersister{Store: store} + + ctx := context.Background() + ev := events.Event{ + Topic: events.TopicHeartbeat, + Timestamp: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC), + Source: "heartbeat", + Payload: map[string]string{"ts": "2026-05-01T00:00:00Z"}, + } + require.NoError(t, persister.Save(ctx, ev)) + + all, err := store.GetAll(ctx) + require.NoError(t, err) + require.Contains(t, all, string(events.TopicHeartbeat)) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/store.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/store.go new file mode 100644 index 00000000..b4968f3c --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/snapshot/store.go @@ -0,0 +1,46 @@ +// ©AngelaMos | 2026 +// store.go + +package snapshot + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/redis/go-redis/v9" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" +) + +const keyPrefix = "state:" + +type Store struct { + rdb *redis.Client +} + +func NewStore(rdb *redis.Client) *Store { return &Store{rdb: rdb} } + +func (s *Store) PutLatest(ctx context.Context, topic events.Topic, payload json.RawMessage) error { + if err := s.rdb.Set(ctx, keyPrefix+string(topic), []byte(payload), 0).Err(); err != nil { + return fmt.Errorf("redis set %s: %w", topic, err) + } + return nil +} + +func (s *Store) GetAll(ctx context.Context) (map[string]json.RawMessage, error) { + keys, err := s.rdb.Keys(ctx, keyPrefix+"*").Result() + if err != nil { + return nil, err + } + out := make(map[string]json.RawMessage, len(keys)) + for _, k := range keys { + v, err := s.rdb.Get(ctx, k).Bytes() + if err != nil { + continue + } + topic := k[len(keyPrefix):] + out[topic] = json.RawMessage(v) + } + return out, nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/broadcaster.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/broadcaster.go new file mode 100644 index 00000000..24d99dbc --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/broadcaster.go @@ -0,0 +1,14 @@ +// ©AngelaMos | 2026 +// broadcaster.go + +package ws + +import "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" + +type HubBroadcaster struct { + Hub *Hub +} + +func (b HubBroadcaster) Broadcast(topic string, payload []byte) { + b.Hub.Broadcast(events.Topic(topic), payload) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/envelope.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/envelope.go new file mode 100644 index 00000000..0f4b4c65 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/envelope.go @@ -0,0 +1,34 @@ +// ©AngelaMos | 2026 +// envelope.go + +package ws + +import ( + "bytes" + "encoding/json" + "time" +) + +func EncodeEnvelope(channel string, payload []byte) ([]byte, error) { + var buf bytes.Buffer + buf.WriteString(`{"ch":`) + chRaw, err := json.Marshal(channel) + if err != nil { + return nil, err + } + buf.Write(chRaw) + buf.WriteString(`,"data":`) + if len(payload) == 0 { + buf.WriteString("null") + } else { + buf.Write(payload) + } + buf.WriteString(`,"ts":`) + tsRaw, err := json.Marshal(time.Now().UTC().Format(time.RFC3339Nano)) + if err != nil { + return nil, err + } + buf.Write(tsRaw) + buf.WriteByte('}') + return buf.Bytes(), nil +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/envelope_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/envelope_test.go new file mode 100644 index 00000000..675acb49 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/envelope_test.go @@ -0,0 +1,33 @@ +// ©AngelaMos | 2026 +// envelope_test.go + +package ws_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/ws" +) + +func TestEncodeEnvelope(t *testing.T) { + out, err := ws.EncodeEnvelope("heartbeat", []byte(`{"ts":"2026-05-01T00:00:00Z"}`)) + require.NoError(t, err) + + var got map[string]any + require.NoError(t, json.Unmarshal(out, &got)) + require.Equal(t, "heartbeat", got["ch"]) + require.Contains(t, got, "data") + require.Contains(t, got, "ts") +} + +func TestEncodeEnvelope_NilPayload(t *testing.T) { + out, err := ws.EncodeEnvelope("heartbeat", nil) + require.NoError(t, err) + + var got map[string]any + require.NoError(t, json.Unmarshal(out, &got)) + require.Nil(t, got["data"]) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/handler.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/handler.go new file mode 100644 index 00000000..257a870a --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/handler.go @@ -0,0 +1,49 @@ +// ©AngelaMos | 2026 +// handler.go + +package ws + +import ( + "net/http" + "strings" + + cdrws "github.com/coder/websocket" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" +) + +type Handler struct { + hub *Hub +} + +func NewHandler(hub *Hub) *Handler { return &Handler{hub: hub} } + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c, err := cdrws.Accept(w, r, &cdrws.AcceptOptions{ + InsecureSkipVerify: false, + }) + if err != nil { + return + } + + topics := parseTopics(r.URL.Query().Get("topics")) + _ = h.hub.Serve(r.Context(), c, topics) +} + +func parseTopics(raw string) []events.Topic { + if raw == "" { + return events.AllTopics() + } + parts := strings.Split(raw, ",") + out := make([]events.Topic, 0, len(parts)) + for _, p := range parts { + t := events.Topic(strings.TrimSpace(p)) + if t.IsValid() { + out = append(out, t) + } + } + if len(out) == 0 { + return events.AllTopics() + } + return out +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/hub.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/hub.go new file mode 100644 index 00000000..4c7c9440 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/hub.go @@ -0,0 +1,139 @@ +// ©AngelaMos | 2026 +// hub.go + +package ws + +import ( + "context" + "log/slog" + "sync" + "time" + + cdrws "github.com/coder/websocket" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" +) + +const ( + defaultSubscriberBuf = 16 + defaultPingInterval = 30 * time.Second + defaultPingTimeout = 10 * time.Second + defaultWriteTimeout = 5 * time.Second +) + +type HubConfig struct { + SubscriberBufferSize int + PingInterval time.Duration + PingTimeout time.Duration + WriteTimeout time.Duration + Logger *slog.Logger +} + +type Hub struct { + mu sync.Mutex + subs map[*subscriber]struct{} + bufSize int + pingInterval time.Duration + pingTimeout time.Duration + writeTimeout time.Duration + logger *slog.Logger +} + +func NewHub(cfg HubConfig) *Hub { + if cfg.SubscriberBufferSize <= 0 { + cfg.SubscriberBufferSize = defaultSubscriberBuf + } + if cfg.PingInterval <= 0 { + cfg.PingInterval = defaultPingInterval + } + if cfg.PingTimeout <= 0 { + cfg.PingTimeout = defaultPingTimeout + } + if cfg.WriteTimeout <= 0 { + cfg.WriteTimeout = defaultWriteTimeout + } + logger := cfg.Logger + if logger == nil { + logger = slog.Default() + } + return &Hub{ + subs: make(map[*subscriber]struct{}), + bufSize: cfg.SubscriberBufferSize, + pingInterval: cfg.PingInterval, + pingTimeout: cfg.PingTimeout, + writeTimeout: cfg.WriteTimeout, + logger: logger, + } +} + +func (h *Hub) Broadcast(topic events.Topic, payload []byte) { + env, err := EncodeEnvelope(string(topic), payload) + if err != nil { + h.logger.Error("encode envelope", "err", err, "topic", topic) + return + } + h.mu.Lock() + defer h.mu.Unlock() + for sub := range h.subs { + if _, ok := sub.topics[topic]; !ok { + continue + } + select { + case sub.msgs <- env: + default: + go sub.closeSlow() + } + } +} + +func (h *Hub) Serve(ctx context.Context, c *cdrws.Conn, topics []events.Topic) error { + sub := newSubscriber(topics, h.bufSize, func() { + _ = c.Close(cdrws.StatusPolicyViolation, "slow consumer") + }) + h.add(sub) + defer h.remove(sub) + + connCtx := c.CloseRead(ctx) + pingT := time.NewTicker(h.pingInterval) + defer pingT.Stop() + + for { + select { + case msg := <-sub.msgs: + wctx, cancel := context.WithTimeout(connCtx, h.writeTimeout) + err := c.Write(wctx, cdrws.MessageText, msg) + cancel() + if err != nil { + return err + } + case <-pingT.C: + pctx, cancel := context.WithTimeout(connCtx, h.pingTimeout) + err := c.Ping(pctx) + cancel() + if err != nil { + return err + } + case <-connCtx.Done(): + _ = c.Close(cdrws.StatusNormalClosure, "") + return connCtx.Err() + } + } +} + +func (h *Hub) add(sub *subscriber) { + h.mu.Lock() + defer h.mu.Unlock() + h.subs[sub] = struct{}{} +} + +func (h *Hub) remove(sub *subscriber) { + h.mu.Lock() + defer h.mu.Unlock() + delete(h.subs, sub) +} + +func (h *Hub) SubscriberCount() int { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.subs) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/hub_test.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/hub_test.go new file mode 100644 index 00000000..b773824d --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/hub_test.go @@ -0,0 +1,123 @@ +// ©AngelaMos | 2026 +// hub_test.go + +package ws_test + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + cdrws "github.com/coder/websocket" + "github.com/stretchr/testify/require" + + "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" + "github.com/carterperez-dev/monitor-the-situation/backend/internal/ws" +) + +func TestHub_SubscribeAndReceive(t *testing.T) { + hub := ws.NewHub(ws.HubConfig{}) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := cdrws.Accept(w, r, &cdrws.AcceptOptions{InsecureSkipVerify: true}) + require.NoError(t, err) + _ = hub.Serve(r.Context(), c, []events.Topic{events.TopicHeartbeat}) + })) + defer srv.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + conn, _, err := cdrws.Dial(ctx, wsURL, nil) + require.NoError(t, err) + defer func() { _ = conn.CloseNow() }() + + require.Eventually(t, func() bool { + return hub.SubscriberCount() == 1 + }, time.Second, 10*time.Millisecond) + + hub.Broadcast(events.TopicHeartbeat, []byte(`{"ts":"x"}`)) + + _, msg, err := conn.Read(ctx) + require.NoError(t, err) + require.Contains(t, string(msg), `"ch":"heartbeat"`) +} + +func TestHub_TopicFiltering(t *testing.T) { + hub := ws.NewHub(ws.HubConfig{}) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := cdrws.Accept(w, r, &cdrws.AcceptOptions{InsecureSkipVerify: true}) + require.NoError(t, err) + _ = hub.Serve(r.Context(), c, []events.Topic{events.TopicHeartbeat}) + })) + defer srv.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + conn, _, err := cdrws.Dial(ctx, wsURL, nil) + require.NoError(t, err) + defer func() { _ = conn.CloseNow() }() + + require.Eventually(t, func() bool { + return hub.SubscriberCount() == 1 + }, time.Second, 10*time.Millisecond) + + hub.Broadcast(events.TopicCVENew, []byte(`{"id":"CVE-2026-0001"}`)) + hub.Broadcast(events.TopicHeartbeat, []byte(`{"ts":"y"}`)) + + readCtx, readCancel := context.WithTimeout(ctx, 500*time.Millisecond) + defer readCancel() + + _, msg, err := conn.Read(readCtx) + require.NoError(t, err) + require.Contains(t, string(msg), `"ch":"heartbeat"`) + require.NotContains(t, string(msg), "CVE-2026-0001") +} + +func TestHub_SlowConsumerClosed(t *testing.T) { + hub := ws.NewHub(ws.HubConfig{SubscriberBufferSize: 2}) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := cdrws.Accept(w, r, &cdrws.AcceptOptions{InsecureSkipVerify: true}) + require.NoError(t, err) + _ = hub.Serve(r.Context(), c, []events.Topic{events.TopicHeartbeat}) + })) + defer srv.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + conn, _, err := cdrws.Dial(ctx, wsURL, nil) + require.NoError(t, err) + defer func() { _ = conn.CloseNow() }() + + require.Eventually(t, func() bool { + return hub.SubscriberCount() == 1 + }, time.Second, 10*time.Millisecond) + + for i := 0; i < 200; i++ { + hub.Broadcast(events.TopicHeartbeat, []byte(`{"i":1}`)) + } + + var ce cdrws.CloseError + for { + _, _, err = conn.Read(ctx) + if err == nil { + continue + } + if errors.As(err, &ce) { + break + } + t.Fatalf("expected close error, got %v", err) + } + require.Equal(t, cdrws.StatusPolicyViolation, ce.Code) +} diff --git a/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/subscriber.go b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/subscriber.go new file mode 100644 index 00000000..851b2a31 --- /dev/null +++ b/PROJECTS/advanced/monitor-the-situation-dashboard/backend/internal/ws/subscriber.go @@ -0,0 +1,24 @@ +// ©AngelaMos | 2026 +// subscriber.go + +package ws + +import "github.com/carterperez-dev/monitor-the-situation/backend/internal/events" + +type subscriber struct { + topics map[events.Topic]struct{} + msgs chan []byte + closeSlow func() +} + +func newSubscriber(topics []events.Topic, bufSize int, closeSlow func()) *subscriber { + t := make(map[events.Topic]struct{}, len(topics)) + for _, top := range topics { + t[top] = struct{}{} + } + return &subscriber{ + topics: t, + msgs: make(chan []byte, bufSize), + closeSlow: closeSlow, + } +}