Skip to content

Commit bead873

Browse files
ymkiuxawsl233777
andauthored
refactor(web-ui): UI polish, static URL, skills redesign, and v0.0.35 bump (#170)
* chore: revert version to 0.0.34 after npm unpublish * refactor(web-ui): redesign Usage tab with "River of Time" aesthetic - Merge current session bar with hero metrics for unified focus - Replace bar chart with SVG bezier curve wave visualization - Add automatic delta calculation (7d vs prior 7d, 30d vs prior 30d) - Redesign empty states with animated SVG illustrations - Simplify list views with bullet-point compact style - Enhance color system with delta indicators and heatmap levels * fix(web-ui): resolve /web-ui/ 404 and duplicate path issues - Server: handle /web-ui/ requests by returning index.html - Server: explicitly serve index.html for /web-ui/index.html requests - Client: normalize URL on mount to fix /web-ui/web-ui/ duplicates - Client: redirect /web-ui/ and /web-ui/index.html to /web-ui Fixes the issue where refreshing in sessions tab caused URL to become /web-ui/web-ui/index.html * fix(web-ui): strengthen URL normalization to handle nested duplicates - Use do-while loop to iteratively remove all /web-ui/web-ui/ patterns - Handle edge cases like /web-ui/web-ui/web-ui/index.html - Add additional check for /web-ui/web-ui/ prefix pattern * test(web-ui): add URL routing and normalization tests - Add test-web-ui-url-routing.js E2E test for server routing - Add test-web-ui-url-normalization.mjs unit test (24 cases) - Update test-web-ui-assets.js expectations for /web-ui/ fix - Fix shouldReplace check logic in app.js (use originalPathname) * refactor(web-ui): remove /web-ui route, use / as single entry point - Server: /web-ui, /web-ui/, /web-ui/index.html now return 404 - Root path / is the only HTML entry point - Client: auto-redirect from /web-ui/* to / with hash/params preserved - Assets still served at /web-ui/app.js, /web-ui/styles.css, etc. - Simplified routing logic, removed redundant path handling * feat(web): hide local provider in codex/claude config mode - filter out 'local' provider from displayProvidersList when configMode is codex/claude - prevents users from selecting local provider in these apps due to existing issues - other modes (terminal) remain unaffected * feat(web): disable local provider in codex/claude config mode - local provider card shows as disabled (50% opacity, non-clickable) in codex mode - claude-local bridge card shows as disabled in claude mode - adds CSS .card.disabled style with pointer-events: none - cards remain visible but cannot be selected * fix(web): correct disabled provider click logic - fix operator precedence issue in click handlers - use proper ternary expressions with correct grouping - prevents white screen error on config load * fix(web): remove parentheses from computed property - isLocalProviderDisabled is a computed returning boolean, not a function - should be accessed as property without () in template - fixes white screen error on config load * feat(web): rename claude-local to local with system badge - display name changed from 'claude-local' to 'local' - adds system badge like codex provider readonly indicator - internal id 'claude-local' remains unchanged for compatibility * feat(web): remove local provider subtitle hint - removes '勾选参与负载均衡的提供商' subtitle - keeps card layout cleaner with just title and system badge * refactor(web): redesign skills tab to minimalist single-column layout - Remove cluttered overview section with 6 stat cards - Consolidate header with inline target switcher - Single-column flow layout for better mobile experience - Add count badges directly in panel headers - Add quick action buttons in import panel - Add individual import button for each skill - Remove help panel (content available elsewhere) - Remove root path display (moved to tooltip) - Preserve all functionality while reducing visual noise Design principles: - Progressive disclosure: scroll to discover - Large touch targets: 56px min height - Visual hierarchy: clear content grouping - Cleaner information architecture * refactor(web): implement static URL, always stay at / - Remove all URL routing, URL stays as pure '/' - Delete query parameters and hash from browser address bar - Remove history.pushState/replaceState operations for tab switching - Remove session filter URL synchronization - Share links still work (generate URL with params for clipboard) - When opening share link, params are applied then URL cleaned - All state managed via memory + localStorage Changes: - app.js: Clean URL on mount, remove resource path fix logic - app.methods.navigation.mjs: Remove URL operations in switchMainTab - sessions-filters-url.mjs: Empty syncSessionsFilterUrl, fix share URL base - app.methods.session-browser.mjs: Clean URL after applying URL state * chore: bump version to 0.0.35 * fix(ci): restore missing CI scripts in package.json * test: update unit tests to match new web-ui structure - Fix sessionUsageDaily -> sessionUsageHourlyHeatmap assertion - Remove obsolete loadSkillsMarketOverview and market-action-grid checks - Update skills-target-chip selector (was market-target-chip) - Fix provider card disabled state and tabindex assertions - Remove market.pill importableDirect/importMissing checks * test: add new keys to parity baseline allowlists - isLocalProviderDisabled (data key) - sessionUsageWave, sessionsUsageSelectedDay, usageHeroDelta, usageHeroDeltaClass (computed keys) * test: add more usage hero keys to parity baseline - usageHeroMainValue - usageHeroSubLabel * test: fix isLocalProviderDisabled key type Move isLocalProviderDisabled from data keys to computed keys as it is defined as a computed property in app.computed.dashboard.mjs * fix(web-ui): canonicalize session tab navigation URL * fix: stabilize web ui navigation and canonical urls * fix: avoid runtime vue compiler dependency * fix: preserve sessions tab render on navigation --------- Co-authored-by: ymkiux <ymkiux@users.noreply.github.com> Co-authored-by: awsl233777 <awsl233777@gmail.com>
1 parent aa3434d commit bead873

43 files changed

Lines changed: 11078 additions & 736 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cli.js

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10058,9 +10058,10 @@ function watchPathsForRestart(targets, onChange) {
1005810058
}
1005910059
// #endregion watchPathsForRestart
1006010060

10061-
function writeJsonResponse(res, statusCode, payload) {
10061+
function writeJsonResponse(res, statusCode, payload, headers = {}) {
1006210062
const body = JSON.stringify(payload, null, 2);
1006310063
res.writeHead(statusCode, {
10064+
...headers,
1006410065
'Content-Type': 'application/json; charset=utf-8',
1006510066
'Content-Length': Buffer.byteLength(body, 'utf-8')
1006610067
});
@@ -10620,7 +10621,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1062010621
const securityHeaders = {
1062110622
'X-Content-Type-Options': 'nosniff',
1062210623
'X-Frame-Options': 'DENY',
10623-
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:"
10624+
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss:"
1062410625
};
1062510626
const origWriteHead = res.writeHead.bind(res);
1062610627
res.writeHead = function (statusCode, headers) {
@@ -10649,34 +10650,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1064910650
|| requestPath.startsWith('/download/')
1065010651
) {
1065110652
const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
10652-
const isLoopback = !remoteAddr
10653-
|| remoteAddr === '127.0.0.1'
10654-
|| remoteAddr === '::1'
10655-
|| remoteAddr === '::ffff:127.0.0.1';
10653+
const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
1065610654
if (!isLoopback) {
1065710655
const rateLimitKey = (remoteAddr || 'unknown') + ':' + requestPath;
1065810656
if (!checkRateLimit(rateLimitKey)) {
10659-
res.writeHead(429, { 'Content-Type': 'application/json; charset=utf-8', 'Retry-After': '60' });
10660-
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
10657+
writeJsonResponse(res, 429, { error: 'Rate limit exceeded' }, { 'Retry-After': '60' });
1066110658
return;
1066210659
}
10663-
const expected = typeof process.env.CODEXMATE_HTTP_TOKEN === 'string'
10664-
? process.env.CODEXMATE_HTTP_TOKEN.trim()
10665-
: '';
10666-
if (!expected) {
10667-
sendJson(403, {
10668-
error: 'Remote access is disabled (set CODEXMATE_HTTP_TOKEN or use --host 127.0.0.1)'
10669-
});
10670-
return;
10671-
}
10672-
const headers = req && req.headers && typeof req.headers === 'object' ? req.headers : {};
10673-
const rawAuth = typeof headers.authorization === 'string' ? headers.authorization.trim() : '';
10674-
const match = rawAuth ? rawAuth.match(/^bearer\s+(.+)$/i) : null;
10675-
const actual = match && match[1]
10676-
? match[1].trim()
10677-
: (rawAuth ? rawAuth : (typeof headers['x-codexmate-token'] === 'string' ? String(headers['x-codexmate-token']).trim() : ''));
10678-
if (!actual || !safeTimingEqual(actual, expected)) {
10679-
sendJson(401, { error: 'Unauthorized' });
10660+
const auth = assertRequestAuthorized(req, res);
10661+
if (!auth.ok) {
1068010662
return;
1068110663
}
1068210664
}
@@ -11408,15 +11390,18 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1140811390
res.end(errorBody, 'utf-8');
1140911391
}
1141011392
});
11411-
} else if (requestPath === '/web-ui') {
11412-
try {
11413-
const html = readBundledWebUiHtml(htmlPath);
11414-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
11415-
res.end(html);
11416-
} catch (error) {
11417-
writeWebUiAssetError(res, requestPath, error);
11418-
}
11393+
} else if (requestPath === '/web-ui/index.html') {
11394+
const rawUrl = typeof req.url === 'string' ? req.url : '';
11395+
const queryIndex = rawUrl.indexOf('?');
11396+
const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : '';
11397+
res.writeHead(302, {
11398+
'Location': `/${query}`,
11399+
'Content-Type': 'text/plain; charset=utf-8',
11400+
'Cache-Control': 'no-store, max-age=0'
11401+
});
11402+
res.end('Found');
1141911403
} else if (requestPath.startsWith('/web-ui/')) {
11404+
// Skip the /web-ui/ directory itself, which is handled above
1142011405
const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
1142111406
const filePath = path.join(__dirname, normalized);
1142211407
if (!isPathInside(filePath, webDir)) {
@@ -11425,11 +11410,22 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1142511410
return;
1142611411
}
1142711412
const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
11413+
11414+
// Empty relativePath means direct /web-ui/ access - return 404
11415+
if (relativePath === '' || relativePath === 'index.html') {
11416+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
11417+
res.end('Not Found');
11418+
return;
11419+
}
11420+
1142811421
const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
1142911422
if (dynamicAsset) {
1143011423
try {
1143111424
const assetBody = dynamicAsset.reader(filePath);
11432-
res.writeHead(200, { 'Content-Type': dynamicAsset.mime });
11425+
res.writeHead(200, {
11426+
'Content-Type': dynamicAsset.mime,
11427+
'Cache-Control': 'no-store, max-age=0'
11428+
});
1143311429
res.end(assetBody, 'utf-8');
1143411430
} catch (error) {
1143511431
writeWebUiAssetError(res, requestPath, error);
@@ -11456,7 +11452,10 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1145611452
: ext === '.json'
1145711453
? 'application/json; charset=utf-8'
1145811454
: 'application/octet-stream';
11459-
res.writeHead(200, { 'Content-Type': mime });
11455+
res.writeHead(200, {
11456+
'Content-Type': mime,
11457+
'Cache-Control': 'no-store, max-age=0'
11458+
});
1146011459
fs.createReadStream(filePath).pipe(res);
1146111460
} else if (requestPath.startsWith('/download/')) {
1146211461
const fileName = requestPath.slice('/download/'.length);
@@ -11517,15 +11516,27 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
1151711516
: ext === '.json'
1151811517
? 'application/json; charset=utf-8'
1151911518
: 'application/octet-stream';
11520-
res.writeHead(200, { 'Content-Type': mime });
11519+
res.writeHead(200, {
11520+
'Content-Type': mime,
11521+
'Cache-Control': 'no-store, max-age=0'
11522+
});
1152111523
fs.createReadStream(filePath).pipe(res);
1152211524
} else {
11523-
try {
11524-
const html = readBundledWebUiHtml(htmlPath);
11525-
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
11526-
res.end(html);
11527-
} catch (error) {
11528-
writeWebUiAssetError(res, requestPath, error);
11525+
// Only serve HTML for root path; /web-ui returns 404.
11526+
if (requestPath === '/') {
11527+
try {
11528+
const html = readBundledWebUiHtml(htmlPath);
11529+
res.writeHead(200, {
11530+
'Content-Type': 'text/html; charset=utf-8',
11531+
'Cache-Control': 'no-store, max-age=0'
11532+
});
11533+
res.end(html);
11534+
} catch (error) {
11535+
writeWebUiAssetError(res, requestPath, error);
11536+
}
11537+
} else {
11538+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
11539+
res.end('Not Found');
1152911540
}
1153011541
}
1153111542
});

package-lock.json

Lines changed: 3 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codexmate",
3-
"version": "0.1.0",
3+
"version": "0.0.35",
44
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
55
"main": "cli.js",
66
"bin": {
@@ -46,6 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@iarna/toml": "^2.2.5",
49+
"@vue/compiler-dom": "^3.5.30",
4950
"json5": "^2.2.3",
5051
"yauzl": "^3.2.1",
5152
"zip-lib": "^1.2.1"

0 commit comments

Comments
 (0)