From 3ab9d290c2177bf8c37504b31831b8e32d88c133 Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 02:34:50 +0900 Subject: [PATCH 1/3] hot fix --- .env.example | 11 +- Makefile | 31 +- apps/api/cmd/server/main.go | 8 +- apps/api/go.mod | 6 +- apps/api/go.sum | 4 + apps/api/internal/api/handler.go | 20 +- apps/api/internal/api/response.go | 144 ++++++++ apps/api/internal/config/config.go | 34 +- apps/api/internal/slack/modal.go | 17 + apps/api/internal/slack/socketmode.go | 477 ++++++++++++++++++++++++++ apps/worker/src/ai/triage.ts | 54 ++- apps/worker/src/index.ts | 12 +- apps/worker/src/slack/client.ts | 33 ++ apps/worker/src/workers/question.ts | 147 ++++++-- docs-host/.vitepress/config.mts | 107 ------ docs-host/index.html | 63 ++++ docs-host/package.json | 9 +- docs-host/server.js | 63 ++++ docs/README.md | 56 +++ docs/_sidebar.md | 22 ++ docs/detail/06_directory.md | 24 +- docs/detail/07_env-vars.md | 6 +- docs/index.md | 26 -- docs/setup/local.md | 18 +- docs/setup/slack.md | 45 ++- infra/docker/compose.yml | 1 - infra/docker/init-localstack.sh | 0 infra/slack/manifest.json | 12 +- pnpm-workspace.yaml | 1 - 29 files changed, 1166 insertions(+), 285 deletions(-) create mode 100644 apps/api/internal/api/response.go create mode 100644 apps/api/internal/slack/socketmode.go delete mode 100644 docs-host/.vitepress/config.mts create mode 100644 docs-host/index.html create mode 100644 docs-host/server.js create mode 100644 docs/README.md create mode 100644 docs/_sidebar.md delete mode 100644 docs/index.md mode change 100644 => 100755 infra/docker/init-localstack.sh diff --git a/.env.example b/.env.example index 46464bc..7446f0f 100644 --- a/.env.example +++ b/.env.example @@ -4,14 +4,15 @@ # .env は絶対にコミットしないこと # === Slack === -SLACK_BOT_TOKEN= -SLACK_SIGNING_SECRET= -SLACK_APP_ID= +SLACK_BOT_TOKEN= # xoxb-... (OAuth & Permissions) +SLACK_APP_TOKEN= # xapp-... (Basic Information → App-Level Tokens → connections:write) +SLACK_APP_ID= # A0XXXXXXX (setup.sh で使用) +SLACK_SIGNING_SECRET= # 不要になりましたが念のため残す SLACK_MENTOR_CHANNEL_ID= SLACK_PROGRESS_CHANNEL_ID= # === Database === -DATABASE_URL=postgres://postgres:postgres@localhost:5432/kcl_support_hub +DATABASE_URL=postgres://postgres:postgres@localhost:5432/kcl_support_hub?sslmode=disable # === AI (Bonsai / Ollama) === # ollama serve && ollama pull qwen2.5:7b を実行してから設定 @@ -34,7 +35,7 @@ ONYX_API_URL= ONYX_API_KEY= # === AWS === -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 # ローカル開発 (LocalStack) 用 AWS_ENDPOINT_URL=http://localhost:4566 AWS_ACCESS_KEY_ID=test diff --git a/Makefile b/Makefile index 5227b12..e788b27 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,12 @@ export .PHONY: help \ up down logs \ + init-sqs \ migrate migrate-down migrate-status \ dev-api dev-worker dev-web \ - docs docs-build \ + docs \ build \ - slack-setup slack-manifest \ + slack-manifest \ lint test \ setup @@ -22,6 +23,7 @@ help: @echo " make up Docker インフラ起動 (postgres + localstack)" @echo " make down Docker インフラ停止" @echo " make logs Docker ログ表示" + @echo " make init-sqs LocalStack SQS キュー作成 (up後に必要)" @echo "" @echo " DB" @echo " make migrate マイグレーション実行" @@ -33,12 +35,10 @@ help: @echo " make dev-worker TypeScript Worker 起動" @echo " make dev-web Next.js ダッシュボード起動 (port 3000)" @echo " make docs ドキュメントサーバー起動 (port 4000)" - @echo " make docs-build ドキュメントを静的ファイルにビルド" @echo "" @echo " Slack" - @echo " make slack-setup URL=https://xxxx.ngrok-free.app" - @echo " Slash Commands + Interactivity URL を一括更新" @echo " make slack-manifest manifest.json の内容を表示" + @echo " ※ Socket Mode 使用中のため ngrok / URL 設定不要" @echo "" @echo " ビルド / テスト" @echo " make build 全サービスをビルド" @@ -46,7 +46,7 @@ help: @echo " make test 全サービスのテスト" @echo "" @echo " 初回セットアップ一括" - @echo " make setup up + migrate をまとめて実行" + @echo " make setup up + init-sqs + migrate をまとめて実行" @echo "" # ----------------------------------------------- @@ -55,6 +55,9 @@ help: up: docker compose -f infra/docker/compose.yml up -d +init-sqs: + docker exec docker-localstack-1 bash /etc/localstack/init/ready.d/init-localstack.sh + down: docker compose -f infra/docker/compose.yml down @@ -83,26 +86,17 @@ dev-worker: cd apps/worker && pnpm dev dev-web: - cd apps/web && pnpm dev + cd apps/web && PORT=3000 pnpm dev # ----------------------------------------------- # ドキュメント # ----------------------------------------------- docs: - cd docs-host && pnpm dev - -docs-build: - cd docs-host && pnpm build + node docs-host/server.js # ----------------------------------------------- # Slack # ----------------------------------------------- -slack-setup: -ifndef URL - $(error URLを指定してください: make slack-setup URL=https://xxxx.ngrok-free.app) -endif - ./infra/slack/setup.sh "$(URL)" - slack-manifest: @cat infra/slack/manifest.json @@ -132,11 +126,10 @@ test: setup: up @echo "インフラ起動を待機中..." @sleep 5 + $(MAKE) init-sqs $(MAKE) migrate @echo "" @echo "セットアップ完了。次のステップ:" @echo " 1. make dev-api (別ターミナル)" @echo " 2. make dev-worker (別ターミナル)" @echo " 3. make dev-web (別ターミナル)" - @echo " 4. ngrok http 8080" - @echo " 5. make slack-setup URL=" diff --git a/apps/api/cmd/server/main.go b/apps/api/cmd/server/main.go index 8c19a79..f46fcad 100644 --- a/apps/api/cmd/server/main.go +++ b/apps/api/cmd/server/main.go @@ -36,6 +36,10 @@ func main() { log.Fatalf("sqs init: %v", err) } + // Start Slack Socket Mode in a background goroutine. + // Connects to Slack via WebSocket — no public URL required. + go slackhandler.RunSocketMode(cfg, sqlDB, sqsClient) + if !cfg.IsDev() { gin.SetMode(gin.ReleaseMode) } @@ -46,10 +50,6 @@ func main() { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) - // Slack webhook routes - r.POST("/slack/commands", slackhandler.HandleSlashCommand(cfg, sqlDB)) - r.POST("/slack/interactions", slackhandler.HandleInteraction(cfg, sqlDB, sqsClient)) - // REST API routes for the dashboard api := r.Group("/api") { diff --git a/apps/api/go.mod b/apps/api/go.mod index 14f967a..d621dff 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -1,8 +1,6 @@ module github.com/Asheze1127/HackHub/apps/api -go 1.24.0 - -toolchain go1.24.5 +go 1.25 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 @@ -39,6 +37,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -46,6 +45,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/slack-go/slack v0.21.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.8.0 // indirect diff --git a/apps/api/go.sum b/apps/api/go.sum index c2da3bb..853f378 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -62,6 +62,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -86,6 +88,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 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/slack-go/slack v0.21.0 h1:TAGnZYFp79LAG/oqFzYhFJ9LwEwXJ93heCkPvwjxc7o= +github.com/slack-go/slack v0.21.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE= github.com/sqlc-dev/pqtype v0.3.0 h1:b09TewZ3cSnO5+M1Kqq05y0+OjqIptxELaSayg7bmqk= github.com/sqlc-dev/pqtype v0.3.0/go.mod h1:oyUjp5981ctiL9UYvj1bVvCKi8OXkCa0u645hce7CAs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/apps/api/internal/api/handler.go b/apps/api/internal/api/handler.go index 49ad09f..3b858c2 100644 --- a/apps/api/internal/api/handler.go +++ b/apps/api/internal/api/handler.go @@ -34,7 +34,11 @@ func HandleListQuestions(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, questions) + resp := make([]QuestionResponse, len(questions)) + for i, question := range questions { + resp[i] = toQuestionResponse(question) + } + c.JSON(http.StatusOK, resp) } } @@ -56,7 +60,7 @@ func HandleGetQuestion(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, question) + c.JSON(http.StatusOK, toQuestionResponse(question)) } } @@ -87,7 +91,11 @@ func HandleListProgress(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, logs) + resp := make([]ProgressLogResponse, len(logs)) + for i, log := range logs { + resp[i] = toProgressLogResponse(log) + } + c.JSON(http.StatusOK, resp) } } @@ -100,7 +108,11 @@ func HandleListTeams(db *sql.DB) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return } - c.JSON(http.StatusOK, teams) + resp := make([]TeamResponse, len(teams)) + for i, team := range teams { + resp[i] = toTeamResponse(team) + } + c.JSON(http.StatusOK, resp) } } diff --git a/apps/api/internal/api/response.go b/apps/api/internal/api/response.go new file mode 100644 index 0000000..77deb09 --- /dev/null +++ b/apps/api/internal/api/response.go @@ -0,0 +1,144 @@ +package api + +import ( + "encoding/json" + "time" + + "github.com/google/uuid" + + "github.com/Asheze1127/HackHub/apps/api/db/sqlcgen" +) + +type QuestionResponse struct { + ID uuid.UUID `json:"id"` + TeamID *string `json:"team_id"` + SlackUserID string `json:"slack_user_id"` + SlackThreadTs *string `json:"slack_thread_ts"` + SlackChannelID string `json:"slack_channel_id"` + Title string `json:"title"` + Body *string `json:"body"` + ErrorMessage *string `json:"error_message"` + Tried *string `json:"tried"` + TriageResult *json.RawMessage `json:"triage_result"` + Category *string `json:"category"` + Confidence *float64 `json:"confidence"` + Status string `json:"status"` + EscalatedToMentor bool `json:"escalated_to_mentor"` + MentorChannelTs *string `json:"mentor_channel_ts"` + SlackEventID *string `json:"slack_event_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ProgressLogResponse struct { + ID uuid.UUID `json:"id"` + TeamID *string `json:"team_id"` + SlackUserID string `json:"slack_user_id"` + Phase string `json:"phase"` + StatusText *string `json:"status_text"` + Blockers *string `json:"blockers"` + IsSos bool `json:"is_sos"` + SlackMessageTs *string `json:"slack_message_ts"` + SlackEventID *string `json:"slack_event_id"` + CreatedAt time.Time `json:"created_at"` +} + +type TeamResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SlackChannelID *string `json:"slack_channel_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toQuestionResponse(q sqlcgen.Question) QuestionResponse { + r := QuestionResponse{ + ID: q.ID, + SlackUserID: q.SlackUserID, + SlackChannelID: q.SlackChannelID, + Title: q.Title, + Status: q.Status, + EscalatedToMentor: q.EscalatedToMentor, + CreatedAt: q.CreatedAt, + UpdatedAt: q.UpdatedAt, + } + if q.TeamID.Valid { + s := q.TeamID.UUID.String() + r.TeamID = &s + } + if q.SlackThreadTs.Valid { + r.SlackThreadTs = &q.SlackThreadTs.String + } + if q.Body.Valid { + r.Body = &q.Body.String + } + if q.ErrorMessage.Valid { + r.ErrorMessage = &q.ErrorMessage.String + } + if q.Tried.Valid { + r.Tried = &q.Tried.String + } + if q.TriageResult.Valid { + raw := json.RawMessage(q.TriageResult.RawMessage) + r.TriageResult = &raw + } + if q.Category.Valid { + r.Category = &q.Category.String + } + if q.Confidence.Valid { + r.Confidence = &q.Confidence.Float64 + } + if q.MentorChannelTs.Valid { + r.MentorChannelTs = &q.MentorChannelTs.String + } + if q.SlackEventID.Valid { + r.SlackEventID = &q.SlackEventID.String + } + return r +} + +func toProgressLogResponse(p sqlcgen.ProgressLog) ProgressLogResponse { + r := ProgressLogResponse{ + ID: p.ID, + SlackUserID: p.SlackUserID, + Phase: p.Phase, + IsSos: p.IsSos, + CreatedAt: p.CreatedAt, + } + if p.TeamID.Valid { + s := p.TeamID.UUID.String() + r.TeamID = &s + } + if p.StatusText.Valid { + r.StatusText = &p.StatusText.String + } + if p.Blockers.Valid { + r.Blockers = &p.Blockers.String + } + if p.SlackMessageTs.Valid { + r.SlackMessageTs = &p.SlackMessageTs.String + } + if p.SlackEventID.Valid { + r.SlackEventID = &p.SlackEventID.String + } + return r +} + +func toTeamResponse(t sqlcgen.Team) TeamResponse { + r := TeamResponse{ + ID: t.ID, + Name: t.Name, + TechStack: t.TechStack, + Phase: t.Phase, + IsSos: t.IsSos, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, + } + if t.SlackChannelID.Valid { + r.SlackChannelID = &t.SlackChannelID.String + } + return r +} diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index 93209ad..63dbec2 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -6,18 +6,19 @@ import ( ) type Config struct { - Port string - Env string - DatabaseURL string - SlackBotToken string - SlackSigningSecret string - SlackMentorChannel string + Port string + Env string + DatabaseURL string + SlackBotToken string + SlackAppToken string + SlackSigningSecret string + SlackMentorChannel string SlackProgressChannel string - AWSRegion string - AWSEndpointURL string - SQSQuestionNewURL string - SQSFollowupURL string - SQSProgressURL string + AWSRegion string + AWSEndpointURL string + SQSQuestionNewURL string + SQSFollowupURL string + SQSProgressURL string } func Load() (*Config, error) { @@ -26,10 +27,11 @@ func Load() (*Config, error) { Env: getEnv("ENV", "development"), DatabaseURL: os.Getenv("DATABASE_URL"), SlackBotToken: os.Getenv("SLACK_BOT_TOKEN"), + SlackAppToken: os.Getenv("SLACK_APP_TOKEN"), SlackSigningSecret: os.Getenv("SLACK_SIGNING_SECRET"), SlackMentorChannel: os.Getenv("SLACK_MENTOR_CHANNEL_ID"), SlackProgressChannel: os.Getenv("SLACK_PROGRESS_CHANNEL_ID"), - AWSRegion: getEnv("AWS_REGION", "ap-northeast-1"), + AWSRegion: getEnv("AWS_REGION", "us-east-1"), AWSEndpointURL: os.Getenv("AWS_ENDPOINT_URL"), SQSQuestionNewURL: os.Getenv("SQS_QUESTION_NEW_URL"), SQSFollowupURL: os.Getenv("SQS_QUESTION_FOLLOWUP_URL"), @@ -38,8 +40,11 @@ func Load() (*Config, error) { if cfg.DatabaseURL == "" { return nil, fmt.Errorf("DATABASE_URL is required") } - if cfg.SlackSigningSecret == "" { - return nil, fmt.Errorf("SLACK_SIGNING_SECRET is required") + if cfg.SlackBotToken == "" { + return nil, fmt.Errorf("SLACK_BOT_TOKEN is required") + } + if cfg.SlackAppToken == "" { + return nil, fmt.Errorf("SLACK_APP_TOKEN is required (xapp-... Socket Mode token)") } return cfg, nil } @@ -54,4 +59,3 @@ func getEnv(key, fallback string) string { } return fallback } - diff --git a/apps/api/internal/slack/modal.go b/apps/api/internal/slack/modal.go index 6b4b01c..fd04ca7 100644 --- a/apps/api/internal/slack/modal.go +++ b/apps/api/internal/slack/modal.go @@ -85,6 +85,23 @@ func OpenProgressModal(botToken, triggerID, channelID string) error { return callViewsOpen(botToken, triggerID, modal) } +// OpenMentorReplyModal opens the mentor reply modal via views.open. +// metadata is stored in private_metadata (format: channelId|threadTs|userId). +func OpenMentorReplyModal(botToken, triggerID, metadata string) error { + modal := map[string]any{ + "type": "modal", + "callback_id": "mentor_reply_modal", + "private_metadata": metadata, + "title": plainText("回答を入力"), + "submit": plainText("送信(匿名)"), + "close": plainText("キャンセル"), + "blocks": []map[string]any{ + textareaBlock("reply_block", "reply", "回答内容", "参加者への回答を入力してください", false), + }, + } + return callViewsOpen(botToken, triggerID, modal) +} + func callViewsOpen(botToken, triggerID string, view map[string]any) error { payload := map[string]any{ "trigger_id": triggerID, diff --git a/apps/api/internal/slack/socketmode.go b/apps/api/internal/slack/socketmode.go new file mode 100644 index 0000000..62e5b33 --- /dev/null +++ b/apps/api/internal/slack/socketmode.go @@ -0,0 +1,477 @@ +package slack + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + + goslack "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + + "github.com/Asheze1127/HackHub/apps/api/internal/config" + "github.com/Asheze1127/HackHub/apps/api/internal/sqs" +) + +// RunSocketMode connects to Slack via WebSocket and handles slash commands +// and view submissions. It blocks until the connection is closed. +func RunSocketMode(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender) { + api := goslack.New( + cfg.SlackBotToken, + goslack.OptionAppLevelToken(cfg.SlackAppToken), + ) + client := socketmode.New(api, + socketmode.OptionLog(log.New(log.Writer(), "[socketmode] ", log.LstdFlags)), + ) + + go func() { + for evt := range client.Events { + switch evt.Type { + case socketmode.EventTypeSlashCommand: + cmd, ok := evt.Data.(goslack.SlashCommand) + if !ok { + client.Ack(*evt.Request) + continue + } + onSlashCommand(cfg, client, &evt, &cmd) + + case socketmode.EventTypeInteractive: + callback, ok := evt.Data.(goslack.InteractionCallback) + if !ok { + client.Ack(*evt.Request) + continue + } + onInteraction(cfg, db, sqsClient, client, &evt, &callback) + + case socketmode.EventTypeEventsAPI: + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + client.Ack(*evt.Request) + if !ok { + continue + } + onEventsAPI(cfg, db, sqsClient, &eventsAPIEvent) + + case socketmode.EventTypeConnecting: + log.Println("[socketmode] connecting to Slack...") + case socketmode.EventTypeConnected: + log.Println("[socketmode] connected") + } + } + }() + + if err := client.Run(); err != nil { + log.Printf("[socketmode] disconnected: %v", err) + } +} + +// onSlashCommand opens the appropriate modal. Must complete within 3 seconds. +func onSlashCommand(cfg *config.Config, client *socketmode.Client, evt *socketmode.Event, cmd *goslack.SlashCommand) { + var err error + switch cmd.Command { + case "/question": + err = OpenQuestionModal(cfg.SlackBotToken, cmd.TriggerID, cmd.ChannelID) + case "/progress": + err = OpenProgressModal(cfg.SlackBotToken, cmd.TriggerID, cmd.ChannelID) + default: + client.Ack(*evt.Request) + return + } + + if err != nil { + log.Printf("[socketmode] open modal failed (%s): %v", cmd.Command, err) + client.Ack(*evt.Request, map[string]interface{}{ + "response_type": "ephemeral", + "text": "モーダルを開けませんでした。しばらく待ってから再試行してください。", + }) + return + } + client.Ack(*evt.Request) +} + +// onInteraction handles interactive callbacks (block_actions and view_submission). +func onInteraction(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender, client *socketmode.Client, evt *socketmode.Event, cb *goslack.InteractionCallback) { + // Handle block_actions (button clicks) + if cb.Type == goslack.InteractionTypeBlockActions { + for _, action := range cb.ActionCallback.BlockActions { + switch action.ActionID { + case "mentor_reply": + if err := OpenMentorReplyModal(cfg.SlackBotToken, cb.TriggerID, action.Value); err != nil { + log.Printf("[socketmode] open mentor reply modal failed: %v", err) + } + case "question_resolved": + onQuestionResolved(cfg, db, action.Value) + case "question_not_resolved": + onQuestionNotResolved(cfg, db, action.Value) + } + } + client.Ack(*evt.Request) + return + } + + if cb.Type != goslack.InteractionTypeViewSubmission { + client.Ack(*evt.Request) + return + } + + // Handle mentor_reply_modal submission + if cb.View.CallbackID == "mentor_reply_modal" { + parts := strings.SplitN(cb.View.PrivateMetadata, "|", 3) + if len(parts) != 3 { + log.Printf("[socketmode] invalid mentor_reply_modal metadata: %s", cb.View.PrivateMetadata) + client.Ack(*evt.Request) + return + } + channelID, threadTs, userID := parts[0], parts[1], parts[2] + reply := blockVal(cb.View.State.Values, "reply_block", "reply") + text := fmt.Sprintf("<@%s> メンターより:\n%s", userID, reply) + if err := callChatPostMessage(cfg.SlackBotToken, channelID, threadTs, text); err != nil { + log.Printf("[socketmode] mentor reply post failed: %v", err) + } + client.Ack(*evt.Request, map[string]interface{}{"response_action": "clear"}) + return + } + + // Idempotency: view.id is unique per modal open. + inserted, err := insertEventIfNew(context.Background(), db, cb.View.ID, "view_submission") + if err != nil { + log.Printf("[socketmode] idempotency db error: %v", err) + client.Ack(*evt.Request) + return + } + if !inserted { + client.Ack(*evt.Request) + return + } + + var enqueueErr error + switch cb.View.CallbackID { + case "question_modal": + enqueueErr = enqueueQuestion(sqsClient, cb) + case "progress_modal": + enqueueErr = enqueueProgress(sqsClient, cb) + default: + log.Printf("[socketmode] unknown callback_id: %s", cb.View.CallbackID) + client.Ack(*evt.Request) + return + } + + if enqueueErr != nil { + log.Printf("[socketmode] enqueue failed for %s: %v", cb.View.CallbackID, enqueueErr) + client.Ack(*evt.Request, map[string]interface{}{ + "response_action": "errors", + "errors": map[string]string{ + "title_block": "送信に失敗しました。しばらく待ってから再試行してください。", + }, + }) + return + } + + client.Ack(*evt.Request, map[string]interface{}{"response_action": "clear"}) +} + +// onQuestionResolved handles the "✅ 解決した" button. +// value format: "questionId|channelId|threadTs|userId" +func onQuestionResolved(cfg *config.Config, db *sql.DB, value string) { + parts := strings.SplitN(value, "|", 4) + if len(parts) != 4 { + log.Printf("[socketmode] invalid question_resolved value: %s", value) + return + } + questionID, channelID, threadTs, userID := parts[0], parts[1], parts[2], parts[3] + + if _, err := db.ExecContext(context.Background(), + `UPDATE questions SET status = 'resolved', updated_at = NOW() WHERE id = $1`, + questionID, + ); err != nil { + log.Printf("[socketmode] mark resolved failed: %v", err) + } + + text := fmt.Sprintf("<@%s> ✅ 解決済みとしてマークしました。お役に立てて良かったです!", userID) + if err := callChatPostMessage(cfg.SlackBotToken, channelID, threadTs, text); err != nil { + log.Printf("[socketmode] post resolved message failed: %v", err) + } +} + +// onQuestionNotResolved handles the "🆘 まだ解決していない" button. +// value format: "questionId|channelId|threadTs|userId" +func onQuestionNotResolved(cfg *config.Config, db *sql.DB, value string) { + parts := strings.SplitN(value, "|", 4) + if len(parts) != 4 { + log.Printf("[socketmode] invalid question_not_resolved value: %s", value) + return + } + questionID, channelID, threadTs, userID := parts[0], parts[1], parts[2], parts[3] + + // Fetch question title for escalation message. + var title string + if err := db.QueryRowContext(context.Background(), + `SELECT title FROM questions WHERE id = $1`, questionID, + ).Scan(&title); err != nil { + log.Printf("[socketmode] fetch question title failed: %v", err) + title = "(タイトル不明)" + } + + // Update status to escalated. + if _, err := db.ExecContext(context.Background(), + `UPDATE questions SET status = 'escalated', escalated_to_mentor = TRUE, updated_at = NOW() WHERE id = $1`, + questionID, + ); err != nil { + log.Printf("[socketmode] mark escalated failed: %v", err) + } + + // Notify user in thread. + userText := fmt.Sprintf("<@%s> メンターに転送しました。しばらくお待ちください 🙏", userID) + if err := callChatPostMessage(cfg.SlackBotToken, channelID, threadTs, userText); err != nil { + log.Printf("[socketmode] post not-resolved message failed: %v", err) + } + + // Post to mentor channel with reply button. + mentorChannel := cfg.SlackMentorChannel + if mentorChannel == "" { + log.Printf("[socketmode] SLACK_MENTOR_CHANNEL_ID not set; skipping mentor escalation") + return + } + threadURL := fmt.Sprintf("https://slack.com/archives/%s/p%s", channelID, strings.ReplaceAll(threadTs, ".", "")) + mentorText := fmt.Sprintf("[ユーザー未解決] %s", title) + mentorBlocks := buildMentorEscalationBlocks(title, channelID, threadTs, userID, threadURL) + if err := callChatPostMessageWithBlocks(cfg.SlackBotToken, mentorChannel, "", mentorText, mentorBlocks); err != nil { + log.Printf("[socketmode] post mentor escalation failed: %v", err) + } +} + +// buildMentorEscalationBlocks builds the Block Kit payload for mentor channel escalation. +func buildMentorEscalationBlocks(title, channelID, threadTs, userID, threadURL string) []map[string]any { + return []map[string]any{ + { + "type": "section", + "text": map[string]any{ + "type": "mrkdwn", + "text": fmt.Sprintf("*[ユーザー未解決] %s*\n元スレッド: %s", title, threadURL), + }, + }, + { + "type": "actions", + "elements": []map[string]any{ + { + "type": "button", + "text": map[string]any{"type": "plain_text", "text": "回答する", "emoji": true}, + "action_id": "mentor_reply", + "value": fmt.Sprintf("%s|%s|%s", channelID, threadTs, userID), + }, + }, + }, + } +} + +// callChatPostMessageWithBlocks posts a Block Kit message to a Slack channel. +func callChatPostMessageWithBlocks(botToken, channelID, threadTs, text string, blocks []map[string]any) error { + payload := map[string]any{ + "channel": channelID, + "text": text, + "blocks": blocks, + } + if threadTs != "" { + payload["thread_ts"] = threadTs + } + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal chat.postMessage payload: %w", err) + } + req, err := http.NewRequest(http.MethodPost, slackAPIBase+"/chat.postMessage", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create chat.postMessage request: %w", err) + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+botToken) + + resp, err := slackHTTPClient.Do(req) + if err != nil { + return fmt.Errorf("chat.postMessage http call: %w", err) + } + defer resp.Body.Close() + + var result struct { + OK bool `json:"ok"` + Error string `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("decode chat.postMessage response: %w", err) + } + if !result.OK { + return fmt.Errorf("chat.postMessage error: %s", result.Error) + } + return nil +} + +// callChatPostMessage posts a message to a Slack channel/thread via the Web API. +func callChatPostMessage(botToken, channelID, threadTs, text string) error { + payload := map[string]any{ + "channel": channelID, + "thread_ts": threadTs, + "text": text, + } + body, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal chat.postMessage payload: %w", err) + } + req, err := http.NewRequest(http.MethodPost, slackAPIBase+"/chat.postMessage", bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create chat.postMessage request: %w", err) + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Authorization", "Bearer "+botToken) + + resp, err := slackHTTPClient.Do(req) + if err != nil { + return fmt.Errorf("chat.postMessage http call: %w", err) + } + defer resp.Body.Close() + + var result struct { + OK bool `json:"ok"` + Error string `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("decode chat.postMessage response: %w", err) + } + if !result.OK { + return fmt.Errorf("chat.postMessage error: %s", result.Error) + } + return nil +} + +// onEventsAPI handles Events API callbacks (e.g. message events). +func onEventsAPI(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender, event *slackevents.EventsAPIEvent) { + if event.InnerEvent.Type != "message" { + return + } + msg, ok := event.InnerEvent.Data.(*slackevents.MessageEvent) + if !ok { + return + } + // Skip bot messages, edited messages, etc. + if msg.BotID != "" || msg.SubType != "" { + return + } + // Only process thread replies (not top-level messages) + if msg.ThreadTimeStamp == "" { + return + } + // Skip the thread-parent itself + if msg.ThreadTimeStamp == msg.TimeStamp { + return + } + + questionID, userID, title, found, err := findQuestionByThread(context.Background(), db, msg.Channel, msg.ThreadTimeStamp) + if err != nil { + log.Printf("[socketmode] findQuestionByThread error: %v", err) + return + } + if !found { + return + } + + followup := questionFollowupMessage{ + QuestionID: questionID, + ChannelID: msg.Channel, + ThreadTs: msg.ThreadTimeStamp, + UserID: userID, + ReplyText: msg.Text, + OriginalTitle: title, + } + body, err := json.Marshal(followup) + if err != nil { + log.Printf("[socketmode] marshal followup message: %v", err) + return + } + if err := sqsClient.Enqueue(context.Background(), sqsClient.QuestionFollowupURL(), string(body)); err != nil { + log.Printf("[socketmode] enqueue followup failed: %v", err) + } +} + +// findQuestionByThread looks up a question by channel + thread_ts. +func findQuestionByThread(ctx context.Context, db *sql.DB, channelID, threadTs string) (questionID, userID, title string, found bool, err error) { + row := db.QueryRowContext(ctx, + `SELECT id, slack_user_id, title FROM questions WHERE slack_channel_id = $1 AND slack_thread_ts = $2 LIMIT 1`, + channelID, threadTs, + ) + if err = row.Scan(&questionID, &userID, &title); err != nil { + if err == sql.ErrNoRows { + return "", "", "", false, nil + } + return "", "", "", false, err + } + return questionID, userID, title, true, nil +} + +// questionFollowupMessage is the SQS payload for question followups. +type questionFollowupMessage struct { + QuestionID string `json:"question_id"` + ChannelID string `json:"channel_id"` + ThreadTs string `json:"thread_ts"` + UserID string `json:"user_id"` + ReplyText string `json:"reply_text"` + OriginalTitle string `json:"original_title"` +} + +func enqueueQuestion(sqsClient sqs.Sender, cb *goslack.InteractionCallback) error { + vals := cb.View.State.Values + msg := questionNewMessage{ + UserID: cb.User.ID, + TeamID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Title: blockVal(vals, "title_block", "title"), + Body: blockVal(vals, "body_block", "body"), + ErrorMessage: blockVal(vals, "error_block", "error_message"), + Tried: blockVal(vals, "tried_block", "tried"), + } + body, err := json.Marshal(msg) + if err != nil { + return err + } + return sqsClient.Enqueue(context.Background(), sqsClient.QuestionNewURL(), string(body)) +} + +func enqueueProgress(sqsClient sqs.Sender, cb *goslack.InteractionCallback) error { + vals := cb.View.State.Values + sosChecked := len(vals["sos_block"]["sos_flag"].SelectedOptions) > 0 + msg := progressMessage{ + UserID: cb.User.ID, + TeamID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Phase: blockSelectedVal(vals, "phase_block", "phase"), + CurrentStatus: blockVal(vals, "status_block", "current_status"), + Blockers: blockVal(vals, "blockers_block", "blockers"), + SOSFlag: sosChecked, + } + body, err := json.Marshal(msg) + if err != nil { + return err + } + return sqsClient.Enqueue(context.Background(), sqsClient.ProgressURL(), string(body)) +} + +// blockVal extracts a plain_text_input value from slack-go's ViewState. +func blockVal(vals map[string]map[string]goslack.BlockAction, blockID, actionID string) string { + if b, ok := vals[blockID]; ok { + if a, ok := b[actionID]; ok { + return a.Value + } + } + return "" +} + +// blockSelectedVal extracts a static_select value from slack-go's ViewState. +func blockSelectedVal(vals map[string]map[string]goslack.BlockAction, blockID, actionID string) string { + if b, ok := vals[blockID]; ok { + if a, ok := b[actionID]; ok && a.SelectedOption.Value != "" { + return a.SelectedOption.Value + } + } + return "" +} diff --git a/apps/worker/src/ai/triage.ts b/apps/worker/src/ai/triage.ts index a008af2..2965e61 100644 --- a/apps/worker/src/ai/triage.ts +++ b/apps/worker/src/ai/triage.ts @@ -22,11 +22,52 @@ interface KnowledgeContext { const TRIAGE_PROMPT_TEMPLATE = `あなたは KCL ハッカソンの質問受付担当です。 参加者の質問を受け取り、指定された JSON 形式で分析結果を返してください。 -【重要な制約】 -- 断定的な技術回答をしないこと。あなたは「受付係」です -- 不確かな場合は needs_more_info を true にして情報を求めること -- short_answer_for_user は 500 文字以内、日本語で -- AI による誤答の可能性がある旨を必要に応じて伝えること +【役割】 +- あなたは「問題切り分けを補助する受付係」です +- 解決に必要な情報が十分なら、まず短く一次回答してください +- 情報が不足しているなら、診断に本当に必要な情報だけを求めてください + +【needs_more_info の判断基準 — 最重要】 +needs_more_info は「次の回答や切り分けに進むために不可欠な事実が足りない場合のみ」true にすること。 +以下の場合は needs_more_info を false にして回答すること: +- 概念・仕組みの説明系の質問(例:「Next.js と React の違いは?」「JWT とは何か?」) +- ベストプラクティス・設計方針・責務分担の質問 +- 一般的な how-to、比較、用語説明 +- 与えられた情報だけで一次回答できる質問 +needs_more_info を true にするのは、たとえば以下のような場合だけです: +- 実際のエラーメッセージがない +- 実行したコマンドや再現手順がない +- 問題が起きているファイル、設定、コード箇所が分からない +- バージョンや実行環境が不明で切り分けできない + +【missing_info の書き方 — 最重要】 +- missing_info は「ユーザーが次の 1 通で返せる具体的な事実・資料名」だけを書くこと +- missing_info に書くのは「何を送ってほしいか」であり、「何を説明してほしいか」ではない +- 抽象的な説明要求は禁止 +- 各項目は短い名詞句にすること +- 1項目につき1情報だけにすること +- 最大 3 件までにすること +- すでに質問文に含まれている情報は要求しないこと +- needs_more_info が false のとき missing_info は必ず空配列 [] にすること + +良い missing_info の例: +- 実際のエラーメッセージ全文 +- 実行したコマンド +- 問題が起きているコードの該当関数 +- package.json の scripts 部分 +- 使用している Node.js のバージョン + +悪い missing_info の例: +- DDD の Utility 設計に関する詳細な説明 +- クリーンアーキテクチャにおける Utility の役割 +- もう少し詳しく +- 関連情報 +- 背景知識の説明 + +【short_answer_for_user の書き方】 +あなたは「受付の一次回答」として、知っている範囲で回答を提供してからフォローアップを聞く形にすること。 +例:「〜です。(知識を簡潔に回答) 追加で〜を教えてもらえると、さらに詳しいアドバイスができます。」 +500 文字以内、日本語で。AI による誤答の可能性がある旨を必要に応じて伝えること。 【チーム情報】 チーム名: {team_name} @@ -80,12 +121,13 @@ async function triageWithProvider( apiKey: string, model: string, ): Promise { - const provider = createOpenAI({ baseURL, apiKey }) + const provider = createOpenAI({ baseURL, apiKey, compatibility: "compatible" }) const { object } = await generateObject({ model: provider(model), schema: QuestionTriageResultSchema, prompt, maxTokens: 1024, + mode: "json", }) return object } diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 1ea531a..3e60935 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1,7 +1,11 @@ -import "dotenv/config" +import dotenv from "dotenv" +import path from "path" +// モノレポルートの .env を優先して読む(apps/worker/.env があればそちらも読む) +dotenv.config({ path: path.resolve(__dirname, "../../../.env") }) +dotenv.config() import { checkConnection, closePool } from "./db/client" import { startConsumer } from "./queue/consumer" -import { handleQuestionNew, type QuestionNewMessage } from "./workers/question" +import { handleQuestionNew, handleQuestionFollowup, type QuestionNewMessage, type QuestionFollowupMessage } from "./workers/question" import { handleProgress, type ProgressMessage } from "./workers/progress" function requireEnv(name: string): string { @@ -36,8 +40,8 @@ async function main(): Promise { }, signal), startConsumer(QUEUES.questionFollowup, async (body) => { - console.log("[worker] question:followup", JSON.stringify(body)) - // TODO: gh-19 Phase 3 — FollowupWorker + console.log("[worker] question:followup received") + await handleQuestionFollowup(body as QuestionFollowupMessage) }, signal), startConsumer(QUEUES.progress, async (body) => { diff --git a/apps/worker/src/slack/client.ts b/apps/worker/src/slack/client.ts index e62bb2f..21e6b87 100644 --- a/apps/worker/src/slack/client.ts +++ b/apps/worker/src/slack/client.ts @@ -46,6 +46,22 @@ export async function postMessage(channelId: string, text: string): Promise { + const data = await callSlack("chat.postMessage", { + channel: channelId, + text, + blocks, + }) + if (!data.ts) throw new Error("chat.postMessage (blocks) returned no ts") + return data.ts +} + // postThreadReply sends a threaded reply to an existing message. export async function postThreadReply( channelId: string, @@ -60,3 +76,20 @@ export async function postThreadReply( if (!data.ts) throw new Error("chat.postMessage (thread) returned no ts") return data.ts } + +// postThreadReplyWithBlocks sends a threaded reply with Block Kit blocks. +export async function postThreadReplyWithBlocks( + channelId: string, + threadTs: string, + text: string, + blocks: unknown[], +): Promise { + const data = await callSlack("chat.postMessage", { + channel: channelId, + thread_ts: threadTs, + text, + blocks, + }) + if (!data.ts) throw new Error("chat.postMessage (thread blocks) returned no ts") + return data.ts +} diff --git a/apps/worker/src/workers/question.ts b/apps/worker/src/workers/question.ts index a204653..cf0956f 100644 --- a/apps/worker/src/workers/question.ts +++ b/apps/worker/src/workers/question.ts @@ -1,6 +1,6 @@ import { triageQuestion, type QuestionInput } from "../ai/triage" import { insertQuestion, updateQuestionTriage, markEscalated } from "../db/questions" -import { postMessage, postThreadReply } from "../slack/client" +import { postMessage, postMessageWithBlocks, postThreadReply, postThreadReplyWithBlocks } from "../slack/client" export interface QuestionNewMessage { user_id: string @@ -59,19 +59,17 @@ export async function handleQuestionNew(msg: QuestionNewMessage): Promise status, }) - // Post Slack thread reply. - if (triage.needs_more_info) { - await postThreadReply(msg.channel_id, threadTs, buildAskMoreInfoReply(triage.missing_info)) - } else { - await postThreadReply(msg.channel_id, threadTs, buildAnswerReply(triage)) - } + // Post Slack thread reply with resolution buttons. + const replyText = buildReply(triage) + const replyBlocks = buildReplyBlocks(replyText, row.id, msg.channel_id, threadTs, msg.user_id) + await postThreadReplyWithBlocks(msg.channel_id, threadTs, replyText, replyBlocks) // Escalate to mentor channel if flagged. if (triage.should_escalate_to_mentor) { const mentorChannel = getMentorChannel() if (mentorChannel) { - const mentorText = buildMentorEscalationText(msg, triage, msg.channel_id, threadTs) - const mentorTs = await postMessage(mentorChannel, mentorText) + const { text, blocks } = buildMentorEscalationBlocks(msg, triage, msg.channel_id, threadTs, msg.user_id) + const mentorTs = await postMessageWithBlocks(mentorChannel, text, blocks) await markEscalated(row.id, mentorTs) } else { // Mentor channel not configured — log and skip escalation. @@ -81,6 +79,37 @@ export async function handleQuestionNew(msg: QuestionNewMessage): Promise } } +export interface QuestionFollowupMessage { + question_id: string + channel_id: string + thread_ts: string + user_id: string + reply_text: string + original_title: string +} + +// handleQuestionFollowup processes a follow-up reply in a question thread. +export async function handleQuestionFollowup(msg: QuestionFollowupMessage): Promise { + const input: QuestionInput = { + title: msg.original_title, + body: msg.reply_text, + } + const triage = await triageQuestion(input) + + const status = triage.should_escalate_to_mentor ? "escalated" : "answered" + await updateQuestionTriage({ + id: msg.question_id, + triage_result: triage, + category: triage.category, + confidence: triage.confidence, + status, + }) + + const replyText = buildReply(triage) + const replyBlocks = buildReplyBlocks(replyText, msg.question_id, msg.channel_id, msg.thread_ts, msg.user_id) + await postThreadReplyWithBlocks(msg.channel_id, msg.thread_ts, replyText, replyBlocks) +} + // buildQuestionText formats the initial channel message. function buildQuestionText(msg: QuestionNewMessage): string { const lines = [`*[質問] ${msg.title}*`, `<@${msg.user_id}> からの質問です。`] @@ -90,41 +119,85 @@ function buildQuestionText(msg: QuestionNewMessage): string { return lines.join("\n") } -// buildAskMoreInfoReply formats the "please provide more information" reply. -function buildAskMoreInfoReply(missingInfo: string[]): string { - const items = missingInfo.length > 0 - ? missingInfo.map((i) => `• ${i}`).join("\n") - : "• もう少し詳しい情報" - return [ - "ありがとうございます!質問を受け取りました 🔍", - "", - "確認させてください:", - items, - "", - "上記を教えていただけると、より正確にサポートできます。", - "このスレッドに返信してください!", - ].join("\n") -} - -// buildAnswerReply formats the AI first-response reply. -function buildAnswerReply(triage: Awaited>): string { +// buildReply formats the AI reply — 常に回答を先に出し、追加情報が必要なら末尾に添える。 +function buildReply(triage: Awaited>): string { const note = triage.confidence >= 0.8 ? "" : "\n\n_※ AIによる一次回答です。内容に誤りがある可能性があります。_" - return `${triage.short_answer_for_user}${note}` + const followup = triage.needs_more_info && triage.missing_info.length > 0 + ? "\n\n追加で教えてもらえると助かります:\n" + triage.missing_info.map((i) => `• ${i}`).join("\n") + : "" + return `${triage.short_answer_for_user}${note}${followup}` } -// buildMentorEscalationText formats the mentor channel escalation message. -function buildMentorEscalationText( +// buildReplyBlocks wraps the AI reply text with resolution buttons. +// button value: "questionId|channelId|threadTs|userId" +function buildReplyBlocks( + text: string, + questionId: string, + channelId: string, + threadTs: string, + userId: string, +): unknown[] { + const value = `${questionId}|${channelId}|${threadTs}|${userId}` + return [ + { type: "section", text: { type: "mrkdwn", text } }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "✅ 解決した", emoji: true }, + style: "primary", + action_id: "question_resolved", + value, + }, + { + type: "button", + text: { type: "plain_text", text: "🆘 まだ解決していない", emoji: true }, + style: "danger", + action_id: "question_not_resolved", + value, + }, + ], + }, + ] +} + +// buildMentorEscalationBlocks formats the mentor channel escalation message as Block Kit. +function buildMentorEscalationBlocks( msg: QuestionNewMessage, triage: Awaited>, channelId: string, threadTs: string, -): string { - return [ - `*[メンター転送] ${msg.title}*`, - `カテゴリ: \`${triage.category}\``, - `AI 判定理由: ${triage.escalation_reason ?? "信頼度が低いため"}`, - `元スレッド: https://slack.com/archives/${channelId}/p${threadTs.replace(".", "")}`, - ].join("\n") + userId: string, +): { text: string; blocks: unknown[] } { + const threadUrl = `https://slack.com/archives/${channelId}/p${threadTs.replace(".", "")}` + const text = `[メンター転送] ${msg.title}` + const blocks: unknown[] = [ + { + type: "section", + text: { + type: "mrkdwn", + text: [ + `*[メンター転送] ${msg.title}*`, + `カテゴリ: \`${triage.category}\``, + `AI 判定理由: ${triage.escalation_reason ?? "信頼度が低いため"}`, + `元スレッド: ${threadUrl}`, + ].join("\n"), + }, + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "回答する", emoji: true }, + action_id: "mentor_reply", + value: `${channelId}|${threadTs}|${userId}`, + }, + ], + }, + ] + return { text, blocks } } diff --git a/docs-host/.vitepress/config.mts b/docs-host/.vitepress/config.mts deleted file mode 100644 index 628feb9..0000000 --- a/docs-host/.vitepress/config.mts +++ /dev/null @@ -1,107 +0,0 @@ -import { defineConfig } from "vitepress" -import { withMermaid } from "vitepress-plugin-mermaid" - -export default withMermaid( - defineConfig({ - srcDir: "../docs", - title: "KCL Support Hub", - description: "ハッカソン参加者向け AI サポートシステム", - lang: "ja", - - themeConfig: { - logo: "/logo.svg", - siteTitle: "KCL Support Hub", - - nav: [ - { text: "はじめに", link: "/specification" }, - { text: "セットアップ", link: "/setup/" }, - { text: "詳細設計", link: "/detail/01_product-definition" }, - ], - - sidebar: { - "/setup/": [ - { - text: "セットアップ", - items: [ - { text: "概要", link: "/setup/" }, - { text: "Slack App", link: "/setup/slack" }, - { text: "ローカル開発", link: "/setup/local" }, - { text: "自前サーバー (VPS)", link: "/setup/own" }, - { text: "クラウド (AWS)", link: "/setup/remote" }, - ], - }, - ], - "/detail/": [ - { - text: "詳細設計", - items: [ - { text: "01 プロダクト定義", link: "/detail/01_product-definition" }, - { text: "02 アーキテクチャ", link: "/detail/02_architecture" }, - { text: "03 AI フロー", link: "/detail/03_ai-flow" }, - { text: "04 データモデル", link: "/detail/04_data-models" }, - { text: "05 実装フェーズ", link: "/detail/05_implementation-phases" }, - { text: "06 ディレクトリ構成", link: "/detail/06_directory" }, - { text: "07 環境変数", link: "/detail/07_env-vars" }, - { text: "08 実装タスク", link: "/detail/08_implementation-tasks" }, - { text: "09 最初の PR", link: "/detail/09_first-pr" }, - ], - }, - ], - "/": [ - { - text: "概要", - items: [ - { text: "設計仕様", link: "/specification" }, - { text: "AI セットアップ", link: "/ai-setup" }, - { text: "Slack セットアップ", link: "/slack-setup" }, - ], - }, - { - text: "セットアップ", - items: [ - { text: "概要", link: "/setup/" }, - { text: "Slack App", link: "/setup/slack" }, - { text: "ローカル開発", link: "/setup/local" }, - { text: "自前サーバー (VPS)", link: "/setup/own" }, - { text: "クラウド (AWS)", link: "/setup/remote" }, - ], - }, - { - text: "詳細設計", - items: [ - { text: "01 プロダクト定義", link: "/detail/01_product-definition" }, - { text: "02 アーキテクチャ", link: "/detail/02_architecture" }, - { text: "03 AI フロー", link: "/detail/03_ai-flow" }, - { text: "04 データモデル", link: "/detail/04_data-models" }, - { text: "05 実装フェーズ", link: "/detail/05_implementation-phases" }, - { text: "06 ディレクトリ構成", link: "/detail/06_directory" }, - { text: "07 環境変数", link: "/detail/07_env-vars" }, - { text: "08 実装タスク", link: "/detail/08_implementation-tasks" }, - { text: "09 最初の PR", link: "/detail/09_first-pr" }, - ], - }, - ], - }, - - socialLinks: [ - { icon: "github", link: "https://github.com/Asheze1127/HackHub" }, - ], - - search: { - provider: "local", - }, - - footer: { - message: "KCL Support Hub", - }, - }, - - markdown: { - lineNumbers: true, - }, - - mermaid: { - theme: "default", - }, - }) -) diff --git a/docs-host/index.html b/docs-host/index.html new file mode 100644 index 0000000..411d76e --- /dev/null +++ b/docs-host/index.html @@ -0,0 +1,63 @@ + + + + + + KCL Support Hub Docs + + + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/docs-host/package.json b/docs-host/package.json index d6a4c06..b6b92db 100644 --- a/docs-host/package.json +++ b/docs-host/package.json @@ -2,13 +2,6 @@ "name": "@kcl/docs", "private": true, "scripts": { - "dev": "vitepress dev --port 4000", - "build": "vitepress build", - "preview": "vitepress preview --port 4000" - }, - "dependencies": { - "mermaid": "^11.4.1", - "vitepress": "^1.6.3", - "vitepress-plugin-mermaid": "^2.0.17" + "dev": "node server.js" } } diff --git a/docs-host/server.js b/docs-host/server.js new file mode 100644 index 0000000..c271554 --- /dev/null +++ b/docs-host/server.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +// docs-host/server.js — KCL Support Hub ドキュメントサーバー +// プロジェクトルートを静的ファイルとして配信します +// docsify は /docs/*.md を AJAX で取得するため、ルートからの配信が必要です + +const http = require("http") +const fs = require("fs") +const path = require("path") + +const PORT = process.env.PORT || 4000 +const PROJECT_ROOT = path.join(__dirname, "..") + +const MIME = { + ".html": "text/html; charset=utf-8", + ".md": "text/plain; charset=utf-8", + ".js": "application/javascript", + ".css": "text/css", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", +} + +const server = http.createServer((req, res) => { + let urlPath = req.url.split("?")[0] + + // / → docs-host/index.html + if (urlPath === "/" || urlPath === "") { + urlPath = "/docs-host/index.html" + } + + const filePath = path.join(PROJECT_ROOT, urlPath) + + // パストラバーサル防止 + if (!filePath.startsWith(PROJECT_ROOT)) { + res.writeHead(403) + res.end("Forbidden") + return + } + + fs.readFile(filePath, (err, data) => { + if (err) { + res.writeHead(404) + res.end(`Not found: ${urlPath}`) + return + } + + const ext = path.extname(filePath) + res.writeHead(200, { + "Content-Type": MIME[ext] || "application/octet-stream", + "Cache-Control": "no-cache", + }) + res.end(data) + }) +}) + +server.listen(PORT, () => { + console.log("") + console.log(" KCL Support Hub — ドキュメントサーバー") + console.log("") + console.log(` http://localhost:${PORT}`) + console.log("") + console.log(" Ctrl+C で停止") +}) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..baf7f46 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,56 @@ +# KCL Support Hub + +> ハッカソン参加者向け AI サポートシステム + +Slack 中心・AI 一次対応・進捗可視化システムのドキュメントです。 + +--- + +## はじめに + +| ドキュメント | 説明 | +|---|---| +| [設計仕様](/specification) | システム全体の仕様と要件定義 | +| [AI セットアップ](/ai-setup) | Bonsai / Ollama の設定方法 | + +## セットアップ + +| ガイド | 対象 | +|---|---| +| [Slack App](/setup/slack) | Slack App の作成・設定(全パターン共通) | +| [ローカル開発](/setup/local) | 開発・動作確認 | +| [自前サーバー (VPS)](/setup/own) | ハッカソン当日・社内 VPS | +| [クラウド (AWS)](/setup/remote) | 本番・長期運用 | + +## クイックスタート + +```bash +# 1. .env を設定 +cp .env.example .env && nano .env + +# 2. インフラ + DB +make setup + +# 3. 各サービス起動 +make dev-api # ターミナル 1 +make dev-worker # ターミナル 2 +make dev-web # ターミナル 3 + +# 4. Slack に公開 +ngrok http 8080 +make slack-setup URL=https://xxxx.ngrok-free.app +``` + +## システム構成 + +```mermaid +graph TB + User["参加者 Slack"] -->|/question /progress| API["API サーバー (Go)"] + API -->|ACK| User + API --> SQS["SQS Queues"] + SQS --> Worker["Worker (Node.js)"] + Worker --> DB[("PostgreSQL")] + Worker -->|AI トリアージ| AI["Bonsai / Ollama"] + Worker -->|スレッド返信| User + Dashboard["Web ダッシュボード"] -->|REST| API +``` diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000..a666a7a --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,22 @@ +- **概要** + - [トップ](/) + - [設計仕様](/specification) + - [AI セットアップ](/ai-setup) + +- **セットアップ** + - [概要](/setup/) + - [Slack App](/setup/slack) + - [ローカル開発](/setup/local) + - [自前サーバー (VPS)](/setup/own) + - [クラウド (AWS)](/setup/remote) + +- **詳細設計** + - [01 プロダクト定義](/detail/01_product-definition) + - [02 アーキテクチャ](/detail/02_architecture) + - [03 AI フロー](/detail/03_ai-flow) + - [04 データモデル](/detail/04_data-models) + - [05 実装フェーズ](/detail/05_implementation-phases) + - [06 ディレクトリ構成](/detail/06_directory) + - [07 環境変数](/detail/07_env-vars) + - [08 実装タスク](/detail/08_implementation-tasks) + - [09 最初の PR](/detail/09_first-pr) diff --git a/docs/detail/06_directory.md b/docs/detail/06_directory.md index 2c12360..202c40c 100644 --- a/docs/detail/06_directory.md +++ b/docs/detail/06_directory.md @@ -192,7 +192,7 @@ services: # 1️⃣ 全体構成(Monorepo想定) -```id="o8d9si" +``` root/ ├── apps/ # 実行可能アプリ │ ├── web/ @@ -213,7 +213,7 @@ root/ # 2️⃣ フロントエンド構成テンプレ -```id="tq93md" +``` apps/web/ ├── src/ │ ├── app/ # ルーティング層 @@ -231,7 +231,7 @@ apps/web/ ## Featureベース構成(推奨) -```id="t6lm52" +``` features/ ├── auth/ │ ├── components/ @@ -249,7 +249,7 @@ features/ # 3️⃣ バックエンド構成テンプレ(Clean Architecture) -```id="9wh13c" +``` apps/api/ ├── cmd/ # エントリポイント ├── internal/ @@ -267,7 +267,7 @@ apps/api/ # 4️⃣ DDDベース構成テンプレ -```id="kl2m91" +``` src/ ├── modules/ │ ├── user/ @@ -283,7 +283,7 @@ src/ # 5️⃣ マイクロサービス構成 -```id="0b5zvx" +``` services/ ├── auth-service/ ├── core-service/ @@ -295,7 +295,7 @@ services/ # 6️⃣ インフラ構成 -```id="v9k0mz" +``` infra/ ├── terraform/ │ ├── modules/ @@ -311,7 +311,7 @@ infra/ # 7️⃣ ドキュメント構成 -```id="az1k93" +``` docs/ ├── 01_feature-list.md ├── 02_db-design.md @@ -325,7 +325,7 @@ docs/ # 8️⃣ テスト構成テンプレ -```id="c32po1" +``` tests/ ├── unit/ ├── integration/ @@ -337,7 +337,7 @@ tests/ # 9️⃣ ベクトルDB / AI機能がある場合 -```id="q91dte" +``` packages/ ├── embeddings/ │ ├── generator.ts @@ -352,7 +352,7 @@ packages/ # 🔟 状態管理分離パターン(FE) -```id="8t1k4d" +``` stores/ ├── auth.store.ts ├── entity.store.ts @@ -363,7 +363,7 @@ stores/ # 11️⃣ API設計分離パターン -```id="nb29df" +``` api/ ├── client.ts ├── endpoints/ diff --git a/docs/detail/07_env-vars.md b/docs/detail/07_env-vars.md index ff981c9..a0795af 100644 --- a/docs/detail/07_env-vars.md +++ b/docs/detail/07_env-vars.md @@ -15,7 +15,7 @@ SLACK_PROGRESS_CHANNEL_ID=C0XXXXXXXX # #progress-board チャンネル DATABASE_URL=postgres://postgres:postgres@localhost:5432/kcl_support_hub # AWS SQS -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 SQS_QUESTION_NEW_URL=https://sqs.ap-northeast-1.amazonaws.com/xxx/question-new SQS_QUESTION_FOLLOWUP_URL=https://sqs.ap-northeast-1.amazonaws.com/xxx/question-followup SQS_PROGRESS_URL=https://sqs.ap-northeast-1.amazonaws.com/xxx/progress @@ -56,7 +56,7 @@ ONYX_API_URL=http://localhost:3000 # Onyx サーバーの URL(な ONYX_API_KEY=xxxx # optional # AWS SQS(ポーリング用) -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 SQS_QUESTION_NEW_URL=https://sqs... SQS_QUESTION_FOLLOWUP_URL=https://sqs... SQS_PROGRESS_URL=https://sqs... @@ -114,7 +114,7 @@ ONYX_API_URL= ONYX_API_KEY= # === AWS === -AWS_REGION=ap-northeast-1 +AWS_REGION=us-east-1 SQS_QUESTION_NEW_URL= SQS_QUESTION_FOLLOWUP_URL= SQS_PROGRESS_URL= diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index f429065..0000000 --- a/docs/index.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -layout: home - -hero: - name: "KCL Support Hub" - text: "ハッカソン参加者向け AI サポートシステム" - tagline: Slack 中心・AI 一次対応・進捗可視化 - actions: - - theme: brand - text: セットアップ - link: /setup/ - - theme: alt - text: 設計仕様 - link: /specification - -features: - - icon: 🤖 - title: AI トリアージ - details: 参加者の質問を Bonsai (Ollama) が自動分類し、スレッドに回答または追加質問を返します。 - - icon: 📊 - title: 進捗可視化 - details: /progress コマンドで投稿された進捗を #progress-board に自動集約。SOS はメンターに即エスカレーション。 - - icon: ⚡ - title: Slack 3 秒制約対応 - details: 受信 → SQS 投入 → ACK を 3 秒以内に完了。重い処理はすべて非同期 Worker で処理します。 ---- diff --git a/docs/setup/local.md b/docs/setup/local.md index 5e131a2..80ffb6e 100644 --- a/docs/setup/local.md +++ b/docs/setup/local.md @@ -127,11 +127,23 @@ BONSAI_MODEL=qwen2.5:7b docker compose -f infra/docker/compose.yml ps # healthy になっているか確認 ``` -**Worker が SQS を受け取らない** -→ LocalStack のキューが作成されているか確認。 +**Worker が `QueueDoesNotExist` エラーを出す** + +原因: LocalStack v3 はコンテナ再作成時に init スクリプトを自動実行するが、macOS の Docker bind mount は execute ビットを落とすため Permission denied で失敗する。 + +```bash +# キューが存在するか確認 +docker exec docker-localstack-1 awslocal sqs list-queues + +# キューがなければ手動作成 +make init-sqs +``` + +もし `make init-sqs` 後もエラーが続く場合は `.env` の `AWS_REGION` を確認する。 +LocalStack は常に `us-east-1` でキューを作成するため、`us-east-1` でなければならない。 ```bash -aws --endpoint-url http://localhost:4566 sqs list-queues +grep AWS_REGION .env # → AWS_REGION=us-east-1 であること ``` **ngrok URL が毎回変わって不便** diff --git a/docs/setup/slack.md b/docs/setup/slack.md index 130faf7..223acf7 100644 --- a/docs/setup/slack.md +++ b/docs/setup/slack.md @@ -2,6 +2,8 @@ local / own / remote のどのパターンでも最初に一度だけ実施します。 +Socket Mode を使うため **ngrok や公開 URL は不要**です。 + --- ## 1. App 作成(manifest を使う・推奨) @@ -15,13 +17,24 @@ local / own / remote のどのパターンでも最初に一度だけ実施し --- -## 2. 値を `.env` に設定 +## 2. App-Level Token を発行 + +Socket Mode には `xapp-...` トークンが必要です。 + +1. **Basic Information → App-Level Tokens** → **Generate Token and Scopes** +2. Token Name: `socket-mode`(任意) +3. Scope: `connections:write` を追加 +4. **Generate** → 表示された `xapp-...` をコピー + +--- + +## 3. 値を `.env` に設定 | 取得場所 | 変数名 | |---|---| | Basic Information → **App ID** | `SLACK_APP_ID` | | OAuth & Permissions → **Bot User OAuth Token** (`xoxb-...`) | `SLACK_BOT_TOKEN` | -| Basic Information → App Credentials → **Signing Secret** | `SLACK_SIGNING_SECRET` | +| Basic Information → App-Level Tokens (`xapp-...`) | `SLACK_APP_TOKEN` | | メンター通知チャンネル → 詳細の `C0XXXXXXXX` | `SLACK_MENTOR_CHANNEL_ID` | | 進捗投稿チャンネル → 詳細の `C0XXXXXXXX` | `SLACK_PROGRESS_CHANNEL_ID` | @@ -29,29 +42,15 @@ local / own / remote のどのパターンでも最初に一度だけ実施し --- -## 3. Request URL の設定 +## 4. 動作確認 -サーバーが起動してから以下を実行(`` は ngrok URL や本番ドメイン): +API サーバーを起動して Slack で `/question` を実行 → モーダルが開けば OK。 ```bash -make slack-setup URL= -# 例: make slack-setup URL=https://xxxx.ngrok-free.app +make dev-api +# → [socketmode] connected と表示されれば接続成功 ``` -スクリプトが以下を自動更新します: - -| 設定箇所 | URL | -|---|---| -| Slash Commands `/question` | `/slack/commands` | -| Slash Commands `/progress` | `/slack/commands` | -| Interactivity & Shortcuts | `/slack/interactions` | - ---- - -## 4. 動作確認 - -Slack で `/question` を実行 → モーダルが開けば OK。 - --- ## 手動設定 @@ -64,6 +63,6 @@ manifest を使わない場合: 3. **Slash Commands** → Create New Command: - `/question`: Description `質問を投稿する` - `/progress`: Description `進捗を共有する` -4. **Interactivity & Shortcuts** → 有効化 -5. **Install App** -6. Request URL は `make slack-setup URL=...` で後から一括設定 +4. **Interactivity & Shortcuts** → 有効化(URL 不要) +5. **Settings → Socket Mode** → Enable Socket Mode +6. **Install App** diff --git a/infra/docker/compose.yml b/infra/docker/compose.yml index b759a39..418bd96 100644 --- a/infra/docker/compose.yml +++ b/infra/docker/compose.yml @@ -19,7 +19,6 @@ services: image: localstack/localstack:3 environment: SERVICES: sqs - DEFAULT_REGION: ap-northeast-1 LOCALSTACK_HOST: localstack ports: - "4566:4566" diff --git a/infra/docker/init-localstack.sh b/infra/docker/init-localstack.sh old mode 100644 new mode 100755 diff --git a/infra/slack/manifest.json b/infra/slack/manifest.json index 156c8ae..4257584 100644 --- a/infra/slack/manifest.json +++ b/infra/slack/manifest.json @@ -34,17 +34,21 @@ "bot": [ "commands", "chat:write", - "channels:read" + "channels:read", + "channels:history", + "groups:history" ] } }, "settings": { "interactivity": { - "is_enabled": true, - "request_url": "https://your-domain.example.com/slack/interactions" + "is_enabled": true + }, + "event_subscriptions": { + "bot_events": ["message.channels", "message.groups"] }, "org_deploy_enabled": false, - "socket_mode_enabled": false, + "socket_mode_enabled": true, "token_rotation_enabled": false } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1b6e158..b8e2f70 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,4 +2,3 @@ packages: - "apps/*" - "packages/*" - "tools/*" - - "docs-host" From 3c3b25502c7da2f57f3b8d7b7c4ab50ff6db9e1a Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 03:17:42 +0900 Subject: [PATCH 2/3] feat: start console opportunity cycle --- apps/console/.gitignore | 19 + apps/console/app/globals.css | 39 + apps/console/app/layout.tsx | 60 + apps/console/app/login/page.tsx | 14 + apps/console/app/mentor/progress/page.tsx | 66 + .../mentor/progress/teams/[teamId]/page.tsx | 54 + apps/console/app/mentor/questions/page.tsx | 50 + apps/console/app/organizer/page.tsx | 14 + apps/console/app/page.tsx | 87 + apps/console/app/platform/page.tsx | 14 + apps/console/app/sponsor/page.tsx | 14 + apps/console/app/tenant/page.tsx | 14 + apps/console/eslint.config.mjs | 16 + apps/console/next.config.ts | 5 + apps/console/package.json | 27 + apps/console/postcss.config.mjs | 7 + apps/console/shared/lib/api.ts | 81 + apps/console/tsconfig.json | 34 + .../10_combined-product-gap-analysis.md | 204 ++ docs/detail/11_opportunity-cycle-1-plan.md | 105 + docs/specification.md | 2 + pnpm-lock.yaml | 2646 ++--------------- 22 files changed, 1096 insertions(+), 2476 deletions(-) create mode 100644 apps/console/.gitignore create mode 100644 apps/console/app/globals.css create mode 100644 apps/console/app/layout.tsx create mode 100644 apps/console/app/login/page.tsx create mode 100644 apps/console/app/mentor/progress/page.tsx create mode 100644 apps/console/app/mentor/progress/teams/[teamId]/page.tsx create mode 100644 apps/console/app/mentor/questions/page.tsx create mode 100644 apps/console/app/organizer/page.tsx create mode 100644 apps/console/app/page.tsx create mode 100644 apps/console/app/platform/page.tsx create mode 100644 apps/console/app/sponsor/page.tsx create mode 100644 apps/console/app/tenant/page.tsx create mode 100644 apps/console/eslint.config.mjs create mode 100644 apps/console/next.config.ts create mode 100644 apps/console/package.json create mode 100644 apps/console/postcss.config.mjs create mode 100644 apps/console/shared/lib/api.ts create mode 100644 apps/console/tsconfig.json create mode 100644 docs/detail/10_combined-product-gap-analysis.md create mode 100644 docs/detail/11_opportunity-cycle-1-plan.md diff --git a/apps/console/.gitignore b/apps/console/.gitignore new file mode 100644 index 0000000..353d5e2 --- /dev/null +++ b/apps/console/.gitignore @@ -0,0 +1,19 @@ +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store + +# env files +.env* + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/console/app/globals.css b/apps/console/app/globals.css new file mode 100644 index 0000000..0313e2c --- /dev/null +++ b/apps/console/app/globals.css @@ -0,0 +1,39 @@ +@import "tailwindcss"; + +:root { + --background: #f5f0e8; + --foreground: #1e293b; + --panel: rgba(255, 255, 255, 0.84); + --panel-strong: rgba(255, 255, 255, 0.94); + --border: rgba(30, 41, 59, 0.12); + --accent: #0f766e; + --accent-strong: #155e75; + --alert: #b91c1c; + --font-body: "Hiragino Sans", "Yu Gothic", "BIZ UDPGothic", sans-serif; + --font-heading: "Hiragino Mincho ProN", "Yu Mincho", serif; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-body); + --font-mono: var(--font-body); +} + +html { + background: + radial-gradient(circle at top left, rgba(15, 118, 110, 0.16), transparent 28%), + radial-gradient(circle at bottom right, rgba(21, 94, 117, 0.16), transparent 34%), + linear-gradient(180deg, #f8f4ec 0%, #efe5d4 100%); +} + +body { + min-height: 100vh; + color: var(--foreground); + font-family: var(--font-body), sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} diff --git a/apps/console/app/layout.tsx b/apps/console/app/layout.tsx new file mode 100644 index 0000000..496ef2a --- /dev/null +++ b/apps/console/app/layout.tsx @@ -0,0 +1,60 @@ +import type { Metadata } from "next" +import Link from "next/link" +import "./globals.css" + +export const metadata: Metadata = { + title: "HackHub Console", + description: "multi-hackathon control plane for mentors, sponsors, and organizers", +} + +const navItems = [ + { href: "/", label: "Overview" }, + { href: "/mentor/questions", label: "Mentor Queue" }, + { href: "/mentor/progress", label: "Mentor Progress" }, + { href: "/platform", label: "Platform" }, + { href: "/tenant", label: "Tenant" }, + { href: "/organizer", label: "Organizer" }, + { href: "/sponsor", label: "Sponsor" }, +] + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+
+
+
+

Opportunity Branch

+ + HackHub Console + +

+ `progress-checker` の運用導線と `hack-evaluater` の多ハッカソン前提をつなぐ新しい control-plane。 +

+
+ + Login Shell + +
+ +
+
{children}
+
+ + + ) +} diff --git a/apps/console/app/login/page.tsx b/apps/console/app/login/page.tsx new file mode 100644 index 0000000..231681a --- /dev/null +++ b/apps/console/app/login/page.tsx @@ -0,0 +1,14 @@ +export default function LoginShellPage() { + return ( +
+

Login shell

+

+ Auth is intentionally deferred, but the route boundary exists now. +

+

+ `progress-checker` では Mentor ログインが必要で、`hack-evaluater` では role / scope 切替が前提です。 + Cycle 1 では認証実装をまだ入れず、この route を後続 slice の固定入口として先に確保しています。 +

+
+ ) +} diff --git a/apps/console/app/mentor/progress/page.tsx b/apps/console/app/mentor/progress/page.tsx new file mode 100644 index 0000000..2ed0a05 --- /dev/null +++ b/apps/console/app/mentor/progress/page.tsx @@ -0,0 +1,66 @@ +import Link from "next/link" +import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" + +export default async function MentorProgressPage() { + const [logs, teams] = await Promise.all([ + fetchProgressLogs({ limit: 30 }).catch(() => []), + fetchTeams().catch(() => []), + ]) + + const teamMap = new Map(teams.map((team) => [team.id, team])) + const sosCount = logs.filter((log) => log.is_sos).length + + return ( +
+
+
+

Mentor

+

+ Progress overview +

+

+ `progress-checker` で必要だった team progress detail への導線を新しい console 側に作っています。 +

+
+
+

SOS

+

{sosCount}

+

最新 progress logs のうち SOS フラグ付きの件数

+
+
+ +
+ {logs.length === 0 ? ( +
+ progress データがまだありません。API 未接続またはデータ未投入の可能性があります。 +
+ ) : ( + logs.map((log) => { + const team = log.team_id ? teamMap.get(log.team_id) : null + return ( +
+
+ {log.phase} + {log.is_sos ? ( + SOS + ) : null} +
+

{team?.name ?? "Unassigned team"}

+ {log.status_text ?

現状: {log.status_text}

: null} + {log.blockers ?

詰まり: {log.blockers}

: null} +
+ {new Date(log.created_at).toLocaleString("ja-JP")} + {log.team_id ? ( + + Team detail + + ) : null} +
+
+ ) + }) + )} +
+
+ ) +} diff --git a/apps/console/app/mentor/progress/teams/[teamId]/page.tsx b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx new file mode 100644 index 0000000..dc61f34 --- /dev/null +++ b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx @@ -0,0 +1,54 @@ +import Link from "next/link" +import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" + +type TeamProgressDetailPageProps = { + params: Promise<{ teamId: string }> +} + +export default async function TeamProgressDetailPage({ params }: TeamProgressDetailPageProps) { + const { teamId } = await params + const [teams, logs] = await Promise.all([ + fetchTeams().catch(() => []), + fetchProgressLogs({ team_id: teamId, limit: 100 }).catch(() => []), + ]) + + const team = teams.find((candidate) => candidate.id === teamId) + + return ( +
+
+ + Back to progress overview + +

+ {team?.name ?? "Unknown team"} +

+

+ `progress-checker` docs にあった team detail route を先に切り出しました。後続 slice で hackathon scope と role 判定を重ねます。 +

+
+ +
+ {logs.length === 0 ? ( +
+ この team の progress log はまだありません。 +
+ ) : ( + logs.map((log) => ( +
+
+ {log.phase} + {log.is_sos ? ( + SOS + ) : null} +
+ {log.status_text ?

現状: {log.status_text}

: null} + {log.blockers ?

詰まり: {log.blockers}

: null} +

{new Date(log.created_at).toLocaleString("ja-JP")}

+
+ )) + )} +
+
+ ) +} diff --git a/apps/console/app/mentor/questions/page.tsx b/apps/console/app/mentor/questions/page.tsx new file mode 100644 index 0000000..9a5879f --- /dev/null +++ b/apps/console/app/mentor/questions/page.tsx @@ -0,0 +1,50 @@ +import Link from "next/link" +import { fetchQuestions } from "@/shared/lib/api" + +export default async function MentorQuestionsPage() { + const questions = await fetchQuestions({ limit: 20 }).catch(() => []) + + return ( +
+
+

Mentor

+

+ Question queue +

+

+ 既存 API の質問一覧を新しい console で再利用しています。後続 slice で hackathon scope と web-auth をここに載せます。 +

+
+ +
+ {questions.length === 0 ? ( +
+ 質問データがまだありません。API 未接続またはデータ未投入の可能性があります。 +
+ ) : ( + questions.map((question) => ( +
+
+ {question.status} + {question.category ? ( + {question.category} + ) : null} + {question.escalated_to_mentor ? ( + escalated + ) : null} +
+

{question.title}

+ {question.body ?

{question.body}

: null} +
+ {new Date(question.created_at).toLocaleString("ja-JP")} + + API detail + +
+
+ )) + )} +
+
+ ) +} diff --git a/apps/console/app/organizer/page.tsx b/apps/console/app/organizer/page.tsx new file mode 100644 index 0000000..fb2e548 --- /dev/null +++ b/apps/console/app/organizer/page.tsx @@ -0,0 +1,14 @@ +export default function OrganizerPage() { + return ( +
+

HackathonOrganizer

+

+ Per-hackathon operations shell +

+

+ 招待管理、Hackathon 単位のスカウト ON/OFF、Judge 送信権限はここで扱います。 + `progress-checker` の mentor 運用画面とは別責務に分け、後続で hackathon scope を明示的に入れます。 +

+
+ ) +} diff --git a/apps/console/app/page.tsx b/apps/console/app/page.tsx new file mode 100644 index 0000000..4ee4d87 --- /dev/null +++ b/apps/console/app/page.tsx @@ -0,0 +1,87 @@ +import Link from "next/link" + +const roleCards = [ + { + href: "/mentor/questions", + eyebrow: "Mentor", + title: "Question queue and progress overview", + body: "既存の `changeHack` API を読みながら、`progress-checker` の mentor 運用導線を新しい console に切り出します。", + }, + { + href: "/platform", + eyebrow: "PlatformAdmin", + title: "Tenant bootstrap shell", + body: "複数ハッカソン前提の control-plane をここから始めます。Tenant 作成と初期割り当ては次の slice で実装します。", + }, + { + href: "/tenant", + eyebrow: "TenantAdmin", + title: "Hackathon and sponsor management shell", + body: "Sponsor 可視範囲、Hackathon 作成、Organizer 割り当ての受け皿になる route shell を先に用意します。", + }, + { + href: "/organizer", + eyebrow: "HackathonOrganizer", + title: "Per-hackathon operation shell", + body: "招待管理、スカウト設定、Judge 権限設定を hackathon 単位で扱うための導線です。", + }, +] + +const cycleSteps = [ + "Step 1: docs と code の差分を固定し、単一イベント前提の drift を明文化する", + "Step 2: 別 app の control-plane を作り、既存 apps/web と責務を分離する", + "Step 3: mentor 導線を実データ接続しながら multi-hackathon 化の受け皿を作る", +] + +export default function HomePage() { + return ( +
+
+

Cycle 1

+

+ Start the hackathon-scoped live board before more single-event assumptions harden. +

+

+ 現在の `apps/web` は KCL 単一イベント向けの support dashboard です。ここではそれを壊さずに、 + hackathon scope を導入できる新しい board を `apps/console` 側で育てます。 +

+
+ {roleCards.map((card) => ( + +

{card.eyebrow}

+

{card.title}

+

{card.body}

+ + ))} +
+
+ + +
+ ) +} diff --git a/apps/console/app/platform/page.tsx b/apps/console/app/platform/page.tsx new file mode 100644 index 0000000..fec900b --- /dev/null +++ b/apps/console/app/platform/page.tsx @@ -0,0 +1,14 @@ +export default function PlatformPage() { + return ( +
+

PlatformAdmin

+

+ Tenant bootstrap shell +

+

+ 次の slice で Tenant 一覧、Tenant 作成、初期 TenantAdmin 割り当てをここに実装します。 + 現時点では multi-hackathon product を `apps/web` から分離するための route shell です。 +

+
+ ) +} diff --git a/apps/console/app/sponsor/page.tsx b/apps/console/app/sponsor/page.tsx new file mode 100644 index 0000000..437935a --- /dev/null +++ b/apps/console/app/sponsor/page.tsx @@ -0,0 +1,14 @@ +export default function SponsorPage() { + return ( +
+

Sponsor

+

+ Sponsor-facing shell +

+

+ 閲覧可能 Hackathon 範囲を前提に、将来的にはスカウト作成や送信履歴をここへ寄せます。 + Cycle 1 では sponsor flow の受け皿を先に切り出します。 +

+
+ ) +} diff --git a/apps/console/app/tenant/page.tsx b/apps/console/app/tenant/page.tsx new file mode 100644 index 0000000..dccf464 --- /dev/null +++ b/apps/console/app/tenant/page.tsx @@ -0,0 +1,14 @@ +export default function TenantPage() { + return ( +
+

TenantAdmin

+

+ Hackathon and sponsor management shell +

+

+ Sponsor 招待、可視範囲設定、Hackathon 作成、Organizer 割り当てはこの route に集約します。 + まだ backend がないため、Cycle 1 では責務の切り出しだけを先に確定させます。 +

+
+ ) +} diff --git a/apps/console/eslint.config.mjs b/apps/console/eslint.config.mjs new file mode 100644 index 0000000..4ddbed1 --- /dev/null +++ b/apps/console/eslint.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig, globalIgnores } from "eslint/config" +import nextVitals from "eslint-config-next/core-web-vitals" +import nextTs from "eslint-config-next/typescript" + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([ + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]) + +export default eslintConfig diff --git a/apps/console/next.config.ts b/apps/console/next.config.ts new file mode 100644 index 0000000..f62ca42 --- /dev/null +++ b/apps/console/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next" + +const nextConfig: NextConfig = {} + +export default nextConfig diff --git a/apps/console/package.json b/apps/console/package.json new file mode 100644 index 0000000..72d38c2 --- /dev/null +++ b/apps/console/package.json @@ -0,0 +1,27 @@ +{ + "name": "@hackhub/console", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build --webpack", + "start": "next start", + "lint": "eslint", + "typecheck": "next typegen && tsc --noEmit" + }, + "dependencies": { + "next": "16.2.2", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.2", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/apps/console/postcss.config.mjs b/apps/console/postcss.config.mjs new file mode 100644 index 0000000..2f8795a --- /dev/null +++ b/apps/console/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/apps/console/shared/lib/api.ts b/apps/console/shared/lib/api.ts new file mode 100644 index 0000000..e9c94bb --- /dev/null +++ b/apps/console/shared/lib/api.ts @@ -0,0 +1,81 @@ +const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080" + +export interface Question { + id: string + team_id: string | null + slack_user_id: string + slack_thread_ts: string | null + slack_channel_id: string + title: string + body: string | null + category: string | null + confidence: number | null + status: string + escalated_to_mentor: boolean + created_at: string +} + +export interface ProgressLog { + id: string + team_id: string | null + slack_user_id: string + phase: string + status_text: string | null + blockers: string | null + is_sos: boolean + created_at: string +} + +export interface Team { + id: string + name: string + slack_channel_id: string | null + tech_stack: string[] + phase: string + is_sos: boolean +} + +async function apiFetch(path: string): Promise { + const response = await fetch(`${API_BASE}${path}`, { + cache: "no-store", + headers: { + "Content-Type": "application/json", + }, + }) + + if (!response.ok) { + throw new Error(`API error ${response.status} for ${path}`) + } + + return response.json() as Promise +} + +export async function fetchQuestions(params?: { + status?: string + limit?: number + offset?: number +}): Promise { + const query = new URLSearchParams() + if (params?.status) query.set("status", params.status) + if (params?.limit != null) query.set("limit", String(params.limit)) + if (params?.offset != null) query.set("offset", String(params.offset)) + const queryString = query.toString() + return apiFetch(`/api/questions${queryString ? `?${queryString}` : ""}`) +} + +export async function fetchProgressLogs(params?: { + team_id?: string + limit?: number + offset?: number +}): Promise { + const query = new URLSearchParams() + if (params?.team_id) query.set("team_id", params.team_id) + if (params?.limit != null) query.set("limit", String(params.limit)) + if (params?.offset != null) query.set("offset", String(params.offset)) + const queryString = query.toString() + return apiFetch(`/api/progress${queryString ? `?${queryString}` : ""}`) +} + +export async function fetchTeams(): Promise { + return apiFetch("/api/teams") +} diff --git a/apps/console/tsconfig.json b/apps/console/tsconfig.json new file mode 100644 index 0000000..3a13f90 --- /dev/null +++ b/apps/console/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/docs/detail/10_combined-product-gap-analysis.md b/docs/detail/10_combined-product-gap-analysis.md new file mode 100644 index 0000000..6f339cd --- /dev/null +++ b/docs/detail/10_combined-product-gap-analysis.md @@ -0,0 +1,204 @@ +# 10. Combined Product Gap Analysis + +--- + +## 1. 前提 + +このリポジトリは、単純に `changeHack` を進めるだけではなく、以下を統合したプロダクトとして扱う。 + +- `progress-checker` の Slack 起点の質問受付・進捗可視化・Issue 化構想 +- `hack-evaluater` の multi-hackathon / role-aware なプロダクトモデル + +加えて、現時点のユーザー指示として以下を優先前提にする。 + +- 複数ハッカソンに対応する +- 対象ユーザーは参加者、企業スポンサー、主催者を含む +- 既存 `apps/web` とは別に新しい web-based control-plane を持つ + +--- + +## 2. すでに実装されているもの + +### `changeHack` で実装済み + +- Slack `/question` の modal 起動 +- Slack `/progress` の modal 起動 +- SQS 経由の `question:new` / `question:followup` / `progress` 非同期処理 +- AI 一次回答 +- 追加質問のスレッド返信 +- メンターチャンネルへのエスカレーション +- 質問一覧 / 質問詳細 / 進捗一覧 / チーム一覧の read API +- 既存 `apps/web` の簡易ダッシュボード + +### ここまでで足りている観点 + +- Slack の 3 秒制約への基本対応 +- 質問と進捗を Postgres に保存する基盤 +- mentor 向けの最低限の監視 UI + +--- + +## 3. `progress-checker` 由来で未実装または不足しているもの + +### High + +- Slack 議論の Issue 化フロー + - `issue` queue + - Issue Worker + - thread history 取得 + - GitHub Issue 作成 + - Slack thread への Issue URL 返却 + +- team progress detail + - `progress-checker` では `/progress/teams/:teamId` が必要 + - `changeHack` の既存 `apps/web` には未実装 + +- Mentor 用 web 運用導線 + - `progress-checker` は login, question queue, question detail, progress overview を前提 + - `changeHack` の既存 `apps/web` は read-only dashboard で、auth / role 前提がない + +### Medium + +- `question_sessions` 的な継続回答の状態管理 + - 現状は thread reply で follow-up を再処理できる + - ただし `awaiting_user` などの明示状態ストアがない + +- `issue` / `question` / `progress` の DLQ と運用パス + - docs にはあるが、実装はまだ最小構成 + +- Web からの mentor 操作 + - 状態更新 + - 解決マーク + - コメント + +### Low + +- Onyx / local knowledge の運用 UI +- 高度な可視化 + +--- + +## 4. `hack-evaluater` 由来で不足しているもの + +### Critical + +- multi-hackathon domain model + - Tenant + - Hackathon + - role binding + - sponsor visibility scope + +- role-aware access model + - `PlatformAdmin` + - `TenantAdmin` + - `HackathonOrganizer` + - `Sponsor` + - `Judge` + - `Hacker` + +- 招待 / 参加 / scope 切替 + - Hackathon 招待 + - Sponsor 招待 + - role / scope switch + +### High + +- sponsor / organizer / platform 向け web 画面 +- Hackathon 単位設定 +- Sponsor の閲覧可能 Hackathon 制御 + +### Medium + +- scout workflow +- email verification +- outbound email notifications + +--- + +## 5. サブエージェントによる追補 findings + +### Critical + +- 現在の `changeHack` は単一イベント前提で、`hackathons`・membership・role binding・sponsor visibility が存在しない +- このまま feature を積むと、後から multi-hackathon へ広げる際の再設計コストが大きい + +### High + +- `/api/questions`・`/api/progress`・`/api/teams` は認証や scope 制約なしで全件読める +- sponsor / organizer 分離前提とは整合しない + +- Slack payload の `team_id` は Slack workspace ID だが、product team と誤解しやすい形で流れている +- しかも worker で persistence 時に落としているため、現在でも domain scope を再構成できない + +### Medium + +- docs は webhook-first / layered frontend を説明しているが、現実装は Socket Mode + app 直置きに寄っている +- question status machine も docs と実装でズレがある + +--- + +## 6. 現行実装の drift + +### Product drift + +- 現実装は KCL 単一イベント向け support hub に寄っている +- sponsor / organizer / platform の業務フローが存在しない +- hackathon scope がないため、後から multi-hackathon を載せると再配線が大きい + +### Architecture drift + +- docs では webhook 受信前提、現実装は Socket Mode を採用 +- これは直ちにバグではないが、インフラ docs と運用 docs にズレがある + +### UI drift + +- `apps/web` は mentor 運用の read-only dashboard に近い +- control-plane と participant-facing / sponsor-facing app が分離されていない + +--- + +## 7. 優先順位 + +### 先にやるべきこと + +1. hackathon を top-level scope にする +2. Slack ingress が product scope を落とさないようにする +3. read path に hackathon filter と role boundary を入れる +4. その上で新しい control-plane を `apps/web` と分離する + +### まだ後ろでよいこと + +1. full sponsor flow の実装 +2. scout send flow +3. Slack discussion to Issue automation +4. auth / invite の本実装 + +理由: + +- いま Issue 化や sponsor flow を足しても、単一イベント前提の UI と domain に強く結びつく +- 先に control-plane を切り出した方が後続の role-aware 実装を載せやすい + +--- + +## 8. Cycle 1 の結論 + +Cycle 1 では、`hackathon-scoped live board` を最初の slice とする。 + +その中でこの branch では、先行して: + +- 新しい control-plane app `apps/console` を追加 +- mentor の question/progress 導線を新 app に移植 +- 後続の hackathon scope 実装を載せる受け皿を作る + +次に実装すべき本体は: + +- `hackathons` 導入 +- `teams.hackathon_id` +- Slack channel / workspace と hackathon の mapping +- hackathon filter 付き read path + +これにより: + +- `progress-checker` の mentor 導線を前進させる +- `hack-evaluater` の multi-role / multi-hackathon 前提を受け入れる器を先に作れる +- 既存 `apps/web` を壊さずに前進できる diff --git a/docs/detail/11_opportunity-cycle-1-plan.md b/docs/detail/11_opportunity-cycle-1-plan.md new file mode 100644 index 0000000..807ca5b --- /dev/null +++ b/docs/detail/11_opportunity-cycle-1-plan.md @@ -0,0 +1,105 @@ +# 11. Opportunity Cycle 1 Plan + +--- + +## 1. cycle の目的 + +`progress-checker` の mentor 運用導線を前進させつつ、`hack-evaluater` の multi-hackathon / role-aware product へ移行できる新しい app 境界を作る。 + +--- + +## 2. cycle で扱う slice + +### Slice name + +hackathon-scoped live board + +### User value + +- 参加者の質問と進捗が複数 hackathon 間で混ざらなくなる +- organizer は hackathon 単位で queue / SOS / board を見られる +- sponsor は許可された hackathon の sponsor-safe board だけを見られる + +### Why now + +- 現在の schema と Slack ingestion は single-event 前提で、これ以上 feature を積むと rework が増える +- hackathon scope を先に入れないと sponsor / organizer / platform の分離が進められない + +--- + +## 3. Cycle 1 の実装範囲 + +### In scope + +- `hackathons` を top-level scope として導入する設計 +- `teams.hackathon_id` 導入方針 +- Slack から入る question / progress に scope を持たせる設計 +- hackathon filter 付き board の方向性整理 +- 新しい Next.js app `apps/console` +- top-level overview page +- login shell page +- platform / tenant / organizer / sponsor の route shell +- mentor question queue page +- mentor progress overview page +- mentor team progress detail page +- combined-product gap analysis doc +- cycle plan doc + +### Out of scope + +- full auth の実装 +- sponsor 可視範囲の backend +- issue queue / Issue Worker +- scout workflow +- invite flow + +--- + +## 4. acceptance criteria + +1. `apps/console` が `apps/web` と独立した別 app として存在する +2. `apps/console` に以下の route が存在する + - `/` + - `/login` + - `/platform` + - `/tenant` + - `/organizer` + - `/sponsor` + - `/mentor/questions` + - `/mentor/progress` + - `/mentor/progress/teams/:teamId` +3. mentor の question/progress 画面は既存 API を使って read-only 表示できる +4. combined product の差分と priority が docs に明文化される +5. hackathon scope を導入するための schema / ingress / read-path の次 slice が明確になる +6. 変更は `opportunity` branch に留まり、`dev` へは merge しない + +--- + +## 5. 次の slice + +### Slice 2 + +multi-hackathon domain schema + +- tenants +- hackathons +- role_bindings +- memberships +- sponsor visibility scopes + +### Slice 3 + +auth and role-aware routing + +- login +- role / scope switch +- protected routes + +### Slice 4 + +Slack payload scoping and issue flow + +- question / progress の hackathon 紐付け +- issue queue +- Issue Worker +- GitHub issue creation diff --git a/docs/specification.md b/docs/specification.md index 1065188..6b2b562 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -27,6 +27,8 @@ Slack を主入口とした AI 支援・進捗管理・メンターエスカレ | [07_env-vars.md](./detail/07_env-vars.md) | 必要な環境変数一覧 | | [08_implementation-tasks.md](./detail/08_implementation-tasks.md) | 具体的な実装タスク一覧 | | [09_first-pr.md](./detail/09_first-pr.md) | 着手すべき最初の PR 案 | +| [10_combined-product-gap-analysis.md](./detail/10_combined-product-gap-analysis.md) | `progress-checker` / `hack-evaluater` / `changeHack` の差分整理 | +| [11_opportunity-cycle-1-plan.md](./detail/11_opportunity-cycle-1-plan.md) | opportunity branch で進める Cycle 1 の実装計画 | --- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec71fc6..a9b263f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,43 @@ importers: specifier: ^5.4.0 version: 5.9.3 + apps/console: + dependencies: + next: + specifier: 16.2.2 + version: 16.2.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: 19.2.4 + version: 19.2.4 + react-dom: + specifier: 19.2.4 + version: 19.2.4(react@19.2.4) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.2.2 + '@types/node': + specifier: ^20 + version: 20.19.39 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + eslint: + specifier: ^9 + version: 9.39.4(jiti@2.6.1) + eslint-config-next: + specifier: 16.2.2 + version: 16.2.2(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.2.2 + typescript: + specifier: ^5 + version: 5.9.3 + apps/web: dependencies: class-variance-authority: @@ -53,7 +90,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.2 - version: 16.2.2(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + version: 16.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) tailwindcss: specifier: ^4 version: 4.2.2 @@ -101,18 +138,6 @@ importers: specifier: ^5.4.0 version: 5.9.3 - docs-host: - dependencies: - mermaid: - specifier: ^11.4.1 - version: 11.14.0 - vitepress: - specifier: ^1.6.3 - version: 1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3) - vitepress-plugin-mermaid: - specifier: ^2.0.17 - version: 2.0.17(mermaid@11.14.0)(vitepress@1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3)) - packages/schemas: dependencies: zod: @@ -217,89 +242,10 @@ packages: vue: optional: true - '@algolia/abtesting@1.16.1': - resolution: {integrity: sha512-Xxk4l00pYI+jE0PNw8y0MvsQWh5278WRtZQav8/BMMi3HKi2xmeuqe11WJ3y8/6nuBHdv39w76OpJb09TMfAVQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/autocomplete-core@1.17.7': - resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} - - '@algolia/autocomplete-plugin-algolia-insights@1.17.7': - resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} - peerDependencies: - search-insights: '>= 1 < 3' - - '@algolia/autocomplete-preset-algolia@1.17.7': - resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - - '@algolia/autocomplete-shared@1.17.7': - resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} - peerDependencies: - '@algolia/client-search': '>= 4.9.1 < 6' - algoliasearch: '>= 4.9.1 < 6' - - '@algolia/client-abtesting@5.50.1': - resolution: {integrity: sha512-4peZlPXMwTOey9q1rQKMdCnwZb/E95/1e+7KujXpLLSh0FawJzg//U2NM+r4AiJy4+naT2MTBhj0K30yshnVTA==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-analytics@5.50.1': - resolution: {integrity: sha512-i+aWHHG8NZvGFHtPeMZkxL2Loc6Fm7iaRo15lYSMx8gFL+at9vgdWxhka7mD1fqxkrxXsQstUBCIsSY8FvkEOw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-common@5.50.1': - resolution: {integrity: sha512-Hw52Fwapyk/7hMSV/fI4+s3H9MGZEUcRh4VphyXLAk2oLYdndVUkc6KBi0zwHSzwPAr+ZBwFPe2x6naUt9mZGw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-insights@5.50.1': - resolution: {integrity: sha512-Bn/wtwhJ7p1OD/6pY+Zzn+zlu2N/SJnH46md/PAbvqIzmjVuwjNwD4y0vV5Ov8naeukXdd7UU9v550+v8+mtlg==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-personalization@5.50.1': - resolution: {integrity: sha512-0V4Tu0RWR8YxkgI9EPVOZHGE4K5pEIhkLNN0CTkP/rnPsqaaSQpNMYW3/mGWdiKOWbX0iVmwLB9QESk3H0jS5g==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-query-suggestions@5.50.1': - resolution: {integrity: sha512-jofcWNYMXJDDr87Z2eivlWY6o71Zn7F7aOvQCXSDAo9QTlyf7BhXEsZymLUvF0O1yU9Q9wvrjAWn8uVHYnAvgw==} - engines: {node: '>= 14.0.0'} - - '@algolia/client-search@5.50.1': - resolution: {integrity: sha512-OteRb8WubcmEvU0YlMJwCXs3Q6xrdkb0v50/qZBJP1TF0CvujFZQM++9BjEkTER/Jr9wbPHvjSFKnbMta0b4dQ==} - engines: {node: '>= 14.0.0'} - - '@algolia/ingestion@1.50.1': - resolution: {integrity: sha512-0GmfSgDQK6oiIVXnJvGxtNFOfosBspRTR7csCOYCTL1P8QtxX2vDCIKwTM7xdSAEbJaZ43QlWg25q0Qdsndz8Q==} - engines: {node: '>= 14.0.0'} - - '@algolia/monitoring@1.50.1': - resolution: {integrity: sha512-ySuigKEe4YjYV3si8NVk9BHQpFj/1B+ON7DhhvTvbrZJseHQQloxzq0yHwKmznSdlO6C956fx4pcfOKkZClsyg==} - engines: {node: '>= 14.0.0'} - - '@algolia/recommend@5.50.1': - resolution: {integrity: sha512-Cp8T/B0gVmjFlzzp6eP47hwKh5FGyeqQp1N48/ANDdvdiQkPqLyFHQVDwLBH0LddfIPQE+yqmZIgmKc82haF4A==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-browser-xhr@5.50.1': - resolution: {integrity: sha512-XKdGGLikfrlK66ZSXh/vWcXZZ8Vg3byDFbJD8pwEvN1FoBRGxhxya476IY2ohoTymLa4qB5LBRlIa+2TLHx3Uw==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-fetch@5.50.1': - resolution: {integrity: sha512-mBAU6WyVsDwhHyGM+nodt1/oebHxgvuLlOAoMGbj/1i6LygDHZWDgL1t5JEs37x9Aywv7ZGhqbM1GsfZ54sU6g==} - engines: {node: '>= 14.0.0'} - - '@algolia/requester-node-http@5.50.1': - resolution: {integrity: sha512-qmo1LXrNKLHvJE6mdQbLnsZAoZvj7VyF2ft4xmbSGWI2WWm87fx/CjUX4kEExt4y0a6T6nEts6ofpUfH5TEE1A==} - engines: {node: '>= 14.0.0'} - '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@antfu/install-pkg@1.1.0': - resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -484,54 +430,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@braintree/sanitize-url@6.0.4': - resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} - - '@braintree/sanitize-url@7.1.2': - resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - - '@chevrotain/cst-dts-gen@12.0.0': - resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} - - '@chevrotain/gast@12.0.0': - resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - - '@chevrotain/regexp-to-ast@12.0.0': - resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - - '@chevrotain/types@12.0.0': - resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - - '@chevrotain/utils@12.0.0': - resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} - '@docsearch/css@3.8.2': - resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} - - '@docsearch/js@3.8.2': - resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} - - '@docsearch/react@3.8.2': - resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} - peerDependencies: - '@types/react': '>= 16.8.0 < 19.0.0' - react: '>= 16.8.0 < 19.0.0' - react-dom: '>= 16.8.0 < 19.0.0' - search-insights: '>= 1 < 3' - peerDependenciesMeta: - '@types/react': - optional: true - react: - optional: true - react-dom: - optional: true - search-insights: - optional: true - '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} @@ -541,144 +443,6 @@ packages: '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -733,15 +497,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@iconify-json/simple-icons@1.2.77': - resolution: {integrity: sha512-oaENvo6C3BkAEWMlcQA3XemxU9v2SFOTlApSUCODAkIu1haeLCjzrmH3HgmGqjRnJjM+LevO8sA+MgdMHBFBDA==} - - '@iconify/types@2.0.0': - resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - - '@iconify/utils@3.1.0': - resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} - '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -914,12 +669,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@mermaid-js/mermaid-mindmap@9.3.0': - resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} - - '@mermaid-js/parser@1.1.0': - resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} - '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -1001,171 +750,9 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@rollup/rollup-android-arm-eabi@4.60.1': - resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.1': - resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.1': - resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.1': - resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.1': - resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.1': - resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.60.1': - resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.1': - resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.60.1': - resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.60.1': - resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.1': - resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.1': - resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.1': - resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.1': - resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} - cpu: [x64] - os: [win32] - '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@shikijs/core@2.5.0': - resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} - - '@shikijs/engine-javascript@2.5.0': - resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} - - '@shikijs/engine-oniguruma@2.5.0': - resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} - - '@shikijs/langs@2.5.0': - resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} - - '@shikijs/themes@2.5.0': - resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} - - '@shikijs/transformers@2.5.0': - resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} - - '@shikijs/types@2.5.0': - resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@smithy/config-resolver@4.4.14': resolution: {integrity: sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==} engines: {node: '>=18.0.0'} @@ -1453,129 +1040,18 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@types/d3-array@3.2.2': - resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} - - '@types/d3-axis@3.0.6': - resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} - - '@types/d3-brush@3.0.6': - resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} - - '@types/d3-chord@3.0.6': - resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} - - '@types/d3-color@3.1.3': - resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} - - '@types/d3-contour@3.0.6': - resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} - - '@types/d3-delaunay@6.0.4': - resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} - - '@types/d3-dispatch@3.0.7': - resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} - - '@types/d3-drag@3.0.7': - resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} - - '@types/d3-dsv@3.0.7': - resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} - - '@types/d3-ease@3.0.2': - resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - - '@types/d3-fetch@3.0.7': - resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} - - '@types/d3-force@3.0.10': - resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} - - '@types/d3-format@3.0.4': - resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} - - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - - '@types/d3-hierarchy@3.1.7': - resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} - - '@types/d3-interpolate@3.0.4': - resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} - - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-polygon@3.0.2': - resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} - - '@types/d3-quadtree@3.0.6': - resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} - - '@types/d3-random@3.0.3': - resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} - - '@types/d3-scale-chromatic@3.1.0': - resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} - - '@types/d3-scale@4.0.9': - resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} - - '@types/d3-selection@3.0.11': - resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - - '@types/d3-shape@3.1.8': - resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} - - '@types/d3-time-format@4.0.3': - resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} - - '@types/d3-time@3.0.4': - resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} - - '@types/d3-timer@3.0.2': - resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} - - '@types/d3-transition@3.0.9': - resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} - - '@types/d3-zoom@3.0.8': - resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} - - '@types/d3@7.4.3': - resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/diff-match-patch@1.0.36': resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/geojson@7946.0.16': - resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/node@20.19.39': resolution: {integrity: sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==} @@ -1593,12 +1069,6 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@types/web-bluetooth@0.0.21': - resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - '@typescript-eslint/eslint-plugin@8.58.0': resolution: {integrity: sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1658,9 +1128,6 @@ packages: resolution: {integrity: sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -1764,16 +1231,6 @@ packages: cpu: [x64] os: [win32] - '@upsetjs/venn.js@2.0.0': - resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - - '@vitejs/plugin-vue@5.2.4': - resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} - engines: {node: ^18.0.0 || >=20.0.0} - peerDependencies: - vite: ^5.0.0 || ^6.0.0 - vue: ^3.2.25 - '@vue/compiler-core@3.5.32': resolution: {integrity: sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==} @@ -1786,15 +1243,6 @@ packages: '@vue/compiler-ssr@3.5.32': resolution: {integrity: sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==} - '@vue/devtools-api@7.7.9': - resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} - - '@vue/devtools-kit@7.7.9': - resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - - '@vue/devtools-shared@7.7.9': - resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - '@vue/reactivity@3.5.32': resolution: {integrity: sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==} @@ -1812,56 +1260,6 @@ packages: '@vue/shared@3.5.32': resolution: {integrity: sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==} - '@vueuse/core@12.8.2': - resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} - - '@vueuse/integrations@12.8.2': - resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} - peerDependencies: - async-validator: ^4 - axios: ^1 - change-case: ^5 - drauu: ^0.4 - focus-trap: ^7 - fuse.js: ^7 - idb-keyval: ^6 - jwt-decode: ^4 - nprogress: ^0.2 - qrcode: ^1.5 - sortablejs: ^1 - universal-cookie: ^7 - peerDependenciesMeta: - async-validator: - optional: true - axios: - optional: true - change-case: - optional: true - drauu: - optional: true - focus-trap: - optional: true - fuse.js: - optional: true - idb-keyval: - optional: true - jwt-decode: - optional: true - nprogress: - optional: true - qrcode: - optional: true - sortablejs: - optional: true - universal-cookie: - optional: true - - '@vueuse/metadata@12.8.2': - resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} - - '@vueuse/shared@12.8.2': - resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1900,10 +1298,6 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} - algoliasearch@5.50.1: - resolution: {integrity: sha512-/bwdue1/8LWELn/DBalGRfuLsXBLXULJo/yOeavJtDu8rBwxIzC6/Rz9Jg19S21VkJvRuZO1k8CZXBMS73mYbA==} - engines: {node: '>= 14.0.0'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1985,9 +1379,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - birpc@2.9.0: - resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} - bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -2003,270 +1394,72 @@ packages: engines: {node: '>=8'} browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.8: - resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001786: - resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chalk@5.6.2: - resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - chevrotain-allstar@0.4.1: - resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} - peerDependencies: - chevrotain: ^12.0.0 - - chevrotain@12.0.0: - resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} - engines: {node: '>=22.0.0'} - - class-variance-authority@0.7.1: - resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - copy-anything@4.0.5: - resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} - engines: {node: '>=18'} - - cose-base@1.0.3: - resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} - - cose-base@2.2.0: - resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - cytoscape-cose-bilkent@4.1.0: - resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape-fcose@2.2.0: - resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} - peerDependencies: - cytoscape: ^3.2.0 - - cytoscape@3.33.2: - resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} - engines: {node: '>=0.10'} - - d3-array@2.12.1: - resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} - - d3-array@3.2.4: - resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} - engines: {node: '>=12'} - - d3-axis@3.0.0: - resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} - engines: {node: '>=12'} - - d3-brush@3.0.0: - resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} - engines: {node: '>=12'} - - d3-chord@3.0.1: - resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} - engines: {node: '>=12'} - - d3-color@3.1.0: - resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} - engines: {node: '>=12'} - - d3-contour@4.0.2: - resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} - engines: {node: '>=12'} - - d3-delaunay@6.0.4: - resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} - engines: {node: '>=12'} - - d3-dispatch@3.0.1: - resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} - engines: {node: '>=12'} - - d3-drag@3.0.0: - resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} - engines: {node: '>=12'} - - d3-dsv@3.0.1: - resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} - engines: {node: '>=12'} - hasBin: true - - d3-ease@3.0.1: - resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} - engines: {node: '>=12'} - - d3-fetch@3.0.1: - resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} - engines: {node: '>=12'} - - d3-force@3.0.0: - resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} - engines: {node: '>=12'} - - d3-format@3.1.2: - resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} - engines: {node: '>=12'} - - d3-geo@3.1.1: - resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} - engines: {node: '>=12'} - - d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - - d3-interpolate@3.0.1: - resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} - engines: {node: '>=12'} + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true - d3-path@1.0.9: - resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} - d3-polygon@3.0.1: - resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} - engines: {node: '>=12'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} - d3-quadtree@3.0.1: - resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} - engines: {node: '>=12'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} - d3-random@3.0.1: - resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} - engines: {node: '>=12'} + caniuse-lite@1.0.30001786: + resolution: {integrity: sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==} - d3-sankey@0.12.3: - resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} - d3-scale-chromatic@3.1.0: - resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} - engines: {node: '>=12'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - d3-scale@4.0.2: - resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} - engines: {node: '>=12'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} - d3-selection@3.0.0: - resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} - engines: {node: '>=12'} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - d3-shape@1.3.7: - resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} - d3-time-format@4.1.0: - resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} - engines: {node: '>=12'} + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - d3-time@3.1.0: - resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} - engines: {node: '>=12'} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - d3-timer@3.0.1: - resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} - engines: {node: '>=12'} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - d3-transition@3.0.1: - resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} - engines: {node: '>=12'} - peerDependencies: - d3-selection: 2 - 3 + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - d3-zoom@3.0.0: - resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} - engines: {node: '>=12'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - d3@7.9.0: - resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} - engines: {node: '>=12'} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} - dagre-d3-es@7.0.14: - resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -2283,9 +1476,6 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -2314,9 +1504,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - delaunator@5.1.0: - resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2328,9 +1515,6 @@ packages: devalue@5.6.4: resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff-match-patch@1.0.5: resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} @@ -2345,9 +1529,6 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} - dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} @@ -2359,9 +1540,6 @@ packages: electron-to-chromium@1.5.331: resolution: {integrity: sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==} - emoji-regex-xs@1.0.0: - resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} - emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2405,11 +1583,6 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2602,18 +1775,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - focus-trap@7.8.0: - resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} - for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -2674,9 +1839,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hachure-fill@0.5.2: - resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -2704,28 +1866,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2746,13 +1892,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - internmap@1.0.1: - resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - - internmap@2.0.3: - resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} - engines: {node: '>=12'} - is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2859,10 +1998,6 @@ packages: resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} engines: {node: '>= 0.4'} - is-what@5.5.0: - resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} - engines: {node: '>=18'} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2919,20 +2054,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - katex@0.16.45: - resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} - hasBin: true - keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - khroma@2.1.0: - resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - - langium@4.2.2: - resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} - engines: {node: '>=20.10.0', npm: '>=10.2.3'} - language-subtag-registry@0.3.23: resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} @@ -2940,12 +2064,6 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} - layout-base@1.0.2: - resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} - - layout-base@2.0.1: - resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3031,9 +2149,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.18.1: - resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3055,43 +2170,14 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - mark.js@8.11.1: - resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} - - marked@16.4.2: - resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} - engines: {node: '>= 20'} - hasBin: true - math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.14.0: - resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -3106,15 +2192,6 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minisearch@7.2.0: - resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} - - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - - mlly@1.8.2: - resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - moo@0.5.3: resolution: {integrity: sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==} @@ -3171,9 +2248,6 @@ packages: node-releases@2.0.37: resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} - non-layered-tidy-tree-layout@2.0.2: - resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3206,9 +2280,6 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - oniguruma-to-es@3.1.1: - resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -3225,16 +2296,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - path-data-parser@0.1.0: - resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3250,12 +2315,6 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -3301,15 +2360,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - points-on-curve@0.2.0: - resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} - - points-on-path@0.2.1: - resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -3338,9 +2388,6 @@ packages: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} - preact@10.29.1: - resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} - prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3348,9 +2395,6 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3381,15 +2425,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -3414,26 +2449,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - - robust-predicates@3.0.3: - resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - - rollup@4.60.1: - resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - roughjs@4.6.6: - resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -3446,15 +2464,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - search-insights@2.17.3: - resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} - secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -3491,9 +2503,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki@2.5.0: - resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} - side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -3514,13 +2523,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - speakingurl@14.0.1: - resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} - engines: {node: '>=0.10.0'} - split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3564,9 +2566,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -3591,13 +2590,6 @@ packages: babel-plugin-macros: optional: true - stylis@4.3.6: - resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} - - superjson@2.2.6: - resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} - engines: {node: '>=16'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3623,9 +2615,6 @@ packages: peerDependencies: vue: '>=3.2.26 < 4' - tabbable@6.4.0: - resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} - tailwind-merge@3.5.0: resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} @@ -3640,10 +2629,6 @@ packages: resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} engines: {node: '>=18'} - tinyexec@1.0.4: - resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} - engines: {node: '>=18'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -3652,19 +2637,12 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' - ts-dedent@2.2.0: - resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} - engines: {node: '>=6.10'} - ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -3717,9 +2695,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3727,21 +2702,6 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -3759,88 +2719,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - uuid@11.1.0: - resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} - hasBin: true - v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitepress-plugin-mermaid@2.0.17: - resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} - peerDependencies: - mermaid: 10 || 11 - vitepress: ^1.0.0 || ^1.0.0-alpha - - vitepress@1.6.4: - resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} - hasBin: true - peerDependencies: - markdown-it-mathjax3: ^4 - postcss: ^8 - peerDependenciesMeta: - markdown-it-mathjax3: - optional: true - postcss: - optional: true - - vscode-jsonrpc@8.2.0: - resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} - engines: {node: '>=14.0.0'} - - vscode-languageserver-protocol@3.17.5: - resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} - - vscode-languageserver-textdocument@1.0.12: - resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} - - vscode-languageserver-types@3.17.5: - resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} - - vscode-languageserver@9.0.1: - resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} - hasBin: true - - vscode-uri@3.1.0: - resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue@3.5.32: resolution: {integrity: sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==} peerDependencies: @@ -3906,9 +2787,6 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - snapshots: '@ai-sdk/anthropic@0.0.56(zod@3.25.76)': @@ -3952,169 +2830,52 @@ snapshots: '@ai-sdk/react@0.0.70(react@19.2.4)(zod@3.25.76)': dependencies: '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - swr: 2.4.1(react@19.2.4) - throttleit: 2.1.0 - optionalDependencies: - react: 19.2.4 - zod: 3.25.76 - - '@ai-sdk/solid@0.0.54(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - transitivePeerDependencies: - - zod - - '@ai-sdk/svelte@0.0.57(svelte@5.55.1)(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - sswr: 2.2.0(svelte@5.55.1) - optionalDependencies: - svelte: 5.55.1 - transitivePeerDependencies: - - zod - - '@ai-sdk/ui-utils@0.0.50(zod@3.25.76)': - dependencies: - '@ai-sdk/provider': 0.0.26 - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - json-schema: 0.4.0 - secure-json-parse: 2.7.0 - zod-to-json-schema: 3.25.2(zod@3.25.76) - optionalDependencies: - zod: 3.25.76 - - '@ai-sdk/vue@0.0.59(vue@3.5.32(typescript@5.9.3))(zod@3.25.76)': - dependencies: - '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) - '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) - swrv: 1.2.0(vue@3.5.32(typescript@5.9.3)) - optionalDependencies: - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - zod - - '@algolia/abtesting@1.16.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3)': - dependencies: - '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3) - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - search-insights - - '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3)': - dependencies: - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - search-insights: 2.17.3 - transitivePeerDependencies: - - '@algolia/client-search' - - algoliasearch - - '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)': - dependencies: - '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - '@algolia/client-search': 5.50.1 - algoliasearch: 5.50.1 - - '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)': - dependencies: - '@algolia/client-search': 5.50.1 - algoliasearch: 5.50.1 - - '@algolia/client-abtesting@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-analytics@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-common@5.50.1': {} - - '@algolia/client-insights@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-personalization@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-query-suggestions@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/client-search@5.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/ingestion@1.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - - '@algolia/monitoring@1.50.1': - dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + swr: 2.4.1(react@19.2.4) + throttleit: 2.1.0 + optionalDependencies: + react: 19.2.4 + zod: 3.25.76 - '@algolia/recommend@5.50.1': + '@ai-sdk/solid@0.0.54(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + transitivePeerDependencies: + - zod - '@algolia/requester-browser-xhr@5.50.1': + '@ai-sdk/svelte@0.0.57(svelte@5.55.1)(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + sswr: 2.2.0(svelte@5.55.1) + optionalDependencies: + svelte: 5.55.1 + transitivePeerDependencies: + - zod - '@algolia/requester-fetch@5.50.1': + '@ai-sdk/ui-utils@0.0.50(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 + '@ai-sdk/provider': 0.0.26 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + json-schema: 0.4.0 + secure-json-parse: 2.7.0 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + zod: 3.25.76 - '@algolia/requester-node-http@5.50.1': + '@ai-sdk/vue@0.0.59(vue@3.5.32(typescript@5.9.3))(zod@3.25.76)': dependencies: - '@algolia/client-common': 5.50.1 + '@ai-sdk/provider-utils': 1.0.22(zod@3.25.76) + '@ai-sdk/ui-utils': 0.0.50(zod@3.25.76) + swrv: 1.2.0(vue@3.5.32(typescript@5.9.3)) + optionalDependencies: + vue: 3.5.32(typescript@5.9.3) + transitivePeerDependencies: + - zod '@alloc/quick-lru@5.2.0': {} - '@antfu/install-pkg@1.1.0': - dependencies: - package-manager-detector: 1.6.0 - tinyexec: 1.0.4 - '@aws-crypto/sha256-browser@5.2.0': dependencies: '@aws-crypto/sha256-js': 5.2.0 @@ -4552,54 +3313,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@braintree/sanitize-url@6.0.4': - optional: true - - '@braintree/sanitize-url@7.1.2': {} - - '@chevrotain/cst-dts-gen@12.0.0': - dependencies: - '@chevrotain/gast': 12.0.0 - '@chevrotain/types': 12.0.0 - - '@chevrotain/gast@12.0.0': - dependencies: - '@chevrotain/types': 12.0.0 - - '@chevrotain/regexp-to-ast@12.0.0': {} - - '@chevrotain/types@12.0.0': {} - - '@chevrotain/utils@12.0.0': {} - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - '@docsearch/css@3.8.2': {} - - '@docsearch/js@3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3)': - dependencies: - '@docsearch/react': 3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3) - preact: 10.29.1 - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/react' - - react - - react-dom - - search-insights - - '@docsearch/react@3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3)': - dependencies: - '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1)(search-insights@2.17.3) - '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.50.1)(algoliasearch@5.50.1) - '@docsearch/css': 3.8.2 - algoliasearch: 5.50.1 - optionalDependencies: - search-insights: 2.17.3 - transitivePeerDependencies: - - '@algolia/client-search' - '@emnapi/core@1.9.2': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -4616,75 +3333,6 @@ snapshots: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: eslint: 9.39.4(jiti@2.6.1) @@ -4742,18 +3390,6 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@iconify-json/simple-icons@1.2.77': - dependencies: - '@iconify/types': 2.0.0 - - '@iconify/types@2.0.0': {} - - '@iconify/utils@3.1.0': - dependencies: - '@antfu/install-pkg': 1.1.0 - '@iconify/types': 2.0.0 - mlly: 1.8.2 - '@img/colour@1.1.0': optional: true @@ -4875,21 +3511,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@mermaid-js/mermaid-mindmap@9.3.0': - dependencies: - '@braintree/sanitize-url': 6.0.4 - cytoscape: 3.33.2 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) - cytoscape-fcose: 2.2.0(cytoscape@3.33.2) - d3: 7.9.0 - khroma: 2.1.0 - non-layered-tidy-tree-layout: 2.0.2 - optional: true - - '@mermaid-js/parser@1.1.0': - dependencies: - langium: 4.2.2 - '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.2 @@ -4943,123 +3564,8 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@rollup/rollup-android-arm-eabi@4.60.1': - optional: true - - '@rollup/rollup-android-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.1': - optional: true - - '@rollup/rollup-darwin-x64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.1': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.1': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.1': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.1': - optional: true - '@rtsao/scc@1.1.0': {} - '@shikijs/core@2.5.0': - dependencies: - '@shikijs/engine-javascript': 2.5.0 - '@shikijs/engine-oniguruma': 2.5.0 - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 3.1.1 - - '@shikijs/engine-oniguruma@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - - '@shikijs/themes@2.5.0': - dependencies: - '@shikijs/types': 2.5.0 - - '@shikijs/transformers@2.5.0': - dependencies: - '@shikijs/core': 2.5.0 - '@shikijs/types': 2.5.0 - - '@shikijs/types@2.5.0': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - '@smithy/config-resolver@4.4.14': dependencies: '@smithy/node-config-provider': 4.3.13 @@ -5426,150 +3932,14 @@ snapshots: tslib: 2.8.1 optional: true - '@types/d3-array@3.2.2': {} - - '@types/d3-axis@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-brush@3.0.6': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-chord@3.0.6': {} - - '@types/d3-color@3.1.3': {} - - '@types/d3-contour@3.0.6': - dependencies: - '@types/d3-array': 3.2.2 - '@types/geojson': 7946.0.16 - - '@types/d3-delaunay@6.0.4': {} - - '@types/d3-dispatch@3.0.7': {} - - '@types/d3-drag@3.0.7': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-dsv@3.0.7': {} - - '@types/d3-ease@3.0.2': {} - - '@types/d3-fetch@3.0.7': - dependencies: - '@types/d3-dsv': 3.0.7 - - '@types/d3-force@3.0.10': {} - - '@types/d3-format@3.0.4': {} - - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.16 - - '@types/d3-hierarchy@3.1.7': {} - - '@types/d3-interpolate@3.0.4': - dependencies: - '@types/d3-color': 3.1.3 - - '@types/d3-path@3.1.1': {} - - '@types/d3-polygon@3.0.2': {} - - '@types/d3-quadtree@3.0.6': {} - - '@types/d3-random@3.0.3': {} - - '@types/d3-scale-chromatic@3.1.0': {} - - '@types/d3-scale@4.0.9': - dependencies: - '@types/d3-time': 3.0.4 - - '@types/d3-selection@3.0.11': {} - - '@types/d3-shape@3.1.8': - dependencies: - '@types/d3-path': 3.1.1 - - '@types/d3-time-format@4.0.3': {} - - '@types/d3-time@3.0.4': {} - - '@types/d3-timer@3.0.2': {} - - '@types/d3-transition@3.0.9': - dependencies: - '@types/d3-selection': 3.0.11 - - '@types/d3-zoom@3.0.8': - dependencies: - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - - '@types/d3@7.4.3': - dependencies: - '@types/d3-array': 3.2.2 - '@types/d3-axis': 3.0.6 - '@types/d3-brush': 3.0.6 - '@types/d3-chord': 3.0.6 - '@types/d3-color': 3.1.3 - '@types/d3-contour': 3.0.6 - '@types/d3-delaunay': 6.0.4 - '@types/d3-dispatch': 3.0.7 - '@types/d3-drag': 3.0.7 - '@types/d3-dsv': 3.0.7 - '@types/d3-ease': 3.0.2 - '@types/d3-fetch': 3.0.7 - '@types/d3-force': 3.0.10 - '@types/d3-format': 3.0.4 - '@types/d3-geo': 3.1.0 - '@types/d3-hierarchy': 3.1.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-path': 3.1.1 - '@types/d3-polygon': 3.0.2 - '@types/d3-quadtree': 3.0.6 - '@types/d3-random': 3.0.3 - '@types/d3-scale': 4.0.9 - '@types/d3-scale-chromatic': 3.1.0 - '@types/d3-selection': 3.0.11 - '@types/d3-shape': 3.1.8 - '@types/d3-time': 3.0.4 - '@types/d3-time-format': 4.0.3 - '@types/d3-timer': 3.0.2 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - '@types/diff-match-patch@1.0.36': {} '@types/estree@1.0.8': {} - '@types/geojson@7946.0.16': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} - '@types/linkify-it@5.0.0': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdurl@2.0.0': {} - '@types/node@20.19.39': dependencies: undici-types: 6.21.0 @@ -5590,10 +3960,6 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@types/unist@3.0.3': {} - - '@types/web-bluetooth@0.0.21': {} - '@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5685,8 +4051,6 @@ snapshots: '@typescript-eslint/types': 8.58.0 eslint-visitor-keys: 5.0.1 - '@ungap/structured-clone@1.3.0': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -5746,16 +4110,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@upsetjs/venn.js@2.0.0': - optionalDependencies: - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))(vue@3.5.32(typescript@5.9.3))': - dependencies: - vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0) - vue: 3.5.32(typescript@5.9.3) - '@vue/compiler-core@3.5.32': dependencies: '@babel/parser': 7.29.2 @@ -5786,24 +4140,6 @@ snapshots: '@vue/compiler-dom': 3.5.32 '@vue/shared': 3.5.32 - '@vue/devtools-api@7.7.9': - dependencies: - '@vue/devtools-kit': 7.7.9 - - '@vue/devtools-kit@7.7.9': - dependencies: - '@vue/devtools-shared': 7.7.9 - birpc: 2.9.0 - hookable: 5.5.3 - mitt: 3.0.1 - perfect-debounce: 1.0.0 - speakingurl: 14.0.1 - superjson: 2.2.6 - - '@vue/devtools-shared@7.7.9': - dependencies: - rfdc: 1.4.1 - '@vue/reactivity@3.5.32': dependencies: '@vue/shared': 3.5.32 @@ -5826,35 +4162,8 @@ snapshots: '@vue/shared': 3.5.32 vue: 3.5.32(typescript@5.9.3) - '@vue/shared@3.5.32': {} - - '@vueuse/core@12.8.2(typescript@5.9.3)': - dependencies: - '@types/web-bluetooth': 0.0.21 - '@vueuse/metadata': 12.8.2 - '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - typescript - - '@vueuse/integrations@12.8.2(focus-trap@7.8.0)(typescript@5.9.3)': - dependencies: - '@vueuse/core': 12.8.2(typescript@5.9.3) - '@vueuse/shared': 12.8.2(typescript@5.9.3) - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - focus-trap: 7.8.0 - transitivePeerDependencies: - - typescript - - '@vueuse/metadata@12.8.2': {} - - '@vueuse/shared@12.8.2(typescript@5.9.3)': - dependencies: - vue: 3.5.32(typescript@5.9.3) - transitivePeerDependencies: - - typescript - + '@vue/shared@3.5.32': {} + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -5896,23 +4205,6 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - algoliasearch@5.50.1: - dependencies: - '@algolia/abtesting': 1.16.1 - '@algolia/client-abtesting': 5.50.1 - '@algolia/client-analytics': 5.50.1 - '@algolia/client-common': 5.50.1 - '@algolia/client-insights': 5.50.1 - '@algolia/client-personalization': 5.50.1 - '@algolia/client-query-suggestions': 5.50.1 - '@algolia/client-search': 5.50.1 - '@algolia/ingestion': 1.50.1 - '@algolia/monitoring': 1.50.1 - '@algolia/recommend': 5.50.1 - '@algolia/requester-browser-xhr': 5.50.1 - '@algolia/requester-fetch': 5.50.1 - '@algolia/requester-node-http': 5.50.1 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -6010,8 +4302,6 @@ snapshots: baseline-browser-mapping@2.10.16: {} - birpc@2.9.0: {} - bowser@2.14.1: {} brace-expansion@1.1.13: @@ -6056,8 +4346,6 @@ snapshots: caniuse-lite@1.0.30001786: {} - ccount@2.0.1: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6065,23 +4353,6 @@ snapshots: chalk@5.6.2: {} - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - chevrotain-allstar@0.4.1(chevrotain@12.0.0): - dependencies: - chevrotain: 12.0.0 - lodash-es: 4.18.1 - - chevrotain@12.0.0: - dependencies: - '@chevrotain/cst-dts-gen': 12.0.0 - '@chevrotain/gast': 12.0.0 - '@chevrotain/regexp-to-ast': 12.0.0 - '@chevrotain/types': 12.0.0 - '@chevrotain/utils': 12.0.0 - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -6096,32 +4367,12 @@ snapshots: color-name@1.1.4: {} - comma-separated-tokens@2.0.3: {} - commander@2.20.3: {} - commander@7.2.0: {} - - commander@8.3.0: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} - convert-source-map@2.0.0: {} - copy-anything@4.0.5: - dependencies: - is-what: 5.5.0 - - cose-base@1.0.3: - dependencies: - layout-base: 1.0.2 - - cose-base@2.2.0: - dependencies: - layout-base: 2.0.1 - create-require@1.1.1: {} cross-spawn@7.0.6: @@ -6132,190 +4383,6 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): - dependencies: - cose-base: 1.0.3 - cytoscape: 3.33.2 - - cytoscape-fcose@2.2.0(cytoscape@3.33.2): - dependencies: - cose-base: 2.2.0 - cytoscape: 3.33.2 - - cytoscape@3.33.2: {} - - d3-array@2.12.1: - dependencies: - internmap: 1.0.1 - - d3-array@3.2.4: - dependencies: - internmap: 2.0.3 - - d3-axis@3.0.0: {} - - d3-brush@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3-chord@3.0.1: - dependencies: - d3-path: 3.1.0 - - d3-color@3.1.0: {} - - d3-contour@4.0.2: - dependencies: - d3-array: 3.2.4 - - d3-delaunay@6.0.4: - dependencies: - delaunator: 5.1.0 - - d3-dispatch@3.0.1: {} - - d3-drag@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-selection: 3.0.0 - - d3-dsv@3.0.1: - dependencies: - commander: 7.2.0 - iconv-lite: 0.6.3 - rw: 1.3.3 - - d3-ease@3.0.1: {} - - d3-fetch@3.0.1: - dependencies: - d3-dsv: 3.0.1 - - d3-force@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-quadtree: 3.0.1 - d3-timer: 3.0.1 - - d3-format@3.1.2: {} - - d3-geo@3.1.1: - dependencies: - d3-array: 3.2.4 - - d3-hierarchy@3.1.2: {} - - d3-interpolate@3.0.1: - dependencies: - d3-color: 3.1.0 - - d3-path@1.0.9: {} - - d3-path@3.1.0: {} - - d3-polygon@3.0.1: {} - - d3-quadtree@3.0.1: {} - - d3-random@3.0.1: {} - - d3-sankey@0.12.3: - dependencies: - d3-array: 2.12.1 - d3-shape: 1.3.7 - - d3-scale-chromatic@3.1.0: - dependencies: - d3-color: 3.1.0 - d3-interpolate: 3.0.1 - - d3-scale@4.0.2: - dependencies: - d3-array: 3.2.4 - d3-format: 3.1.2 - d3-interpolate: 3.0.1 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - - d3-selection@3.0.0: {} - - d3-shape@1.3.7: - dependencies: - d3-path: 1.0.9 - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - - d3-time-format@4.1.0: - dependencies: - d3-time: 3.1.0 - - d3-time@3.1.0: - dependencies: - d3-array: 3.2.4 - - d3-timer@3.0.1: {} - - d3-transition@3.0.1(d3-selection@3.0.0): - dependencies: - d3-color: 3.1.0 - d3-dispatch: 3.0.1 - d3-ease: 3.0.1 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-timer: 3.0.1 - - d3-zoom@3.0.0: - dependencies: - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-transition: 3.0.1(d3-selection@3.0.0) - - d3@7.9.0: - dependencies: - d3-array: 3.2.4 - d3-axis: 3.0.0 - d3-brush: 3.0.0 - d3-chord: 3.0.1 - d3-color: 3.1.0 - d3-contour: 4.0.2 - d3-delaunay: 6.0.4 - d3-dispatch: 3.0.1 - d3-drag: 3.0.0 - d3-dsv: 3.0.1 - d3-ease: 3.0.1 - d3-fetch: 3.0.1 - d3-force: 3.0.0 - d3-format: 3.1.2 - d3-geo: 3.1.1 - d3-hierarchy: 3.1.2 - d3-interpolate: 3.0.1 - d3-path: 3.1.0 - d3-polygon: 3.0.1 - d3-quadtree: 3.0.1 - d3-random: 3.0.1 - d3-scale: 4.0.2 - d3-scale-chromatic: 3.1.0 - d3-selection: 3.0.0 - d3-shape: 3.2.0 - d3-time: 3.1.0 - d3-time-format: 4.1.0 - d3-timer: 3.0.1 - d3-transition: 3.0.1(d3-selection@3.0.0) - d3-zoom: 3.0.0 - - dagre-d3-es@7.0.14: - dependencies: - d3: 7.9.0 - lodash-es: 4.18.1 - damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -6336,8 +4403,6 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - dayjs@1.11.20: {} - debug@3.2.7: dependencies: ms: 2.1.3 @@ -6360,20 +4425,12 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delaunator@5.1.0: - dependencies: - robust-predicates: 3.0.3 - dequal@2.0.3: {} detect-libc@2.1.2: {} devalue@5.6.4: {} - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - diff-match-patch@1.0.5: {} diff@4.0.4: {} @@ -6384,10 +4441,6 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.3.3: - optionalDependencies: - '@types/trusted-types': 2.0.7 - dotenv@16.6.1: {} dunder-proto@1.0.1: @@ -6398,8 +4451,6 @@ snapshots: electron-to-chromium@1.5.331: {} - emoji-regex-xs@1.0.0: {} - emoji-regex@9.2.2: {} enhanced-resolve@5.20.1: @@ -6511,32 +4562,6 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -6561,6 +4586,26 @@ snapshots: - eslint-plugin-import-x - supports-color + eslint-config-next@16.2.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@next/eslint-plugin-next': 16.2.2 + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.39.4(jiti@2.6.1)) + globals: 16.4.0 + typescript-eslint: 8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-webpack + - eslint-plugin-import-x + - supports-color + eslint-import-resolver-node@0.3.10: dependencies: debug: 3.2.7 @@ -6624,6 +4669,33 @@ snapshots: - eslint-import-resolver-webpack - supports-color + eslint-plugin-import@2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.5 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.6.1)): dependencies: aria-query: 5.3.2 @@ -6809,17 +4881,10 @@ snapshots: flatted@3.4.2: {} - focus-trap@7.8.0: - dependencies: - tabbable: 6.4.0 - for-each@0.3.5: dependencies: is-callable: 1.2.7 - fsevents@2.3.3: - optional: true - function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -6886,8 +4951,6 @@ snapshots: graceful-fs@4.2.11: {} - hachure-fill@0.5.2: {} - has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -6910,38 +4973,12 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 - hookable@5.5.3: {} - - html-void-elements@3.0.0: {} - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - ignore@5.3.2: {} ignore@7.0.5: {} @@ -6959,10 +4996,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - internmap@1.0.1: {} - - internmap@2.0.3: {} - is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -7079,8 +5112,6 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 - is-what@5.5.0: {} - isarray@2.0.5: {} isexe@2.0.0: {} @@ -7131,35 +5162,16 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 - katex@0.16.45: - dependencies: - commander: 8.3.0 - keyv@4.5.4: dependencies: json-buffer: 3.0.1 - khroma@2.1.0: {} - - langium@4.2.2: - dependencies: - '@chevrotain/regexp-to-ast': 12.0.0 - chevrotain: 12.0.0 - chevrotain-allstar: 0.4.1(chevrotain@12.0.0) - vscode-languageserver: 9.0.1 - vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.1.0 - language-subtag-registry@0.3.23: {} language-tags@1.0.9: dependencies: language-subtag-registry: 0.3.23 - layout-base@1.0.2: {} - - layout-base@2.0.1: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -7220,8 +5232,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.18.1: {} - lodash.merge@4.6.2: {} loose-envify@1.4.0: @@ -7242,67 +5252,10 @@ snapshots: make-error@1.3.6: {} - mark.js@8.11.1: {} - - marked@16.4.2: {} - math-intrinsics@1.1.0: {} - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - merge2@1.4.1: {} - mermaid@11.14.0: - dependencies: - '@braintree/sanitize-url': 7.1.2 - '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.1.0 - '@types/d3': 7.4.3 - '@upsetjs/venn.js': 2.0.0 - cytoscape: 3.33.2 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) - cytoscape-fcose: 2.2.0(cytoscape@3.33.2) - d3: 7.9.0 - d3-sankey: 0.12.3 - dagre-d3-es: 7.0.14 - dayjs: 1.11.20 - dompurify: 3.3.3 - katex: 0.16.45 - khroma: 2.1.0 - lodash-es: 4.18.1 - marked: 16.4.2 - roughjs: 4.6.6 - stylis: 4.3.6 - ts-dedent: 2.2.0 - uuid: 11.1.0 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-encode@2.0.1: {} - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -7318,17 +5271,6 @@ snapshots: minimist@1.2.8: {} - minisearch@7.2.0: {} - - mitt@3.0.1: {} - - mlly@1.8.2: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - moo@0.5.3: {} ms@2.1.3: {} @@ -7382,9 +5324,6 @@ snapshots: node-releases@2.0.37: {} - non-layered-tidy-tree-layout@2.0.2: - optional: true - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -7427,12 +5366,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - oniguruma-to-es@3.1.1: - dependencies: - emoji-regex-xs: 1.0.0 - regex: 6.1.0 - regex-recursion: 6.0.2 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -7456,14 +5389,10 @@ snapshots: dependencies: p-limit: 3.1.0 - package-manager-detector@1.6.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 - path-data-parser@0.1.0: {} - path-exists@4.0.0: {} path-expression-matcher@1.2.1: {} @@ -7472,10 +5401,6 @@ snapshots: path-parse@1.0.7: {} - pathe@2.0.3: {} - - perfect-debounce@1.0.0: {} - pg-cloudflare@1.3.0: optional: true @@ -7517,19 +5442,6 @@ snapshots: picomatch@4.0.4: {} - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.2 - pathe: 2.0.3 - - points-on-curve@0.2.0: {} - - points-on-path@0.2.1: - dependencies: - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -7554,8 +5466,6 @@ snapshots: dependencies: xtend: 4.0.2 - preact@10.29.1: {} - prelude-ls@1.2.1: {} prop-types@15.8.1: @@ -7564,8 +5474,6 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - property-information@7.1.0: {} - punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -7597,16 +5505,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -7633,54 +5531,10 @@ snapshots: reusify@1.1.0: {} - rfdc@1.4.1: {} - - robust-predicates@3.0.3: {} - - rollup@4.60.1: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.1 - '@rollup/rollup-android-arm64': 4.60.1 - '@rollup/rollup-darwin-arm64': 4.60.1 - '@rollup/rollup-darwin-x64': 4.60.1 - '@rollup/rollup-freebsd-arm64': 4.60.1 - '@rollup/rollup-freebsd-x64': 4.60.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 - '@rollup/rollup-linux-arm-musleabihf': 4.60.1 - '@rollup/rollup-linux-arm64-gnu': 4.60.1 - '@rollup/rollup-linux-arm64-musl': 4.60.1 - '@rollup/rollup-linux-loong64-gnu': 4.60.1 - '@rollup/rollup-linux-loong64-musl': 4.60.1 - '@rollup/rollup-linux-ppc64-gnu': 4.60.1 - '@rollup/rollup-linux-ppc64-musl': 4.60.1 - '@rollup/rollup-linux-riscv64-gnu': 4.60.1 - '@rollup/rollup-linux-riscv64-musl': 4.60.1 - '@rollup/rollup-linux-s390x-gnu': 4.60.1 - '@rollup/rollup-linux-x64-gnu': 4.60.1 - '@rollup/rollup-linux-x64-musl': 4.60.1 - '@rollup/rollup-openbsd-x64': 4.60.1 - '@rollup/rollup-openharmony-arm64': 4.60.1 - '@rollup/rollup-win32-arm64-msvc': 4.60.1 - '@rollup/rollup-win32-ia32-msvc': 4.60.1 - '@rollup/rollup-win32-x64-gnu': 4.60.1 - '@rollup/rollup-win32-x64-msvc': 4.60.1 - fsevents: 2.3.3 - - roughjs@4.6.6: - dependencies: - hachure-fill: 0.5.2 - path-data-parser: 0.1.0 - points-on-curve: 0.2.0 - points-on-path: 0.2.1 - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - rw@1.3.3: {} - safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -7700,12 +5554,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - safer-buffer@2.1.2: {} - scheduler@0.27.0: {} - search-insights@2.17.3: {} - secure-json-parse@2.7.0: {} semver@6.3.1: {} @@ -7772,17 +5622,6 @@ snapshots: shebang-regex@3.0.0: {} - shiki@2.5.0: - dependencies: - '@shikijs/core': 2.5.0 - '@shikijs/engine-javascript': 2.5.0 - '@shikijs/engine-oniguruma': 2.5.0 - '@shikijs/langs': 2.5.0 - '@shikijs/themes': 2.5.0 - '@shikijs/types': 2.5.0 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -7813,10 +5652,6 @@ snapshots: source-map-js@1.2.1: {} - space-separated-tokens@2.0.2: {} - - speakingurl@14.0.1: {} - split2@4.2.0: {} sql-formatter@15.7.3: @@ -7886,11 +5721,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} @@ -7904,12 +5734,6 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 - stylis@4.3.6: {} - - superjson@2.2.6: - dependencies: - copy-anything: 4.0.5 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -7947,8 +5771,6 @@ snapshots: dependencies: vue: 3.5.32(typescript@5.9.3) - tabbable@6.4.0: {} - tailwind-merge@3.5.0: {} tailwindcss@4.2.2: {} @@ -7957,8 +5779,6 @@ snapshots: throttleit@2.1.0: {} - tinyexec@1.0.4: {} - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -7968,14 +5788,10 @@ snapshots: dependencies: is-number: 7.0.0 - trim-lines@3.0.1: {} - ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 - ts-dedent@2.2.0: {} - ts-node@10.9.2(@types/node@20.19.39)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -8053,8 +5869,6 @@ snapshots: typescript@5.9.3: {} - ufo@1.6.3: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -8064,29 +5878,6 @@ snapshots: undici-types@6.21.0: {} - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -8125,103 +5916,8 @@ snapshots: dependencies: react: 19.2.4 - uuid@11.1.0: {} - v8-compile-cache-lib@3.0.1: {} - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.8 - rollup: 4.60.1 - optionalDependencies: - '@types/node': 20.19.39 - fsevents: 2.3.3 - lightningcss: 1.32.0 - - vitepress-plugin-mermaid@2.0.17(mermaid@11.14.0)(vitepress@1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3)): - dependencies: - mermaid: 11.14.0 - vitepress: 1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3) - optionalDependencies: - '@mermaid-js/mermaid-mindmap': 9.3.0 - - vitepress@1.6.4(@algolia/client-search@5.50.1)(@types/node@20.19.39)(lightningcss@1.32.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3): - dependencies: - '@docsearch/css': 3.8.2 - '@docsearch/js': 3.8.2(@algolia/client-search@5.50.1)(search-insights@2.17.3) - '@iconify-json/simple-icons': 1.2.77 - '@shikijs/core': 2.5.0 - '@shikijs/transformers': 2.5.0 - '@shikijs/types': 2.5.0 - '@types/markdown-it': 14.1.2 - '@vitejs/plugin-vue': 5.2.4(vite@5.4.21(@types/node@20.19.39)(lightningcss@1.32.0))(vue@3.5.32(typescript@5.9.3)) - '@vue/devtools-api': 7.7.9 - '@vue/shared': 3.5.32 - '@vueuse/core': 12.8.2(typescript@5.9.3) - '@vueuse/integrations': 12.8.2(focus-trap@7.8.0)(typescript@5.9.3) - focus-trap: 7.8.0 - mark.js: 8.11.1 - minisearch: 7.2.0 - shiki: 2.5.0 - vite: 5.4.21(@types/node@20.19.39)(lightningcss@1.32.0) - vue: 3.5.32(typescript@5.9.3) - optionalDependencies: - postcss: 8.5.8 - transitivePeerDependencies: - - '@algolia/client-search' - - '@types/node' - - '@types/react' - - async-validator - - axios - - change-case - - drauu - - fuse.js - - idb-keyval - - jwt-decode - - less - - lightningcss - - nprogress - - qrcode - - react - - react-dom - - sass - - sass-embedded - - search-insights - - sortablejs - - stylus - - sugarss - - terser - - typescript - - universal-cookie - - vscode-jsonrpc@8.2.0: {} - - vscode-languageserver-protocol@3.17.5: - dependencies: - vscode-jsonrpc: 8.2.0 - vscode-languageserver-types: 3.17.5 - - vscode-languageserver-textdocument@1.0.12: {} - - vscode-languageserver-types@3.17.5: {} - - vscode-languageserver@9.0.1: - dependencies: - vscode-languageserver-protocol: 3.17.5 - - vscode-uri@3.1.0: {} - vue@3.5.32(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.32 @@ -8298,5 +5994,3 @@ snapshots: zod: 3.25.76 zod@3.25.76: {} - - zwitch@2.0.4: {} From 28a3519cb9c82812ee665013bab1c0820333a8ab Mon Sep 17 00:00:00 2001 From: Asheze1127 Date: Wed, 8 Apr 2026 03:46:04 +0900 Subject: [PATCH 3/3] feat: add hackathon scoped mentor board --- apps/api/cmd/server/main.go | 1 + .../db/migrations/007_create_hackathons.sql | 39 +++++++ apps/api/db/queries/hackathons.sql | 8 ++ apps/api/db/queries/progress.sql | 15 ++- apps/api/db/queries/questions.sql | 15 ++- apps/api/db/queries/teams.sql | 17 ++- apps/api/db/sqlcgen/hackathons.sql.go | 49 +++++++++ apps/api/db/sqlcgen/models.go | 26 +++-- apps/api/db/sqlcgen/progress.sql.go | 74 ++++++++++--- apps/api/db/sqlcgen/questions.sql.go | 103 +++++++++++++----- apps/api/db/sqlcgen/teams.sql.go | 74 ++++++++++--- apps/api/internal/api/handler.go | 77 ++++++++++--- apps/api/internal/api/response.go | 54 ++++++--- apps/api/internal/slack/handler.go | 64 +++++------ apps/api/internal/slack/socketmode.go | 28 ++--- apps/console/app/mentor/progress/page.tsx | 50 ++++++--- .../mentor/progress/teams/[teamId]/page.tsx | 51 +++++++-- apps/console/app/mentor/questions/page.tsx | 36 +++++- apps/console/app/page.tsx | 10 +- .../shared/components/hackathon-switcher.tsx | 36 ++++++ apps/console/shared/lib/api.ts | 25 ++++- apps/console/shared/lib/hackathon-scope.ts | 34 ++++++ apps/worker/src/db/progress.ts | 6 +- apps/worker/src/db/questions.ts | 6 +- apps/worker/src/db/teams.ts | 29 +++++ apps/worker/src/workers/progress.ts | 9 +- apps/worker/src/workers/question.ts | 9 +- .../10_combined-product-gap-analysis.md | 13 ++- docs/detail/11_opportunity-cycle-1-plan.md | 35 +++--- docs/specification.md | 9 +- 30 files changed, 775 insertions(+), 227 deletions(-) create mode 100644 apps/api/db/migrations/007_create_hackathons.sql create mode 100644 apps/api/db/queries/hackathons.sql create mode 100644 apps/api/db/sqlcgen/hackathons.sql.go create mode 100644 apps/console/shared/components/hackathon-switcher.tsx create mode 100644 apps/console/shared/lib/hackathon-scope.ts create mode 100644 apps/worker/src/db/teams.ts diff --git a/apps/api/cmd/server/main.go b/apps/api/cmd/server/main.go index f46fcad..705d100 100644 --- a/apps/api/cmd/server/main.go +++ b/apps/api/cmd/server/main.go @@ -53,6 +53,7 @@ func main() { // REST API routes for the dashboard api := r.Group("/api") { + api.GET("/hackathons", apihandler.HandleListHackathons(sqlDB)) api.GET("/questions", apihandler.HandleListQuestions(sqlDB)) api.GET("/questions/:id", apihandler.HandleGetQuestion(sqlDB)) api.GET("/progress", apihandler.HandleListProgress(sqlDB)) diff --git a/apps/api/db/migrations/007_create_hackathons.sql b/apps/api/db/migrations/007_create_hackathons.sql new file mode 100644 index 0000000..30cf0c3 --- /dev/null +++ b/apps/api/db/migrations/007_create_hackathons.sql @@ -0,0 +1,39 @@ +-- +goose Up +CREATE TABLE hackathons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + slug TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +ALTER TABLE teams + ADD COLUMN hackathon_id UUID REFERENCES hackathons (id), + ADD COLUMN slack_workspace_id TEXT; + +INSERT INTO hackathons (id, slug, name) +VALUES ( + '00000000-0000-0000-0000-000000000001', + 'default-hackathon', + 'Default Hackathon' +); + +UPDATE teams +SET hackathon_id = '00000000-0000-0000-0000-000000000001' +WHERE hackathon_id IS NULL; + +ALTER TABLE teams + ALTER COLUMN hackathon_id SET NOT NULL; + +CREATE INDEX idx_teams_hackathon_id ON teams (hackathon_id); +CREATE INDEX idx_teams_slack_scope ON teams (slack_workspace_id, slack_channel_id); + +-- +goose Down +DROP INDEX idx_teams_slack_scope; +DROP INDEX idx_teams_hackathon_id; + +ALTER TABLE teams + DROP COLUMN slack_workspace_id, + DROP COLUMN hackathon_id; + +DROP TABLE hackathons; diff --git a/apps/api/db/queries/hackathons.sql b/apps/api/db/queries/hackathons.sql new file mode 100644 index 0000000..5799132 --- /dev/null +++ b/apps/api/db/queries/hackathons.sql @@ -0,0 +1,8 @@ +-- name: ListHackathons :many +SELECT + * +FROM + hackathons +ORDER BY + created_at ASC, + name ASC; diff --git a/apps/api/db/queries/progress.sql b/apps/api/db/queries/progress.sql index 80a13b1..21fcc26 100644 --- a/apps/api/db/queries/progress.sql +++ b/apps/api/db/queries/progress.sql @@ -16,20 +16,25 @@ RETURNING -- name: ListProgressLogs :many SELECT - * + progress_logs.* FROM progress_logs + LEFT JOIN teams ON teams.id = progress_logs.team_id WHERE ( sqlc.narg (team_id)::uuid IS NULL - OR team_id = sqlc.narg (team_id)::uuid + OR progress_logs.team_id = sqlc.narg (team_id)::uuid + ) + AND ( + sqlc.narg (hackathon_id)::uuid IS NULL + OR teams.hackathon_id = sqlc.narg (hackathon_id)::uuid ) ORDER BY - created_at DESC + progress_logs.created_at DESC LIMIT - $2 + sqlc.arg(page_limit)::int OFFSET - $3; + sqlc.arg(page_offset)::int; -- name: LatestProgressPerTeam :many SELECT DISTINCT diff --git a/apps/api/db/queries/questions.sql b/apps/api/db/queries/questions.sql index 76820df..a56048d 100644 --- a/apps/api/db/queries/questions.sql +++ b/apps/api/db/queries/questions.sql @@ -35,20 +35,25 @@ WHERE -- name: ListQuestions :many SELECT - * + questions.* FROM questions + LEFT JOIN teams ON teams.id = questions.team_id WHERE ( sqlc.narg (status)::text IS NULL - OR status = sqlc.narg (status) + OR questions.status = sqlc.narg (status)::text + ) + AND ( + sqlc.narg (hackathon_id)::uuid IS NULL + OR teams.hackathon_id = sqlc.narg (hackathon_id)::uuid ) ORDER BY - created_at DESC + questions.created_at DESC LIMIT - $2 + sqlc.arg(page_limit)::int OFFSET - $3; + sqlc.arg(page_offset)::int; -- name: UpdateQuestionStatus :exec UPDATE questions diff --git a/apps/api/db/queries/teams.sql b/apps/api/db/queries/teams.sql index 7e6902b..f4b2bcf 100644 --- a/apps/api/db/queries/teams.sql +++ b/apps/api/db/queries/teams.sql @@ -11,14 +11,27 @@ SELECT * FROM teams +WHERE + ( + sqlc.narg (hackathon_id)::uuid IS NULL + OR hackathon_id = sqlc.narg (hackathon_id)::uuid + ) ORDER BY name; -- name: InsertTeam :one INSERT INTO - teams (name, slack_channel_id, tech_stack, phase, is_sos) + teams ( + name, + slack_channel_id, + tech_stack, + phase, + is_sos, + hackathon_id, + slack_workspace_id + ) VALUES - ($1, $2, $3, $4, $5) + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; diff --git a/apps/api/db/sqlcgen/hackathons.sql.go b/apps/api/db/sqlcgen/hackathons.sql.go new file mode 100644 index 0000000..d971b92 --- /dev/null +++ b/apps/api/db/sqlcgen/hackathons.sql.go @@ -0,0 +1,49 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: hackathons.sql + +package sqlcgen + +import ( + "context" +) + +const listHackathons = `-- name: ListHackathons :many +SELECT + id, slug, name, created_at, updated_at +FROM + hackathons +ORDER BY + created_at ASC, + name ASC +` + +func (q *Queries) ListHackathons(ctx context.Context) ([]Hackathon, error) { + rows, err := q.db.QueryContext(ctx, listHackathons) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Hackathon{} + for rows.Next() { + var i Hackathon + if err := rows.Scan( + &i.ID, + &i.Slug, + &i.Name, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/apps/api/db/sqlcgen/models.go b/apps/api/db/sqlcgen/models.go index f9665ec..388d591 100644 --- a/apps/api/db/sqlcgen/models.go +++ b/apps/api/db/sqlcgen/models.go @@ -12,6 +12,14 @@ import ( "github.com/sqlc-dev/pqtype" ) +type Hackathon struct { + ID uuid.UUID `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type KnowledgeItem struct { ID uuid.UUID `json:"id"` Category string `json:"category"` @@ -75,12 +83,14 @@ type SlackEvent struct { } type Team struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - SlackChannelID sql.NullString `json:"slack_channel_id"` - TechStack []string `json:"tech_stack"` - Phase string `json:"phase"` - IsSos bool `json:"is_sos"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SlackChannelID sql.NullString `json:"slack_channel_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + HackathonID uuid.UUID `json:"hackathon_id"` + SlackWorkspaceID sql.NullString `json:"slack_workspace_id"` } diff --git a/apps/api/db/sqlcgen/progress.sql.go b/apps/api/db/sqlcgen/progress.sql.go index 8ab6329..24bb794 100644 --- a/apps/api/db/sqlcgen/progress.sql.go +++ b/apps/api/db/sqlcgen/progress.sql.go @@ -13,12 +13,20 @@ import ( ) const insertProgressLog = `-- name: InsertProgressLog :one -INSERT INTO progress_logs ( - team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_event_id -) VALUES ( - $1, $2, $3, $4, $5, $6, $7 -) -RETURNING id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at +INSERT INTO + progress_logs ( + team_id, + slack_user_id, + phase, + status_text, + blockers, + is_sos, + slack_event_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) +RETURNING + id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at ` type InsertProgressLogParams struct { @@ -58,9 +66,13 @@ func (q *Queries) InsertProgressLog(ctx context.Context, arg InsertProgressLogPa } const latestProgressPerTeam = `-- name: LatestProgressPerTeam :many -SELECT DISTINCT ON (team_id) id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at -FROM progress_logs -ORDER BY team_id, created_at DESC +SELECT DISTINCT + ON (team_id) id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at +FROM + progress_logs +ORDER BY + team_id, + created_at DESC ` func (q *Queries) LatestProgressPerTeam(ctx context.Context) ([]ProgressLog, error) { @@ -98,20 +110,42 @@ func (q *Queries) LatestProgressPerTeam(ctx context.Context) ([]ProgressLog, err } const listProgressLogs = `-- name: ListProgressLogs :many -SELECT id, team_id, slack_user_id, phase, status_text, blockers, is_sos, slack_message_ts, slack_event_id, created_at FROM progress_logs -WHERE ($1::uuid IS NULL OR team_id = $1) -ORDER BY created_at DESC -LIMIT $2 OFFSET $3 +SELECT + progress_logs.id, progress_logs.team_id, progress_logs.slack_user_id, progress_logs.phase, progress_logs.status_text, progress_logs.blockers, progress_logs.is_sos, progress_logs.slack_message_ts, progress_logs.slack_event_id, progress_logs.created_at +FROM + progress_logs + LEFT JOIN teams ON teams.id = progress_logs.team_id +WHERE + ( + $1::uuid IS NULL + OR progress_logs.team_id = $1::uuid + ) + AND ( + $2::uuid IS NULL + OR teams.hackathon_id = $2::uuid + ) +ORDER BY + progress_logs.created_at DESC +LIMIT + $4::int +OFFSET + $3::int ` type ListProgressLogsParams struct { - TeamID uuid.NullUUID `json:"team_id"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + TeamID uuid.NullUUID `json:"team_id"` + HackathonID uuid.NullUUID `json:"hackathon_id"` + PageOffset int32 `json:"page_offset"` + PageLimit int32 `json:"page_limit"` } func (q *Queries) ListProgressLogs(ctx context.Context, arg ListProgressLogsParams) ([]ProgressLog, error) { - rows, err := q.db.QueryContext(ctx, listProgressLogs, arg.TeamID, arg.Limit, arg.Offset) + rows, err := q.db.QueryContext(ctx, listProgressLogs, + arg.TeamID, + arg.HackathonID, + arg.PageOffset, + arg.PageLimit, + ) if err != nil { return nil, err } @@ -145,7 +179,11 @@ func (q *Queries) ListProgressLogs(ctx context.Context, arg ListProgressLogsPara } const updateProgressSlackTS = `-- name: UpdateProgressSlackTS :exec -UPDATE progress_logs SET slack_message_ts = $2 WHERE id = $1 +UPDATE progress_logs +SET + slack_message_ts = $2 +WHERE + id = $1 ` type UpdateProgressSlackTSParams struct { diff --git a/apps/api/db/sqlcgen/questions.sql.go b/apps/api/db/sqlcgen/questions.sql.go index 3540ad0..869d14a 100644 --- a/apps/api/db/sqlcgen/questions.sql.go +++ b/apps/api/db/sqlcgen/questions.sql.go @@ -14,7 +14,12 @@ import ( ) const getQuestion = `-- name: GetQuestion :one -SELECT id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at FROM questions WHERE id = $1 +SELECT + id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at +FROM + questions +WHERE + id = $1 ` func (q *Queries) GetQuestion(ctx context.Context, id uuid.UUID) (Question, error) { @@ -44,7 +49,12 @@ func (q *Queries) GetQuestion(ctx context.Context, id uuid.UUID) (Question, erro } const getQuestionByEventID = `-- name: GetQuestionByEventID :one -SELECT id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at FROM questions WHERE slack_event_id = $1 +SELECT + id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at +FROM + questions +WHERE + slack_event_id = $1 ` func (q *Queries) GetQuestionByEventID(ctx context.Context, slackEventID sql.NullString) (Question, error) { @@ -74,13 +84,23 @@ func (q *Queries) GetQuestionByEventID(ctx context.Context, slackEventID sql.Nul } const insertQuestion = `-- name: InsertQuestion :one -INSERT INTO questions ( - team_id, slack_user_id, slack_thread_ts, slack_channel_id, - title, body, error_message, tried, status, slack_event_id -) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, 'pending', $9 -) -RETURNING id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at +INSERT INTO + questions ( + team_id, + slack_user_id, + slack_thread_ts, + slack_channel_id, + title, + body, + error_message, + tried, + status, + slack_event_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, 'pending', $9) +RETURNING + id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at ` type InsertQuestionParams struct { @@ -132,20 +152,42 @@ func (q *Queries) InsertQuestion(ctx context.Context, arg InsertQuestionParams) } const listQuestions = `-- name: ListQuestions :many -SELECT id, team_id, slack_user_id, slack_thread_ts, slack_channel_id, title, body, error_message, tried, triage_result, category, confidence, status, escalated_to_mentor, mentor_channel_ts, slack_event_id, created_at, updated_at FROM questions -WHERE ($1::text IS NULL OR status = $1) -ORDER BY created_at DESC -LIMIT $2 OFFSET $3 +SELECT + questions.id, questions.team_id, questions.slack_user_id, questions.slack_thread_ts, questions.slack_channel_id, questions.title, questions.body, questions.error_message, questions.tried, questions.triage_result, questions.category, questions.confidence, questions.status, questions.escalated_to_mentor, questions.mentor_channel_ts, questions.slack_event_id, questions.created_at, questions.updated_at +FROM + questions + LEFT JOIN teams ON teams.id = questions.team_id +WHERE + ( + $1::text IS NULL + OR questions.status = $1::text + ) + AND ( + $2::uuid IS NULL + OR teams.hackathon_id = $2::uuid + ) +ORDER BY + questions.created_at DESC +LIMIT + $4::int +OFFSET + $3::int ` type ListQuestionsParams struct { - Status sql.NullString `json:"status"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Status sql.NullString `json:"status"` + HackathonID uuid.NullUUID `json:"hackathon_id"` + PageOffset int32 `json:"page_offset"` + PageLimit int32 `json:"page_limit"` } func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]Question, error) { - rows, err := q.db.QueryContext(ctx, listQuestions, arg.Status, arg.Limit, arg.Offset) + rows, err := q.db.QueryContext(ctx, listQuestions, + arg.Status, + arg.HackathonID, + arg.PageOffset, + arg.PageLimit, + ) if err != nil { return nil, err } @@ -190,10 +232,11 @@ const updateQuestionEscalated = `-- name: UpdateQuestionEscalated :exec UPDATE questions SET escalated_to_mentor = TRUE, - mentor_channel_ts = $2, - status = 'escalated', - updated_at = NOW() -WHERE id = $1 + mentor_channel_ts = $2, + status = 'escalated', + updated_at = NOW() +WHERE + id = $1 ` type UpdateQuestionEscalatedParams struct { @@ -208,8 +251,11 @@ func (q *Queries) UpdateQuestionEscalated(ctx context.Context, arg UpdateQuestio const updateQuestionStatus = `-- name: UpdateQuestionStatus :exec UPDATE questions -SET status = $2, updated_at = NOW() -WHERE id = $1 +SET + status = $2, + updated_at = NOW() +WHERE + id = $1 ` type UpdateQuestionStatusParams struct { @@ -226,11 +272,12 @@ const updateQuestionTriage = `-- name: UpdateQuestionTriage :exec UPDATE questions SET triage_result = $2, - category = $3, - confidence = $4, - status = $5, - updated_at = NOW() -WHERE id = $1 + category = $3, + confidence = $4, + status = $5, + updated_at = NOW() +WHERE + id = $1 ` type UpdateQuestionTriageParams struct { diff --git a/apps/api/db/sqlcgen/teams.sql.go b/apps/api/db/sqlcgen/teams.sql.go index 5874bf4..7f58b48 100644 --- a/apps/api/db/sqlcgen/teams.sql.go +++ b/apps/api/db/sqlcgen/teams.sql.go @@ -14,7 +14,12 @@ import ( ) const getTeam = `-- name: GetTeam :one -SELECT id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at FROM teams WHERE id = $1 +SELECT + id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at, hackathon_id, slack_workspace_id +FROM + teams +WHERE + id = $1 ` func (q *Queries) GetTeam(ctx context.Context, id uuid.UUID) (Team, error) { @@ -29,22 +34,37 @@ func (q *Queries) GetTeam(ctx context.Context, id uuid.UUID) (Team, error) { &i.IsSos, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, + &i.SlackWorkspaceID, ) return i, err } const insertTeam = `-- name: InsertTeam :one -INSERT INTO teams (name, slack_channel_id, tech_stack, phase, is_sos) -VALUES ($1, $2, $3, $4, $5) -RETURNING id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at +INSERT INTO + teams ( + name, + slack_channel_id, + tech_stack, + phase, + is_sos, + hackathon_id, + slack_workspace_id + ) +VALUES + ($1, $2, $3, $4, $5, $6, $7) +RETURNING + id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at, hackathon_id, slack_workspace_id ` type InsertTeamParams struct { - Name string `json:"name"` - SlackChannelID sql.NullString `json:"slack_channel_id"` - TechStack []string `json:"tech_stack"` - Phase string `json:"phase"` - IsSos bool `json:"is_sos"` + Name string `json:"name"` + SlackChannelID sql.NullString `json:"slack_channel_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + HackathonID uuid.UUID `json:"hackathon_id"` + SlackWorkspaceID sql.NullString `json:"slack_workspace_id"` } func (q *Queries) InsertTeam(ctx context.Context, arg InsertTeamParams) (Team, error) { @@ -54,6 +74,8 @@ func (q *Queries) InsertTeam(ctx context.Context, arg InsertTeamParams) (Team, e pq.Array(arg.TechStack), arg.Phase, arg.IsSos, + arg.HackathonID, + arg.SlackWorkspaceID, ) var i Team err := row.Scan( @@ -65,16 +87,28 @@ func (q *Queries) InsertTeam(ctx context.Context, arg InsertTeamParams) (Team, e &i.IsSos, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, + &i.SlackWorkspaceID, ) return i, err } const listTeams = `-- name: ListTeams :many -SELECT id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at FROM teams ORDER BY name +SELECT + id, name, slack_channel_id, tech_stack, phase, is_sos, created_at, updated_at, hackathon_id, slack_workspace_id +FROM + teams +WHERE + ( + $1::uuid IS NULL + OR hackathon_id = $1::uuid + ) +ORDER BY + name ` -func (q *Queries) ListTeams(ctx context.Context) ([]Team, error) { - rows, err := q.db.QueryContext(ctx, listTeams) +func (q *Queries) ListTeams(ctx context.Context, hackathonID uuid.NullUUID) ([]Team, error) { + rows, err := q.db.QueryContext(ctx, listTeams, hackathonID) if err != nil { return nil, err } @@ -91,6 +125,8 @@ func (q *Queries) ListTeams(ctx context.Context) ([]Team, error) { &i.IsSos, &i.CreatedAt, &i.UpdatedAt, + &i.HackathonID, + &i.SlackWorkspaceID, ); err != nil { return nil, err } @@ -106,7 +142,12 @@ func (q *Queries) ListTeams(ctx context.Context) ([]Team, error) { } const updateTeamPhase = `-- name: UpdateTeamPhase :exec -UPDATE teams SET phase = $2, updated_at = NOW() WHERE id = $1 +UPDATE teams +SET + phase = $2, + updated_at = NOW() +WHERE + id = $1 ` type UpdateTeamPhaseParams struct { @@ -120,7 +161,12 @@ func (q *Queries) UpdateTeamPhase(ctx context.Context, arg UpdateTeamPhaseParams } const updateTeamSOS = `-- name: UpdateTeamSOS :exec -UPDATE teams SET is_sos = $2, updated_at = NOW() WHERE id = $1 +UPDATE teams +SET + is_sos = $2, + updated_at = NOW() +WHERE + id = $1 ` type UpdateTeamSOSParams struct { diff --git a/apps/api/internal/api/handler.go b/apps/api/internal/api/handler.go index 3b858c2..6ecdee3 100644 --- a/apps/api/internal/api/handler.go +++ b/apps/api/internal/api/handler.go @@ -16,19 +16,54 @@ const ( maxLimit = 100 ) +func parseUUIDQuery(c *gin.Context, key string) (uuid.NullUUID, bool) { + raw := c.Query(key) + if raw == "" { + return uuid.NullUUID{}, true + } + parsed, err := uuid.Parse(raw) + if err != nil { + return uuid.NullUUID{}, false + } + return uuid.NullUUID{UUID: parsed, Valid: true}, true +} + +// HandleListHackathons handles GET /api/hackathons. +func HandleListHackathons(db *sql.DB) gin.HandlerFunc { + q := sqlcgen.New(db) + return func(c *gin.Context) { + hackathons, err := q.ListHackathons(c.Request.Context()) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) + return + } + resp := make([]HackathonResponse, len(hackathons)) + for i, hackathon := range hackathons { + resp[i] = toHackathonResponse(hackathon) + } + c.JSON(http.StatusOK, resp) + } +} + // HandleListQuestions handles GET /api/questions. -// Optional query params: status (string), limit (int, default 20), offset (int, default 0). +// Optional query params: status (string), hackathon_id (uuid), limit (int, default 20), offset (int, default 0). func HandleListQuestions(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) return func(c *gin.Context) { status := c.Query("status") + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return + } limit := parseIntQuery(c, "limit", defaultLimit, maxLimit) offset := parseIntQuery(c, "offset", 0, -1) questions, err := q.ListQuestions(c.Request.Context(), sqlcgen.ListQuestionsParams{ - Status: sql.NullString{String: status, Valid: status != ""}, - Limit: int32(limit), - Offset: int32(offset), + Status: sql.NullString{String: status, Valid: status != ""}, + HackathonID: hackathonID, + PageLimit: int32(limit), + PageOffset: int32(offset), }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) @@ -65,27 +100,29 @@ func HandleGetQuestion(db *sql.DB) gin.HandlerFunc { } // HandleListProgress handles GET /api/progress. -// Optional query params: team_id (uuid), limit, offset. +// Optional query params: team_id (uuid), hackathon_id (uuid), limit, offset. func HandleListProgress(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) return func(c *gin.Context) { limit := parseIntQuery(c, "limit", defaultLimit, maxLimit) offset := parseIntQuery(c, "offset", 0, -1) - var teamID uuid.NullUUID - if raw := c.Query("team_id"); raw != "" { - parsed, err := uuid.Parse(raw) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team_id"}) - return - } - teamID = uuid.NullUUID{UUID: parsed, Valid: true} + teamID, ok := parseUUIDQuery(c, "team_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid team_id"}) + return + } + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return } logs, err := q.ListProgressLogs(c.Request.Context(), sqlcgen.ListProgressLogsParams{ - TeamID: teamID, - Limit: int32(limit), - Offset: int32(offset), + TeamID: teamID, + HackathonID: hackathonID, + PageLimit: int32(limit), + PageOffset: int32(offset), }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) @@ -100,10 +137,16 @@ func HandleListProgress(db *sql.DB) gin.HandlerFunc { } // HandleListTeams handles GET /api/teams. +// Optional query params: hackathon_id (uuid). func HandleListTeams(db *sql.DB) gin.HandlerFunc { q := sqlcgen.New(db) return func(c *gin.Context) { - teams, err := q.ListTeams(c.Request.Context()) + hackathonID, ok := parseUUIDQuery(c, "hackathon_id") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid hackathon_id"}) + return + } + teams, err := q.ListTeams(c.Request.Context(), hackathonID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db error"}) return diff --git a/apps/api/internal/api/response.go b/apps/api/internal/api/response.go index 77deb09..8f9f4aa 100644 --- a/apps/api/internal/api/response.go +++ b/apps/api/internal/api/response.go @@ -30,6 +30,14 @@ type QuestionResponse struct { UpdatedAt time.Time `json:"updated_at"` } +type HackathonResponse struct { + ID uuid.UUID `json:"id"` + Slug string `json:"slug"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + type ProgressLogResponse struct { ID uuid.UUID `json:"id"` TeamID *string `json:"team_id"` @@ -44,14 +52,26 @@ type ProgressLogResponse struct { } type TeamResponse struct { - ID uuid.UUID `json:"id"` - Name string `json:"name"` - SlackChannelID *string `json:"slack_channel_id"` - TechStack []string `json:"tech_stack"` - Phase string `json:"phase"` - IsSos bool `json:"is_sos"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` + SlackChannelID *string `json:"slack_channel_id"` + SlackWorkspaceID *string `json:"slack_workspace_id"` + HackathonID string `json:"hackathon_id"` + TechStack []string `json:"tech_stack"` + Phase string `json:"phase"` + IsSos bool `json:"is_sos"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func toHackathonResponse(h sqlcgen.Hackathon) HackathonResponse { + return HackathonResponse{ + ID: h.ID, + Slug: h.Slug, + Name: h.Name, + CreatedAt: h.CreatedAt, + UpdatedAt: h.UpdatedAt, + } } func toQuestionResponse(q sqlcgen.Question) QuestionResponse { @@ -129,16 +149,20 @@ func toProgressLogResponse(p sqlcgen.ProgressLog) ProgressLogResponse { func toTeamResponse(t sqlcgen.Team) TeamResponse { r := TeamResponse{ - ID: t.ID, - Name: t.Name, - TechStack: t.TechStack, - Phase: t.Phase, - IsSos: t.IsSos, - CreatedAt: t.CreatedAt, - UpdatedAt: t.UpdatedAt, + ID: t.ID, + Name: t.Name, + HackathonID: t.HackathonID.String(), + TechStack: t.TechStack, + Phase: t.Phase, + IsSos: t.IsSos, + CreatedAt: t.CreatedAt, + UpdatedAt: t.UpdatedAt, } if t.SlackChannelID.Valid { r.SlackChannelID = &t.SlackChannelID.String } + if t.SlackWorkspaceID.Valid { + r.SlackWorkspaceID = &t.SlackWorkspaceID.String + } return r } diff --git a/apps/api/internal/slack/handler.go b/apps/api/internal/slack/handler.go index e97cfab..0e917f3 100644 --- a/apps/api/internal/slack/handler.go +++ b/apps/api/internal/slack/handler.go @@ -18,7 +18,7 @@ import ( // HandleSlashCommand handles POST /slack/commands. // Slack sends application/x-www-form-urlencoded with fields: -// command, trigger_id, user_id, team_id, channel_id, text. +// command, trigger_id, user_id, team_id (workspace id), channel_id, text. func HandleSlashCommand(cfg *config.Config, db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { // Read raw body first so we can verify the signature. @@ -166,13 +166,13 @@ func HandleInteraction(cfg *config.Config, db *sql.DB, sqsClient sqs.Sender) gin func handleQuestionSubmission(ctx context.Context, sqsClient sqs.Sender, p *interactionPayload) error { vals := p.View.State.Values msg := questionNewMessage{ - UserID: p.User.ID, - TeamID: p.Team.ID, - ChannelID: p.View.PrivateMetadata, - Title: strVal(vals, "title_block", "title"), - Body: strVal(vals, "body_block", "body"), - ErrorMessage: strVal(vals, "error_block", "error_message"), - Tried: strVal(vals, "tried_block", "tried"), + UserID: p.User.ID, + SlackWorkspaceID: p.Team.ID, + ChannelID: p.View.PrivateMetadata, + Title: strVal(vals, "title_block", "title"), + Body: strVal(vals, "body_block", "body"), + ErrorMessage: strVal(vals, "error_block", "error_message"), + Tried: strVal(vals, "tried_block", "tried"), } body, err := json.Marshal(msg) if err != nil { @@ -186,13 +186,13 @@ func handleProgressSubmission(ctx context.Context, sqsClient sqs.Sender, p *inte vals := p.View.State.Values sosChecked := len(vals["sos_block"]["sos_flag"].SelectedOptions) > 0 msg := progressMessage{ - UserID: p.User.ID, - TeamID: p.Team.ID, - ChannelID: p.View.PrivateMetadata, - Phase: selectedVal(vals, "phase_block", "phase"), - CurrentStatus: strVal(vals, "status_block", "current_status"), - Blockers: strVal(vals, "blockers_block", "blockers"), - SOSFlag: sosChecked, + UserID: p.User.ID, + SlackWorkspaceID: p.Team.ID, + ChannelID: p.View.PrivateMetadata, + Phase: selectedVal(vals, "phase_block", "phase"), + CurrentStatus: strVal(vals, "status_block", "current_status"), + Blockers: strVal(vals, "blockers_block", "blockers"), + SOSFlag: sosChecked, } body, err := json.Marshal(msg) if err != nil { @@ -270,9 +270,9 @@ type viewState struct { type stateValues map[string]map[string]elementState type elementState struct { - Type string `json:"type"` - Value string `json:"value"` - SelectedOption *selectOption `json:"selected_option"` + Type string `json:"type"` + Value string `json:"value"` + SelectedOption *selectOption `json:"selected_option"` SelectedOptions []selectOption `json:"selected_options"` } @@ -286,21 +286,21 @@ type selectOption struct { // --- SQS message types --- type questionNewMessage struct { - UserID string `json:"user_id"` - TeamID string `json:"team_id"` - ChannelID string `json:"channel_id"` - Title string `json:"title"` - Body string `json:"body,omitempty"` - ErrorMessage string `json:"error_message,omitempty"` - Tried string `json:"tried,omitempty"` + UserID string `json:"user_id"` + SlackWorkspaceID string `json:"slack_workspace_id"` + ChannelID string `json:"channel_id"` + Title string `json:"title"` + Body string `json:"body,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` + Tried string `json:"tried,omitempty"` } type progressMessage struct { - UserID string `json:"user_id"` - TeamID string `json:"team_id"` - ChannelID string `json:"channel_id"` - Phase string `json:"phase"` - CurrentStatus string `json:"current_status,omitempty"` - Blockers string `json:"blockers,omitempty"` - SOSFlag bool `json:"sos_flag"` + UserID string `json:"user_id"` + SlackWorkspaceID string `json:"slack_workspace_id"` + ChannelID string `json:"channel_id"` + Phase string `json:"phase"` + CurrentStatus string `json:"current_status,omitempty"` + Blockers string `json:"blockers,omitempty"` + SOSFlag bool `json:"sos_flag"` } diff --git a/apps/api/internal/slack/socketmode.go b/apps/api/internal/slack/socketmode.go index 62e5b33..d0fe43f 100644 --- a/apps/api/internal/slack/socketmode.go +++ b/apps/api/internal/slack/socketmode.go @@ -422,13 +422,13 @@ type questionFollowupMessage struct { func enqueueQuestion(sqsClient sqs.Sender, cb *goslack.InteractionCallback) error { vals := cb.View.State.Values msg := questionNewMessage{ - UserID: cb.User.ID, - TeamID: cb.Team.ID, - ChannelID: cb.View.PrivateMetadata, - Title: blockVal(vals, "title_block", "title"), - Body: blockVal(vals, "body_block", "body"), - ErrorMessage: blockVal(vals, "error_block", "error_message"), - Tried: blockVal(vals, "tried_block", "tried"), + UserID: cb.User.ID, + SlackWorkspaceID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Title: blockVal(vals, "title_block", "title"), + Body: blockVal(vals, "body_block", "body"), + ErrorMessage: blockVal(vals, "error_block", "error_message"), + Tried: blockVal(vals, "tried_block", "tried"), } body, err := json.Marshal(msg) if err != nil { @@ -441,13 +441,13 @@ func enqueueProgress(sqsClient sqs.Sender, cb *goslack.InteractionCallback) erro vals := cb.View.State.Values sosChecked := len(vals["sos_block"]["sos_flag"].SelectedOptions) > 0 msg := progressMessage{ - UserID: cb.User.ID, - TeamID: cb.Team.ID, - ChannelID: cb.View.PrivateMetadata, - Phase: blockSelectedVal(vals, "phase_block", "phase"), - CurrentStatus: blockVal(vals, "status_block", "current_status"), - Blockers: blockVal(vals, "blockers_block", "blockers"), - SOSFlag: sosChecked, + UserID: cb.User.ID, + SlackWorkspaceID: cb.Team.ID, + ChannelID: cb.View.PrivateMetadata, + Phase: blockSelectedVal(vals, "phase_block", "phase"), + CurrentStatus: blockVal(vals, "status_block", "current_status"), + Blockers: blockVal(vals, "blockers_block", "blockers"), + SOSFlag: sosChecked, } body, err := json.Marshal(msg) if err != nil { diff --git a/apps/console/app/mentor/progress/page.tsx b/apps/console/app/mentor/progress/page.tsx index 2ed0a05..9a95b96 100644 --- a/apps/console/app/mentor/progress/page.tsx +++ b/apps/console/app/mentor/progress/page.tsx @@ -1,14 +1,25 @@ import Link from "next/link" -import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { fetchHackathons, fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { resolveActiveHackathon, type HackathonSearchParams, withHackathonScope } from "@/shared/lib/hackathon-scope" -export default async function MentorProgressPage() { - const [logs, teams] = await Promise.all([ - fetchProgressLogs({ limit: 30 }).catch(() => []), - fetchTeams().catch(() => []), - ]) +type MentorProgressPageProps = { + searchParams: Promise +} + +export default async function MentorProgressPage({ searchParams }: MentorProgressPageProps) { + const params = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(params, hackathons) + const [logs, teams] = activeHackathon + ? await Promise.all([ + fetchProgressLogs({ hackathon_id: activeHackathon.id, limit: 30 }).catch(() => null), + fetchTeams({ hackathon_id: activeHackathon.id }).catch(() => null), + ]) + : [[], []] - const teamMap = new Map(teams.map((team) => [team.id, team])) - const sosCount = logs.filter((log) => log.is_sos).length + const teamMap = new Map((teams ?? []).map((team) => [team.id, team])) + const sosCount = (logs ?? []).filter((log) => log.is_sos).length return (
@@ -19,20 +30,33 @@ export default async function MentorProgressPage() { Progress overview

- `progress-checker` で必要だった team progress detail への導線を新しい console 側に作っています。 + `progress-checker` で必要だった team progress detail への導線を、hackathon ごとに分けて新しい console 側に置いています。

+

SOS

{sosCount}

-

最新 progress logs のうち SOS フラグ付きの件数

+

選択中の hackathon の最新 progress logs に含まれる SOS 件数

- {logs.length === 0 ? ( + {hackathons.length === 0 ? ( +
+ hackathon がまだ設定されていません。migration と team backfill を先に適用してください。 +
+ ) : logs === null || teams === null ? ( +
+ progress board を読み込めませんでした。API 側の `hackathon_id` filter を確認してください。 +
+ ) : logs.length === 0 ? (
- progress データがまだありません。API 未接続またはデータ未投入の可能性があります。 + {activeHackathon?.name ?? "選択中の hackathon"} に progress データがまだありません。
) : ( logs.map((log) => { @@ -51,7 +75,7 @@ export default async function MentorProgressPage() {
{new Date(log.created_at).toLocaleString("ja-JP")} {log.team_id ? ( - + Team detail ) : null} diff --git a/apps/console/app/mentor/progress/teams/[teamId]/page.tsx b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx index dc61f34..a355005 100644 --- a/apps/console/app/mentor/progress/teams/[teamId]/page.tsx +++ b/apps/console/app/mentor/progress/teams/[teamId]/page.tsx @@ -1,35 +1,66 @@ import Link from "next/link" -import { fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { fetchHackathons, fetchProgressLogs, fetchTeams } from "@/shared/lib/api" +import { isUuidLike, resolveActiveHackathon, type HackathonSearchParams, withHackathonScope } from "@/shared/lib/hackathon-scope" type TeamProgressDetailPageProps = { params: Promise<{ teamId: string }> + searchParams: Promise } -export default async function TeamProgressDetailPage({ params }: TeamProgressDetailPageProps) { +export default async function TeamProgressDetailPage({ params, searchParams }: TeamProgressDetailPageProps) { const { teamId } = await params - const [teams, logs] = await Promise.all([ - fetchTeams().catch(() => []), - fetchProgressLogs({ team_id: teamId, limit: 100 }).catch(() => []), - ]) + const resolvedSearchParams = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(resolvedSearchParams, hackathons) + const isValidTeamId = isUuidLike(teamId) + const [teams, logs] = activeHackathon && isValidTeamId + ? await Promise.all([ + fetchTeams({ hackathon_id: activeHackathon.id }).catch(() => null), + fetchProgressLogs({ hackathon_id: activeHackathon.id, team_id: teamId, limit: 100 }).catch(() => null), + ]) + : [[], []] - const team = teams.find((candidate) => candidate.id === teamId) + const team = (teams ?? []).find((candidate) => candidate.id === teamId) + const backHref = withHackathonScope("/mentor/progress", activeHackathon?.id ?? null) return (
- + Back to progress overview

{team?.name ?? "Unknown team"}

- `progress-checker` docs にあった team detail route を先に切り出しました。後続 slice で hackathon scope と role 判定を重ねます。 + `progress-checker` docs にあった team detail route に hackathon scope を重ねています。空状態と境界エラーを分けて表示します。

+
- {logs.length === 0 ? ( + {hackathons.length === 0 ? ( +
+ hackathon がまだ設定されていません。migration と team backfill を先に適用してください。 +
+ ) : !isValidTeamId ? ( +
+ URL の teamId が不正です。progress overview から遷移し直してください。 +
+ ) : logs === null || teams === null ? ( +
+ team detail を読み込めませんでした。API 側の `hackathon_id` filter を確認してください。 +
+ ) : !team ? ( +
+ この team は選択中の hackathon に属していないか、まだ scope backfill されていません。 +
+ ) : logs.length === 0 ? (
この team の progress log はまだありません。
diff --git a/apps/console/app/mentor/questions/page.tsx b/apps/console/app/mentor/questions/page.tsx index 9a5879f..61484e5 100644 --- a/apps/console/app/mentor/questions/page.tsx +++ b/apps/console/app/mentor/questions/page.tsx @@ -1,8 +1,19 @@ import Link from "next/link" -import { fetchQuestions } from "@/shared/lib/api" +import { HackathonSwitcher } from "@/shared/components/hackathon-switcher" +import { fetchHackathons, fetchQuestions } from "@/shared/lib/api" +import { resolveActiveHackathon, type HackathonSearchParams } from "@/shared/lib/hackathon-scope" -export default async function MentorQuestionsPage() { - const questions = await fetchQuestions({ limit: 20 }).catch(() => []) +type MentorQuestionsPageProps = { + searchParams: Promise +} + +export default async function MentorQuestionsPage({ searchParams }: MentorQuestionsPageProps) { + const params = await searchParams + const hackathons = await fetchHackathons().catch(() => []) + const activeHackathon = resolveActiveHackathon(params, hackathons) + const questions = activeHackathon + ? await fetchQuestions({ hackathon_id: activeHackathon.id, limit: 20 }).catch(() => null) + : [] return (
@@ -12,14 +23,27 @@ export default async function MentorQuestionsPage() { Question queue

- 既存 API の質問一覧を新しい console で再利用しています。後続 slice で hackathon scope と web-auth をここに載せます。 + 既存 API の質問一覧を hackathon 単位で読み分けます。auth はまだ deferred ですが、board の event 混在はここで止めます。

+
- {questions.length === 0 ? ( + {hackathons.length === 0 ? ( +
+ hackathon がまだ設定されていません。migration と team backfill を先に適用してください。 +
+ ) : questions === null ? ( +
+ 質問一覧を読み込めませんでした。API 側で `hackathon_id` filter が反映されているか確認してください。 +
+ ) : questions.length === 0 ? (
- 質問データがまだありません。API 未接続またはデータ未投入の可能性があります。 + {activeHackathon?.name ?? "選択中の hackathon"} に質問データがまだありません。
) : ( questions.map((question) => ( diff --git a/apps/console/app/page.tsx b/apps/console/app/page.tsx index 4ee4d87..aea66a9 100644 --- a/apps/console/app/page.tsx +++ b/apps/console/app/page.tsx @@ -4,8 +4,8 @@ const roleCards = [ { href: "/mentor/questions", eyebrow: "Mentor", - title: "Question queue and progress overview", - body: "既存の `changeHack` API を読みながら、`progress-checker` の mentor 運用導線を新しい console に切り出します。", + title: "Hackathon-scoped mentor live board", + body: "既存の `changeHack` API を hackathon 単位で読み、`progress-checker` の mentor 導線を新しい console に移します。", }, { href: "/platform", @@ -30,7 +30,7 @@ const roleCards = [ const cycleSteps = [ "Step 1: docs と code の差分を固定し、単一イベント前提の drift を明文化する", "Step 2: 別 app の control-plane を作り、既存 apps/web と責務を分離する", - "Step 3: mentor 導線を実データ接続しながら multi-hackathon 化の受け皿を作る", + "Step 3: hackathon scope を read path に入れ、mentor 導線を event 混在なしで見られるようにする", ] export default function HomePage() { @@ -39,11 +39,11 @@ export default function HomePage() {

Cycle 1

- Start the hackathon-scoped live board before more single-event assumptions harden. + Start the hackathon-scoped mentor board before more single-event assumptions harden.

現在の `apps/web` は KCL 単一イベント向けの support dashboard です。ここではそれを壊さずに、 - hackathon scope を導入できる新しい board を `apps/console` 側で育てます。 + まず mentor の question / progress 導線に hackathon scope を入れられる新しい board を `apps/console` 側で育てます。

{roleCards.map((card) => ( diff --git a/apps/console/shared/components/hackathon-switcher.tsx b/apps/console/shared/components/hackathon-switcher.tsx new file mode 100644 index 0000000..db17bfc --- /dev/null +++ b/apps/console/shared/components/hackathon-switcher.tsx @@ -0,0 +1,36 @@ +import Link from "next/link" +import type { Hackathon } from "@/shared/lib/api" +import { withHackathonScope } from "@/shared/lib/hackathon-scope" + +type HackathonSwitcherProps = { + basePath: string + hackathons: Hackathon[] + activeHackathonId: string | null +} + +export function HackathonSwitcher({ basePath, hackathons, activeHackathonId }: HackathonSwitcherProps) { + if (hackathons.length === 0) { + return null + } + + return ( +
+ {hackathons.map((hackathon) => { + const isActive = hackathon.id === activeHackathonId + return ( + + {hackathon.name} + + ) + })} +
+ ) +} diff --git a/apps/console/shared/lib/api.ts b/apps/console/shared/lib/api.ts index e9c94bb..6b8d4fc 100644 --- a/apps/console/shared/lib/api.ts +++ b/apps/console/shared/lib/api.ts @@ -1,5 +1,13 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080" +export interface Hackathon { + id: string + slug: string + name: string + created_at: string + updated_at: string +} + export interface Question { id: string team_id: string | null @@ -30,6 +38,8 @@ export interface Team { id: string name: string slack_channel_id: string | null + slack_workspace_id: string | null + hackathon_id: string tech_stack: string[] phase: string is_sos: boolean @@ -52,11 +62,13 @@ async function apiFetch(path: string): Promise { export async function fetchQuestions(params?: { status?: string + hackathon_id?: string limit?: number offset?: number }): Promise { const query = new URLSearchParams() if (params?.status) query.set("status", params.status) + if (params?.hackathon_id) query.set("hackathon_id", params.hackathon_id) if (params?.limit != null) query.set("limit", String(params.limit)) if (params?.offset != null) query.set("offset", String(params.offset)) const queryString = query.toString() @@ -65,17 +77,26 @@ export async function fetchQuestions(params?: { export async function fetchProgressLogs(params?: { team_id?: string + hackathon_id?: string limit?: number offset?: number }): Promise { const query = new URLSearchParams() if (params?.team_id) query.set("team_id", params.team_id) + if (params?.hackathon_id) query.set("hackathon_id", params.hackathon_id) if (params?.limit != null) query.set("limit", String(params.limit)) if (params?.offset != null) query.set("offset", String(params.offset)) const queryString = query.toString() return apiFetch(`/api/progress${queryString ? `?${queryString}` : ""}`) } -export async function fetchTeams(): Promise { - return apiFetch("/api/teams") +export async function fetchTeams(params?: { hackathon_id?: string }): Promise { + const query = new URLSearchParams() + if (params?.hackathon_id) query.set("hackathon_id", params.hackathon_id) + const queryString = query.toString() + return apiFetch(`/api/teams${queryString ? `?${queryString}` : ""}`) +} + +export async function fetchHackathons(): Promise { + return apiFetch("/api/hackathons") } diff --git a/apps/console/shared/lib/hackathon-scope.ts b/apps/console/shared/lib/hackathon-scope.ts new file mode 100644 index 0000000..5b63b1e --- /dev/null +++ b/apps/console/shared/lib/hackathon-scope.ts @@ -0,0 +1,34 @@ +import type { Hackathon } from "./api" + +export type HackathonSearchParams = { + hackathon_id?: string | string[] +} + +export function readSingleSearchParam(value: string | string[] | undefined): string | null { + if (Array.isArray(value)) { + return value[0] ?? null + } + return value ?? null +} + +export function resolveActiveHackathon( + searchParams: HackathonSearchParams, + hackathons: Hackathon[], +): Hackathon | null { + const requestedId = readSingleSearchParam(searchParams.hackathon_id) + if (!requestedId) { + return hackathons[0] ?? null + } + return hackathons.find((hackathon) => hackathon.id === requestedId) ?? hackathons[0] ?? null +} + +export function withHackathonScope(path: string, hackathonId: string | null): string { + if (!hackathonId) { + return path + } + return `${path}?hackathon_id=${encodeURIComponent(hackathonId)}` +} + +export function isUuidLike(value: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value) +} diff --git a/apps/worker/src/db/progress.ts b/apps/worker/src/db/progress.ts index 2f75e65..011eae1 100644 --- a/apps/worker/src/db/progress.ts +++ b/apps/worker/src/db/progress.ts @@ -1,6 +1,7 @@ import { getPool } from "./client" export interface InsertProgressParams { + team_id?: string slack_user_id: string phase: string current_status?: string @@ -23,10 +24,11 @@ export interface ProgressRow { export async function insertProgressLog(params: InsertProgressParams): Promise { const { rows } = await getPool().query( `INSERT INTO progress_logs - (slack_user_id, phase, status_text, blockers, is_sos) - VALUES ($1, $2, $3, $4, $5) + (team_id, slack_user_id, phase, status_text, blockers, is_sos) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [ + params.team_id ?? null, params.slack_user_id, params.phase, params.current_status ?? null, diff --git a/apps/worker/src/db/questions.ts b/apps/worker/src/db/questions.ts index 43c9ae1..5b7a7d5 100644 --- a/apps/worker/src/db/questions.ts +++ b/apps/worker/src/db/questions.ts @@ -1,6 +1,7 @@ import { getPool } from "./client" export interface InsertQuestionParams { + team_id?: string slack_user_id: string slack_channel_id: string slack_thread_ts: string @@ -32,10 +33,11 @@ export interface QuestionRow { export async function insertQuestion(params: InsertQuestionParams): Promise { const { rows } = await getPool().query( `INSERT INTO questions - (slack_user_id, slack_channel_id, slack_thread_ts, title, body, error_message, tried, status) - VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending') + (team_id, slack_user_id, slack_channel_id, slack_thread_ts, title, body, error_message, tried, status) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'pending') RETURNING *`, [ + params.team_id ?? null, params.slack_user_id, params.slack_channel_id, params.slack_thread_ts, diff --git a/apps/worker/src/db/teams.ts b/apps/worker/src/db/teams.ts new file mode 100644 index 0000000..0348943 --- /dev/null +++ b/apps/worker/src/db/teams.ts @@ -0,0 +1,29 @@ +import { getPool } from "./client" + +export interface TeamScopeRow { + id: string + name: string + hackathon_id: string +} + +export async function resolveTeamScopeBySlackChannel(params: { + slack_channel_id: string + slack_workspace_id?: string +}): Promise { + const workspaceId = params.slack_workspace_id?.trim() ?? "" + const { rows } = await getPool().query( + `SELECT id, name, hackathon_id + FROM teams + WHERE slack_channel_id = $1 + AND ( + slack_workspace_id = $2 + OR slack_workspace_id IS NULL + OR $2 = '' + ) + ORDER BY CASE WHEN slack_workspace_id = $2 THEN 0 ELSE 1 END, created_at ASC + LIMIT 1`, + [params.slack_channel_id, workspaceId], + ) + + return rows[0] ?? null +} diff --git a/apps/worker/src/workers/progress.ts b/apps/worker/src/workers/progress.ts index 46ca3e2..23a77b6 100644 --- a/apps/worker/src/workers/progress.ts +++ b/apps/worker/src/workers/progress.ts @@ -1,9 +1,10 @@ import { insertProgressLog, updateProgressSlackTs } from "../db/progress" +import { resolveTeamScopeBySlackChannel } from "../db/teams" import { postMessage } from "../slack/client" export interface ProgressMessage { user_id: string - team_id: string + slack_workspace_id: string channel_id: string phase: string current_status?: string @@ -19,7 +20,13 @@ function getProgressChannel(): string { // 1. Save progress_log to DB // 2. Post to #progress-board channel (SOS messages use a prominent format) export async function handleProgress(msg: ProgressMessage): Promise { + const resolvedTeam = await resolveTeamScopeBySlackChannel({ + slack_channel_id: msg.channel_id, + slack_workspace_id: msg.slack_workspace_id, + }) + const row = await insertProgressLog({ + team_id: resolvedTeam?.id, slack_user_id: msg.user_id, phase: msg.phase, current_status: msg.current_status, diff --git a/apps/worker/src/workers/question.ts b/apps/worker/src/workers/question.ts index cf0956f..25bf752 100644 --- a/apps/worker/src/workers/question.ts +++ b/apps/worker/src/workers/question.ts @@ -1,10 +1,11 @@ import { triageQuestion, type QuestionInput } from "../ai/triage" import { insertQuestion, updateQuestionTriage, markEscalated } from "../db/questions" +import { resolveTeamScopeBySlackChannel } from "../db/teams" import { postMessage, postMessageWithBlocks, postThreadReply, postThreadReplyWithBlocks } from "../slack/client" export interface QuestionNewMessage { user_id: string - team_id: string + slack_workspace_id: string channel_id: string title: string body?: string @@ -25,12 +26,18 @@ function getMentorChannel(): string { // 5. Reply in thread (ask for more info OR send short answer) // 6. Escalate to mentor channel if needed export async function handleQuestionNew(msg: QuestionNewMessage): Promise { + const resolvedTeam = await resolveTeamScopeBySlackChannel({ + slack_channel_id: msg.channel_id, + slack_workspace_id: msg.slack_workspace_id, + }) + // Post the initial question to the channel so we have a thread to reply to. const questionText = buildQuestionText(msg) const threadTs = await postMessage(msg.channel_id, questionText) // Persist the question in pending state. const row = await insertQuestion({ + team_id: resolvedTeam?.id, slack_user_id: msg.user_id, slack_channel_id: msg.channel_id, slack_thread_ts: threadTs, diff --git a/docs/detail/10_combined-product-gap-analysis.md b/docs/detail/10_combined-product-gap-analysis.md index 6f339cd..ef74730 100644 --- a/docs/detail/10_combined-product-gap-analysis.md +++ b/docs/detail/10_combined-product-gap-analysis.md @@ -182,23 +182,24 @@ ## 8. Cycle 1 の結論 -Cycle 1 では、`hackathon-scoped live board` を最初の slice とする。 +Cycle 1 では、`hackathon-scoped mentor live board` を最初の slice とする。 -その中でこの branch では、先行して: +この branch では先行して: - 新しい control-plane app `apps/console` を追加 - mentor の question/progress 導線を新 app に移植 - 後続の hackathon scope 実装を載せる受け皿を作る -次に実装すべき本体は: +その上で次の reviewable step として: - `hackathons` 導入 - `teams.hackathon_id` -- Slack channel / workspace と hackathon の mapping -- hackathon filter 付き read path +- default hackathon への既存 team backfill +- Slack channel / workspace から real `team_id` を引く seam +- hackathon filter 付き mentor read path これにより: -- `progress-checker` の mentor 導線を前進させる +- `progress-checker` の mentor 導線を event 混在なしで前進させる - `hack-evaluater` の multi-role / multi-hackathon 前提を受け入れる器を先に作れる - 既存 `apps/web` を壊さずに前進できる diff --git a/docs/detail/11_opportunity-cycle-1-plan.md b/docs/detail/11_opportunity-cycle-1-plan.md index 807ca5b..9eda825 100644 --- a/docs/detail/11_opportunity-cycle-1-plan.md +++ b/docs/detail/11_opportunity-cycle-1-plan.md @@ -12,7 +12,7 @@ ### Slice name -hackathon-scoped live board +hackathon-scoped mentor live board ### User value @@ -31,10 +31,12 @@ hackathon-scoped live board ### In scope -- `hackathons` を top-level scope として導入する設計 -- `teams.hackathon_id` 導入方針 -- Slack から入る question / progress に scope を持たせる設計 -- hackathon filter 付き board の方向性整理 +- `hackathons` table の導入 +- `teams.hackathon_id` と default hackathon backfill +- Slack submission で real `team_id` を channel から解決する seam +- `GET /api/hackathons` +- `GET /api/questions`, `GET /api/progress`, `GET /api/teams` の `hackathon_id` filter +- hackathon filter 付き mentor board - 新しい Next.js app `apps/console` - top-level overview page - login shell page @@ -68,9 +70,9 @@ hackathon-scoped live board - `/mentor/questions` - `/mentor/progress` - `/mentor/progress/teams/:teamId` -3. mentor の question/progress 画面は既存 API を使って read-only 表示できる +3. mentor の question/progress 画面は hackathon ごとに read-only 表示できる 4. combined product の差分と priority が docs に明文化される -5. hackathon scope を導入するための schema / ingress / read-path の次 slice が明確になる +5. channel mapping 済みの Slack `/question` と `/progress` は real `team_id` を保存できる 6. 変更は `opportunity` branch に留まり、`dev` へは merge しない --- @@ -79,27 +81,24 @@ hackathon-scoped live board ### Slice 2 -multi-hackathon domain schema +role and scope routing shell -- tenants -- hackathons -- role_bindings -- memberships -- sponsor visibility scopes +- login +- active context +- protected mentor / organizer / tenant routes ### Slice 3 -auth and role-aware routing +tenant and sponsor visibility model -- login -- role / scope switch -- protected routes +- tenants +- role_bindings +- sponsor visibility scopes ### Slice 4 Slack payload scoping and issue flow -- question / progress の hackathon 紐付け - issue queue - Issue Worker - GitHub issue creation diff --git a/docs/specification.md b/docs/specification.md index 6b2b562..528aa00 100644 --- a/docs/specification.md +++ b/docs/specification.md @@ -9,6 +9,9 @@ KCL Support Hub は、ハッカソン参加者が「質問しやすく」「詰まりにくく」なるための、 Slack を主入口とした AI 支援・進捗管理・メンターエスカレーションシステムです。 +現行 `changeHack` の MVP は KCL 向け support hub ですが、opportunity branch では +`progress-checker` と `hack-evaluater` を統合した multi-hackathon / role-aware product へ拡張する前提で進めます。 + 既存の `progress-checker` の設計思想を継承しつつ、 **Bonsai-8B をローカル推論エンジンとして活用し**、運営コストを最小化しながら初心者支援を実現します。 @@ -51,13 +54,13 @@ Slack を主入口とした AI 支援・進捗管理・メンターエスカレ | 継承する | 継承しない | |---|---| -| ロール分離設計(Organizer / Mentor / Hacker) | マルチテナント・スポンサー・スカウト機能 | +| ロール分離設計(PlatformAdmin / TenantAdmin / HackathonOrganizer / Sponsor / Judge / Hacker) | scout workflow の本実装 | | `features/container` 構造のフロントエンド | 複雑な権限スコープ管理 | -| `handler / service / repository` の3層バックエンド | 企業向け招待フロー | +| `handler / service / repository` の3層バックエンド | 企業向け招待フローの本実装 | | Zod + React Hook Form の組み合わせ | | --- ## 新プロダクトの一言要約 -> Slack から質問するだけで、Bonsai が一次整理し、運営・メンターが俯瞰できる、KCL 専用の「質問受付係 + 進捗ボード」 +> Slack から質問と進捗を集め、まず mentor 導線を hackathon 単位で整理し、将来的に organizer / sponsor へ広げる「質問受付係 + 進捗ボード」