diff --git a/.github/r2-cors.json b/.github/r2-cors.json
new file mode 100644
index 000000000..d5a9929a1
--- /dev/null
+++ b/.github/r2-cors.json
@@ -0,0 +1,11 @@
+{
+ "CORSRules": [
+ {
+ "AllowedHeaders": ["*"],
+ "AllowedMethods": ["GET", "HEAD"],
+ "AllowedOrigins": ["*"],
+ "ExposeHeaders": ["ETag"],
+ "MaxAgeSeconds": 3600
+ }
+ ]
+}
diff --git a/.github/workflows/sync-changelog.yml b/.github/workflows/sync-changelog.yml
index a31e101cb..dcc00c0b3 100644
--- a/.github/workflows/sync-changelog.yml
+++ b/.github/workflows/sync-changelog.yml
@@ -3,10 +3,14 @@ name: Sync Changelog to R2
on:
release:
types: [published]
+ workflow_run:
+ workflows: ['Release']
+ types: [completed]
workflow_dispatch:
jobs:
sync:
+ if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -24,6 +28,10 @@ jobs:
- name: Upload to R2
run: |
pip install awscli --quiet
+ aws s3api put-bucket-cors \
+ --bucket "${R2_BUCKET_NAME}" \
+ --cors-configuration file://.github/r2-cors.json \
+ --endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
aws s3 cp releases-cn.json "s3://${R2_BUCKET_NAME}/changelog/releases-cn.json" \
--endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" \
--content-type application/json
diff --git a/docs/app/[lang]/changelog/page.tsx b/docs/app/[lang]/changelog/page.tsx
index 022f9217f..04d855b9a 100644
--- a/docs/app/[lang]/changelog/page.tsx
+++ b/docs/app/[lang]/changelog/page.tsx
@@ -1,6 +1,6 @@
-import { fetchChangelog } from '@/lib/changelog';
import { LandingNav } from '@/components/landing/LandingNav';
-import { ChangelogList } from '@/components/landing/ChangelogList';
+import { ChangelogRuntime } from '@/components/landing/ChangelogRuntime';
+import { fetchChangelog } from '@/lib/changelog';
const i18n = {
en: {
@@ -17,7 +17,7 @@ export default async function ChangelogPage({ params }: { params: Promise<{ lang
const { lang } = await params;
const l = lang === 'cn' ? 'cn' : 'en';
const t = i18n[l];
- const data = await fetchChangelog(l);
+ const initialData = await fetchChangelog(l);
return (
@@ -29,11 +29,7 @@ export default async function ChangelogPage({ params }: { params: Promise<{ lang
- {data.releases.length === 0 ? (
-
No releases found.
- ) : (
-
- )}
+
);
diff --git a/docs/app/[lang]/page.tsx b/docs/app/[lang]/page.tsx
index 872db0022..78c104888 100644
--- a/docs/app/[lang]/page.tsx
+++ b/docs/app/[lang]/page.tsx
@@ -5,11 +5,14 @@ import { InfiniteMovingCards } from '@/components/aceternity/InfiniteMovingCards
import { Spotlight } from '@/components/aceternity/Spotlight';
import { LandingNav } from '@/components/landing/LandingNav';
import { InstallTabs } from '@/components/landing/InstallTabs';
+import { LandingLatestUpdates } from '@/components/landing/LandingLatestUpdates';
import { RevealSection } from '@/components/landing/RevealSection';
+import { getAppVersion } from '@/lib/appVersion';
+import { fetchChangelog } from '@/lib/changelog';
+import { fetchLatestReleaseInfo } from '@/lib/latestRelease';
import {
ArrowRight,
Bot,
- CheckCircle2,
Database,
FileCode,
GitCompare,
@@ -166,33 +169,6 @@ const capabilities = {
],
};
-const latestUpdates = {
- en: {
- version: 'v0.5.4',
- title: 'Latest updates',
- desc: 'Mirrored from the latest GitHub release notes.',
- link: 'Read the changelog',
- items: [
- 'JDBC SSH tunnels and proxy support',
- 'Grouped object browser with context menus',
- 'Redis batch operations and command runner',
- 'LIKE / NOT LIKE filters in the data grid',
- ],
- },
- cn: {
- version: 'v0.5.4',
- title: '最近更新',
- desc: '同步 GitHub 最新 Release Notes。',
- link: '查看更新日志',
- items: [
- 'JDBC SSH 隧道和代理支持',
- '对象浏览器分组与右键菜单',
- 'Redis 批量操作和命令行',
- '数据表格 LIKE / NOT LIKE 过滤',
- ],
- },
-};
-
const testimonials = {
en: [
{
@@ -402,7 +378,9 @@ export default async function LandingPage({
const capabilityItems = capabilities[l];
const starLabel = await getGitHubStarLabel();
const metricItems = metrics(starLabel)[l];
- const latest = latestUpdates[l];
+ const appVersion = getAppVersion();
+ const [initialChangelog, initialLatestRelease] = await Promise.all([fetchChangelog(l), fetchLatestReleaseInfo()]);
+ const initialDownloadVersion = initialLatestRelease?.version ?? appVersion;
const testimonialItems = testimonials[l];
return (
@@ -422,7 +400,7 @@ export default async function LandingPage({
{t.heroSubtitle}
-
+
@@ -524,25 +502,7 @@ export default async function LandingPage({
{/* Updates */}
-
- {latest.version}
-
-
{latest.title}
-
{latest.desc}
-
-
- {latest.items.map((item) => (
- -
-
- {item}
-
- ))}
-
-
- {latest.link}
-
-
-
+
{/* Final CTA */}
diff --git a/docs/components/landing/ChangelogRuntime.tsx b/docs/components/landing/ChangelogRuntime.tsx
new file mode 100644
index 000000000..3ba1fc1a4
--- /dev/null
+++ b/docs/components/landing/ChangelogRuntime.tsx
@@ -0,0 +1,49 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { ChangelogList } from '@/components/landing/ChangelogList';
+import { fetchChangelog, type ChangelogRelease } from '@/lib/changelog';
+
+type ChangelogRuntimeProps = {
+ lang: 'en' | 'cn';
+ initialReleases?: ChangelogRelease[];
+};
+
+const text = {
+ en: {
+ loading: 'Loading releases...',
+ empty: 'No releases found.',
+ },
+ cn: {
+ loading: '正在加载版本记录...',
+ empty: '暂无版本记录。',
+ },
+};
+
+export function ChangelogRuntime({ lang, initialReleases = [] }: ChangelogRuntimeProps) {
+ const [releases, setReleases] = useState(initialReleases);
+
+ useEffect(() => {
+ let active = true;
+
+ fetchChangelog(lang).then((data) => {
+ if (active && data.releases.length > 0) {
+ setReleases(data.releases);
+ }
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [lang]);
+
+ if (releases === null) {
+ return {text[lang].loading}
;
+ }
+
+ if (releases.length === 0) {
+ return {text[lang].empty}
;
+ }
+
+ return ;
+}
diff --git a/docs/components/landing/InstallTabs.tsx b/docs/components/landing/InstallTabs.tsx
index ae4ac2063..a600dccb1 100644
--- a/docs/components/landing/InstallTabs.tsx
+++ b/docs/components/landing/InstallTabs.tsx
@@ -2,32 +2,12 @@
import { ChevronDown, Download, Server } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
+import { createInstallOptions, type InstallOption } from '@/lib/downloadLinks';
+import { fetchLatestReleaseInfo } from '@/lib/latestRelease';
type InstallTabsProps = {
lang: 'en' | 'cn';
-};
-
-type InstallOption = {
- id: string;
- label: string;
- href: string;
-};
-
-const allOptions: Record = {
- en: [
- { id: 'macos-arm', label: 'For macOS (Apple Silicon)', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_aarch64.dmg' },
- { id: 'macos-intel', label: 'For macOS (Intel)', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_x64.dmg' },
- { id: 'windows', label: 'For Windows', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_x64-setup.exe' },
- { id: 'linux', label: 'For Linux x64', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_amd64.AppImage' },
- { id: 'linux-arm', label: 'For Linux ARM64', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_aarch64.AppImage' },
- ],
- cn: [
- { id: 'macos-arm', label: '适用于 macOS (Apple Silicon)', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_aarch64.dmg' },
- { id: 'macos-intel', label: '适用于 macOS (Intel)', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_x64.dmg' },
- { id: 'windows', label: '适用于 Windows', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_x64-setup.exe' },
- { id: 'linux', label: '适用于 Linux x64', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_amd64.AppImage' },
- { id: 'linux-arm', label: '适用于 Linux ARM64', href: 'https://dl.dbxio.com/releases/latest/DBX_0.5.9_aarch64.AppImage' },
- ],
+ version: string;
};
const downloadLabel = { en: 'Download DBX', cn: '下载 DBX' };
@@ -63,8 +43,9 @@ function PlatformIcon({ id, size, variant }: { id: string; size: number; variant
return
;
}
-export function InstallTabs({ lang }: InstallTabsProps) {
- const options = allOptions[lang];
+export function InstallTabs({ lang, version }: InstallTabsProps) {
+ const [downloadVersion, setDownloadVersion] = useState(version);
+ const options = useMemo(() => createInstallOptions(lang, downloadVersion), [lang, downloadVersion]);
const [open, setOpen] = useState(false);
const [platformId, setPlatformId] = useState('macos-arm');
@@ -72,6 +53,21 @@ export function InstallTabs({ lang }: InstallTabsProps) {
setPlatformId(detectPlatformId());
}, []);
+ useEffect(() => {
+ let active = true;
+
+ setDownloadVersion(version);
+ fetchLatestReleaseInfo().then((release) => {
+ if (active && release?.version) {
+ setDownloadVersion(release.version);
+ }
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [version]);
+
const primary = useMemo(() => options.find((o) => o.id === platformId) ?? options[0], [options, platformId]);
const menuOptions = useMemo(() => options.filter((o) => o.id !== platformId), [options, platformId]);
diff --git a/docs/components/landing/LandingLatestUpdates.tsx b/docs/components/landing/LandingLatestUpdates.tsx
new file mode 100644
index 000000000..8da05f521
--- /dev/null
+++ b/docs/components/landing/LandingLatestUpdates.tsx
@@ -0,0 +1,57 @@
+'use client';
+
+import Link from 'next/link';
+import { ArrowRight, CheckCircle2 } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { RevealSection } from '@/components/landing/RevealSection';
+import { fetchChangelog, type ChangelogRelease } from '@/lib/changelog';
+import { buildLandingLatestUpdates } from '@/lib/landingLatest';
+import { fetchLatestReleaseInfo, type LatestReleaseInfo } from '@/lib/latestRelease';
+
+type LandingLatestUpdatesProps = {
+ lang: 'en' | 'cn';
+ fallbackVersion: string;
+ initialRelease?: ChangelogRelease;
+ initialLatestRelease?: LatestReleaseInfo | null;
+};
+
+export function LandingLatestUpdates({ lang, fallbackVersion, initialRelease, initialLatestRelease }: LandingLatestUpdatesProps) {
+ const [latest, setLatest] = useState(() => buildLandingLatestUpdates(lang, initialRelease, fallbackVersion, initialLatestRelease));
+
+ useEffect(() => {
+ let active = true;
+
+ setLatest(buildLandingLatestUpdates(lang, initialRelease, fallbackVersion, initialLatestRelease));
+ Promise.all([fetchLatestReleaseInfo(), fetchChangelog(lang)]).then(([releaseInfo, data]) => {
+ if (!active) return;
+
+ setLatest(buildLandingLatestUpdates(lang, data.releases[0] ?? initialRelease, fallbackVersion, releaseInfo ?? initialLatestRelease));
+ });
+
+ return () => {
+ active = false;
+ };
+ }, [lang, fallbackVersion, initialRelease, initialLatestRelease]);
+
+ return (
+
+ {latest.version}
+
+
{latest.title}
+
{latest.desc}
+
+
+ {latest.items.map((item) => (
+ -
+
+ {item}
+
+ ))}
+
+
+ {latest.link}
+
+
+
+ );
+}
diff --git a/docs/content/docs/changelog.cn.mdx b/docs/content/docs/changelog.cn.mdx
index e3542eabd..a4d6f3238 100644
--- a/docs/content/docs/changelog.cn.mdx
+++ b/docs/content/docs/changelog.cn.mdx
@@ -3,195 +3,6 @@ title: 更新日志
description: 按版本查看 DBX 的新增功能、体验改进和问题修复,内容同步自 GitHub Releases。
---
-这里整理 DBX 的版本变化,按版本号从新到旧排列。内容以 GitHub Releases 为准,保留功能、改进和修复条目,下载安装说明请查看对应 GitHub Release 页面。
+更新日志会直接读取 R2 上的最新 Release 数据,因此发布后无需重新构建文档站也能刷新。
-## v0.5.4
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.4)
-
-### 新功能
-
-- **JDBC SSH 隧道和代理支持** — JDBC 连接新增 SSH 隧道和代理支持,自动解析和改写 JDBC URL 中的 host:port,通过隧道安全访问远程数据库
-- **对象浏览器分组与右键菜单** — 对象浏览器按类型分组显示数据库对象,新增右键上下文菜单操作
-- **Redis 批量操作和命令行** — Redis 新增批量键操作和命令执行器
-- **数据库代理支持** — 新增数据库连接和 AI 的代理 (Proxy) 支持
-- **数据表格 LIKE/NOT LIKE 过滤** — 右键菜单新增 LIKE、NOT LIKE、大于、小于等过滤条件
-- **数据库对象源码编辑** — 支持查看和编辑数据库对象(存储过程、函数、视图等)的源码
-- **Connection URL 动态占位符** — 连接对话框根据数据库类型动态显示对应的 URL 格式示例 (contributed by @Abeautifulsnow)
-- **AI Thinking 开关** — AI 设置新增 thinking 开关,可关闭本地模型(如 Ollama/vLLM)的思考输出以节省 token (contributed by @rarnu)
-- **PostgreSQL 新类型支持** — 新增 PostgreSQL 整数数组和 float4 类型支持 (contributed by @xKrah)
-- **西班牙语支持** — 新增西班牙语本地化,重构语言切换为下拉菜单 (contributed by @Max29xD)
-
-### 改进
-
-- **设置页面优化** — AI 模型设置移至全局设置页面,优化设置页面布局和选中项样式 (contributed by @rarnu)
-- **单元格值不再截断** — 修复详情面板和复制/导出操作中单元格值被截断为 256 字符的问题,现在显示完整内容 (contributed by @Abeautifulsnow)
-- **虚拟滚动优化** — 优化数据表格虚拟滚动,减少快速滚动时的白屏闪烁
-- **数据表格工具栏** — 优化数据表格工具栏布局
-- **Redis 键浏览器国际化** — Redis 键浏览器新增国际化支持 (contributed by @xKrah)
-- **设置页面链接** — 改进设置对话框中的链接显示
-
-### 修复
-
-- **Oracle RAC/SCAN 连接** — 修复 Oracle RAC/SCAN 连接重定向问题 (closes #209)
-- **Oracle 表数据加载与编辑** — 修复 Oracle 表数据加载和编辑功能异常
-- **Oracle Schema 切换** — 简化编辑器选择器并支持 schema 切换 (closes #205)
-- **Oracle 非英文 Windows 连接错误** — 修复 Oracle 在非英文 Windows 系统上的连接错误检测 (closes #208)
-- **Oracle Schema Diff** — 加速 Oracle schema diff 并增加注释同步 (closes #198)
-- **侧边栏性能** — 修复 100+ 数据库时侧边栏 UI 卡顿 (closes #201)
-- **侧边栏搜索** — 修复侧边栏搜索无法找到被对象浏览器隐藏的表
-- **侧边栏刷新** — 修复展开节点刷新时子节点缓存未清除的问题
-- **WHERE 过滤持久化** — 修复 WHERE 过滤条件在 tab 切换时丢失的问题 (closes #210)
-- **数据表格分页** — 修复保存数据后分页大小被重置的问题
-- **Enter 键补全** — 修复 Enter 键优先选中自动补全建议项 (closes #202)
-- **SQL Server 对象源码** — 修复 SQL Server 对象源码定义保存问题
-- **构建修复** — 修复单引号导致构建失败的问题 (contributed by @xKrah)
-- **侧边栏双击** — 修复侧边栏需要双击才能打开对象浏览器的问题
-- **虚拟树优化** — 修复非空树的虚拟化判断函数
-
-## v0.5.3
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.3)
-
-### 新功能
-
-- **AI SQL 智能执行** — AI 助手支持自动执行 SQL 语句,内置安全防护策略,根据语句类型自动判断是否允许执行
-- **MongoDB 表视图** — 新增 MongoDB 集合的表格视图,支持内联编辑数据
-- **导入 DBeaver 连接** — 支持从 DBeaver 导入数据库连接配置
-- **导入 Navicat 连接** — 支持从 Navicat NCX 文件导入数据库连接配置
-- **存储过程/函数浏览** — 对象浏览器支持显示存储过程和函数
-- **虚拟对象浏览器** — 新增虚拟对象浏览器,统一展示数据库对象
-- **数据库活动历史** — 支持追踪和查看数据库操作历史记录
-- **外部表格数据源基础** — 新增外部表格数据源的通用底座架构,为后续接入 CSV/XLSX/在线表格等数据源奠定基础 (contributed by @BlueSkyXN)
-- **数据表格加载指示器** — 数据表格新增加载遮罩层和耗时计时器,查询执行状态一目了然
-
-### 改进
-
-- **编辑器增强** — 编辑器功能增强、DataGrid 重构和侧边栏交互改进
-- **XLSX 导出** — 表格右键菜单新增 XLSX 导出选项
-- **全栈性能优化** — 全栈性能优化和安全加固
-- **Oracle 加载提速** — 优化 Oracle 数据库初始表加载速度
-- **DuckDB 编译优化** — DuckDB 捆绑编译改为可选,减小构建体积
-
-### 修复
-
-- **侧边栏树状态** — 修复侧边栏树状态显示问题
-- **GitHub 链接** — 修复设置页面 About 中 GitHub 链接不可点击的问题
-- **Web 连接池** — 修复带前缀的 Web 连接池被意外断开的问题
-- **连接配置缓存** — 修复 Web 连接配置缓存同步问题
-- **Oracle Schema 缓存** — 修复 Oracle Schema 缓存刷新和表列表不完整的问题
-- **Oracle NCHAR 解码** — 修复 Oracle NCHAR 值解码不正确的问题
-- **侧边栏搜索折叠** — 修复侧边栏搜索结果无法折叠的问题
-
-## v0.5.2
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.2)
-
-### 新功能
-
-- **JDBC 插件支持** — 新增可选的 JDBC 插件模块,支持通过 JDBC 驱动连接更多数据库类型
-- **SQL 代码库** — 支持保存和管理常用 SQL 语句,存储在应用数据库中,随时复用
-- **DataGrip 风格列过滤器** — 数据表格支持类似 DataGrip 的列过滤功能,快速筛选数据
-- **数据对比与结构同步** — 新增数据对比和表结构同步功能
-- **连接 URL 和 XLSX 导出** — 支持通过连接 URL 快速创建连接,支持导出数据为 XLSX 格式
-- **连接颜色标识** — 连接支持自定义颜色标识,方便区分不同环境
-- **表格剪贴板快捷键** — 数据表格支持复制粘贴等剪贴板快捷键操作
-- **布局偏好设置** — 支持自定义界面布局偏好
-- **SSH 超时配置** — SSH 隧道连接支持配置超时时间
-
-### 改进
-
-- **界面外观优化** — 优化面板、标签页和应用整体外观布局
-
-### 修复
-
-- **Redis 键表列对齐** — 修复 Redis 键表格列宽未对齐的问题
-- **连接状态同步** — 修复展开缓存树时连接状态未同步的问题
-- **连接对话框错误** — 修复连接对话框的错误提示问题
-- **SQL 注释后查询检测** — 修复 SQL 注释后的结果查询未被正确检测的问题
-- **列过滤弹窗优化** — 优化列过滤弹窗的交互体验
-- **查询标签页持久化** — 修复未保存的查询标签页丢失的问题
-- **隐藏系统数据库对象** — 修复侧栏显示系统数据库对象的问题
-- **文件选择器行为** — 修复 Web 版文件选择器的行为不一致问题
-
-## v0.5.1
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.1)
-
-### 新功能
-
-- **设置页关于标签** — 设置对话框新增"关于"标签页,方便查看版本信息
-- **数据库对象列表缓存** — 侧栏数据库对象列表支持缓存,减少重复查询,加速切换
-- **从分组菜单创建连接** — 侧栏分组右键菜单支持直接创建新连接
-- **编辑器 Tab 缩进** — SQL 编辑器支持 Tab 键缩进
-- **Windows 绿色版压缩包** — 新增 Windows 便携版(ZIP),无需安装即可使用
-
-### 改进
-
-- **查询结果流式加载** — 查询结果使用流式传输并限制行数,提升大数据量查询的响应速度
-- **侧栏搜索性能优化** — 侧栏搜索匹配算法优化,过滤速度更快
-- **表格右键菜单增强** — 数据表格右键菜单功能增强
-- **数据表工具栏条件刷新** — 修复表数据编辑的工具栏,条件过滤和刷新不再丢失状态 (contributed by @rarnu)
-- **AI 对话历史可切换** — AI 助手对话历史支持开关控制
-
-### 修复
-
-- **SQL Server 编辑支持** — 修复 SQL Server 数据编辑相关问题
-- **更新后重启** — 修复应用更新后重启失败的问题
-- **表格翻页滚动位置** — 修复翻页后表格滚动位置未重置的问题
-- **MySQL 事务编辑** — 修复 MySQL 事务编辑使用原始 SQL 执行的问题
-- **侧栏刷新保持展开** — 修复侧栏刷新后树节点展开状态丢失的问题
-- **标题栏双击切换** — 修复标题栏双击时窗口状态重复切换的问题 (#179)
-- **默认库过滤数据库树** — 修复默认数据库配置过滤数据库树显示的问题 (#175)
-- **标签文字被箭头遮挡** — 修复标签页滚动箭头与文字重叠的问题
-- **表格与弹窗布局** — 优化表格与 SQL 文件弹窗的布局显示
-- **事务状态位置** — 事务状态指示移至操作按钮旁边,更直观
-- **WHERE/ORDER BY 条件** — 修复 WHERE 和 ORDER BY 输入框条件重复和状态丢失的问题
-- **默认数据库处理** — 统一默认数据库的处理逻辑
-
-## v0.5.0
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.0)
-
-### 新功能
-
-- **Redis 浏览器全面重构** — 键浏览器升级为 Navicat 风格表格视图,支持键值编辑、分页浏览和全库键扫描 (contributed by @Bacon2994)
-- **自定义标题栏** — 窗口标题栏改为自定义样式,集成窗口控制按钮
-- **数据表格 WHERE/ORDER BY 过滤** — 数据表格新增独立的 WHERE 和 ORDER BY 输入框,支持自定义条件过滤和排序 (#165)
-- **结果集数据过滤** — 查询结果集内支持对已加载数据进行实时过滤 (contributed by @rarnu)
-- **单元格值编辑器** — 单元格详情面板支持直接编辑值和设置 NULL
-- **列宽自适应** — 数据表格自动根据表头文字和数据内容调整最佳列宽
-- **复制为 INSERT** — 数据表格右键菜单新增"复制为 INSERT 语句"功能 (#166)
-- **创建/删除数据库和 Schema** — 侧栏右键菜单支持创建和删除数据库及 Schema
-- **SQL Server CROSS APPLY 补全** — SQL 编辑器支持 CROSS APPLY / OUTER APPLY 语法补全 (#168)
-- **OceanBase Oracle 模式** — 自动检测 OceanBase Oracle 模式,使用 Oracle 风格的 Schema 查询 (#155)
-- **鼠标中键关闭标签** — 支持鼠标中键点击关闭标签页 (#162)
-- **排序提示图标** — 鼠标悬停列标题时显示可排序提示图标
-- **窗口状态记忆** — 窗口位置和大小在关闭后自动保存,重新打开时恢复
-- **MCP 连接管理** — MCP 工具新增 `dbx_add_connection` 和 `dbx_remove_connection`,操作后自动通知桌面端刷新
-
-### 改进
-
-- **DataGrid 工具栏增强** — 数据表格工具栏和数据集交互优化 (contributed by @rarnu)
-- **JSON/JSONB 渲染性能** — 预序列化 JSON/JSONB 值并截断单元格显示,大幅提升大数据量渲染性能
-- **信息栏合并** — 数据表底部信息栏和事务工具栏合并为一行,节省屏幕空间
-- **标签页样式优化** — 标签模式切换改用图标,标签标题改为 Navicat 风格
-
-### 修复
-
-- **标签页右键菜单** — 修复关闭所有/关闭其他/关闭右侧等标签页右键菜单不可用的问题
-- **查询结果表头和排序** — 修复查询结果表头提示和全量排序问题 (contributed by @Bacon2994)
-- **MCP 连接配置** — 修复 MCP 写入的 config_json 与 Rust ConnectionConfig 结构不匹配的问题,改为从 SQLite 读取连接
-- **Tailwind 样式丢失** — 修复 tmpfs 环境下 Tailwind 样式缺失的问题 (#167)
-- **数据表格滚动抖动** — 禁用数据表格过度滚动回弹,防止表头和数据错位 (#156)
-- **GaussDB Schema 列表** — 移除 GaussDB list_schemas 的 EXISTS 过滤以保持一致性 (#154)
-- **侧栏数据库过滤** — 连接配置指定数据库时,侧栏只显示该数据库 (#160)
-- **Oracle 11g 兼容性** — 修复 Oracle 11g 连接兼容性问题和侧栏改进
-- **MySQL 全 NULL 显示** — 增加 MySQL 类型回退,防止缺少类型映射时所有列值显示为 NULL (#40)
-- **AI 执行 SQL** — 修复 AI 执行时传递过期 SQL 状态的问题 (#153)
-- **JSONB 编辑显示** — 修复 JSONB 值在编辑模式和详情面板中未正确显示为 JSON 字符串的问题 (#152)
-- **MongoDB 副本集 SSH** — 修复 MongoDB 副本集通过 SSH 隧道连接失败的问题 (#151)
-- **Redis 二进制误判** — 修复反斜杠字符串被误判为二进制数据的问题 (contributed by @Bacon2994)
-- **Toast 通知层级** — 修复 Toast 通知被对话框遮挡的问题
-- **DDL 面板文本选择** — 修复 DDL 面板无法选中文本的问题
-- **原生右键菜单** — 禁用浏览器原生右键菜单,新增侧栏刷新按钮 (#159)
+[打开实时更新日志](/cn/changelog)
diff --git a/docs/content/docs/changelog.mdx b/docs/content/docs/changelog.mdx
index 9aa0d02e1..94e328eb5 100644
--- a/docs/content/docs/changelog.mdx
+++ b/docs/content/docs/changelog.mdx
@@ -3,195 +3,6 @@ title: Changelog
description: Track DBX release notes by version, mirrored from GitHub Releases.
---
-This page mirrors the user-facing release notes from GitHub Releases. It keeps feature, improvement, and fix entries in the docs site; download tables and installer details remain on each GitHub Release page.
+The changelog is loaded from the latest release data on R2, so it can refresh after a release without rebuilding the docs site.
-## v0.5.4
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.4)
-
-### Added
-
-- **JDBC SSH tunnels and proxy support** — JDBC connections can use SSH tunnels and proxies, with host:port values parsed and rewritten from JDBC URLs.
-- **Grouped object browser and context menus** — Database objects are grouped by type and expose context-menu actions.
-- **Redis batch operations and command runner** — Redis adds batch key operations and a command executor.
-- **Database proxy support** — Database connections and AI settings now support proxy configuration.
-- **Data grid LIKE / NOT LIKE filters** — Grid context menus add LIKE, NOT LIKE, greater-than, and less-than filters.
-- **Database object source editing** — View and edit source for database objects such as procedures, functions, and views.
-- **Dynamic Connection URL placeholders** — The connection dialog shows URL examples that match the selected database type (contributed by @Abeautifulsnow).
-- **AI Thinking toggle** — AI settings can hide thinking output from local models such as Ollama or vLLM to save tokens (contributed by @rarnu).
-- **PostgreSQL type support** — Adds support for integer arrays and float4 (contributed by @xKrah).
-- **Spanish localization** — Adds Spanish UI text and refactors language switching into a dropdown (contributed by @Max29xD).
-
-### Improved
-
-- **Settings page** — Moves AI model settings into global settings and improves the settings layout (contributed by @rarnu).
-- **Full cell values** — Details, copy, and export flows no longer truncate cell values to 256 characters (contributed by @Abeautifulsnow).
-- **Virtual scrolling** — Reduces blank flashing during fast grid scrolling.
-- **Data grid toolbar** — Refines the grid toolbar layout.
-- **Redis browser i18n** — Adds localization support to the Redis key browser (contributed by @xKrah).
-- **Settings links** — Improves link presentation in the settings dialog.
-
-### Fixed
-
-- Fixes Oracle RAC/SCAN redirects (closes #209).
-- Fixes Oracle table loading and editing.
-- Simplifies Oracle selector behavior and supports schema switching (closes #205).
-- Fixes Oracle connection error detection on non-English Windows systems (closes #208).
-- Speeds up Oracle schema diff and adds comment synchronization (closes #198).
-- Fixes sidebar lag with 100+ databases (closes #201).
-- Fixes sidebar search missing tables hidden by the object browser.
-- Fixes stale child caches when refreshing expanded sidebar nodes.
-- Preserves WHERE filters across tab switches (closes #210).
-- Keeps data grid page size after saving data.
-- Makes Enter prefer autocomplete suggestions when available (closes #202).
-- Fixes SQL Server object source saving.
-- Fixes a build failure caused by single quotes (contributed by @xKrah).
-- Fixes object browser items requiring double-click to open.
-- Fixes virtual tree detection for non-empty trees.
-
-## v0.5.3
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.3)
-
-### Added
-
-- **AI SQL smart execution** — The AI assistant can execute SQL with built-in statement safety checks.
-- **MongoDB table view** — MongoDB collections can be viewed and edited in a grid.
-- **DBeaver import** — Import database connection configs from DBeaver.
-- **Navicat import** — Import database connections from Navicat NCX files.
-- **Procedure and function browsing** — The object browser can show procedures and functions.
-- **Virtual object browser** — A unified browser for database objects.
-- **Database activity history** — Track and review database operation history.
-- **External table data source foundation** — Adds the shared architecture for future CSV, XLSX, and online sheet sources (contributed by @BlueSkyXN).
-- **Data grid loading indicator** — Adds a loading mask and elapsed-time display for query execution.
-
-### Improved
-
-- Editor features, DataGrid internals, and sidebar interactions.
-- XLSX export from the table context menu.
-- Full-stack performance and safety hardening.
-- Faster initial Oracle table loading.
-- Optional DuckDB bundling to reduce build size.
-
-### Fixed
-
-- Sidebar tree state display.
-- GitHub link clickability in Settings > About.
-- Unexpected disconnection of prefixed web connection pools.
-- Web connection config cache synchronization.
-- Oracle schema cache refresh and incomplete table lists.
-- Oracle NCHAR decoding.
-- Sidebar search result collapsing.
-
-## v0.5.2
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.2)
-
-### Added
-
-- Optional JDBC plugin support for connecting to more database types through JDBC drivers.
-- Saved SQL library for reusable SQL snippets.
-- DataGrip-style column filters in the data grid.
-- Data comparison and table structure synchronization.
-- Connection URL creation and XLSX export.
-- Custom connection colors for distinguishing environments.
-- Clipboard shortcuts in the data grid.
-- Layout preference settings.
-- SSH tunnel timeout configuration.
-
-### Improved
-
-- Panels, tabs, and general application layout.
-
-### Fixed
-
-- Redis key table column alignment.
-- Connection state synchronization when expanding cached trees.
-- Connection dialog error messages.
-- Result-query detection after SQL comments.
-- Column filter popup interactions.
-- Unsaved query tab persistence.
-- Hidden system database objects in the sidebar.
-- Web file picker inconsistencies.
-
-## v0.5.1
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.1)
-
-### Added
-
-- About tab in Settings.
-- Cached database object lists in the sidebar.
-- Create new connections from group context menus.
-- Tab indentation in the SQL editor.
-- Windows portable ZIP builds.
-
-### Improved
-
-- Streaming query results with row limits for large result sets.
-- Faster sidebar search filtering.
-- More capable grid context menus.
-- Better toolbar state retention for data editing filters and refresh (contributed by @rarnu).
-- Toggleable AI conversation history.
-
-### Fixed
-
-- SQL Server data editing issues.
-- Restart after app updates.
-- Grid scroll position after pagination.
-- MySQL transaction editing through raw SQL.
-- Sidebar expanded state after refresh.
-- Repeated title-bar double-click state toggles (#179).
-- Default database filtering in the database tree (#175).
-- Tab text overlapping scroll arrows.
-- Table and SQL file dialog layouts.
-- Transaction status placement.
-- Duplicate or lost WHERE / ORDER BY state.
-- Default database handling consistency.
-
-## v0.5.0
-
-[GitHub Release](https://github.com/t8y2/dbx/releases/tag/v0.5.0)
-
-### Added
-
-- Fully rebuilt Redis browser with a Navicat-style table view, value editing, pagination, and full keyspace scanning (contributed by @Bacon2994).
-- Custom window title bar with integrated window controls.
-- Dedicated WHERE and ORDER BY inputs for data-grid filtering and sorting (#165).
-- In-result filtering for loaded query data (contributed by @rarnu).
-- Editable cell details panel with explicit NULL support.
-- Auto-fit column widths based on headers and data.
-- Copy selected rows as INSERT statements (#166).
-- Create and drop databases and schemas from the sidebar.
-- SQL Server CROSS APPLY / OUTER APPLY completion (#168).
-- OceanBase Oracle mode detection (#155).
-- Middle-click tab close (#162).
-- Sort hint icons on column hover.
-- Window position and size persistence.
-- MCP connection management with `dbx_add_connection` and `dbx_remove_connection`.
-
-### Improved
-
-- DataGrid toolbar and dataset interactions (contributed by @rarnu).
-- JSON / JSONB rendering performance for large data sets.
-- Combined data-grid information and transaction toolbar row.
-- Navicat-style tab presentation.
-
-### Fixed
-
-- Tab context-menu actions such as close all, close others, and close right.
-- Query result headers and full sorting (contributed by @Bacon2994).
-- MCP config_json compatibility with Rust `ConnectionConfig`.
-- Tailwind styles missing under tmpfs (#167).
-- Data-grid overscroll causing header and data misalignment (#156).
-- GaussDB schema-list filtering (#154).
-- Sidebar database filtering when a connection specifies a database (#160).
-- Oracle 11g compatibility and sidebar behavior.
-- MySQL fallback type mapping to avoid all-NULL columns (#40).
-- AI execution receiving stale SQL state (#153).
-- JSONB display in edit mode and the details panel (#152).
-- MongoDB replica-set connections through SSH tunnels (#151).
-- Redis backslash strings being misclassified as binary data (contributed by @Bacon2994).
-- Toast notifications hidden behind dialogs.
-- Text selection in the DDL panel.
-- Browser-native context menu conflicts and adds a sidebar refresh button (#159).
+[Open the live changelog](/en/changelog)
diff --git a/docs/lib/appVersion.ts b/docs/lib/appVersion.ts
new file mode 100644
index 000000000..17721b937
--- /dev/null
+++ b/docs/lib/appVersion.ts
@@ -0,0 +1,5 @@
+import appPackage from '../../package.json';
+
+export function getAppVersion() {
+ return appPackage.version;
+}
diff --git a/docs/lib/changelog.ts b/docs/lib/changelog.ts
index 524e36bbb..d5889c7d2 100644
--- a/docs/lib/changelog.ts
+++ b/docs/lib/changelog.ts
@@ -1,3 +1,5 @@
+import { requestJson } from '@/lib/httpJson';
+
export type ChangelogItem = {
title: string;
desc: string;
@@ -21,17 +23,111 @@ export type ChangelogData = {
releases: ChangelogRelease[];
};
-const BASE_URL = process.env.CHANGELOG_BASE_URL || 'https://dl.dbxio.com/changelog';
+type GitHubRelease = {
+ tag_name: string;
+ name: string | null;
+ published_at: string | null;
+ body: string | null;
+ draft: boolean;
+ prerelease: boolean;
+};
+
+const DEFAULT_BASE_URL = 'https://dl.dbxio.com/changelog';
+const GITHUB_RELEASES_URL = 'https://api.github.com/repos/t8y2/dbx/releases?per_page=30';
+
+const SECTION_MAP: Record = {
+ 新功能: 'added',
+ Added: 'added',
+ 改进: 'improved',
+ Improved: 'improved',
+ 修复: 'fixed',
+ Fixed: 'fixed',
+ 变更: 'changed',
+ Changed: 'changed',
+ 移除: 'removed',
+ Removed: 'removed',
+};
+
+export function changelogUrl(lang: 'en' | 'cn') {
+ const baseUrl =
+ (typeof process !== 'undefined' &&
+ (process.env.NEXT_PUBLIC_CHANGELOG_BASE_URL || process.env.CHANGELOG_BASE_URL)) ||
+ DEFAULT_BASE_URL;
+
+ return `${baseUrl}/releases-${lang}.json`;
+}
export async function fetchChangelog(lang: 'en' | 'cn'): Promise {
- const url = `${BASE_URL}/releases-${lang}.json`;
+ const url = changelogUrl(lang);
try {
- const res = await fetch(url, { next: { revalidate: 3600 } });
- if (!res.ok) {
- return { updatedAt: '', releases: [] };
+ return await requestJson(url);
+ } catch {
+ return fetchGitHubChangelog();
+ }
+}
+
+function stripDownloadSection(body: string) {
+ const markers = ['### 下载安装', '### Download', '### 系统要求', '### System Requirements'];
+ let idx = body.length;
+
+ for (const marker of markers) {
+ const markerIndex = body.indexOf(marker);
+ if (markerIndex !== -1 && markerIndex < idx) {
+ idx = markerIndex;
}
- return res.json() as Promise;
+ }
+
+ return body.slice(0, idx).trim();
+}
+
+function parseReleaseBody(body: string) {
+ const sections: ChangelogSection[] = [];
+ let current: ChangelogSection | null = null;
+
+ for (const line of stripDownloadSection(body).split('\n')) {
+ const headerMatch = line.match(/^###\s+(.+)/);
+ if (headerMatch) {
+ const title = headerMatch[1].trim();
+ current = { type: SECTION_MAP[title] || 'other', title, items: [] };
+ sections.push(current);
+ continue;
+ }
+
+ if (!current) continue;
+
+ const itemMatch = line.match(/^-\s+\*\*(.+?)\*\*\s*[—–-]\s*(.+)/);
+ if (itemMatch) {
+ current.items.push({ title: itemMatch[1].trim(), desc: itemMatch[2].trim() });
+ continue;
+ }
+
+ const plainMatch = line.match(/^-\s+(.+)/);
+ if (plainMatch) {
+ current.items.push({ title: plainMatch[1].trim(), desc: '' });
+ }
+ }
+
+ return sections.filter((section) => section.items.length > 0);
+}
+
+export async function fetchGitHubChangelog(): Promise {
+ try {
+ const releases = await requestJson(GITHUB_RELEASES_URL, {
+ headers: { Accept: 'application/vnd.github+json' },
+ });
+
+ return {
+ updatedAt: new Date().toISOString(),
+ releases: releases
+ .filter((release) => !release.draft && !release.prerelease)
+ .map((release) => ({
+ tag: release.tag_name,
+ name: release.name || release.tag_name,
+ date: (release.published_at || '').slice(0, 10),
+ sections: parseReleaseBody(release.body || ''),
+ })),
+ };
} catch {
return { updatedAt: '', releases: [] };
}
diff --git a/docs/lib/downloadLinks.ts b/docs/lib/downloadLinks.ts
new file mode 100644
index 000000000..41f47e99f
--- /dev/null
+++ b/docs/lib/downloadLinks.ts
@@ -0,0 +1,51 @@
+export type InstallLang = 'en' | 'cn';
+
+export type InstallOption = {
+ id: string;
+ label: string;
+ href: string;
+};
+
+type DownloadArtifact = {
+ id: string;
+ labels: Record;
+ suffix: string;
+};
+
+const DOWNLOAD_BASE_URL = 'https://dl.dbxio.com/releases/latest';
+
+const downloadArtifacts: DownloadArtifact[] = [
+ {
+ id: 'macos-arm',
+ labels: { en: 'For macOS (Apple Silicon)', cn: '适用于 macOS (Apple Silicon)' },
+ suffix: 'aarch64.dmg',
+ },
+ {
+ id: 'macos-intel',
+ labels: { en: 'For macOS (Intel)', cn: '适用于 macOS (Intel)' },
+ suffix: 'x64.dmg',
+ },
+ {
+ id: 'windows',
+ labels: { en: 'For Windows', cn: '适用于 Windows' },
+ suffix: 'x64-setup.exe',
+ },
+ {
+ id: 'linux',
+ labels: { en: 'For Linux x64', cn: '适用于 Linux x64' },
+ suffix: 'amd64.AppImage',
+ },
+ {
+ id: 'linux-arm',
+ labels: { en: 'For Linux ARM64', cn: '适用于 Linux ARM64' },
+ suffix: 'aarch64.AppImage',
+ },
+];
+
+export function createInstallOptions(lang: InstallLang, version: string): InstallOption[] {
+ return downloadArtifacts.map((artifact) => ({
+ id: artifact.id,
+ label: artifact.labels[lang],
+ href: `${DOWNLOAD_BASE_URL}/DBX_${version}_${artifact.suffix}`,
+ }));
+}
diff --git a/docs/lib/httpJson.ts b/docs/lib/httpJson.ts
new file mode 100644
index 000000000..49d14d0ba
--- /dev/null
+++ b/docs/lib/httpJson.ts
@@ -0,0 +1,34 @@
+function requestJsonWithXhr(url: string): Promise {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ xhr.responseType = 'json';
+ xhr.setRequestHeader('Accept', 'application/json');
+ xhr.onload = () => {
+ if (xhr.status < 200 || xhr.status >= 300) {
+ reject(new Error(`Request failed with status ${xhr.status}`));
+ return;
+ }
+
+ resolve((xhr.response ?? JSON.parse(xhr.responseText)) as T);
+ };
+ xhr.onerror = () => reject(new Error('Request failed'));
+ xhr.send();
+ });
+}
+
+export async function requestJson(url: string, init?: RequestInit): Promise {
+ if (typeof fetch === 'function') {
+ const res = await fetch(url, init);
+ if (!res.ok) {
+ throw new Error(`Request failed with status ${res.status}`);
+ }
+ return res.json() as Promise;
+ }
+
+ if (typeof XMLHttpRequest === 'function') {
+ return requestJsonWithXhr(url);
+ }
+
+ throw new Error('No browser request API is available');
+}
diff --git a/docs/lib/landingLatest.ts b/docs/lib/landingLatest.ts
new file mode 100644
index 000000000..35cfb14e5
--- /dev/null
+++ b/docs/lib/landingLatest.ts
@@ -0,0 +1,63 @@
+import type { ChangelogRelease } from '@/lib/changelog';
+import type { LatestReleaseInfo } from '@/lib/latestRelease';
+
+type LandingLatestUpdates = {
+ version: string;
+ title: string;
+ desc: string;
+ link: string;
+ items: string[];
+};
+
+const text = {
+ en: {
+ title: 'Latest updates',
+ desc: 'Mirrored from the latest GitHub release notes.',
+ link: 'Read the changelog',
+ fallbackItems: [
+ 'Desktop and Docker release assets',
+ 'Database workflow improvements',
+ 'Bug fixes and reliability updates',
+ 'Documentation and packaging updates',
+ ],
+ },
+ cn: {
+ title: '最近更新',
+ desc: '同步 GitHub 最新 Release Notes。',
+ link: '查看更新日志',
+ fallbackItems: [
+ '桌面版与 Docker 发布资产',
+ '数据库工作流改进',
+ '问题修复与稳定性更新',
+ '文档与打包流程更新',
+ ],
+ },
+};
+
+function releaseItems(release: ChangelogRelease, lang: 'en' | 'cn') {
+ const separator = lang === 'cn' ? ',' : ': ';
+
+ return release.sections
+ .flatMap((section) => section.items)
+ .map((item) => (item.desc ? `${item.title}${separator}${item.desc}` : item.title))
+ .slice(0, 4);
+}
+
+export function buildLandingLatestUpdates(
+ lang: 'en' | 'cn',
+ release: ChangelogRelease | undefined,
+ appVersion: string,
+ latestRelease?: LatestReleaseInfo | null,
+): LandingLatestUpdates {
+ const t = text[lang];
+ const items = release ? releaseItems(release, lang) : [];
+ const version = latestRelease?.version ? `v${latestRelease.version}` : release?.tag ?? `v${appVersion}`;
+
+ return {
+ version,
+ title: t.title,
+ desc: t.desc,
+ link: t.link,
+ items: items.length > 0 ? items : t.fallbackItems,
+ };
+}
diff --git a/docs/lib/latestRelease.ts b/docs/lib/latestRelease.ts
new file mode 100644
index 000000000..3c689be54
--- /dev/null
+++ b/docs/lib/latestRelease.ts
@@ -0,0 +1,45 @@
+import { requestJson } from '@/lib/httpJson';
+
+export type LatestReleaseInfo = {
+ version: string;
+ notes?: string;
+ pub_date?: string;
+};
+
+type GitHubLatestRelease = {
+ tag_name: string;
+ body: string | null;
+ published_at: string | null;
+};
+
+const LATEST_RELEASE_URL = 'https://dl.dbxio.com/releases/latest/latest.json';
+const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/t8y2/dbx/releases/latest';
+
+function normalizeVersion(version: string) {
+ return version.replace(/^v/, '');
+}
+
+export async function fetchLatestReleaseInfo(): Promise {
+ try {
+ const release = await requestJson(LATEST_RELEASE_URL);
+ return release.version ? { ...release, version: normalizeVersion(release.version) } : null;
+ } catch {
+ return fetchGitHubLatestReleaseInfo();
+ }
+}
+
+export async function fetchGitHubLatestReleaseInfo(): Promise {
+ try {
+ const release = await requestJson(GITHUB_LATEST_RELEASE_URL, {
+ headers: { Accept: 'application/vnd.github+json' },
+ });
+
+ return {
+ version: normalizeVersion(release.tag_name),
+ notes: release.body || undefined,
+ pub_date: release.published_at || undefined,
+ };
+ } catch {
+ return null;
+ }
+}
diff --git a/tests/docsReleaseIntegration.test.ts b/tests/docsReleaseIntegration.test.ts
new file mode 100644
index 000000000..1f72bd876
--- /dev/null
+++ b/tests/docsReleaseIntegration.test.ts
@@ -0,0 +1,87 @@
+import { readFileSync } from "node:fs";
+import { strict as assert } from "node:assert";
+import test from "node:test";
+import appPackage from "../package.json" with { type: "json" };
+import { createInstallOptions } from "../docs/lib/downloadLinks.ts";
+
+test("docs install links are generated from the app package version", () => {
+ const options = createInstallOptions("en", appPackage.version);
+ const hrefs = options.map((option) => option.href);
+
+ assert.equal(hrefs.length, 5);
+ assert.ok(hrefs.every((href) => href.includes(`/DBX_${appPackage.version}_`)));
+ assert.equal(hrefs.some((href) => href.includes("0.5.9")), false);
+});
+
+test("docs landing page reads latest release data instead of hard-coding the old latest update", () => {
+ const source = readFileSync("docs/app/[lang]/page.tsx", "utf8");
+
+ assert.equal(source.includes("version: 'v0.5.4'"), false);
+ assert.match(source, /LandingLatestUpdates/);
+ assert.match(source, /fetchLatestReleaseInfo/);
+ assert.match(source, /initialLatestRelease/);
+});
+
+test("docs install widget reads the latest release version from latest.json at runtime", () => {
+ const source = readFileSync("docs/components/landing/InstallTabs.tsx", "utf8");
+ const latestRelease = readFileSync("docs/lib/latestRelease.ts", "utf8");
+
+ assert.equal(source.includes("fetchChangelog"), false);
+ assert.match(source, /fetchLatestReleaseInfo/);
+ assert.match(latestRelease, /latest\.json/);
+});
+
+test("homepage update badge keeps latest.json version ahead of changelog tags", () => {
+ const source = readFileSync("docs/components/landing/LandingLatestUpdates.tsx", "utf8");
+
+ assert.match(source, /Promise\.all\(\[fetchLatestReleaseInfo\(\), fetchChangelog\(lang\)\]\)/);
+ assert.match(source, /releaseInfo \?\? initialLatestRelease/);
+});
+
+test("docs changelog page loads releases in the browser from R2", () => {
+ const source = readFileSync("docs/app/[lang]/changelog/page.tsx", "utf8");
+
+ assert.match(source, /initialData/);
+ assert.match(source, /ChangelogRuntime/);
+});
+
+test("legacy docs changelog pages do not keep stale release entries", () => {
+ const en = readFileSync("docs/content/docs/changelog.mdx", "utf8");
+ const cn = readFileSync("docs/content/docs/changelog.cn.mdx", "utf8");
+
+ assert.equal(en.includes("## v0.5.4"), false);
+ assert.equal(cn.includes("## v0.5.4"), false);
+ assert.match(en, /\/en\/changelog/);
+ assert.match(cn, /\/cn\/changelog/);
+});
+
+test("docs deploy does not rerun just to refresh R2 changelog data", () => {
+ const source = readFileSync(".github/workflows/docs.yml", "utf8");
+
+ assert.equal(source.includes("workflow_run:"), false);
+ assert.equal(source.includes("Sync Changelog to R2"), false);
+});
+
+test("changelog sync configures R2 CORS for browser runtime reads", () => {
+ const workflow = readFileSync(".github/workflows/sync-changelog.yml", "utf8");
+ const cors = JSON.parse(readFileSync(".github/r2-cors.json", "utf8")) as {
+ CORSRules: Array<{
+ AllowedMethods: string[];
+ AllowedOrigins: string[];
+ }>;
+ };
+ const rule = cors.CORSRules[0];
+
+ assert.match(workflow, /put-bucket-cors/);
+ assert.deepEqual(rule.AllowedOrigins, ["*"]);
+ assert.ok(rule.AllowedMethods.includes("GET"));
+ assert.ok(rule.AllowedMethods.includes("HEAD"));
+});
+
+test("changelog sync also runs after release workflow publishes with GITHUB_TOKEN", () => {
+ const workflow = readFileSync(".github/workflows/sync-changelog.yml", "utf8");
+
+ assert.match(workflow, /workflow_run:/);
+ assert.match(workflow, /workflows: \['Release'\]/);
+ assert.match(workflow, /github\.event\.workflow_run\.conclusion == 'success'/);
+});