feat(launch): 热力图与 webhook 发布闭环(T2/T3)
- T2 (op-artalk 车道):gitea heatmap 构建期拉取+fallback+空态三保险,
53×7 灰阶格复用 tokens(亮暗自动),日界锚定 Asia/Shanghai,零 JS,
zh/en 首页挂载;fallback 经死端点实测不破构建
- T3:webhook_receiver.py(HMAC 强制+main-only+构建锁)以 systemd
--user 常驻 13021;gitea hook 注册(events=push, branch=main)
- 三个深坑:gitea 容器回宿主须走桥网关 IP 而非 127.0.0.1;gitea 28
egress 拦私网须 ALLOWED_HOST_LIST(ALLOWLIST 是废弃名);systemd
用户服务缺用户 PATH 致 build 静默失败(stamp 不更新即症状)
验证: 测试投递 202→构建→staging 戳更新(09:48:37 实测);
dist 热力图 376 格真数据(9 次贡献)zh/en 双页
Spec: 2026-10-25_add-blog-launch
Co-authored-by: GLM-5.3
This commit is contained in:
1 parent
0c54a43bb5
commit
938558ad4c
12 files changed
+692
-110
No files matched your search
@@ -0,0 +1,83 @@
|
||||
// 贡献热力图数据(极客层 P0):构建期拉自托管 gitea 年历,聚合 53 周 × 7 日
|
||||
// 三级回落(远端 → 仓内样例 → 空数组)保证网络单点故障不破构建;见 Heatmap.astro
|
||||
|
||||
export interface HeatDay {
|
||||
date: string; // YYYY-MM-DD
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface HeatmapYear {
|
||||
weeks: HeatDay[][]; // 53 列(周)× 7 行(日,周一为首行)
|
||||
total: number;
|
||||
}
|
||||
|
||||
export const HEATMAP_API = 'https://git.wait4x.online/api/v1/users/wait4x/heatmap';
|
||||
|
||||
interface RawHeat {
|
||||
timestamp: number;
|
||||
contributions: number;
|
||||
}
|
||||
|
||||
/** 分档阈值:0 / 1-2 / 3-5 / 6-9 / 10+ → 灰阶 l0-l4,固定阈值可解释(GitHub 同款哲学) */
|
||||
export function levelOf(count: number): number {
|
||||
if (count >= 10) return 4;
|
||||
if (count >= 6) return 3;
|
||||
if (count >= 3) return 2;
|
||||
if (count >= 1) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 站点受众时区:日界按上海(+8)切,保证 gitea 后台与博客热力图对同一天计数一致;
|
||||
// 显式固定而非取构建机时区——避免构建环境变化悄悄改变历史归日
|
||||
const DAY_FMT = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
});
|
||||
|
||||
function toDateString(tsSec: number): string {
|
||||
return DAY_FMT.format(new Date(tsSec * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* 聚合原始贡献点为 53 周年历:起点为「今天所在周的周一」前推 52 周,
|
||||
* 网格尾部未来的日子填 0(格渲染为空档),首列可能含早于窗口的 0 天。
|
||||
*/
|
||||
export async function loadHeatmap(): Promise<HeatmapYear> {
|
||||
let raw: RawHeat[] = [];
|
||||
try {
|
||||
const res = await fetch(HEATMAP_API, {
|
||||
headers: { Accept: 'application/json', 'User-Agent': 'wait4x-blog-build' },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${HEATMAP_API} -> ${res.status}`);
|
||||
raw = (await res.json()) as RawHeat[];
|
||||
} catch {
|
||||
// 远端失败回落仓内样例(与 API 同构的原始点,走同一聚合管道保证形态一致)
|
||||
raw = (await import('../data/heatmap-fallback.json')).default as RawHeat[];
|
||||
}
|
||||
|
||||
const byDate = new Map<string, number>();
|
||||
for (const { timestamp, contributions } of raw) {
|
||||
const key = toDateString(timestamp);
|
||||
byDate.set(key, (byDate.get(key) ?? 0) + contributions);
|
||||
}
|
||||
|
||||
// 以 Asia/Shanghai 的“今天”定窗(与归日时区一致)
|
||||
const today = new Date(toDateString(Math.floor(Date.now() / 1000)) + 'T12:00:00+08:00');
|
||||
const dow = (today.getUTCDay() + 6) % 7; // 周一=0
|
||||
const gridStart = new Date(today.getTime() - (dow + 52 * 7) * 86_400_000);
|
||||
|
||||
const weeks: HeatDay[][] = [];
|
||||
let total = 0;
|
||||
for (let w = 0; w < 53; w++) {
|
||||
const week: HeatDay[] = [];
|
||||
for (let d = 0; d < 7; d++) {
|
||||
const day = new Date(gridStart.getTime() + (w * 7 + d) * 86_400_000);
|
||||
const key = toDateString(Math.floor(day.getTime() / 1000));
|
||||
const count = day > today ? 0 : (byDate.get(key) ?? 0);
|
||||
total += count;
|
||||
week.push({ date: key, count });
|
||||
}
|
||||
weeks.push(week);
|
||||
}
|
||||
return { weeks, total };
|
||||
}
|
||||
Reference in new issue
Block a user