diff --git a/apps/backend/etc/callhome.md b/apps/backend/etc/callhome.md new file mode 100644 index 0000000..601a82a --- /dev/null +++ b/apps/backend/etc/callhome.md @@ -0,0 +1,57 @@ +# Call Home 會議重點與 Action Items + +**日期:** 2026/07/28 + +## 會議重點 + +### 1. 整合方向 + +Call Home 可拆成三個部分: + +1. **Trigger/State**:事件收集、分類、去重與 Recovery。 +2. **Delivery**:Email、SMS、Phone、ServiceNow、Salesforce。 +3. **Service Process**:自動開單、Ticket 狀態與結案責任。 + +### 2. 可以直接重用的能力 + +- **SSM 團隊提供:** Event Mapping、事件白名單、資料收集、去重與 Recovery 邏輯。 +- **SCC-FLEX 提供:** Policy Engine、統一 Webhook、多通路通知與 Salesforce API。 +- **Local Administrator:** 沿用 SCC-FLEX 現有 Recipient/Receiver 設定,不另外開發獨立模組。 + +### 3. 目前主要缺口 + +- 尚未定義統一 Event Payload 與 Correlation Key。 +- SCC-FLEX 尚缺完整 Deduplication、Soft/Hard State 與 Event Lifecycle。 +- Service Team 可接受的 Event 白名單尚未確認。 +- Device Recovered 與 Ticket Closed 的關係尚未定義。 +- Salesforce Ticket 的 Owner、Close 權限與 Ask-to-Close 流程尚未確認。 +- Call Home License 模式尚未確認。 + +### 4. 初步共識 + +- Supermicro Service 情境傾向**自動建立 Ticket**。 +- 自動開單前必須先完成 Event 白名單、去重及 Idempotency。 +- 初期先支援 Compute/Supermicro Hardware。 +- CDU、PDU、Liquid Cooling 等產品後續再評估。 + +## Action Items + +| Owner | Action Item | Status | +|---|---|---| +| Daniel | 依 Trigger、Delivery、Service Process 三部分整理整合需求與責任邊界。 | 待整理 | +| Daniel | 建立跨團隊討論群組並發布會議記錄。 | 待處理 | +| Daniel/PM | 確認 DCM Single Key、Service Key、每台 Host 計費及 Support Advisor 的關係。 | 待確認 | +| PM/Service Team | 提供可自動開單的 Event 白名單、必要 Payload/附件及維護 Owner。 | 待確認 | +| SSM 團隊 | 提供 Event Mapping、Trigger List、Deduplication、Recovery 與資料收集規則。 | 待指派 | +| SCC-FLEX 團隊 | 定義 Event Payload、Correlation Key、Soft/Hard State、Deduplication 與 Idempotency。 | 待排程 | +| Daniel/IT/Service Team | 確認 Salesforce API Owner、Ticket Update、Close 與 Ask-to-Close 規則。 | 待確認 | +| PM/產品團隊 | 決定 Event 選擇方式及 Ticket List/Ask-to-Close 是否列入產品範圍。 | 待討論 | + +## 下一步 + +下一次會議只需要確認四件事: + +1. Service Team Event 白名單。 +2. SSM 可提供的資料與介面。 +3. Salesforce Ticket Lifecycle 與責任人。 +4. License 與產品範圍。 diff --git a/apps/backend/generate/api/growth_p2.api b/apps/backend/generate/api/growth_p2.api index 5a774fb..d6d38b1 100644 --- a/apps/backend/generate/api/growth_p2.api +++ b/apps/backend/generate/api/growth_p2.api @@ -98,9 +98,11 @@ type ( } UtmLinkPublic { - Id string `json:"id"` - Code string `json:"code"` + Id string `json:"id"` + Code string `json:"code"` + // TrackPath 保留給同源前端;TrackUrl 是後端算好的絕對網址,貼到 Threads 用這個。 TrackPath string `json:"track_path"` + TrackUrl string `json:"track_url"` DestinationUrl string `json:"destination_url"` OutcomeId string `json:"outcome_id,optional"` Label string `json:"label,optional"` diff --git a/apps/backend/internal/logic/benchmark/get_benchmark_logic.go b/apps/backend/internal/logic/benchmark/get_benchmark_logic.go index 9c7dd18..9b50d91 100644 --- a/apps/backend/internal/logic/benchmark/get_benchmark_logic.go +++ b/apps/backend/internal/logic/benchmark/get_benchmark_logic.go @@ -27,12 +27,11 @@ func (l *GetBenchmarkLogic) GetBenchmark(req *types.BenchmarkReq) (*types.Benchm if !ok || uid <= 0 { return nil, fmt.Errorf("unauthorized") } - niche := req.Niche - if niche == "" { - niche = "general" - } + niche := usecase.NormalizeNiche(req.Niche) var yourEng, yourViews float64 - // contribute + read from own posts if studio available + // 只讀自己的貼文算出對照值。這裡刻意不 ContributeBenchmark:查詢是讀路徑, + // 若順手寫入,任何人只要輸入某個利基名稱就能把自己的數字灌進那個桶。 + // 貢獻的唯一入口是 insights summary(那裡的數字來自使用者自己的已同步貼文)。 if l.svcCtx.Studio != nil { posts, err := l.svcCtx.Studio.ListOwnPosts(l.ctx, uid, req.AccountId) if err == nil && len(posts) > 0 { @@ -52,15 +51,14 @@ func (l *GetBenchmarkLogic) GetBenchmark(req *types.BenchmarkReq) (*types.Benchm if views > 0 { yourEng = float64(likes+replies) / float64(views) } - _ = l.svcCtx.Growth.ContributeBenchmark(l.ctx, uid, niche, yourEng, yourViews) } } } sample, medEng, medViews, avail, hint := l.svcCtx.Growth.GetBenchmark(l.ctx, niche, yourEng, yourViews) return &types.BenchmarkData{ Available: avail, SampleSize: sample, Niche: niche, - MedianEngRate: usecase.Round2(medEng), MedianViews: usecase.Round2(medViews), - YourEngRate: usecase.Round2(yourEng), YourViews: usecase.Round2(yourViews), + MedianEngRate: usecase.RoundRate(medEng), MedianViews: usecase.Round2(medViews), + YourEngRate: usecase.RoundRate(yourEng), YourViews: usecase.Round2(yourViews), PercentileHint: hint, Message: hint, }, nil } diff --git a/apps/backend/internal/logic/insightsapi/get_insights_summary_logic.go b/apps/backend/internal/logic/insightsapi/get_insights_summary_logic.go index 5c295a3..8a929e9 100644 --- a/apps/backend/internal/logic/insightsapi/get_insights_summary_logic.go +++ b/apps/backend/internal/logic/insightsapi/get_insights_summary_logic.go @@ -80,7 +80,7 @@ func (l *GetInsightsSummaryLogic) GetInsightsSummary(req *types.InsightsSummaryR } top = append(top, types.InsightsPostPublic{ Id: p.ID, TextPreview: prev, PublishedAt: ts, - ViewCount: p.ViewCount, LikeCount: p.LikeCount, ReplyCount: p.ReplyCount, EngRate: usecase.Round2(eng), + ViewCount: p.ViewCount, LikeCount: p.LikeCount, ReplyCount: p.ReplyCount, EngRate: usecase.RoundRate(eng), }) } sort.Slice(top, func(i, j int) bool { return top[i].EngRate > top[j].EngRate }) @@ -98,7 +98,7 @@ func (l *GetInsightsSummaryLogic) GetInsightsSummary(req *types.InsightsSummaryR e = float64(a.likes+a.replies) / float64(a.views) } buckets = append(buckets, types.InsightsMonthBucket{ - MonthKey: mk, Posts: a.posts, Views: a.views, Likes: a.likes, Replies: a.replies, AvgEngRate: usecase.Round2(e), + MonthKey: mk, Posts: a.posts, Views: a.views, Likes: a.likes, Replies: a.replies, AvgEngRate: usecase.RoundRate(e), }) } sort.Slice(buckets, func(i, j int) bool { return buckets[i].MonthKey < buckets[j].MonthKey }) @@ -116,7 +116,7 @@ func (l *GetInsightsSummaryLogic) GetInsightsSummary(req *types.InsightsSummaryR } return &types.InsightsSummaryData{ AccountId: req.AccountId, PostCount: n, TotalViews: totalV, TotalLikes: totalL, TotalReplies: totalR, - AvgEngRate: usecase.Round2(avgEng), TopPosts: top, MonthBuckets: buckets, + AvgEngRate: usecase.RoundRate(avgEng), TopPosts: top, MonthBuckets: buckets, Conclusion: conclusion, Suggestions: sugs, }, nil } diff --git a/apps/backend/internal/logic/utm/create_utm_link_logic.go b/apps/backend/internal/logic/utm/create_utm_link_logic.go index 0fe27f5..c1cb5c6 100644 --- a/apps/backend/internal/logic/utm/create_utm_link_logic.go +++ b/apps/backend/internal/logic/utm/create_utm_link_logic.go @@ -30,9 +30,6 @@ func (l *CreateUtmLinkLogic) CreateUtmLink(req *types.CreateUtmLinkReq) (*types. if err != nil { return nil, err } - return &types.UtmLinkPublic{ - Id: u.ID, Code: u.Code, TrackPath: "/api/v1/public/u/" + u.Code, - DestinationUrl: u.DestinationURL, OutcomeId: u.OutcomeID, Label: u.Label, - Clicks: u.Clicks, CreatedAt: u.CreatedAt, - }, nil + out := publicView(l.svcCtx.Config, u) + return &out, nil } diff --git a/apps/backend/internal/logic/utm/list_utm_links_logic.go b/apps/backend/internal/logic/utm/list_utm_links_logic.go index 9f07b4e..c572a48 100644 --- a/apps/backend/internal/logic/utm/list_utm_links_logic.go +++ b/apps/backend/internal/logic/utm/list_utm_links_logic.go @@ -32,11 +32,7 @@ func (l *ListUtmLinksLogic) ListUtmLinks() (*types.UtmLinkListData, error) { } out := make([]types.UtmLinkPublic, 0, len(list)) for _, u := range list { - out = append(out, types.UtmLinkPublic{ - Id: u.ID, Code: u.Code, TrackPath: "/api/v1/public/u/" + u.Code, - DestinationUrl: u.DestinationURL, OutcomeId: u.OutcomeID, Label: u.Label, - Clicks: u.Clicks, CreatedAt: u.CreatedAt, - }) + out = append(out, publicView(l.svcCtx.Config, u)) } return &types.UtmLinkListData{List: out}, nil } diff --git a/apps/backend/internal/logic/utm/publicview.go b/apps/backend/internal/logic/utm/publicview.go new file mode 100644 index 0000000..f5075bd --- /dev/null +++ b/apps/backend/internal/logic/utm/publicview.go @@ -0,0 +1,38 @@ +package utm + +import ( + "strings" + + "apps/backend/internal/config" + "apps/backend/internal/module/growth/domain" + "apps/backend/internal/types" +) + +const trackPathPrefix = "/api/v1/public/u/" + +// publicView 把追蹤連結轉成對外型別。TrackUrl 必須由後端算:連結會被貼進 Threads +// 貼文,前端自己拼 window.location.origin 在 API 與前端不同 host 時會產出 404 連結。 +func publicView(c config.Config, u *domain.UtmLink) types.UtmLinkPublic { + path := trackPathPrefix + u.Code + return types.UtmLinkPublic{ + Id: u.ID, + Code: u.Code, + TrackPath: path, + TrackUrl: publicAPIBase(c) + path, + DestinationUrl: u.DestinationURL, + OutcomeId: u.OutcomeID, + Label: u.Label, + Clicks: u.Clicks, + CreatedAt: u.CreatedAt, + } +} + +// publicAPIBase 與 threads oauth callback 用同一套推導:PublicAPIBase 優先,否則 +// PublicWebBase(正式站 nginx、dev 的 vite 都會把 /api 轉到 gateway)。兩者皆空 +// 時回空字串,TrackUrl 退回相對路徑讓瀏覽器自己解。 +func publicAPIBase(c config.Config) string { + if b := strings.TrimRight(strings.TrimSpace(c.PublicAPIBase), "/"); b != "" { + return b + } + return strings.TrimRight(strings.TrimSpace(c.PublicWebBase), "/") +} diff --git a/apps/backend/internal/logic/utm/publicview_test.go b/apps/backend/internal/logic/utm/publicview_test.go new file mode 100644 index 0000000..2f4466b --- /dev/null +++ b/apps/backend/internal/logic/utm/publicview_test.go @@ -0,0 +1,36 @@ +package utm + +import ( + "testing" + + "apps/backend/internal/config" + "apps/backend/internal/module/growth/domain" + + "github.com/stretchr/testify/require" +) + +func TestTrackUrlIsAbsoluteSoItCanBePastedIntoAPost(t *testing.T) { + var c config.Config + c.PublicAPIBase = "https://api.example.com/" + c.PublicWebBase = "https://app.example.com" + + out := publicView(c, &domain.UtmLink{ID: "l1", Code: "abc123", DestinationURL: "https://shop.example.com/p/1"}) + + require.Equal(t, "https://api.example.com/api/v1/public/u/abc123", out.TrackUrl) + require.Equal(t, "/api/v1/public/u/abc123", out.TrackPath) +} + +func TestTrackUrlFallsBackToWebBaseWhichProxiesApi(t *testing.T) { + var c config.Config + c.PublicWebBase = "http://127.0.0.1:5173" + + out := publicView(c, &domain.UtmLink{Code: "xyz"}) + + require.Equal(t, "http://127.0.0.1:5173/api/v1/public/u/xyz", out.TrackUrl) +} + +func TestTrackUrlStaysRelativeWhenNoBaseIsConfigured(t *testing.T) { + out := publicView(config.Config{}, &domain.UtmLink{Code: "xyz"}) + + require.Equal(t, "/api/v1/public/u/xyz", out.TrackUrl) +} diff --git a/apps/backend/internal/module/growth/usecase/p2.go b/apps/backend/internal/module/growth/usecase/p2.go index c630356..cd8573d 100644 --- a/apps/backend/internal/module/growth/usecase/p2.go +++ b/apps/backend/internal/module/growth/usecase/p2.go @@ -292,11 +292,17 @@ func (s *Service) TrackUtmClick(ctx context.Context, code string) (dest string, // --- Benchmark --- -func (s *Service) ContributeBenchmark(ctx context.Context, ownerUID int64, niche string, engRate, avgViews float64) error { - niche = strings.TrimSpace(niche) +// NormalizeNiche 讓利基桶有機會對得上:大小寫與前後空白不該切出不同的桶。 +func NormalizeNiche(niche string) string { + niche = strings.ToLower(strings.TrimSpace(niche)) if niche == "" { - niche = "general" + return "general" } + return niche +} + +func (s *Service) ContributeBenchmark(ctx context.Context, ownerUID int64, niche string, engRate, avgViews float64) error { + niche = NormalizeNiche(niche) h := sha256.Sum256([]byte(fmt.Sprintf("%d|%s", ownerUID, niche))) return s.Repo.UpsertBenchmarkSample(ctx, &domain.BenchmarkSample{ Niche: niche, EngRate: engRate, AvgViews: avgViews, @@ -305,10 +311,7 @@ func (s *Service) ContributeBenchmark(ctx context.Context, ownerUID int64, niche } func (s *Service) GetBenchmark(ctx context.Context, niche string, yourEng, yourViews float64) (sampleSize int, medianEng, medianViews float64, available bool, hint string) { - niche = strings.TrimSpace(niche) - if niche == "" { - niche = "general" - } + niche = NormalizeNiche(niche) list, err := s.Repo.ListBenchmarkSamples(ctx, niche, 500) if err != nil || len(list) < 5 { return len(list), 0, 0, false, "樣本不足 5,暫不顯示中位數(多貢獻幾次成效後再看)" @@ -534,3 +537,9 @@ func toPDFSafe(s string) string { func Round2(v float64) float64 { return math.Round(v*100) / 100 } + +// RoundRate 用在互動率這種比率(0.0234)。比率不能走 Round2 —— 那會輾成 0.02, +// 前端 *100 後所有人都變成整數百分比。保留 4 位小數 = 百分比的 2 位。 +func RoundRate(v float64) float64 { + return math.Round(v*10000) / 10000 +} diff --git a/apps/backend/internal/module/growth/usecase/service_test.go b/apps/backend/internal/module/growth/usecase/service_test.go index dc84738..81e5adb 100644 --- a/apps/backend/internal/module/growth/usecase/service_test.go +++ b/apps/backend/internal/module/growth/usecase/service_test.go @@ -220,3 +220,26 @@ func TestUTM_RefusesToRedirectToAStoredBadDestination(t *testing.T) { _, err = svc.TrackUtmClick(ctx, link.Code) require.ErrorIs(t, err, domain.ErrValidation) } + +// 互動率是比率,走 Round2 會被輾成整數百分比(0.0234 → 0.02 → 前端 2.00%)。 +func TestRoundRateKeepsTwoDecimalsOfAPercentage(t *testing.T) { + require.Equal(t, 0.0234, RoundRate(0.023449)) + require.Equal(t, 0.1235, RoundRate(0.12345)) + require.Equal(t, 0.0, RoundRate(0)) + require.Equal(t, 0.02, Round2(0.023449), "Round2 仍供瀏覽數這類整數量使用") +} + +func TestBenchmarkBucketsIgnoreCaseAndPadding(t *testing.T) { + svc := New(repository.NewMemory()) + ctx := context.Background() + + for uid := int64(1); uid <= 5; uid++ { + require.NoError(t, svc.ContributeBenchmark(ctx, uid, " SkinCare ", 0.02, 100)) + } + + size, medEng, medViews, available, _ := svc.GetBenchmark(ctx, "skincare", 0.02, 100) + require.True(t, available) + require.Equal(t, 5, size) + require.Equal(t, 0.02, medEng) + require.Equal(t, 100.0, medViews) +} diff --git a/apps/backend/internal/types/types.go b/apps/backend/internal/types/types.go index aab05c6..2bd513e 100644 --- a/apps/backend/internal/types/types.go +++ b/apps/backend/internal/types/types.go @@ -1796,6 +1796,7 @@ type UtmLinkPublic struct { Id string `json:"id"` Code string `json:"code"` TrackPath string `json:"track_path"` + TrackUrl string `json:"track_url"` DestinationUrl string `json:"destination_url"` OutcomeId string `json:"outcome_id,optional"` Label string `json:"label,optional"` diff --git a/apps/web/package.json b/apps/web/package.json index 459af3e..8d8b72b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -7,6 +7,7 @@ "dev": "vite --host 0.0.0.0", "build": "tsc -b && vite build", "lint": "oxlint", + "check:tokens": "node scripts/check-tokens.mjs", "test": "vitest run", "test:unit": "vitest run", "preview": "vite preview --host 0.0.0.0" diff --git a/apps/web/public/brand-mark.svg b/apps/web/public/brand-mark.svg index fbd20a3..2099214 100644 --- a/apps/web/public/brand-mark.svg +++ b/apps/web/public/brand-mark.svg @@ -1,19 +1,19 @@ - - - + + + - - - + + + - - + + @@ -21,19 +21,19 @@ - + - - + + - + - - + + - - + + - - + + diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg index 46c8710..206129c 100644 --- a/apps/web/public/favicon.svg +++ b/apps/web/public/favicon.svg @@ -1,15 +1,15 @@ - - + + - - - - - - + + + + + + diff --git a/apps/web/public/icons.svg b/apps/web/public/icons.svg deleted file mode 100644 index 788874e..0000000 --- a/apps/web/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/web/scripts/check-tokens.mjs b/apps/web/scripts/check-tokens.mjs new file mode 100644 index 0000000..cc0d30a --- /dev/null +++ b/apps/web/scripts/check-tokens.mjs @@ -0,0 +1,105 @@ +/** + * 樣式把關:顏色與字級只能來自 tokens.css。 + * + * 會擋下的事: + * 1. css 寫死 font-size 數值(em 例外:那是刻意的相對縮放) + * 2. tsx 的 inline style 寫死 fontSize 或色碼 + * 3. css 出現任何 css 檔都沒定義的 --hb-*(--hb-warn 這種拼錯永遠吃 fallback,很難發現) + * 4. css 出現寫死的顏色 + * 5. 把兩個有彩度的 token 互相 color-mix(藍配橘是補色,混出來一定是濁褐) + * + * 第 4 條放過兩種情形:純黑純白(只用於陰影與高光的 color-mix,本身不是主題色), + * 以及 global.css 主題預覽色塊那 12 個值 —— 那裡必須同時顯示淺色與深色, + * 不能吃會隨主題切換的 var()。 + */ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; + +const STYLE_DIR = "src/styles"; +const NEUTRAL = /^#(fff|000|ffffff|000000)$/i; +/** + * 有彩度的 token。這些彼此不能混:主色在 H≈187、暖色在 H≈32,是補色關係, + * 任何比例混出來都是濁褐。要調淡就跟 surface/line/transparent 混,要同一顏色 + * 的深淺兩階就用 X 與 X-deep。 + */ +const CHROMATIC = + /^--hb-(brand|brand-hover|brand-deep|brand-dim|magic|accent-warm|warning|warning-deep|danger|danger-deep|success|gold|gold-bright)$/; +const MIX = /color-mix\(in srgb,\s*var\((--hb-[a-z0-9-]+)\)[^,]*,\s*var\((--hb-[a-z0-9-]+)\)\s*\)/g; +/** 主題預覽色塊:見 global.css 的 .hb-theme-picker__swatch--* */ +const HEX_BUDGET = { "global.css": 12, "layout.css": 0, "ui.css": 0 }; + +const errors = []; + +function walk(dir) { + const out = []; + for (const name of readdirSync(dir)) { + const path = join(dir, name); + if (statSync(path).isDirectory()) out.push(...walk(path)); + else out.push(path); + } + return out; +} + +const cssFiles = readdirSync(STYLE_DIR).filter((f) => f.endsWith(".css")); +// 除了 tokens.css 的全域 token,元件內宣告的區域變數(如 --hb-top-ctrl)也算已定義。 +const defined = new Set( + cssFiles.flatMap((f) => [ + ...readFileSync(join(STYLE_DIR, f), "utf8").matchAll(/^\s*(--hb-[a-z0-9-]+)\s*:/gm), + ].map((m) => m[1])), +); + +for (const file of readdirSync(STYLE_DIR)) { + if (!file.endsWith(".css") || file === "tokens.css") continue; + const path = join(STYLE_DIR, file); + const text = readFileSync(path, "utf8"); + + text.split("\n").forEach((line, i) => { + const at = `${path}:${i + 1}`; + if (/font-size:\s*[0-9.]+(rem|px|pt)/.test(line)) { + errors.push(`${at} 寫死 font-size,請改用 var(--hb-text-*):${line.trim()}`); + } + for (const [, name] of line.matchAll(/var\((--hb-[a-z0-9-]+)/g)) { + if (!defined.has(name)) { + errors.push(`${at} 用了 tokens.css 沒有的 ${name}(拼錯?)`); + } + } + for (const [, a, b] of line.matchAll(MIX)) { + if (CHROMATIC.test(a) && CHROMATIC.test(b)) { + errors.push( + `${at} 混了兩個有彩度的顏色(${a} + ${b}),會變濁。` + + `要調淡請跟 surface/line/transparent 混,要深一階請用 -deep。`, + ); + } + } + }); + + const hex = (text.match(/#[0-9a-fA-F]{3,8}\b/g) ?? []).filter((h) => !NEUTRAL.test(h)); + const budget = HEX_BUDGET[file] ?? 0; + if (hex.length > budget) { + errors.push( + `${path} 寫死顏色 ${hex.length} 處(上限 ${budget}):${[...new Set(hex)].join(" ")}。` + + `顏色請走 tokens.css,否則深淺模式會有一半畫面不跟著切。`, + ); + } +} + +for (const path of walk("src")) { + if (!path.endsWith(".tsx")) continue; + const text = readFileSync(path, "utf8"); + text.split("\n").forEach((line, i) => { + const at = `${path}:${i + 1}`; + if (/fontSize:\s*"[0-9.]/.test(line)) { + errors.push(`${at} inline fontSize 寫死,請改用 var(--hb-text-*)`); + } + if (/(color|background|borderColor):\s*"#[0-9a-fA-F]{3,8}"/.test(line)) { + errors.push(`${at} inline 色碼寫死,請改用 var(--hb-*)`); + } + }); +} + +if (errors.length) { + console.error(`樣式把關失敗(${errors.length}):`); + for (const e of errors) console.error(" " + e); + process.exit(1); +} +console.log("樣式把關通過:顏色與字級都來自 tokens.css"); diff --git a/apps/web/src/components/layout/AccountMenu.tsx b/apps/web/src/components/layout/AccountMenu.tsx index ef66e99..8542705 100644 --- a/apps/web/src/components/layout/AccountMenu.tsx +++ b/apps/web/src/components/layout/AccountMenu.tsx @@ -32,7 +32,7 @@ export function AccountMenu() { const avatarAccount = { display_name: name, username: member?.email || "user", - avatar_color: "#20b49c", + avatar_color: "var(--hb-brand)", avatar_url: member?.avatar_url, }; diff --git a/apps/web/src/components/persona/PersonaWorkbench.tsx b/apps/web/src/components/persona/PersonaWorkbench.tsx index f0f4f2e..b007cc1 100644 --- a/apps/web/src/components/persona/PersonaWorkbench.tsx +++ b/apps/web/src/components/persona/PersonaWorkbench.tsx @@ -377,7 +377,10 @@ export function PersonaWorkbench() { {toneOf(p) || t("common.dash")} {p.learning_summary ? ( - + {p.learning_summary} ) : null} @@ -511,7 +514,7 @@ export function PersonaWorkbench() { }) } /> -

+

{t("persona.guardChars", { n: selected.guard.maxChars })} {selected.guard.banAiTone ? t("persona.banAi") : ""} {!isPersonaReady(selected) ? t("persona.notReadySuffix") : ""} @@ -613,7 +616,7 @@ export function PersonaWorkbench() { {hasAnalysis ? (

-

+

{t("persona.sampleMeta", { n: selected.style.sampleCount })} {" · "} {selected.style.source === "benchmark" @@ -643,7 +646,7 @@ export function PersonaWorkbench() { ) : null}

) : ( -

+

{t("persona.analyzeHint")}

)} @@ -652,7 +655,7 @@ export function PersonaWorkbench() { {detailTab === "fingerprint" ? (
-

+

{t("persona.fingerprintHint")}