artifacty 0.13.1 → 0.15.0

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.
@@ -1,9 +1,70 @@
1
1
  # Upgrade Snapshots and Recovery
2
2
 
3
+ English | [한국어](ko/upgrade-backups.md)
4
+
3
5
  Artifacty creates a local snapshot before it migrates an existing store to a newer schema. This snapshot is mandatory. If snapshot creation or verification fails, Artifacty stops before migration.
4
6
 
5
7
  Use upgrade snapshots to recover the original store state. Use `artifacty backup` and `artifacty export` for portable JSON backups and store transfers. These are separate backup systems. `artifacty restore <id>` restores an archived artifact; it does not restore a store snapshot.
6
8
 
9
+ ## Application Self-Update
10
+
11
+ The application updater and schema migration snapshots serve different purposes. `artifacty update` without an artifact ID updates the application. `artifacty upgrade` is an alias. `artifacty update <id>` continues to update an artifact.
12
+
13
+ ### Check and select a release
14
+
15
+ ```bash
16
+ artifacty update --check
17
+ artifacty upgrade --check --target-version <version> --home /path/to/store
18
+ ```
19
+
20
+ `--check` reports the release without changing the installation, service, or store. It works on other CLI platforms too and reports whether automatic installation is supported. It still needs network access to check the release. For the managed service, the updater compares the package version used by that service, not just the invoking CLI version. `--target-version` accepts an exact version. Without that option, the updater selects the latest release. `--home` selects the store to protect. Automatic downgrades are not supported. An explicitly selected older version is rejected; an older `latest` release leaves the newer installation unchanged.
21
+
22
+ For installed versions before 0.14.0, bootstrap the updater with:
23
+
24
+ ```bash
25
+ npx --yes artifacty@latest update --home /path/to/store && export PATH="$HOME/.local/bin:$PATH"
26
+ ```
27
+
28
+ The command uses POSIX Linux shell syntax. After a successful update, it puts `~/.local/bin` first on `PATH` for the current session. For future sessions, add that directory to your shell's `PATH`.
29
+
30
+ The `latest` tag must resolve to version 0.14.0 or newer to include the updater.
31
+
32
+ ### Update from the browser
33
+
34
+ Sign in with an administrator browser session. Open **Administration → Server update** (`/admin/update`). API tokens do not replace that session for these routes.
35
+
36
+ 1. Select **Check for updates**. This checks the latest npm release without installing or restarting the service.
37
+ 2. If browser updates are supported and a newer version is available, review the displayed target version.
38
+ 3. Select the confirmation checkbox for backup and restart.
39
+ 4. Select **Back up and update**.
40
+ 5. Keep the page open while the server restarts. Read the update result after reconnection.
41
+
42
+ Confirmation expires after ten minutes and belongs to the browser session that checked the version. If it expires or the installed version changes, check again. Form submissions require the exact same origin as the server. No password re-entry is required.
43
+
44
+ Browser installation requires this server to be the non-root Linux systemd user service. Other installations can check the latest release, but cannot start the update from this page. The worker runs in an independent systemd transient service, so stopping the Artifacty server does not stop the update worker. Status is stored outside the artifact store under `~/.local/share/artifacty/update-jobs`.
45
+
46
+ Opening the page or reading status does not contact npm or install a package. Status checks can inspect systemd to identify a stopped worker. If dispatch is uncertain, the job remains reserved. An interrupted worker or incomplete recovery requires manual inspection before another update; it is not reported as success. Keep the retained snapshot and releases until recovery is verified.
47
+
48
+ ### Automatic installation limits
49
+
50
+ Automatic installation and restart support Linux with the systemd user service `com.artifacty.server`. Run the updater as the service's user. The unit must use the simple `ExecStart` form produced by Artifacty's installer and must explicitly pass `--home` for the selected store. Unsupported command expansion or quoting stops the update before service shutdown. Automatic updates reject root execution. Do not run the updater as root to replace a root-owned global package.
51
+
52
+ The updater stages packages under `~/.local/share/artifacty/releases`. It changes the managed service's executable through a systemd drop-in and preserves its existing arguments and environment. User-owned launchers go under `~/.local/bin`. If that directory is not on `PATH`, add it before using those launchers. Existing unmanaged files in that directory are not overwritten. The existing root-owned global package remains unchanged. If `PATH` selects another command first, the result supplies an absolute `nextCommand`. The updater does not edit shell configuration.
53
+
54
+ Automatic switching does not support macOS, Windows, foreground servers, detached background servers, or arbitrary service units. Use the manual installation and snapshot procedures for those environments.
55
+
56
+ ### Store protection and rollback
57
+
58
+ Before updating, stop other HTTP servers and local MCP clients that use the same store. The updater manages the supported systemd service itself. It cannot identify every other process with an open store. The service is unavailable during the update. A store rollback discards writes made after the snapshot; do not keep other writers active during this procedure.
59
+
60
+ For an active service, the updater checks current health before shutdown. If its bind address or IP policy prevents that check, it refuses the update before downtime. Unsupported service configurations also fail before shutdown.
61
+
62
+ The updater installs and verifies the exact target package in its staging directory. It then stops the managed service and creates and verifies a snapshot before switching code. If the service was active, it restarts the service and checks its health and PID. An inactive service remains inactive. It preserves the previous code and service configuration for rollback. If switching or startup fails, it attempts to restore the changed code, service configuration, and store, then restart the previous service. If service stop fails, it does not restore a store while the service may still be writing to it. A restored store includes the snapshot's credentials and security state. Review any recovery error before allowing clients to reconnect.
63
+
64
+ An abrupt process or host termination during the switch can leave `update.lock` or a stopped service. The next update reports the lock instead of retrying blindly. Preserve snapshots, staged releases, and recovery files. Inspect the service and recovery state before a manual retry. Automatic rollback handles caught failures; it does not guarantee recovery from power loss or forced process termination.
65
+
66
+ Keep the snapshot and the update result until the restarted service works. If automatic rollback cannot finish, the updater reports an explicit error and preserves the `backupId`. Use that snapshot with the recovery procedures below. Do not delete recovery files or force an older build to open an unsupported schema.
67
+
7
68
  ## When Artifacty Creates a Snapshot
8
69
 
9
70
  - **Schema migration:** Artifacty always snapshots an existing store before migration. This includes a legacy `index.json` store. A new empty store needs no snapshot.
@@ -1,5 +1,7 @@
1
1
  # Documentation writing style
2
2
 
3
+ English | [한국어](ko/writing-style.md)
4
+
3
5
  Artifacty uses an ASD-STE100-inspired style for English documentation. Korean documentation follows the same clarity principles with natural Korean phrasing. This is an editorial guide, not a claim of compliance with ASD-STE100.
4
6
 
5
7
  Apply the sentence and procedure rules consistently. Use technical vocabulary where it makes the meaning more precise. Keep introductions natural. The project's “75%” target describes this balance; it is not a compliance score.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artifacty",
3
- "version": "0.13.1",
3
+ "version": "0.15.0",
4
4
  "description": "Local artifact exchange for heterogeneous LLM agents via HTTP and MCP.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -53,7 +53,12 @@
53
53
  "SECURITY.md",
54
54
  "THIRD_PARTY_NOTICES.md",
55
55
  "AGENTS.md",
56
- "CLAUDE.md"
56
+ "CLAUDE.md",
57
+ "README.ko.md",
58
+ "SECURITY.ko.md",
59
+ "docs/README.md",
60
+ "docs/ko",
61
+ "docs/claude-artifact-formats.md"
57
62
  ],
58
63
  "engines": {
59
64
  "node": ">=22.12"
package/src/cli.js CHANGED
@@ -33,6 +33,25 @@ async function main() {
33
33
  return;
34
34
  }
35
35
 
36
+ // Application updates must not initialize or migrate the live store first.
37
+ // Keep `update <id>` as the existing artifact-content command.
38
+ if ((command === "update" && options._.length === 0) || command === "upgrade") {
39
+ const allowed = new Set(["_", "home", "targetVersion", "check"]);
40
+ const unknown = Object.keys(options).filter((key) => !allowed.has(key));
41
+ if (options._.length || unknown.length) {
42
+ throw new Error("Usage: artifacty update [--check] [--target-version <version>] [--home <path>]. To update content, use artifacty update <id> --file <path>.");
43
+ }
44
+ const { updateApplication } = await import("./lib/self-update.js");
45
+ printJson(await updateApplication({
46
+ packageRoot: PACKAGE_ROOT,
47
+ home: options.home,
48
+ targetVersion: options.targetVersion,
49
+ check: Boolean(options.check),
50
+ onProgress: (message) => process.stderr.write(`artifacty update: ${message}\n`)
51
+ }));
52
+ return;
53
+ }
54
+
36
55
  // Recovery must work before loading storage or triggering a schema migration.
37
56
  if (command === "backups") {
38
57
  const { listUpgradeBackups, verifyUpgradeBackup, restoreUpgradeBackup } = await import("./lib/upgrade-backup.js");
@@ -898,7 +917,7 @@ function parseArgs(args) {
898
917
  }
899
918
 
900
919
  const key = arg.slice(2);
901
- if (key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force" || key === "skip-mcp" || key === "mcp-http" || key === "no-password-reset" || key === "relations" || key === "structured" || key === "json" || key === "once" || key === "full" || key === "force-users" || key === "allow-purge" || key === "shared" || key === "embeddings") {
920
+ if (key === "check" || key === "raw" || key === "dry-run" || key === "trust" || key === "include-archived" || key === "allow-secrets" || key === "generate-token" || key === "detach" || key === "foreground" || key === "force" || key === "skip-mcp" || key === "mcp-http" || key === "no-password-reset" || key === "relations" || key === "structured" || key === "json" || key === "once" || key === "full" || key === "force-users" || key === "allow-purge" || key === "shared" || key === "embeddings") {
902
921
  options[toCamelCase(key)] = true;
903
922
  continue;
904
923
  }
@@ -991,6 +1010,8 @@ Usage:
991
1010
  artifacty import --agent claude|codex|gemini|copilot|cursor|auto (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--tag tag]
992
1011
  artifacty install claude|codex|gemini|copilot|cursor|all [--dry-run] [--config <path>] [--server-path <path>] [--url http://127.0.0.1:8787] [--mcp-url http://127.0.0.1:8787/mcp] [--api-token token] [--transport local|bridge] [--timeout 30000]
993
1012
  artifacty check [--server-path <path>] [--timeout 5000]
1013
+ artifacty update [--check] [--target-version <version>] [--home <path>]
1014
+ artifacty upgrade [--check] [--target-version <version>] [--home <path>]
994
1015
  artifacty update <id> (--file <path> | --content <text>) [--title <title>] [--format html|markdown|text|json|code|svg|mermaid|react|notebook] [--expected-version N] [--visibility private|team]
995
1016
  artifacty samples install|remove
996
1017
  artifacty archive <id>
@@ -331,28 +331,83 @@ function invalidJsonMessage(error) {
331
331
  return messages.invalidJson.replaceAll("{message}", error.message);
332
332
  }
333
333
 
334
+ // Deliberately renders a small, escaped Markdown subset. Links and raw HTML
335
+ // remain text; previewing a document must not activate its URLs or scripts.
334
336
  function markdownPreview(content) {
335
- return String(content || "")
336
- .split(/\r?\n/)
337
- .map((line) => {
338
- if (/^###\s+/.test(line)) {
339
- return `<h3>${escapeHtml(line.replace(/^###\s+/, ""))}</h3>`;
340
- }
341
- if (/^##\s+/.test(line)) {
342
- return `<h2>${escapeHtml(line.replace(/^##\s+/, ""))}</h2>`;
343
- }
344
- if (/^#\s+/.test(line)) {
345
- return `<h1>${escapeHtml(line.replace(/^#\s+/, ""))}</h1>`;
346
- }
347
- if (/^[-*]\s+/.test(line)) {
348
- return `<p>• ${escapeHtml(line.replace(/^[-*]\s+/, ""))}</p>`;
349
- }
350
- if (!line.trim()) {
351
- return "<br>";
337
+ const lines = String(content || "").split(/\r?\n/);
338
+ const output = [];
339
+ let paragraph = [];
340
+ let list = "";
341
+ const inline = (value) => escapeHtml(value).replace(/`([^`]+)`/g, "<code>$1</code>");
342
+ const flush = () => {
343
+ if (paragraph.length) {
344
+ output.push(`<p>${inline(paragraph.join(" "))}</p>`);
345
+ paragraph = [];
346
+ }
347
+ if (list) {
348
+ output.push(`</${list}>`);
349
+ list = "";
350
+ }
351
+ };
352
+ const cells = (line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
353
+ const tableRow = (line = "") => /^\s*\|.*\|\s*$/.test(line);
354
+
355
+ for (let index = 0; index < lines.length; index += 1) {
356
+ const line = lines[index];
357
+ const fence = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
358
+ if (fence) {
359
+ flush();
360
+ const language = /^[a-zA-Z0-9+#_.-]+$/.test(fence[2].trim()) ? fence[2].trim().toLowerCase() : "";
361
+ const source = [];
362
+ const closing = new RegExp(`^ {0,3}${fence[1][0]}{${fence[1].length},}\\s*$`);
363
+ while (++index < lines.length && !closing.test(lines[index])) source.push(lines[index]);
364
+ output.push(`<pre><code${language ? ` class="language-${language}"` : ""}>${escapeHtml(source.join("\n"))}</code></pre>`);
365
+ continue;
366
+ }
367
+ if (!line.trim()) {
368
+ flush();
369
+ continue;
370
+ }
371
+ const headers = cells(line);
372
+ const separators = cells(lines[index + 1] || "");
373
+ if (tableRow(line) && headers.length === separators.length && separators.every((cell) => /^:?-{3,}:?$/.test(cell))) {
374
+ flush();
375
+ const alignment = (column) => {
376
+ const value = separators[column];
377
+ return value.endsWith(":") ? (value.startsWith(":") ? "center" : "right") : "left";
378
+ };
379
+ const row = (values, tag) => `<tr>${headers.map((_, column) => `<${tag} style="text-align:${alignment(column)}">${inline(values[column] || "")}</${tag}>`).join("")}</tr>`;
380
+ output.push(`<div class="artifact-table-scroll"><table class="artifact-table"><thead>${row(headers, "th")}</thead><tbody>`);
381
+ index += 1;
382
+ while (tableRow(lines[index + 1])) output.push(row(cells(lines[++index]), "td"));
383
+ output.push("</tbody></table></div>");
384
+ continue;
385
+ }
386
+ const heading = /^(#{1,6})\s+(.+)$/.exec(line);
387
+ if (heading) {
388
+ flush();
389
+ output.push(`<h${heading[1].length}>${inline(heading[2])}</h${heading[1].length}>`);
390
+ continue;
391
+ }
392
+ const item = /^([-*+] |\d+[.)] )\s*(.+)$/.exec(line);
393
+ if (item) {
394
+ const type = /^\d/.test(item[1]) ? "ol" : "ul";
395
+ if (paragraph.length || (list && list !== type)) flush();
396
+ if (!list) {
397
+ output.push(`<${type}>`);
398
+ list = type;
352
399
  }
353
- return `<p>${escapeHtml(line)}</p>`;
354
- })
355
- .join("");
400
+ const task = /^\[([ xX])\]\s+(.+)$/.exec(item[2]);
401
+ output.push(task
402
+ ? `<li class="task-list-item"><label><input type="checkbox" disabled${task[1].toLowerCase() === "x" ? " checked" : ""}> ${inline(task[2])}</label></li>`
403
+ : `<li>${inline(item[2])}</li>`);
404
+ continue;
405
+ }
406
+ if (list) flush();
407
+ paragraph.push(line.trim());
408
+ }
409
+ flush();
410
+ return output.join("\n");
356
411
  }
357
412
 
358
413
  function escapeHtml(value) {
@@ -0,0 +1,33 @@
1
+ const panel = document.querySelector("[data-update-status]");
2
+
3
+ if (panel?.dataset.active === "true") {
4
+ const messages = JSON.parse(panel.dataset.messages);
5
+ const stateLabel = panel.querySelector("[data-update-state]");
6
+ const errorLabel = panel.querySelector("[data-update-error]");
7
+ const versionLabel = document.querySelector("[data-update-version]");
8
+
9
+ async function refreshStatus() {
10
+ let retry = true;
11
+ try {
12
+ const response = await fetch("/admin/update/status", { credentials: "same-origin", cache: "no-store", headers: { Accept: "application/json" } });
13
+ if ([401, 403].includes(response.status)) {
14
+ stateLabel.textContent = messages.signIn;
15
+ panel.querySelector("[data-update-login]").hidden = false;
16
+ return;
17
+ }
18
+ if (!response.ok) throw new Error("Status unavailable");
19
+ const status = await response.json();
20
+ if (status.currentVersion) versionLabel.textContent = status.currentVersion;
21
+ if (status.run) {
22
+ stateLabel.textContent = messages[status.run.state] || messages.running;
23
+ errorLabel.textContent = status.run.error || "";
24
+ retry = ["queued", "running"].includes(status.run.state);
25
+ }
26
+ } catch {
27
+ stateLabel.textContent = messages.reconnect;
28
+ }
29
+ if (retry) window.setTimeout(refreshStatus, 3000);
30
+ }
31
+
32
+ window.setTimeout(refreshStatus, 1500);
33
+ }
@@ -352,7 +352,9 @@ function isPidRunning(pid) {
352
352
  async function fetchHealth(url) {
353
353
  try {
354
354
  const response = await fetch(`${url}/health`, {
355
- signal: AbortSignal.timeout(500)
355
+ // Match the updater health deadline so brief CPU or I/O contention
356
+ // does not report a live background server as stopped.
357
+ signal: AbortSignal.timeout(2000)
356
358
  });
357
359
  return { ok: response.ok, status: response.status };
358
360
  } catch {
package/src/lib/i18n.js CHANGED
@@ -3,6 +3,59 @@ export const SUPPORTED_LOCALES = ["en", "ko"];
3
3
 
4
4
  const TRANSLATIONS = {
5
5
  en: {
6
+ "update.releaseNotes": "Release notes",
7
+ "update.checkFailed": "Could not check the latest version. Try checking again.",
8
+ "update.checkAgain": "Check again to prepare a new update confirmation.",
9
+ "update.available": "An update is available. Update to v{version}?",
10
+ "update.signIn": "Your session has expired or access was denied. Sign in again or refresh this page.",
11
+ "update.title": "Server update",
12
+ "update.installed": "Installed version",
13
+ "update.latest": "Available version",
14
+ "update.check": "Check for updates",
15
+ "update.help": "Updates from this page require an Artifacty Linux systemd user service. Check for updates to verify this installation.",
16
+ "update.backup": "The updater creates a backup before installing the selected version. The server briefly stops and restarts. Keep this page open to reconnect.",
17
+ "update.manual": "For other installations, use the update instructions for your installation method.",
18
+ "update.unsupported": "This installation cannot update from the browser.",
19
+ "update.current": "This installation is up to date.",
20
+ "update.confirm": "I agree to back up the store and restart the server to install this version.",
21
+ "update.start": "Back up and update",
22
+ "update.progress": "Update status",
23
+ "update.refresh": "Refresh status",
24
+ "update.reconnect": "The server is restarting or unavailable. Reconnecting…",
25
+ "update.queued": "Waiting to start",
26
+ "update.running": "Update in progress",
27
+ "update.succeeded": "Update complete. Refresh this page to use the new version.",
28
+ "update.failed": "Update failed. Review the error before retrying.",
29
+ "update.interrupted": "The previous update was interrupted. Check the server before retrying.",
30
+ "update.expires": "Confirmation expires",
31
+
32
+ "artifact.details": "Details",
33
+ "nav.skip": "Skip to content",
34
+ "nav.artifacts": "Artifacts",
35
+ "nav.administration": "Administration",
36
+ "nav.users": "Users",
37
+ "nav.backup": "Backup",
38
+ "nav.webhooks": "Webhooks",
39
+ "nav.account": "Account",
40
+ "nav.accountTokens": "Account & API tokens",
41
+ "nav.password": "Password",
42
+ "nav.signIn": "Sign in",
43
+ "nav.signOut": "Sign out",
44
+ "nav.apiDocs": "API docs",
45
+ "nav.versions": "Versions",
46
+
47
+ "dashboard.navigation": "Dashboard navigation",
48
+ "dashboard.more": "Menu",
49
+ "dashboard.searchPlaceholder": "Search artifacts",
50
+ "dashboard.advanced": "Advanced filters",
51
+ "dashboard.activeFilters": "{count} active",
52
+ "filter.artifactType": "Artifact type",
53
+ "filter.reviewStatus": "Review status",
54
+ "filter.mode": "Search mode",
55
+ "filter.groupBy": "Group by",
56
+ "filter.relatedTo": "Related artifact ID",
57
+ "filter.relation": "Relation",
58
+
6
59
  "sharing.staticPreview": "Static preview; scripts and external resources are disabled.",
7
60
  "sharing.safeSource": "Source preview: active content is disabled for public share links.",
8
61
  "sharing.networkCondition": "All permissions also require an allowed connection IP",
@@ -278,6 +331,59 @@ const TRANSLATIONS = {
278
331
  "views.namePlaceholder": "View name"
279
332
  },
280
333
  ko: {
334
+ "update.releaseNotes": "변경 내용 보기",
335
+ "update.checkFailed": "최신 버전을 확인하지 못했습니다. 다시 확인해 주세요.",
336
+ "update.checkAgain": "업데이트를 진행하려면 다시 확인하여 새 동의 화면을 여세요.",
337
+ "update.available": "업데이트가 있습니다. v{version}로 업데이트할까요?",
338
+ "update.signIn": "세션이 만료되었거나 접근 권한이 없습니다. 다시 로그인하거나 화면을 새로고침하세요.",
339
+ "update.title": "서버 업데이트",
340
+ "update.installed": "설치된 버전",
341
+ "update.latest": "업데이트할 버전",
342
+ "update.check": "업데이트 확인",
343
+ "update.help": "이 화면에서는 Artifacty Linux systemd 사용자 서비스만 업데이트할 수 있습니다. 업데이트 확인을 누르면 설치 환경도 확인합니다.",
344
+ "update.backup": "선택한 버전을 설치하기 전에 백업을 만듭니다. 서버가 잠시 중지된 뒤 다시 시작됩니다. 이 화면을 열어 두면 다시 연결합니다.",
345
+ "update.manual": "다른 방식으로 설치했다면 해당 설치 방식의 업데이트 안내를 따라 주세요.",
346
+ "update.unsupported": "이 설치 환경에서는 브라우저에서 업데이트할 수 없습니다.",
347
+ "update.current": "현재 최신 버전입니다.",
348
+ "update.confirm": "저장소를 백업하고 서버를 다시 시작하여 이 버전을 설치하는 데 동의합니다.",
349
+ "update.start": "백업 후 업데이트",
350
+ "update.progress": "업데이트 상태",
351
+ "update.refresh": "상태 새로고침",
352
+ "update.reconnect": "서버가 다시 시작 중이거나 연결할 수 없습니다. 다시 연결하고 있습니다…",
353
+ "update.queued": "시작 대기 중",
354
+ "update.running": "업데이트 진행 중",
355
+ "update.succeeded": "업데이트가 완료되었습니다. 새 버전을 사용하려면 화면을 새로고침하세요.",
356
+ "update.failed": "업데이트하지 못했습니다. 다시 시도하기 전에 오류를 확인하세요.",
357
+ "update.interrupted": "이전 업데이트가 중단되었습니다. 다시 시도하기 전에 서버 상태를 확인하세요.",
358
+ "update.expires": "확인 유효 기한",
359
+
360
+ "artifact.details": "세부 정보",
361
+ "nav.skip": "본문으로 이동",
362
+ "nav.artifacts": "아티팩트",
363
+ "nav.administration": "관리",
364
+ "nav.users": "사용자",
365
+ "nav.backup": "백업",
366
+ "nav.webhooks": "Webhook",
367
+ "nav.account": "계정",
368
+ "nav.accountTokens": "계정 및 API 토큰",
369
+ "nav.password": "비밀번호",
370
+ "nav.signIn": "로그인",
371
+ "nav.signOut": "로그아웃",
372
+ "nav.apiDocs": "API 문서",
373
+ "nav.versions": "버전 관리",
374
+
375
+ "dashboard.navigation": "대시보드 탐색",
376
+ "dashboard.more": "메뉴",
377
+ "dashboard.searchPlaceholder": "아티팩트 검색",
378
+ "dashboard.advanced": "상세 필터",
379
+ "dashboard.activeFilters": "{count}개 적용",
380
+ "filter.artifactType": "아티팩트 유형",
381
+ "filter.reviewStatus": "리뷰 상태",
382
+ "filter.mode": "검색 모드",
383
+ "filter.groupBy": "묶어서 보기",
384
+ "filter.relatedTo": "관련 아티팩트 ID",
385
+ "filter.relation": "관계",
386
+
281
387
  "sharing.staticPreview": "정적 미리보기: 스크립트와 외부 리소스는 비활성화됩니다.",
282
388
  "sharing.safeSource": "소스 미리보기: 공개 공유 링크에서는 실행 가능한 콘텐츠를 비활성화합니다.",
283
389
  "sharing.networkCondition": "모든 권한은 허용된 IP에서 접속해야 사용할 수 있습니다",