ework-web 0.10.37 → 0.10.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ework-web",
3
- "version": "0.10.37",
3
+ "version": "0.10.39",
4
4
  "type": "module",
5
5
  "description": "ework-web — standalone multi-project issue tracker. Local SQLite-backed, no external API dependency. Bun + TypeScript + SSR HTML.",
6
6
  "license": "MIT",
package/src/config.ts CHANGED
@@ -64,8 +64,8 @@ export const configSchema = z.object({
64
64
  collapseLines: z.coerce.number().int().positive().default(16),
65
65
  fileRoots: z
66
66
  .string()
67
- .transform((s) => s.split(",").map((x) => x.trim()).filter(Boolean))
68
- .default("/tmp"),
67
+ .default("/tmp")
68
+ .transform((s) => s.split(",").map((x) => x.trim()).filter(Boolean)),
69
69
  fileMaxLines: z.coerce.number().int().positive().default(2000),
70
70
  fileMaxBytes: z.coerce.number().int().positive().default(512 * 1024),
71
71
  translateUrl: z.string().default(""),
@@ -1,4 +1,36 @@
1
1
  import { THEME_CSS, escapeHtml, tabNavHTML } from "./render/layout";
2
+ import hljs from "highlight.js";
3
+ import { renderMarkdown } from "./render/markdown";
4
+
5
+ // Extension → hljs language map (mirrors fileview.ts for parity).
6
+ const EXT_LANG: Record<string, string> = {
7
+ js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "javascript",
8
+ ts: "typescript", tsx: "typescript", mts: "typescript", cts: "typescript",
9
+ py: "python", rb: "ruby", go: "go", rs: "rust", java: "java", kt: "kotlin",
10
+ c: "c", h: "c", cpp: "cpp", cc: "cpp", cxx: "cpp", hpp: "cpp", hxx: "cpp",
11
+ cs: "csharp", php: "php", swift: "swift", scala: "scala", sh: "bash",
12
+ bash: "bash", zsh: "bash", fish: "bash", yml: "yaml", yaml: "yaml",
13
+ json: "json", json5: "json", toml: "toml", xml: "xml", html: "xml",
14
+ htm: "xml", css: "css", scss: "scss", less: "less", sql: "sql",
15
+ dockerfile: "dockerfile", makefile: "makefile", lua: "lua", pl: "perl",
16
+ };
17
+
18
+ function extToLang(p: string): string {
19
+ const base = (p.split("/").pop() || "").toLowerCase();
20
+ if (base === "dockerfile" || base.endsWith(".dockerfile")) return "dockerfile";
21
+ if (base === "makefile" || base.endsWith(".mk")) return "makefile";
22
+ const m = base.match(/\.([a-z0-9]+)$/);
23
+ const ext = m && m[1];
24
+ return ext ? (EXT_LANG[ext] || "") : "";
25
+ }
26
+
27
+ function hljsHighlight(text: string, lang: string): string {
28
+ try {
29
+ return hljs.highlight(text, { language: lang }).value;
30
+ } catch {
31
+ return escapeHtml(text);
32
+ }
33
+ }
2
34
 
3
35
  export class RemoteFileError extends Error {
4
36
  status: number;
@@ -50,7 +82,8 @@ async function fetchDaemon(
50
82
  apiPath: string,
51
83
  params: Record<string, string>
52
84
  ): Promise<Response> {
53
- const url = new URL(`${endpoint}${apiPath}`);
85
+ const base = /^https?:\/\//.test(endpoint) ? endpoint : `http://${endpoint}`;
86
+ const url = new URL(`${base}${apiPath}`);
54
87
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
55
88
  const resp = await fetch(url.toString(), { signal: AbortSignal.timeout(10_000) });
56
89
  return resp;
@@ -153,31 +186,66 @@ function renderFileContent(
153
186
  const enc = encodeURIComponent;
154
187
  const daemonParam = `&daemon=${enc(endpoint)}`;
155
188
  const user = viewerLogin ? { login: viewerLogin, is_admin: 0 } : undefined;
156
- const lines = data.rows.map((r) =>
157
- `<div class="l"><span class="ln">${r.n}</span><span class="lt">${escapeHtml(r.t)}</span></div>`
158
- ).join("");
189
+
190
+ const ext = (data.path.split(".").pop() || "").toLowerCase();
191
+ const isMd = ext === "md" || ext === "markdown";
192
+ const lang = extToLang(data.path);
193
+ const fullText = data.rows.map((r) => r.t).join("\n");
194
+ const shownBytes = data.rows.reduce((s, r) => s + r.t.length + 1, 0);
195
+
196
+ let body: string;
197
+ if (isMd) {
198
+ body = `<div class="md-render">${renderMarkdown(fullText, "")}</div>`;
199
+ } else if (lang && shownBytes <= 100000) {
200
+ const nums = data.rows.map((r) => r.n).join("\n");
201
+ body = `<div class="fv-code"><pre class="fv-gutter">${escapeHtml(nums)}</pre><pre class="fv-src"><code class="hljs language-${escapeHtml(lang)}">${hljsHighlight(fullText, lang)}</code></pre></div>`;
202
+ } else {
203
+ body = `<pre class="fv-plain"><code>${data.rows.map((r) => `<span class="ln"><span class="lnn">${r.n}</span><span class="lnc">${escapeHtml(r.t)}</span></span>`).join("")}</code></pre>`;
204
+ }
159
205
 
160
206
  return `<!doctype html>
161
207
  <html lang="zh"><head><meta charset="utf-8">
162
208
  <meta name="viewport" content="width=device-width,initial-scale=1">
163
209
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
164
210
  <title>ework-web · ${escapeHtml(data.path)}</title>
211
+ <link rel="stylesheet" href="/static/highlight.css">
165
212
  <style>${THEME_CSS}
166
213
  .nav{display:flex;align-items:center;gap:.5rem;padding:.55rem 1rem;background:var(--header-bg);color:var(--header-text);font-size:13px}
167
214
  .nav a{color:var(--header-text);opacity:.95}
168
- .wrap{max-width:1000px;margin:0 auto;padding:1rem}
215
+ .wrap{max-width:1100px;margin:0 auto;padding:1rem}
169
216
  h1{font-size:14px;margin:0 0 .3rem;font-family:ui-monospace,monospace;word-break:break-all}
170
217
  .remote-badge{display:inline-block;background:#1f6feb33;color:#58a6ff;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600;margin-left:.5rem}
171
218
  .note{color:var(--text-muted);font-size:12px;margin:0 0 .5rem}
172
219
  .parent-link{font-size:13px;margin-bottom:.4rem}
173
220
  .parent-link a{color:var(--text-muted)}
174
- .fview{background:var(--bg-elev);border:1px solid var(--border);border-radius:6px;overflow:auto;max-height:80vh;font-size:12.5px;font-family:ui-monospace,monospace}
175
- .l{display:flex}
176
- .ln{color:var(--text-muted);min-width:3.5rem;text-align:right;padding:0 .5rem;user-select:none;border-right:1px solid var(--border)}
177
- .lt{padding:0 .5rem;white-space:pre-wrap;word-break:break-all;flex:1}
178
- .tabs{display:flex;gap:.3rem;padding:.5rem 1rem;border-bottom:1px solid var(--border);font-size:13px}
179
- .tab{padding:.3rem .7rem;border-radius:6px 6px 0 0;text-decoration:none;color:var(--text-muted)}
180
- .tab.active{background:var(--accent);color:#fff}
221
+ .fv-code{display:flex;border:1px solid var(--border);border-radius:8px;overflow:auto;font-size:12.5px;line-height:1.5}
222
+ .fv-gutter{background:var(--bg-muted);color:var(--text-muted);text-align:right;padding:.6rem .7rem;user-select:none;white-space:pre;flex-shrink:0;border:none;margin:0}
223
+ .fv-src{margin:0;flex:1;overflow:auto;background:var(--bg-elev);padding:.6rem .8rem;border:none}
224
+ .fv-src code{background:none;padding:0;white-space:pre;font-size:12.5px}
225
+ .fv-plain{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:.6rem 0;overflow:auto;font-size:12.5px;line-height:1.5}
226
+ .fv-plain .ln{display:flex}
227
+ .fv-plain .lnn{user-select:none;text-align:right;color:var(--text-muted);padding:0 .7rem 0 .8rem;min-width:3.5em;flex-shrink:0;border-right:1px solid var(--border);background:var(--bg-muted)}
228
+ .fv-plain .lnc{padding:0 .8rem;white-space:pre;overflow-wrap:anywhere}
229
+ .md-render{max-width:820px;margin:0 auto;padding:1rem 1.5rem 5rem;font-size:14.5px;line-height:1.7;color:var(--text)}
230
+ .md-render h1,.md-render h2,.md-render h3,.md-render h4{line-height:1.3;margin:1.4em 0 .6em;font-weight:600}
231
+ .md-render h1{font-size:1.7em;border-bottom:1px solid var(--border);padding-bottom:.3em}
232
+ .md-render h2{font-size:1.4em;border-bottom:1px solid var(--border);padding-bottom:.3em}
233
+ .md-render h3{font-size:1.2em}
234
+ .md-render h4{font-size:1.05em}
235
+ .md-render p{margin:.6em 0}
236
+ .md-render ul,.md-render ol{margin:.6em 0;padding-left:1.6em}
237
+ .md-render li{margin:.25em 0}
238
+ .md-render a{color:var(--accent);text-decoration:none}
239
+ .md-render a:hover{text-decoration:underline}
240
+ .md-render code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em;background:var(--bg-muted);padding:.15em .35em;border-radius:4px}
241
+ .md-render pre{background:var(--bg-elev);border:1px solid var(--border);border-radius:8px;padding:.7rem 1rem;overflow:auto;margin:.8em 0}
242
+ .md-render pre code{background:none;padding:0;font-size:13px}
243
+ .md-render blockquote{border-left:3px solid var(--border);margin:.8em 0;padding:.2em 0 .2em 1em;color:var(--text-muted)}
244
+ .md-render table{border-collapse:collapse;margin:.8em 0;width:100%}
245
+ .md-render th,.md-render td{border:1px solid var(--border);padding:.4em .7em;text-align:left}
246
+ .md-render th{background:var(--bg-muted);font-weight:600}
247
+ .md-render img{max-width:100%}
248
+ .md-render hr{border:none;border-top:1px solid var(--border);margin:1.4em 0}
181
249
  </style></head><body>
182
250
  <header class="nav"><a href="/" style="color:var(--header-text)">🏠 ework-web</a></header>
183
251
  ${tabNavHTML("sessions", user)}
@@ -185,7 +253,7 @@ ${tabNavHTML("sessions", user)}
185
253
  <div class="parent-link">📁 <a href="/file?path=${enc(data.path.split("/").slice(0, -1).join("/") || "/")}${daemonParam}">..</a></div>
186
254
  <h1>${escapeHtml(data.path)}<span class="remote-badge">远程 ${daemonLabel}</span></h1>
187
255
  ${data.note ? `<p class="note">${escapeHtml(data.note)}</p>` : ""}
188
- <div class="fview">${lines}</div>
256
+ ${body}
189
257
  </main></body></html>`;
190
258
  }
191
259
 
package/src/store.ts CHANGED
@@ -5,6 +5,22 @@
5
5
  import { getDB } from "./db";
6
6
  import { createHash } from "node:crypto";
7
7
 
8
+ let _visibilityChecked = false;
9
+ let _hasVisibilityColumn = true;
10
+
11
+ async function visibilitySupported(): Promise<boolean> {
12
+ if (_visibilityChecked) return _hasVisibilityColumn;
13
+ _visibilityChecked = true;
14
+ try {
15
+ await getDB().get("SELECT visibility FROM {{projects}} LIMIT 0");
16
+ _hasVisibilityColumn = true;
17
+ } catch {
18
+ _hasVisibilityColumn = false;
19
+ console.warn("[store] projects.visibility column missing — treating all projects as public");
20
+ }
21
+ return _hasVisibilityColumn;
22
+ }
23
+
8
24
  export type UserKind = "human" | "bot" | "system";
9
25
 
10
26
  export interface UserRow {
@@ -181,12 +197,15 @@ export async function listProjectsWithCounts(
181
197
  ): Promise<ProjectWithCounts[]> {
182
198
  const isAdmin = viewer?.is_admin === 1;
183
199
  const login = viewer?.login;
184
- const visibilityFilter = isAdmin
200
+ const hasVis = isAdmin ? true : await visibilitySupported();
201
+ const visibilityFilter = !hasVis
185
202
  ? ""
186
- : login
187
- ? ` AND (p.visibility != 'private' OR EXISTS (SELECT 1 FROM {{project_members}} pm WHERE pm.project_id = p.id AND pm.user_login = ?))`
188
- : ` AND p.visibility != 'private'`;
189
- const args = !isAdmin && login ? [login] : [];
203
+ : isAdmin
204
+ ? ""
205
+ : login
206
+ ? ` AND (p.visibility != 'private' OR EXISTS (SELECT 1 FROM {{project_members}} pm WHERE pm.project_id = p.id AND pm.user_login = ?))`
207
+ : ` AND p.visibility != 'private'`;
208
+ const args = hasVis && !isAdmin && login ? [login] : [];
190
209
  return await getDB().all<ProjectWithCounts>(
191
210
  `SELECT p.*,
192
211
  COALESCE(SUM(CASE WHEN i.state = 'open' THEN 1 ELSE 0 END), 0) AS open_count,
@@ -404,7 +423,7 @@ export async function listAllIssues(opts: ListIssuesOpts = {}): Promise<IssueWit
404
423
  sql += " AND EXISTS (SELECT 1 FROM {{issue_labels}} il JOIN {{labels}} l ON l.id = il.label_id WHERE il.issue_id = i.id AND l.name = ?)";
405
424
  args.push(label);
406
425
  }
407
- if (!opts.viewerIsAdmin) {
426
+ if (!opts.viewerIsAdmin && await visibilitySupported()) {
408
427
  sql += " AND (p.visibility != 'private'";
409
428
  if (opts.viewerLogin) {
410
429
  sql += " OR EXISTS (SELECT 1 FROM {{project_members}} pm WHERE pm.project_id = p.id AND pm.user_login = ?)";