@maudecode/kanvil 0.4.4 → 0.5.1
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/apps/server/.env.example +5 -0
- package/apps/server/dist/{auth-B-Tz8SF4.mjs → auth-Bm6cIseE.mjs} +314 -127
- package/apps/server/dist/client.mjs +10 -9
- package/apps/server/dist/{config-CRADzIvL.mjs → config-CrJuk5mY.mjs} +15 -7
- package/apps/server/dist/index.mjs +98 -30
- package/apps/server/dist/migrate.mjs +2 -2
- package/apps/server/dist/recover.mjs +4 -4
- package/apps/server/dist/{src-BCtZ3wbK.mjs → src-D80LNZKP.mjs} +58 -20
- package/apps/web/dist/assets/{CompositeItem-DvEBmVYM.js → CompositeItem-Tueq2VKR.js} +1 -1
- package/apps/web/dist/assets/{Trans-NwgJNiLl.js → Trans-BgU0eWAX.js} +1 -1
- package/apps/web/dist/assets/admin-settings-Ckis3XJG.js +1 -0
- package/apps/web/dist/assets/{archive-restore-j20QLQCl.js → archive-restore-CSpoH6mS.js} +1 -1
- package/apps/web/dist/assets/az-AZ-bFxpMMZN.js +1 -0
- package/apps/web/dist/assets/{az-Dj0LhQ5O.js → az-DUcDY8Bp.js} +1 -1
- package/apps/web/dist/assets/{corner-left-up-BAGDaT_1.js → corner-left-up-C3OGlXIB.js} +1 -1
- package/apps/web/dist/assets/{de-BWVFKRmU.js → de-89C1V8Ut.js} +1 -1
- package/apps/web/dist/assets/de-DE-BeXI30Ll.js +1 -0
- package/apps/web/dist/assets/default-Dnep8Yat.js +56 -0
- package/apps/web/dist/assets/{delivery-settings-B26dRk_O.js → delivery-settings-cMldZ9K7.js} +1 -1
- package/apps/web/dist/assets/{el-IYSYQmsa.js → el-D6CICkhi.js} +1 -1
- package/apps/web/dist/assets/el-GR-SgRHlWhY.js +1 -0
- package/apps/web/dist/assets/{ellipsis-Cx1SIn8a.js → ellipsis-lJJU8zB7.js} +1 -1
- package/apps/web/dist/assets/{es-C3DJP0aS.js → es-B20Izib2.js} +1 -1
- package/apps/web/dist/assets/es-ES-BF1J5vap.js +1 -0
- package/apps/web/dist/assets/{forms-BwtRqvGj.js → forms--87R7VQW.js} +1 -1
- package/apps/web/dist/assets/{fr-BlVUPa15.js → fr-DVlGnGsu.js} +1 -1
- package/apps/web/dist/assets/fr-FR-D_sldwf6.js +1 -0
- package/apps/web/dist/assets/{hi-CINzKB6O.js → hi-D1F5Pi_I.js} +1 -1
- package/apps/web/dist/assets/hi-IN-CfOw4TWf.js +1 -0
- package/apps/web/dist/assets/{highlight-XHbOOx7A.js → highlight-ygDJPvRy.js} +1 -1
- package/apps/web/dist/assets/id-ID-CLKtHO7Q.js +1 -0
- package/apps/web/dist/assets/{id-CpyQwNyO.js → id-LESXKW7G.js} +1 -1
- package/apps/web/dist/assets/index-Bn-MADyj.css +2 -0
- package/apps/web/dist/assets/index-Ds3IHeIi.js +17 -0
- package/apps/web/dist/assets/{invitations-Ch02KEAB.js → invitations-C5WbdDUQ.js} +1 -1
- package/apps/web/dist/assets/{it-CxMkGyiF.js → it-CgRGAs6u.js} +1 -1
- package/apps/web/dist/assets/it-IT-eVZNW5qX.js +1 -0
- package/apps/web/dist/assets/{item-CRS9Ai9p.js → item-BD1Poxfs.js} +1 -1
- package/apps/web/dist/assets/{ja-D72d7qkv.js → ja-B7arhzgX.js} +1 -1
- package/apps/web/dist/assets/ja-JP-D96njn-P.js +1 -0
- package/apps/web/dist/assets/ko-KR-Bash_ieA.js +1 -0
- package/apps/web/dist/assets/{ko-BHuNjRQ3.js → ko-Ro_UonyA.js} +1 -1
- package/apps/web/dist/assets/{label-settings-tE57CalJ.js → label-settings-BaikCfal.js} +1 -1
- package/apps/web/dist/assets/{markdown-blocks-CN-hJdJS.js → markdown-blocks-toSPlHSc.js} +1 -1
- package/apps/web/dist/assets/{member-directory-CoLB-2Oc.js → member-directory-BjUy1KuK.js} +1 -1
- package/apps/web/dist/assets/{mk-D3TPH_Mz.js → mk-DR3kmrIp.js} +1 -1
- package/apps/web/dist/assets/mk-MK-BWQWGIRx.js +1 -0
- package/apps/web/dist/assets/{nl-BHJbk-Wr.js → nl-D3uRJJLT.js} +1 -1
- package/apps/web/dist/assets/nl-NL-D1tvo0Gd.js +1 -0
- package/apps/web/dist/assets/{pl-JW_P_Gb9.js → pl-CtzDOLdV.js} +1 -1
- package/apps/web/dist/assets/pl-PL-DNgGUEcQ.js +1 -0
- package/apps/web/dist/assets/{project-lifecycle-DuV33mlL.js → project-lifecycle-BBHcRi6p.js} +1 -1
- package/apps/web/dist/assets/project-settings-BaYuxYjg.js +1 -0
- package/apps/web/dist/assets/pt-BR-CZsUZQ6P.js +1 -0
- package/apps/web/dist/assets/{pt-BR-DWL_sEp3.js → pt-BR-FarXHHuf.js} +1 -1
- package/apps/web/dist/assets/{public-project-M93Zpx_e.js → public-project-DGf3nkg2.js} +1 -1
- package/apps/web/dist/assets/{rotate-ccw-DcHaluuA.js → rotate-ccw-Byx7O3RH.js} +1 -1
- package/apps/web/dist/assets/{ru-DQNby7hW.js → ru-DHMEVxvB.js} +1 -1
- package/apps/web/dist/assets/ru-RU-4ew92YE_.js +1 -0
- package/apps/web/dist/assets/{search-DQtwggUH.js → search-DwsPEC4E.js} +35 -35
- package/apps/web/dist/assets/{send-gpGpxsCa.js → send-DlGV2XQS.js} +1 -1
- package/apps/web/dist/assets/settings-C_sN8Y1h.js +1 -0
- package/apps/web/dist/assets/{sliders-horizontal-BxgR4pFt.js → sliders-horizontal-CEHGGft_.js} +1 -1
- package/apps/web/dist/assets/{switch-UPVGS5kv.js → switch-B_rsZYBf.js} +1 -1
- package/apps/web/dist/assets/{table-D8iAxhJH.js → table-2AEfwytk.js} +1 -1
- package/apps/web/dist/assets/ticket-detail-C2l8nwF-.js +2 -0
- package/apps/web/dist/assets/ticket-editor-B0UB98HS.js +2 -0
- package/apps/web/dist/assets/ticket-list-D_MHudj1.js +6 -0
- package/apps/web/dist/assets/{ticket-list-controls-ua3m0uRB.js → ticket-list-controls-jKg4i-NA.js} +1 -1
- package/apps/web/dist/assets/{tr-BNZEkwqd.js → tr-CjdSDu0z.js} +1 -1
- package/apps/web/dist/assets/tr-TR-CM381XCw.js +1 -0
- package/apps/web/dist/assets/{uk-Dmlqvk63.js → uk-C8od6u_H.js} +1 -1
- package/apps/web/dist/assets/uk-UA-AeOiWCVo.js +1 -0
- package/apps/web/dist/assets/{upload-Cu0J8ZFJ.js → upload-BeTAGU_W.js} +1 -1
- package/apps/web/dist/assets/{useQueries-BEzeS0hU.js → useQueries-BtRgTciJ.js} +1 -1
- package/apps/web/dist/assets/{vi-KALEc3bk.js → vi-BwdBtgfS.js} +1 -1
- package/apps/web/dist/assets/vi-VN-td08xXv9.js +1 -0
- package/apps/web/dist/assets/{visual-editor-DyhI5fa5.js → visual-editor-CzB_qwvB.js} +3 -3
- package/apps/web/dist/assets/{workspace-settings-DGr-Lwnz.js → workspace-settings-DOr9j8X1.js} +1 -1
- package/apps/web/dist/assets/zh-CN-D3dhEYLS.js +1 -0
- package/apps/web/dist/assets/{zh-CN-DfruhRl9.js → zh-CN-DM-UoFmL.js} +1 -1
- package/apps/web/dist/assets/zh-TW-CfhOEFo8.js +1 -0
- package/apps/web/dist/assets/{zh-TW-C4keUYMa.js → zh-TW-q2nrwFjt.js} +1 -1
- package/apps/web/dist/index.html +8 -8
- package/docs/api.md +19 -7
- package/docs/cli.md +1 -1
- package/docs/releasing.md +16 -2
- package/docs/self-hosting.md +8 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/admin-settings-BVWOe67Z.js +0 -1
- package/apps/web/dist/assets/az-AZ-DlsPlUZi.js +0 -1
- package/apps/web/dist/assets/de-DE-DLCaqUUe.js +0 -1
- package/apps/web/dist/assets/default-D6I-Xfqm.js +0 -56
- package/apps/web/dist/assets/el-GR-P2XcwrSU.js +0 -1
- package/apps/web/dist/assets/es-ES-CJoX8f3w.js +0 -1
- package/apps/web/dist/assets/fr-FR-yLkQdy6a.js +0 -1
- package/apps/web/dist/assets/hi-IN-y8dqej7z.js +0 -1
- package/apps/web/dist/assets/id-ID-DSvh8mGH.js +0 -1
- package/apps/web/dist/assets/index-DyQNFS69.css +0 -2
- package/apps/web/dist/assets/index-MTE_naHG.js +0 -17
- package/apps/web/dist/assets/it-IT-BUcfUqNo.js +0 -1
- package/apps/web/dist/assets/ja-JP-C-jLP_Ay.js +0 -1
- package/apps/web/dist/assets/ko-KR-C5Ozdm2E.js +0 -1
- package/apps/web/dist/assets/mk-MK-BdM_SsE8.js +0 -1
- package/apps/web/dist/assets/nl-NL-CwjB5sva.js +0 -1
- package/apps/web/dist/assets/pl-PL-6-PMhTBd.js +0 -1
- package/apps/web/dist/assets/project-settings-9yJbfZRu.js +0 -1
- package/apps/web/dist/assets/pt-BR-TD_wt6Hr.js +0 -1
- package/apps/web/dist/assets/ru-RU-Bk1a6Oln.js +0 -1
- package/apps/web/dist/assets/settings-Co61IE04.js +0 -1
- package/apps/web/dist/assets/ticket-detail-BKilKtg_.js +0 -2
- package/apps/web/dist/assets/ticket-editor-B5eOWIj4.js +0 -2
- package/apps/web/dist/assets/ticket-list-C965rmmf.js +0 -5
- package/apps/web/dist/assets/tr-TR-CUfBb7K9.js +0 -1
- package/apps/web/dist/assets/uk-UA-BDaXBsDP.js +0 -1
- package/apps/web/dist/assets/vi-VN-Bgb-L6mE.js +0 -1
- package/apps/web/dist/assets/zh-CN-BypkvBe6.js +0 -1
- package/apps/web/dist/assets/zh-TW-CgMsT_XS.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as e,l as t,o as n,s as r}from"./en-US-DKYSujv3.js";import{t as i}from"./isSameWeek-2ftYGvBT.js";import{Dt as a}from"./index-
|
|
1
|
+
import{c as e,l as t,o as n,s as r}from"./en-US-DKYSujv3.js";import{t as i}from"./isSameWeek-2ftYGvBT.js";import{Dt as a}from"./index-Ds3IHeIi.js";var o={lessThanXSeconds:{one:`不到 1 秒`,other:`不到 {{count}} 秒`},xSeconds:{one:`1 秒`,other:`{{count}} 秒`},halfAMinute:`半分钟`,lessThanXMinutes:{one:`不到 1 分钟`,other:`不到 {{count}} 分钟`},xMinutes:{one:`1 分钟`,other:`{{count}} 分钟`},xHours:{one:`1 小时`,other:`{{count}} 小时`},aboutXHours:{one:`大约 1 小时`,other:`大约 {{count}} 小时`},xDays:{one:`1 天`,other:`{{count}} 天`},aboutXWeeks:{one:`大约 1 个星期`,other:`大约 {{count}} 个星期`},xWeeks:{one:`1 个星期`,other:`{{count}} 个星期`},aboutXMonths:{one:`大约 1 个月`,other:`大约 {{count}} 个月`},xMonths:{one:`1 个月`,other:`{{count}} 个月`},aboutXYears:{one:`大约 1 年`,other:`大约 {{count}} 年`},xYears:{one:`1 年`,other:`{{count}} 年`},overXYears:{one:`超过 1 年`,other:`超过 {{count}} 年`},almostXYears:{one:`将近 1 年`,other:`将近 {{count}} 年`}},s=(e,t,n)=>{let r,i=o[e];return r=typeof i==`string`?i:t===1?i.one:i.other.replace(`{{count}}`,String(t)),n?.addSuffix?n.comparison&&n.comparison>0?r+`内`:r+`前`:r},c={date:t({formats:{full:`y'年'M'月'd'日' EEEE`,long:`y'年'M'月'd'日'`,medium:`yyyy-MM-dd`,short:`yy-MM-dd`},defaultWidth:`full`}),time:t({formats:{full:`zzzz a h:mm:ss`,long:`z a h:mm:ss`,medium:`a h:mm:ss`,short:`a h:mm`},defaultWidth:`full`}),dateTime:t({formats:{full:`{{date}} {{time}}`,long:`{{date}} {{time}}`,medium:`{{date}} {{time}}`,short:`{{date}} {{time}}`},defaultWidth:`full`})};function l(e,t,n){return i(e,t,n)?`eeee p`:e.getTime()>t.getTime()?`'下个'eeee p`:`'上个'eeee p`}var u={lastWeek:l,yesterday:`'昨天' p`,today:`'今天' p`,tomorrow:`'明天' p`,nextWeek:l,other:`PP p`},d={code:`zh-CN`,formatDistance:s,formatLong:c,formatRelative:(e,t,n,r)=>{let i=u[e];return typeof i==`function`?i(t,n,r):i},localize:{ordinalNumber:(e,t)=>{let n=Number(e);switch(t?.unit){case`date`:return n.toString()+`日`;case`hour`:return n.toString()+`时`;case`minute`:return n.toString()+`分`;case`second`:return n.toString()+`秒`;default:return`第 `+n.toString()}},era:e({values:{narrow:[`前`,`公元`],abbreviated:[`前`,`公元`],wide:[`公元前`,`公元`]},defaultWidth:`wide`}),quarter:e({values:{narrow:[`1`,`2`,`3`,`4`],abbreviated:[`第一季`,`第二季`,`第三季`,`第四季`],wide:[`第一季度`,`第二季度`,`第三季度`,`第四季度`]},defaultWidth:`wide`,argumentCallback:e=>e-1}),month:e({values:{narrow:[`一`,`二`,`三`,`四`,`五`,`六`,`七`,`八`,`九`,`十`,`十一`,`十二`],abbreviated:[`1月`,`2月`,`3月`,`4月`,`5月`,`6月`,`7月`,`8月`,`9月`,`10月`,`11月`,`12月`],wide:[`一月`,`二月`,`三月`,`四月`,`五月`,`六月`,`七月`,`八月`,`九月`,`十月`,`十一月`,`十二月`]},defaultWidth:`wide`}),day:e({values:{narrow:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],short:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],abbreviated:[`周日`,`周一`,`周二`,`周三`,`周四`,`周五`,`周六`],wide:[`星期日`,`星期一`,`星期二`,`星期三`,`星期四`,`星期五`,`星期六`]},defaultWidth:`wide`}),dayPeriod:e({values:{narrow:{am:`上`,pm:`下`,midnight:`凌晨`,noon:`午`,morning:`早`,afternoon:`下午`,evening:`晚`,night:`夜`},abbreviated:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜间`},wide:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜间`}},defaultWidth:`wide`,formattingValues:{narrow:{am:`上`,pm:`下`,midnight:`凌晨`,noon:`午`,morning:`早`,afternoon:`下午`,evening:`晚`,night:`夜`},abbreviated:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜间`},wide:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜间`}},defaultFormattingWidth:`wide`})},match:{ordinalNumber:n({matchPattern:/^(第\s*)?\d+(日|时|分|秒)?/i,parsePattern:/\d+/i,valueCallback:e=>parseInt(e,10)}),era:r({matchPatterns:{narrow:/^(前)/i,abbreviated:/^(前)/i,wide:/^(公元前|公元)/i},defaultMatchWidth:`wide`,parsePatterns:{any:[/^(前)/i,/^(公元)/i]},defaultParseWidth:`any`}),quarter:r({matchPatterns:{narrow:/^[1234]/i,abbreviated:/^第[一二三四]刻/i,wide:/^第[一二三四]刻钟/i},defaultMatchWidth:`wide`,parsePatterns:{any:[/(1|一)/i,/(2|二)/i,/(3|三)/i,/(4|四)/i]},defaultParseWidth:`any`,valueCallback:e=>e+1}),month:r({matchPatterns:{narrow:/^(一|二|三|四|五|六|七|八|九|十[二一]?)/i,abbreviated:/^(一|二|三|四|五|六|七|八|九|十[二一]?|\d|1[0-2])月/i,wide:/^(一|二|三|四|五|六|七|八|九|十[二一]?)月/i},defaultMatchWidth:`wide`,parsePatterns:{narrow:[/^一/i,/^二/i,/^三/i,/^四/i,/^五/i,/^六/i,/^七/i,/^八/i,/^九/i,/^十(?!(一|二))/i,/^十一/i,/^十二/i],any:[/^(一|1(?!\d))/i,/^(二|2)/i,/^(三|3)/i,/^(四|4)/i,/^(五|5)/i,/^(六|6)/i,/^(七|7)/i,/^(八|8)/i,/^(九|9)/i,/^(十(?!(一|二))|10)/i,/^(十一|11)/i,/^(十二|12)/i]},defaultParseWidth:`any`}),day:r({matchPatterns:{narrow:/^[一二三四五六日]/i,short:/^[一二三四五六日]/i,abbreviated:/^周[一二三四五六日]/i,wide:/^星期[一二三四五六日]/i},defaultMatchWidth:`wide`,parsePatterns:{any:[/日/i,/一/i,/二/i,/三/i,/四/i,/五/i,/六/i]},defaultParseWidth:`any`}),dayPeriod:r({matchPatterns:{any:/^(上午?|下午?|午夜|[中正]午|早上?|下午|晚上?|凌晨|)/i},defaultMatchWidth:`any`,parsePatterns:{any:{am:/^上午?/i,pm:/^下午?/i,midnight:/^午夜/i,noon:/^[中正]午/i,morning:/^早上/i,afternoon:/^下午/i,evening:/^晚上?/i,night:/^凌晨/i}},defaultParseWidth:`any`})},options:{weekStartsOn:1,firstWeekContainsDate:4},labels:{labelDayButton:(e,t,n,r)=>{let i=(r??new a(n)).format(e,`PPPP`);return t.today&&(i=`今天,${i}`),t.selected&&(i=`${i},已选择`),i},labelMonthDropdown:`选择月份`,labelNext:`前往下个月`,labelPrevious:`前往上个月`,labelWeekNumber:e=>`第 ${e} 周`,labelYearDropdown:`选择年份`,labelGrid:(e,t,n)=>(n??new a(t)).formatMonthYear(e),labelGridcell:(e,t,n,r)=>{let i=(r??new a(n)).format(e,`PPPP`);return t?.today&&(i=`今天,${i}`),i},labelNav:`导航栏`,labelWeekNumberHeader:`周数`,labelWeekday:(e,t,n)=>(n??new a(t)).format(e,`cccc`)}};export{d as zhCN};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var e={common:{status:{backlog:`待規劃`,todo:`待處理`,in_progress:`進行中`,in_review:`審查中`,done:`已完成`,cancelled:`已取消`},priority:{urgent:`緊急`,high:`高`,medium:`中`,low:`低`,none:`無優先順序`},kind:{feature:`功能`,bug:`錯誤`,chore:`雜務`,research:`研究`,documentation:`文件`},difficulty:{low:`低`,med:`中`,high:`高`,none:`無難度`},role:{reader:`唯讀`,writer:`可編輯`,admin:`管理員`},permission:{ticket:{write:{label:`編輯工單`,description:`建立、編輯、移動、認領及留言工單;建立專案`},delete:{label:`刪除工單`,description:`永久刪除工單`}},project:{manage:{label:`管理專案`,description:`專案詳細資料、工作流程、標籤、欄位、整合及生命週期`}},member:{manage:{label:`管理成員`,description:`新增、變更及移除成員`}},invitation:{manage:{label:`管理邀請`,description:`透過電子郵件邀請成員`}},sharing:{manage:{label:`管理共用`,description:`公開唯讀連結及行事曆訂閱`}},workspace:{manage:{label:`管理工作區`,description:`工作區詳細資料、共用標籤及角色`}}},customFieldType:{text:`文字`,number:`數字`,date:`日期`,checkbox:`核取方塊`,select:`單選`,multiselect:`複選`},customField:{removedOption:`已移除的選項`,checked:`是`},filterField:{status:`狀態`,priority:`優先順序`,kind:`類型`,difficulty:`難度`,label:`標籤`,assigneeId:`負責人`,customField:`欄位`},sort:{priority:`優先順序`,updated:`最近更新`,created:`最近建立`,title:`標題`,labels:`標籤`,due:`到期日`,start:`開始日期`,number:`工單編號`,status:`狀態`,difficulty:`難度`,kind:`類型`,assignee:`負責人`,manual:`手動`},due:{overdue:`已逾期`,today:`今天到期`,week:`本週到期`,scheduled:`有到期日`,none:`無到期日`},relation:{blocks:{outgoing:`阻擋`,incoming:`被阻擋於`},related:{outgoing:`相關`,incoming:`相關`},duplicate:{outgoing:`重複於`,incoming:`被重複於`},causes:{outgoing:`導致`,incoming:`由其導致`},clones:{outgoing:`複製自`,incoming:`被複製為`},implements:{outgoing:`實作`,incoming:`由其實作`},reviews:{outgoing:`審查`,incoming:`由其審查`}},remoteKind:{branch:`分支`,pull_request:`提取要求`,issue:`議題`},notificationCategory:{assignment:`指派`,status:`狀態變更`,comment:`留言`,mention:`提及`,update:`其他更新`,due_soon:`即將到期`,overdue:`已逾期`},destinationEvent:{created:`新工單`},deliveryKind:{email:`電子郵件`,webhook:`Webhook`},change:{pull_request:{name:`提取要求`,opened:`提取要求已開啟`,draft:`草稿提取要求`,merged:`提取要求已合併`,closed:`提取要求未合併即關閉`},merge_request:{name:`合併要求`,opened:`合併要求已開啟`,draft:`草稿合併要求`,merged:`合併要求已合併`,closed:`合併要求未合併即關閉`},branchCreated:`分支已建立`},issueLinkState:{active:`同步中`,paused:`已暫停`,review:`需要審查`,creating:`正在建立議題`,unknown:`建立未確認`},importStatus:{previewed:`可以匯入`,running:`匯入中`,completed:`已匯入`,failed:`失敗`,cancelled:`已取消`},importProvider:{linear:{project:`團隊`,setup:`請在 Linear 的 Settings › Security & access 建立個人 API 金鑰,具備讀取權限即可。`},jira:{project:`專案`,setup:`Jira Cloud 使用帳號電子郵件及在 id.atlassian.com 建立的 API 權杖登入;Data Center 和 Server 使用個人存取權杖。具備專案的瀏覽權限即可。`},trello:{project:`看板`,setup:`請使用你在 trello.com/power-ups/admin 管理的 Power-Up 的 API 金鑰及為其產生的權杖,具備讀取權限即可。`},plane:{project:`專案`,setup:`請在 Plane 的 Profile settings › Personal access tokens 建立個人存取權杖。Plane Cloud 的 API 位址為 https://api.plane.so;工作區 URL 代碼是其網址的第一段。`},planka:{project:`看板`,setup:`請使用可開啟該看板的 PLANKA 帳號登入;Kanvil 開啟的工作階段會在讀取結束時關閉。PLANKA 2 管理員也可以改為核發 API 金鑰。`},kaneo:{project:`專案`,setup:`請在 Kaneo 的 Settings › Account › Developer 建立 API 金鑰。`}},plankaSignIn:{password:`使用者名稱和密碼`,api_key:`API 金鑰`},jiraEdition:{cloud:`Cloud`,data_center:`Data Center 或 Server`},event:{time_entry:{created:`已建立時間紀錄`,updated:`已更新時間紀錄`,deleted:`已刪除時間紀錄`},label:{created:`標籤已建立`,updated:`標籤已更新`,deleted:`標籤已刪除`},workflow:{created:`工作流程狀態已新增`,updated:`工作流程狀態已變更`,deleted:`工作流程狀態已移除`},custom_field:{created:`欄位已建立`,updated:`欄位已更新`,deleted:`欄位已刪除`},ticket:{created:`工單已建立`,updated:`工單已更新`,deleted:`工單已刪除`,moved:`從其他專案移入`,moved_away:`已移至其他專案`,due_soon:`即將到期`,overdue:`已逾期`,claimed:`工作已認領`},claim:{renewed:`認領已續期`,released:`認領已釋出`,recovered:`認領已收回`},comment:{created:`已新增留言`,updated:`留言已編輯`,deleted:`留言已刪除`},relation:{added:`已新增關聯`,removed:`已移除關聯`},attachment:{created:`已附加檔案`,deleted:`已移除檔案`},evidence:{added:`已附加佐證`},resource:{added:`已新增連結`,updated:`連結已編輯`,removed:`已移除連結`},calendar_feed:{created:`行事曆訂閱已建立`,revoked:`行事曆訂閱已撤銷`},project:{created:`專案已建立`,updated:`專案已更新`,archived:`專案已封存`,restored:`專案已還原`,transferred:`專案已移至其他工作區`,deleted:`專案已刪除`},membership:{updated:`專案存取權已變更`},workspace:{owner_changed:`工作區擁有者已變更`},role:{created:`角色已建立`,updated:`角色已變更`,deleted:`角色已刪除`},invitation:{updated:`邀請已變更`},destination:{created:`已新增目的地`,updated:`目的地已變更`,deleted:`已移除目的地`},repository:{connected:`儲存庫已連線`,updated:`儲存庫連線已變更`,disconnected:`儲存庫已中斷連線`,received:`已收到儲存庫 Webhook`},issue_link:{updated:`議題同步已變更`}},error:{network:`無法連線至 Kanvil。請檢查網路連線後再試一次。`,fallback:`無法完成要求`,input:`輸入`,code:{unauthorized:`請登入以繼續。`,session_required:`此動作需要使用已登入的帳號。`,account_disabled:`此帳號已停用。`,forbidden:`您沒有執行此動作的權限。`,insufficient_scope:`此憑證不允許執行此動作。`,workspace_scope_required:`此動作需要使用具備工作區存取權的帳號。`,invalid_origin:`不允許此來源。`,not_found:`找不到此項目,或您無權檢視。`,version_conflict:`載入後此內容已變更。請重新載入後再試一次。`,time_entry_conflict:`此使用者在此工單上已有進行中的時間紀錄。`,claim_conflict:`另一個執行者已認領此工單。`,idempotency_conflict:`此要求已用於另一項變更。`,project_archived:`請先還原已封存的專案再進行變更。`,rate_limited:`短時間內變更過多。請稍候再試。`,snapshot_required:`即時動態已落後。請重新載入以繼續。`,request_failed:`無法完成要求`}},action:{add:`新增`,apply:`套用`,back:`返回`,cancel:`取消`,clear:`清除`,close:`關閉`,confirm:`確認`,continue:`繼續`,copy:`複製`,create:`建立`,delete:`刪除`,done:`完成`,edit:`編輯`,open:`開啟`,remove:`移除`,rename:`重新命名`,reset:`重設`,retry:`重試`,save:`儲存`,search:`搜尋`},state:{copied:`已複製`,loading:`載入中…`,saved:`已儲存`,saving:`儲存中…`,none:`無`,unassigned:`未指派`},evidenceKind:{pull_request:`提取要求`,test:`測試`,review:`審查`,deployment:`部署`,device:`裝置`,other:`其他`},evidenceOutcome:{reported:`已回報`,pending:`待定`,passed:`通過`,failed:`失敗`},readinessReason:{project_archived:`專案已封存`,not_open:`工單必須處於待處理狀態且未封存`,not_enabled:`尚未啟用代理程式工作`,missing_description:`新增說明`,missing_acceptance_criteria:`新增驗收條件`,missing_difficulty:`選擇難度`,duplicate:`此工單已標示為重複`,claim_recovery_required:`收回擁有權前,請確認先前的執行者已停止`,claimed:`已由 {{worker}} 認領`,dependency:`{{key}} 尚未完成`},person:{deleted:`已刪除的帳號`,former:`前成員`},signIn:{codeTitle:`使用驗證碼登入`,resetTitle:`重設密碼`,codeSent:`如果 {{email}} 有帳號,驗證碼已在寄送途中。驗證碼將於 10 分鐘後過期。`,code:`驗證碼`,newPassword:`新密碼`,sendCode:`以電子郵件寄送驗證碼`,back:`返回登入`,resend:`重新寄送驗證碼`,resent:`已重新寄送驗證碼`,emailCode:`以電子郵件寄送登入驗證碼`,forgot:`忘記密碼?`,resetDone:`密碼已重設。請使用新密碼登入。`,resetDoneTitle:`密碼已重設`,resetDoneBody:`請使用新密碼登入。`,invalidCode:`此驗證碼錯誤、已使用或已過期。請重新寄送以取得有效的驗證碼。`,localPassword:`本機密碼`},language:{label:`語言`,system:`系統預設`}},shell:{ui:{showValue:`顯示{{label}}`,hideValue:`隱藏{{label}}`,previousPage:`上一頁{{noun}}`,nextPage:`下一頁{{noun}}`,pageRange:`第 {{from}}–{{to}} 項,共 {{total}} 項`,priority:`{{label}}優先順序`,working:`處理中…`,searchPlaceholder:`搜尋…`,clearValue:`清除{{label}}`,copyValue:`複製{{label}}`,copied:`已複製{{value}}`},documentTitle:`{{name}} · Kanvil`,openKanvil:`開啟 Kanvil`,page:{home:`首頁`,myWork:`我的工作`,inbox:`收件匣`,search:`搜尋`,settings:`設定`,planning:`規劃`,ticket:`工單`,tickets:`工單`,projects:`專案`},connection:{restored:`連線已恢復`,lost:`連線已中斷`,reconnecting:`正在重新連線…`,reload:`重新載入`,signIn:`登入以重新連線`},app:{skipToContent:`跳至內容`,assigned:`指派給您的`,openCount_other:`{{count, number}} 個未完成`,noAssigned:`沒有指派給您的未完成工單`,allInMyWork_other:`在「我的工作」中檢視全部 {{count, number}} 個`,openMyWork:`開啟「我的工作」`,recentActivity:`最近活動`,noActivity:`尚無活動`,searchProjects:`搜尋專案`,projectCount_other:`{{count, number}} 個專案`,noMatchingProjects:`沒有相符的專案`,clearSearch:`清除搜尋`,noWorkspaces:`尚無工作區`,noActiveProjects:`沒有使用中的專案`,noProjects:`尚無專案`,archivedProjects:`已封存的專案`,archived:`已封存`,noOpenTickets:`沒有未完成的工單`,overdueCount_other:`{{count, number}} 個已逾期`,percentDone:`已完成 {{percent}}`,nextDue:`下次到期 <time/>`,projectSettingsFor:`{{name}} 的專案設定`,pageNotFound:`找不到頁面`,openHome:`開啟首頁`,tryAgain:`再試一次`,adminRequired:`需要執行個體管理員權限`,projectNotFound:`找不到專案`,noWorkspaceSelected:`未選取工作區`},auth:{signIn:`登入`,createOwner:`建立擁有者帳號`,ownerHint:`第一個帳號負責管理此執行個體。`,invitedHint:`請使用受邀的電子郵件地址登入以接受邀請。`,signInWith:`使用 {{provider}} 登入`,useEmail:`使用電子郵件和密碼`,or:`或`,name:`名稱`,setupToken:`設定權杖`,setupTokenHint:`請使用執行個體設定中的 KANVIL_SETUP_TOKEN。`,email:`電子郵件`,password:`密碼`,signingIn:`正在登入…`,createAccount:`建立帳號`,consent:{expired:`授權要求已過期。請重新連線用戶端。`,title:`授權 {{client}}`,titleUnnamed:`授權用戶端`,subtitle:`此用戶端要求以您的帳號身分存取。專案權限仍然適用。`,deny:`拒絕`,allow:`允許存取`},invitation:{unavailable:`邀請無法使用`,join:`加入 {{workspace}}`,invited:`受邀帳號`,from:`邀請者`,access:`存取權`,removedProjects:`已移除的專案`,wholeWorkspace:`整個工作區`,expires:`到期時間`,expired:`此邀請已過期。請向工作區管理員索取新的邀請。`,revoked:`此邀請已撤銷。請向工作區管理員索取新的邀請。`,accepted:`此邀請已被接受。請向工作區管理員索取新的邀請。`,wrongAccount:`您目前以 <account>{{current}}</account> 的身分登入。請以 {{invited}} 的身分登入以接受邀請。`,signOut:`登出`,joined:`已加入 {{workspace}}`,accept:`接受邀請`},callback:{cancelled:`登入已取消。準備好後請再試一次。`,linkedElsewhere:`此身分已連結至另一個 Kanvil 帳號。請使用其他身分或聯絡管理員。`,notLinked:`此身分未連結至 Kanvil 帳號。請使用本機帳號登入,並在設定中連結。`,expired:`登入要求已過期或無法驗證。請在此畫面再試一次。`,emailMismatch:`無法連結身分識別提供者的電子郵件。請聯絡管理員。`,failed:`無法完成登入。請再試一次或聯絡管理員。`}},you:`您`,inbox:{markAllRead:`全部標示為已讀`,clearRead:`清除已讀`,cleared_other:`已清除 {{count, number}} 則已讀通知`,unread:`未讀`,mentions:`提及`,all:`所有通知`,noUnread:`沒有未讀通知`,noMentions:`沒有提及`,noNotifications:`沒有通知`,markUnread:`標示為未讀`,markRead:`標示為已讀`,dismiss:`移除`},search:{field:`搜尋工作區`,placeholder:`搜尋工單、專案、留言、活動…`,tab:{all:`全部`,project:`專案`,ticket:`工單`,comment:`留言`,activity:`活動`},indexing:`外部搜尋正在同步,工單結果暫時來自 PostgreSQL。`,empty:`搜尋工單、專案、留言及活動`,noResults:`找不到「{{query}}」的結果`,results:`搜尋結果`,pages:`搜尋結果分頁`,previous:`上一頁`,next:`下一頁`,type:{project:`專案`,ticket:`工單`,comment:`留言`,activity:`活動`}},column:{key:`工單`,labels:`標籤`,scopes:`範圍`,status:`狀態`,priority:`優先順序`,difficulty:`難度`,kind:`類型`,assigneeId:`負責人`,startAt:`開始`,dueAt:`到期`,createdAt:`建立時間`,updatedAt:`更新時間`},public:{notShared:`此專案未共用`,notSharedHint:`此連結已關閉或從未存在。成員可登入後檢視。`,noTickets:`沒有工單`,ticketUnavailable:`工單無法使用`,backToTickets:`返回工單`,description:`說明`,noDescription:`_尚無說明_`,acceptanceCriteria:`驗收條件`,noAcceptanceCriteria:`_尚無驗收條件_`,ticketProperties:`工單屬性`,properties:`屬性`,status:`狀態`,priority:`優先順序`,kind:`類型`,difficulty:`難度`,startDate:`開始日期`,dueDate:`到期日`,labels:`標籤`,scopes:`範圍`,created:`建立時間`,updated:`更新時間`,relationships:`關聯`,parent:`父工單`,child:`子工單`},sidebar:{workspace:`工作區`,unread_other:`{{count, number}} 則未讀`,userSettings:`使用者設定`,administration:`管理`,signOut:`登出`,projectSettings:`專案設定`},workspace:{new:`新增工作區`,name:`工作區名稱`,key:`工作區代碼`,create:`建立工作區`,workspaces:`工作區`,switch:`切換工作區`,settings:`工作區設定`},palette:{open:`前往…`,title:`前往`,description:`尋找專案、工單、頁面或動作`,placeholder:`搜尋工單、專案、頁面、動作…`,searching:`搜尋中…`,noResults:`沒有結果`,jumpTo:`跳至`,openKey:`在 {{project}} 中開啟 {{key}}`,resultsIn:`{{workspace}} 中的結果`,results:`結果`,seeAll_other:`檢視全部 {{count, number}} 個結果`,actions:`動作`,pages:`頁面`,newTicketIn:`在 {{project}} 中新增工單`,myTicketsIn:`我在 {{project}} 中的工單`,lightMode:`切換為淺色模式`,darkMode:`切換為深色模式`,keywords:{jump:`{{query}} 跳至 開啟`,allResults:`所有結果 {{query}}`,newTicket:`新增工單 建立`,myTickets:`我的工單 指派給我`,lightMode:`切換 淺色模式 主題 外觀`,darkMode:`切換 深色模式 主題 外觀`,help:`鍵盤快速鍵 說明`,home:`首頁 總覽 所有專案`,myWork:`我的工作 指派的工單`,search:`搜尋 留言 活動 專案 工單`,inbox:`收件匣 通知`,settings:`設定 {{scope}} {{label}}`}},actionCategory:{general:`一般`,ticketList:`工單清單`,inbox:`收件匣`,ticket:`工單`,search:`搜尋`,markdown:`Markdown`},action:{palette:`前往任何地方`,help:`鍵盤快速鍵`,sidebar:`切換側邊欄`,newTicket:`新增工單`,search:`搜尋工單`,highlight:`醒目提示下一個或上一個工單`,open:`開啟醒目提示的工單`,preview:`在清單旁預覽醒目提示的工單`,select:`選取醒目提示的工單`,properties:`設定醒目提示工單的狀態、優先順序或負責人`,inboxHighlight:`醒目提示下一則或上一則通知`,inboxOpen:`在收件匣旁檢視醒目提示通知的工單`,inboxRead:`將醒目提示的通知標示為已讀或未讀`,inboxDismiss:`移除醒目提示的通知`,step:`開啟下一個或上一個工單`,edit:`編輯工單`,save:`儲存工單或留言`,results:`在搜尋結果間移動`,insertBlock:`在視覺化編輯器中插入表格、圖表、檔案或其他區塊`},move:{top:{label:`將醒目提示的工單移至頂端`,menu:`移至頂端`},up:{label:`將醒目提示的工單上移`,menu:`上移`},down:{label:`將醒目提示的工單下移`,menu:`下移`},bottom:{label:`將醒目提示的工單移至底端`,menu:`移至底端`},previous:{label:`將醒目提示的工單移至上一個狀態`,menu:`移至上一個狀態`},next:{label:`將醒目提示的工單移至下一個狀態`,menu:`移至下一個狀態`}},copy:{link:{label:`複製連結`,copied:`工單連結`},keyAndTitle:{label:`複製編號和標題`,copied:`工單`},title:{label:`複製標題`,copied:`標題`},markdown:{label:`複製為 Markdown`,copied:`Markdown`},branch:{label:`複製分支名稱`,copied:`分支名稱`}},settingsNav:{account:`帳號`,settings:`設定`,workspace:`工作區`,workspaceSettings:`工作區設定`,projects:`專案`,projectSettings:`專案設定`,administration:`管理`,section:{profile:`個人資料與安全性`,appearance:`外觀`,preferences:`偏好設定`,credentials:`API 金鑰`,notifications:`通知`,general:`一般`,members:`成員`,roles:`角色`,labels:`標籤`,workflow:`工作流程`,fields:`欄位`,integrations:`整合`,calendar:`行事曆訂閱`,storage:`檔案儲存空間`,search:`搜尋`,events:`事件傳遞`,users:`使用者`,workspaces:`工作區`,authentication:`驗證`}},browser:{copyFailed:`複製失敗。請選取文字後手動複製。`,readFailed:`無法讀取檔案`,encodeFailed:`無法編碼檔案`,imageTooBig:`請選擇小於 10 MB 的圖片。`,cannotResize:`此瀏覽器無法調整圖片大小。`,prepareFailed:`無法處理圖片。`,stillTooLarge:`調整大小後圖片仍然過大。請選擇較簡單的圖片。`},primitive:{close:`關閉`,more:`更多`,breadcrumb:`階層連結`,sidebar:`側邊欄`,sidebarDescription:`顯示行動版側邊欄。`,toggleSidebar:`切換側邊欄`,resizeSidebar:`調整側邊欄寬度`,expandSidebar:`展開側邊欄`,commandTitle:`命令面板`,commandDescription:`搜尋要執行的命令…`}},ticket:{list:{selectSection:`選取{{section}}工單`,expandSection:`展開{{section}}工單`,collapseSection:`收合{{section}}工單`,sectionTickets:`{{section}}工單`,later:`之後`,selectAll:`選取所有顯示的工單`,selectTicket:`選取 {{key}}`,title:`標題`,all:`全部`,planned:`已規劃`,archived:`已封存`,allTickets:`所有工單`,customView:`自訂檢視`,sectionCount:`{{n, number}} 個{{section}}`,planning:`{{project}} 規劃`,collection:`{{source}}({{sections}})`,moved:`{{key}} 已移動`,movedTo:`{{key}} 已移至{{status}}`,movedAbove:`{{key}} 已移至 {{neighbor}} 上方`,movedBelow:`{{key}} 已移至 {{neighbor}} 下方`,movedToAbove:`{{key}} 已移至{{status}},位於 {{neighbor}} 上方`,movedToBelow:`{{key}} 已移至{{status}},位於 {{neighbor}} 下方`,emptyFiltered:`沒有相符的工單`,emptyPlanning:`沒有已規劃或已封存的工單`,emptyProject:`尚無工單`,emptyWork:`沒有指派給您的未完成工單`,createFirst:`建立第一個工單`,activeWork:`進行中的工作`,views:`工單檢視`,saveCurrentView:`儲存目前的檢視`,deleteView:`刪除檢視 {{name}}`,deleteViewConfirm:`要從已儲存的檢視中移除 {{name}} 嗎?工單不受影響。`,planningLink:`規劃`,newTicketShortcut:`新增工單({{shortcut}})`,saveView:`儲存檢視`,viewSaved:`檢視已儲存`,viewName:`檢視名稱`,retry:`重新載入工單`,board:`工單看板`,tickets:`工單`,resizeColumn:`調整「{{column}}」欄寬`,count_other:`{{count, number}} 個工單`,scheduledCount_other:`{{count, number}} 個已排程工單`},move:{destination:`目的地`,projectPlaceholder:`移至專案…`,blockers:`移動前請先解決`,submit:`移動工單`,pending:`移動中…`},picker:{placeholder:`選擇工單`,loading:`正在載入工單…`,searchLabel:`搜尋{{label}}`,searchPlaceholder:`依標題或編號搜尋…`,noParent:`無父工單`,searching:`搜尋中…`,empty:`找不到工單`},property:{status:`狀態`,priority:`優先順序`,kind:`類型`,difficulty:`難度`,assignee:`負責人`,you:`{{name}}(您)`,assignTo:`指派給…`,set:`設定{{label}}…`,search:`搜尋{{label}}`,noMatches:`沒有相符項目`},field:{mixed:`混合`,unchecked:`否`,noValue:`無值`},project:{label:`專案`,placeholder:`在專案中建立…`,search:`搜尋專案`},tags:{labels:`標籤`,scopes:`範圍`,summary:`{{label}}:{{tags}}`,edit:`編輯{{label}}`,editValues:`編輯{{label}}:{{tags}}`,editMixed:`編輯{{label}}:混合`,mixed:`混合{{label}}`,none:`無{{label}}`,mixedNote:`所選工單的{{label}}各不相同。`,clear:`清除{{label}}`,replace:`取代{{label}}`},tagInput:{remove:`移除{{label}} {{tag}}`,addAnother:`再新增一個…`,add:`新增{{label}}…`,choose:`選擇{{label}}`,chooseLabels:`選擇標籤`,searchPlaceholder:`搜尋標籤…`,searchLabel:`搜尋專案標籤`,empty:`沒有相符的標籤`},link:{previewUnavailable:`無法預覽`},userPicker:{label:`帳號`,placeholder:`選擇帳號`,searchPlaceholder:`搜尋名稱或電子郵件…`,searchLabel:`搜尋帳號`,useEmail:`使用 {{email}}`,emptyAnyAccount:`搜尋成員,或輸入現有帳號的電子郵件。`,empty:`沒有相符的成員`},date:{placeholder:`選擇日期`,clear:`清除{{label}}`,start:`開始日期`,startShort:`開始`,noStart:`無開始日期`,due:`到期日`,dueShort:`到期`,noDue:`無到期日`},filter:{me:`我`,searchTickets:`搜尋工單`,searchTicketsPlaceholder:`搜尋工單…`,filters:`篩選`,filtersActive:`篩選({{active, number}})`,due:`到期`,anyDue:`任何到期日`,archived:`已封存`,hideArchived:`隱藏已封存`,includeArchived:`包含已封存`,archivedOnly:`僅已封存`,includingArchived:`包含已封存`,reset:`重設篩選`,clear:`清除篩選`,active:`目前的工單篩選`,searchChip:`搜尋:{{query}}`,chipAny:`{{field}}:`,chipNone:`{{field}}不是:`,notValues:`不是 {{values}}`,match:`{{label}}比對方式`,is:`是`,isNot:`不是`,searchPlaceholder:`{{search}}…`,anyValue:{status:`所有狀態`,priority:`所有優先順序`,kind:`所有類型`,difficulty:`所有難度`,label:`所有標籤`,assigneeId:`任何人`,customField:`任何欄位值`},searchValues:{status:`搜尋狀態`,priority:`搜尋優先順序`,kind:`搜尋類型`,difficulty:`搜尋難度`,label:`搜尋標籤`,assigneeId:`搜尋人員`,customField:`搜尋欄位`}},display:{title:`顯示`,orderBy:`排序依據`,direction:`方向`,ascending:`遞增`,descending:`遞減`,reset:`重設顯示`,groupBy:`分組依據`,dueDate:`到期日`,layout:`版面配置`,list:`清單`,board:`看板`,backgrounds:`專案背景`,show:`顯示`,hide:`隱藏`,rowSpacing:`列間距`,compact:`緊湊`,comfortable:`寬鬆`,labelDisplay:`標籤顯示`,compactPreview:`緊湊預覽`,allLabels:`所有標籤`,properties:`屬性`},selection:{region:`已選取的工單`,clear:`清除選取`,selected_other:`已選取 {{count, number}} 個`,selectedOf_other:`{{count, number}} / {{total}}<wide> 已選取</wide>`,applied_other:`已將{{label}}套用至 {{count, number}} 個工單`,partlyApplied_other:`已套用至 {{done, number}} / {{count, number}} 個工單`,mixedDue:`到期日各不相同`,fields:`欄位`,more:`更多動作`,moveTo:`移至{{status}}`,restoreChange:`還原`,restore:`還原所選項目`,archive:`封存所選項目`,delete:`刪除所選項目`,deleteLimit:`一次最多刪除 {{limit, number}} 個`,archiveConfirm_other:`要封存 {{collection}} 中的 {{count, number}} 個工單嗎?`,deleteConfirm_other:`要永久刪除 {{collection}} 中的 {{count, number}} 個工單及其留言、關聯和附件嗎?未選取的子工單將失去父工單。`,deleted_other:`已刪除 {{count, number}} 個工單`},menu:{copied:`已複製{{label}}`,open:`開啟工單`,preview:`預覽`,newTab:`在新分頁中開啟`,edit:`編輯工單`,editTitle:`編輯 {{key}}`,duplicate:`建立工單副本`,created:`已建立 {{key}}`,move:`移動`,moveToProject:`移至專案…`,moveTitle:`移動 {{key}}`,moved:`已移動為 {{key}}`,follow:`追蹤工單`,unfollow:`取消追蹤工單`,archive:`封存工單`,unarchive:`取消封存工單`,delete:`刪除工單`,deleteConfirm:`要永久刪除 {{key}} 及其留言、關聯和附件嗎?`,openProject:`開啟專案`,projectLink:`專案連結`,copyLink:`複製連結`,newTicket:`新增工單`,projectSettings:`專案設定`,manageAccount:`管理帳號`,email:`電子郵件`,copyEmail:`複製電子郵件`},schedule:{range:`{{from}} – {{to}}`,calendar:`行事曆`,timeline:`時間軸`,today:`今天`,previousMonth:`上個月`,nextMonth:`下個月`,previousWeeks:`前兩週`,nextWeeks:`後兩週`,emptyWeeks:`這幾週沒有排程`,monthTickets:`{{title}}的工單`,showDay_other:`顯示 {{day}} 的全部 {{count, number}} 個工單`,more_other:`還有 {{count, number}} 個`,ticketColumn:`工單`,scheduledTickets:`已排程工單`},copy:{description:`說明`,acceptanceCriteria:`驗收條件`}},detail:{field:{title:`標題`,description:`說明`,acceptanceCriteria:`驗收條件`,parent:`父工單`,parentTicket:`父工單`,labels:`標籤`,scopes:`範圍`,holdReason:`暫緩原因`,url:`URL`},page:{preview:`工單預覽`,unavailable:`工單無法使用`,noProject:`此連結未指明工單所屬的專案。`,gone:`此工單已刪除,或您已沒有存取權。`,closePreview:`關閉預覽`,openFullPage:`以完整頁面開啟`,backToTickets:`返回工單`,tickets:`工單`,previousTicket:`上一個工單`,previousTicketShortcut:`上一個工單(K)`,nextTicket:`下一個工單`,nextTicketShortcut:`下一個工單(J)`,position:`{{position, number}} / {{total, number}}`,subscribe:`訂閱`,unsubscribe:`取消訂閱`,subscribed:`已訂閱更新`,unsubscribed:`已取消訂閱`,archive:`封存工單`,unarchive:`取消封存工單`,archivedToast:`工單已封存`,unarchivedToast:`工單已取消封存`,archived:`已封存`,edit:`編輯工單`,editShortcut:`編輯工單(E)`,editTitle:`編輯 {{key}}`,actions:`工單動作`,copyKey:`複製工單編號`,keyCopied:`已複製 {{key}}`,noDescription:`_尚無說明_`,noAcceptanceCriteria:`_尚無驗收條件_`,coordination:`工單協作`,properties:`屬性`,created:`建立時間`,updated:`更新時間`},section:{discussion:`討論`,evidence:`佐證`,activity:`活動`},readiness:{heading:`代理程式就緒狀態`,ready:`可以認領`,notReady:`尚不可認領`,hold:`暫緩:{{reason}}`,claimedUntil:`已由 <strong>{{worker}}</strong> 認領,直到 {{date}}。`},claim:{release:`釋出我的認領`,releaseDescription:`確認執行者已停止寫入後釋出擁有權?`,recover:`收回已過期的認領`,recoverHelp:`請先確認執行者已停止。`,reason:`收回原因`,confirmed:`我已確認執行者已停止`,submit:`收回擁有權`},editor:{storageUnavailable:`此瀏覽器無法儲存草稿。儲存前請保持編輯器開啟。`,saved:`{{key}} 已儲存`,created:`{{key}} 已建立`,properties:`工單屬性`,readyForAgents:`可供代理程式處理`,readyForAgentsHelp:`在需求、阻擋項目和擁有權允許時,代理程式可以認領此工單`,agents:`代理程式`,notReady:`未就緒`,moreProperties:`更多屬性`,holdPlaceholder:`為什麼要暫緩工作?`,claimsNote:`認領仍取決於需求、阻擋項目和目前的擁有權。`,restoreSize:`還原編輯器大小`,expand:`展開編輯器`,staleTitle:`此工單已變更。`,staleDescription:`您的草稿仍以版本 {{version}} 為基礎;儲存時需要使用目前的版本。`,discardAndReload:`捨棄草稿並重新載入`,titlePlaceholder:`工單標題`,descriptionPlaceholder:`新增背景、需求或重現步驟…`,criteriaPlaceholder:`- [ ] 如何確認已完成?`,removeCriteria:`移除驗收條件`,addCriteria:`新增驗收條件`,savedLocally:`草稿已儲存在此裝置上`,notSavedLocally:`無法儲存草稿`,discardDraft:`捨棄草稿`,createMore:`繼續建立`,save:`儲存工單`,create:`建立工單`},resource:{removeConfirm:`要移除 {{name}} 嗎?`,uploading:`上傳中…`},attachment:{heading:`附件`,tooLarge:`每個檔案不得超過 10 MB。`,attached_other:`已附加 {{count, number}} 個檔案`,attach:`附加檔案`,emptyWritable:`尚無附件。將檔案拖放到這裡。`,empty:`尚無附件`,remove:`移除附件`,fileInput:`檔案(最大 10 MB)`},evidence:{description:`處理此工單的人員和整合所回報的連結及結果。`,add:`新增佐證`,empty:`尚無佐證`,added:`佐證已新增`,kindLabel:`類型`,outcomeLabel:`回報的結果`,commit:`提交(完整雜湊)`,note:`備註`},link:{heading:`連結`,add:`新增連結`,edit:`編輯連結`,save:`儲存連結`,remove:`移除連結`,empty:`尚無連結`,stateUnknown:`狀態不明`,remote:`{{kind}} · {{state}}`,remoteState:{open:`開啟`,closed:`已關閉`,merged:`已合併`,draft:`草稿`,active:`使用中`,deleted:`已刪除`}},activity:{previous:`較早的活動`,next:`較新的活動`},relations:{heading:`關聯`,add:`新增關聯`,linkExisting:`連結現有工單`,newChild:`新增子工單`,newChildTitle:`{{key}} 的新子工單`,children:`子工單 <count>{{done, number}}/{{total, number}}</count>`,previousChildren:`上一組子工單`,nextChildren:`下一組子工單`,empty:`尚無關聯`,type:`關聯類型`,ticket:`工單`,statusOf:`{{key}} 的狀態`,assigneeOf:`{{key}} 的負責人`,remove:{blocks:`移除阻擋關聯`,related:`移除相關關聯`,duplicate:`移除重複關聯`,causes:`移除導致關聯`,clones:`移除複製關聯`,implements:`移除實作關聯`,reviews:`移除審查關聯`}},discussion:{empty:`尚無留言`,previous:`較早的留言`,next:`較新的留言`,comment:`留言`,placeholder:`撰寫留言…`,posting:`張貼中…`,edited:`已編輯`,edit:`編輯留言`,delete:`刪除留言`,deleteConfirm:`要永久刪除此留言嗎?`},markdown:{format:{heading:`標題`,bold:`粗體`,italic:`斜體`,link:`連結`,linkHelp:`為所選文字加上連結`,bullets:`項目符號清單`,numbers:`編號清單`,tasks:`檢查清單`,code:`程式碼區塊`},formatting:`{{label}}格式`,editor:`{{label}}編輯器`,insertFile:`插入檔案`,insertFileInto:`將檔案插入{{label}}`,write:`撰寫`,visual:`視覺化`,preview:`預覽`,unsupported:`此文字使用了視覺化編輯器無法保留的 Markdown,例如 HTML 或註腳。`,nothingToPreview:`沒有可預覽的內容`,mention:`提及成員`,insertBlock:`插入區塊`,block:{quote:`引言`,table:`表格`,diagram:`圖表`,divider:`分隔線`,file:`檔案`},table:{addRow:`新增列`,addColumn:`新增欄`,deleteRow:`刪除列`,deleteColumn:`刪除欄`,deleteTable:`刪除表格`},linkAddress:`連結網址`,diagram:`圖表`,diagramFailed:`無法繪製此圖表:{{error}}`,diagramSource:`圖表原始碼`}},settings:{account:{title:`設定`,security:`登入與安全性`,localPassword:`本機密碼`,localPasswordDescription:`變更密碼會將您的其他工作階段登出。`,changePassword:`變更密碼`,oidcLinked:`可使用此身分識別提供者登入同一個帳號。`,oidcUnlinked:`透過此提供者登入前,請先連結您的帳號。`,linked:`已連結`,link:`連結 {{name}}`,currentPassword:`目前的密碼`,newPassword:`新密碼`,confirmPassword:`確認新密碼`,passwordMismatch:`兩次輸入的新密碼不一致。`,passwordUpdated:`密碼已更新,其他工作階段已登出。`},preferences:{behavior:`行為`,languageDescription:`套用於此瀏覽器,包括登入頁面。`,weekStart:`每週起始日`,weekStartDescription:`套用於所有日期選擇器。`,weekStartLanguage:`依語言預設`,sidebar:`啟動時的側邊欄`,sidebarDescription:`下次開啟 Kanvil 時生效。`,sidebarLast:`維持上次狀態`,sidebarExpanded:`展開`,sidebarCollapsed:`收合`,ticketLists:`工單清單`,reset:`重設偏好設定`,resetDescription:`將此瀏覽器中的這些選項還原為預設值。`,resetDone:`偏好設定已重設`},notifications:{title:`通知`,inbox:`收件匣`,inboxDescription:`您追蹤的工單更新會顯示在 Kanvil 中。`,inboxSwitch:`在收件匣接收已訂閱工單的更新`,push:`推播通知`,pushDescription:`將更新傳送到您已註冊的裝置。`,pushUnavailable:`此執行個體未啟用推播。`,pushSwitch:`在已註冊的裝置上接收推播通知`,categories:`類別`,categorySwitch:`{{label}}通知`,reminder:`即將到期提醒`,reminderDescription:`在追蹤的工單到期前多久提醒。`,reminderUnit:`提醒單位`,reminderAmount:`提醒數值`,unit:{minutes:`分鐘`,hours:`小時`,days:`天`,weeks:`週`}},layout:{nav:`設定`,section:`設定區段`,nested:`{{parent}} · {{child}}`,name:`名稱`,description:`說明`,descriptionPlaceholder:`新增說明`,noDescription:`尚無說明`,stale:`此內容已在其他地方變更。請取消後重新開始。`},profile:{title:`個人資料`,displayName:`顯示名稱`,edit:`編輯個人資料`,changePhoto:`變更相片`,removePhoto:`移除相片`,photoFile:`個人資料相片檔案`,photoUpdated:`相片已更新`,photoRemoved:`相片已移除`,saved:`個人資料已儲存`},credentials:{global:`全域`,globalDescription:`存取帳戶有權存取的所有專案和工作區,包括日後取得的存取權限。`,title:`API 金鑰`,description:`為指令碼、整合和代理程式提供可隨時撤銷的存取權。`,create:`建立 API 金鑰`,creating:`建立中…`,created:`API 金鑰已建立`,name:`名稱`,access:`存取權`,projects:`專案`,expires:`到期時間`,actions:`動作`,unavailableProject:`無法使用的專案`,untitled:`未命名的金鑰`,expired:`已過期`,disabled:`已停用`,custom:`自訂`,noProjects:`沒有專案存取權`,never:`永不`,revoke:`撤銷 API 金鑰`,revokeNamed:`要撤銷 {{name}} 嗎?使用此金鑰的連線將立即失去存取權。`,revokeUnnamed:`要撤銷此 API 金鑰嗎?使用此金鑰的連線將立即失去存取權。`,empty:`尚無 API 金鑰`,emptyDescription:`當指令碼、整合或代理程式需要存取您的專案時,可建立金鑰。`,copyNow:`請立即複製此金鑰。之後將無法再次檢視。`,newKey:`新的 API 金鑰`,copy:`複製 API 金鑰`,noProjectTitle:`請先選擇專案`,noProjectDescription:`API 金鑰至少需要存取一個專案。`,goToProjects:`前往專案`,keyName:`金鑰名稱`,keyNamePlaceholder:`例如:發行自動化`,projectAccess:`專案存取權`,findProject:`尋找專案`,projectDescription:`{{key}} · {{workspace}}`,workspace:`工作區`,noMatches:`沒有相符的專案`,roleLimit:`此金鑰的權限仍受您現有專案角色的限制。`,accessLevel:`存取層級`,preset:{read:`唯讀`,write:`讀取和寫入`,automation:`讀取、寫入及認領工作`,custom:`自訂權限`},permission:{intake:{name:`提交報告`,description:`僅建立隔離工單。無法讀取或編輯工單。請單獨使用。`},read:{name:`讀取工單`,description:`檢視專案、工單、留言和附件。`},write:{name:`編輯工單`,description:`建立和更新工單、留言和附件。`},claim:{name:`認領工作`,description:`認領、續期和釋出工單擁有權。`},admin:{name:`管理專案和工作區`,description:`在角色允許的範圍內變更專案和工作區的設定及成員。`}},expiry:{7:`7 天`,30:`30 天`,90:`90 天`,365:`1 年`,custom:`自訂日期`,never:`永不過期`},expiresOn:`到期日`,neverDescription:`金鑰在您刪除前會持續有效。`},calendarFeeds:{title:`行事曆訂閱`,new:`新增行事曆訂閱`,created:`行事曆訂閱已建立`,create:`建立訂閱`,adminOnly:`由專案管理員共用和撤銷行事曆訂閱。`,labels:`標籤`,everyLabel:`所有標籤`,stopped:`· 已停止:建立者已失去存取權`,revoke:`撤銷 {{name}}`,revokeDescription:`訂閱 {{name}} 的行事曆將立即停止更新。工單不受影響。`,revoked:`行事曆訂閱已撤銷`,empty:`尚無行事曆訂閱`,copyNow:`請立即將此連結複製到行事曆應用程式中。此連結只會顯示一次,任何持有連結的人都能讀取共用的工單。`,link:`訂閱連結`,name:`訂閱名稱`,onlyLabels:`僅限具有這些標籤的工單`,explanation:`訂閱會將設有開始日期或到期日的未完成、未封存工單列為 {{zone}} 時區的全天活動。任何持有連結的人都能看到其編號、標題、狀態、優先順序和日期。`},project:{title:`專案設定`,edit:`編輯專案`,copyPrefix:`複製工單前綴`,saved:`專案已儲存`,noOpenTickets:`沒有未完成的工單`,openTickets_other:`{{count, number}} 個未完成工單`,overdueTickets_other:`· {{count, number}} 個已逾期`,created:`· 建立於 <time/>`,access:`專案存取權`,accessWorkspace:`工作區成員依其工作區角色取得存取權。`,accessPrivate:`只有您新增的成員和工作區管理員可以檢視此專案。`,accessUpdated:`專案存取權已更新`,visibility:{workspace:`工作區中的所有人`,private:`僅限專案成員`},workspace:`工作區`,workspaceSettings:`工作區設定`,prefix:`工單前綴`,prefixDescription:`變更後,先前的前綴和工單編號仍可使用。`,changePrefix:`變更前綴`,changePrefixTitle:`變更工單前綴`,changePrefixDescription:`{{name}} 中的所有工單都會使用新前綴,並保留原本的編號。`,changing:`變更中…`,prefixChanged:`工單現在使用 {{key}}`,publicLink:`公開連結`,publicDisclosure:`任何持有連結的人都能讀取此專案中所有未封存工單的標題、說明、驗收條件、狀態、優先順序、類型、難度、標籤、範圍、日期,以及指向其他工單的連結。人員、留言、活動、附件、佐證和認領仍維持私密。`,publicOn:`公開連結已開啟`,publicOff:`公開連結已關閉`,off:`關閉`,copyPublicLink:`複製公開連結`,openPublic:`開啟公開檢視`,stopSharing:`停止共用`,share:`公開共用`,background:`背景`,backgroundDescription:`顯示在此專案的工單清單和看板後方。`,backgroundUpdated:`背景已更新`,backgroundRemoved:`背景已移除`,currentBackground:`目前的背景`,uploading:`上傳中…`,replace:`取代`,upload:`上傳圖片`,backgroundFile:`專案背景檔案`},tickets_other:`{{count, number}} 個工單`,moveUp:`上移 {{name}}`,moveDown:`下移 {{name}}`,workflow:{title:`工作流程`,new:`新增狀態`,edit:`編輯狀態`,editNamed:`編輯狀態 {{name}}`,deleteNamed:`刪除狀態 {{name}}`,onlyState:`{{name}} 是唯一的{{category}}狀態`,saved:`狀態已儲存`,name:`狀態名稱`,category:`類別`,color:`狀態色彩`,categoryChange:`其工單的狀態會變更為{{status}}。`,save:`儲存狀態`,delete:`刪除狀態`,deleteTitle:`刪除 {{name}}`,deleted:`狀態已刪除`,reassign_other:`將 {{count, number}} 個工單移至`},labels:{title:`標籤`,search:`搜尋標籤`,new:`新增標籤`,edit:`編輯標籤`,workspace:`工作區`,editNamed:`編輯標籤 {{name}}`,deleteNamed:`刪除標籤 {{name}}`,deleteUsed_other:`要從 {{count, number}} 個工單(包括已封存的工單)中移除 {{name}} 嗎?使用此標籤的已儲存檢視將移除其標籤篩選。`,deleteUsedShared_other:`要從所有專案的 {{count, number}} 個工單(包括已封存的工單)中移除 {{name}} 嗎?使用此標籤的已儲存檢視將移除其標籤篩選。`,deleteUnused:`要刪除未使用的標籤 {{name}} 嗎?使用此標籤的已儲存檢視將移除其標籤篩選。`,deleted:`標籤已刪除`,noMatches:`沒有相符的標籤`,empty:`尚無標籤`,saved:`標籤已儲存`,name:`標籤名稱`,color:`標籤色彩`,rename:`重新命名會一併更新工單和已儲存檢視中的此標籤。`,renameShared:`重新命名會一併更新所有專案中工單和已儲存檢視裡的此標籤。`,adopt:`採用專案標籤`,adoptDescription:`已在 {{projects}} 中定義。其工單會保留此標籤並採用此色彩。`,save:`儲存標籤`},fields:{title:`欄位`,new:`新增欄位`,edit:`編輯欄位`,editNamed:`編輯欄位 {{name}}`,deleteNamed:`刪除欄位 {{name}}`,deleteUsed_other:`要刪除 {{name}} 並從 {{count, number}} 個工單(包括已封存的工單)中移除其值嗎?已儲存的檢視將失去以此欄位設定的篩選。`,deleteUnused:`要刪除未使用的欄位 {{name}} 嗎?已儲存的檢視將失去以此欄位設定的篩選。`,deleted:`欄位已刪除`,empty:`尚無欄位`,saved:`欄位已儲存`,required:`必填`,requiredDescription:`新工單必須填寫此值。`,defaultValue:`預設值 {{value}}`,default:`預設值`,name:`欄位名稱`,type:`類型`,options:`選項`,option:`選項 {{number}}`,optionInline:`選項 {{number}}`,optionColor:`{{name}} 的色彩`,removeOption:`移除 {{name}}`,addOption:`新增選項`,reassign:`具有 {{name}} 的工單`,clearValue:`清除值`,moveTo:`移至 {{name}}`,save:`儲存欄位`},lifecycle:{archivedToast:`專案已封存`,restoredToast:`專案已還原`,archivedNotice:`封存於 <time/> · 唯讀`,restore:`還原`,archive:`封存`,archivedDescription:`封存於 <time/>。其工單為唯讀,且不會出現在清單、搜尋、我的工作、行事曆訂閱和代理程式佇列中。`,archiveDescription:`將專案從清單、搜尋、我的工作、行事曆訂閱和代理程式佇列中隱藏,並將其工單設為唯讀。歷程記錄會保留。`,archiveProject:`封存專案`,archiveConfirm:`在還原之前,{{name}} 會從專案清單和代理程式佇列中移除。必須先釋出已認領的工單。`,blocker:`{{message}}:{{tickets}}`,row:{tickets:`工單`,losesAccess:`失去存取權`,gainsAccess:`取得存取權`,labelsCopied:`已複製的標籤`,labelsMerged:`已合併的標籤`,followsRemoved:`已移除的追蹤`,invitations:`邀請`,comments:`留言`,files:`檔案`,kept:`保留`,projects:`專案`,members:`成員`,sharedLabels:`共用標籤`,signIn:`登入`,sessions:`工作階段`,credentials:`憑證`,devices:`裝置`,access:`存取權`,claims:`認領`,imports:`匯入`,profile:`個人資料`},transferTickets_other:`{{count, number}} 個,保留其編號和連結`,noOne:`無人`,transferInvitations_other:`{{count, number}} 個待處理的邀請將不再包含此專案`,files_other:`{{count, number}} 個 · {{size}}`,keptProject_other:`{{count, number}} 筆活動記錄和 {{key}} 前綴`,keptWorkspace_other:`{{count, number}} 筆活動記錄和 {{slug}} URL 代碼`,archivedProject:`已封存`,membersLoseAccess_other:`{{count, number}} 人失去存取權;其帳號會保留`,pendingInvitations_other:`{{count, number}} 個待處理`,move:`移動`,moveTitle:`移動 {{name}}`,moved:`已移至 {{name}}`,movedUnnamed:`已移至該工作區`,destination:`目的地工作區`,moveProject:`移動專案`,moving:`移動中…`,export:`匯出`,exportDescription:`將工單及其工作流程、欄位、標籤、關聯、留言、佐證和連結匯出為具版本的 JSON。附件和活動歷程會保留在此。`,exporting:`匯出中…`,exportJson:`匯出 JSON`,delete:`刪除`,deleteTitle:`刪除 {{name}}`,deleted:`{{name}} 已刪除`,typeToConfirm:`輸入 {{value}} 以確認`,deleting:`刪除中…`,deleteProject:`刪除專案`,deleteProjectDescription:`永久移除工單、留言和檔案。活動歷程會保留,且 {{key}} 不會再被使用。`,signInAgain:`重新登入`,deleteWorkspace:`刪除工作區`,deleteWorkspaceDescription:`永久刪除工作區中的所有專案及其工單、留言和檔案,並移除其成員。帳號和活動歷程會保留。`,noSignInMethods:`沒有已連結的登入方式`,sessions_other:`{{count, number}} 個工作階段已登出`,credentials_other:`{{count, number}} 個 API 金鑰或應用程式連線已撤銷`,devices_other:`{{count, number}} 個裝置或通知管道已移除`,workspaceCount_other:`{{count, number}} 個工作區`,directProjectCount_other:`{{count, number}} 個直接專案`,accessWorkspaces:`已移除 {{workspaces}}`,accessBoth:`已移除 {{workspaces}}和 {{projects}}`,claims_other:`{{count, number}} 個認領已釋出`,imports_other:`{{count, number}} 個匯入已取消`,profileErased:`名稱、電子郵件和相片已清除`,commentCount_other:`{{count, number}} 則留言`,activityCount_other:`{{count, number}} 筆活動記錄`,keptAccount:`{{tickets}}、{{comments}}和 {{events}}將顯示為「{{name}}」`,deleteAccount:`刪除帳號`,deleteOwnAccount:`永久移除您的登入方式、工作階段、金鑰、裝置、成員資格和個人資料。工單、留言和活動會保留,並顯示為「{{name}}」。`,deleteOtherAccount:`永久移除此帳號的登入方式、工作階段、金鑰、裝置、成員資格和個人資料。工單、留言和活動會保留,並顯示為「{{name}}」。`},icon:{change:`變更圖示`,title:`專案圖示`,library:`圖示庫`,upload:`上傳圖片`,search:`搜尋圖示`,searchPlaceholder:`搜尋圖示…`,color:`圖示色彩`,choose:`選擇 {{name}}`,noMatches:`沒有相符的圖示`,showMore:`顯示更多圖示`,uploading:`上傳中…`,chooseImage:`選擇圖片`,formats:`PNG、JPEG 或 WebP · 最大 10 MB`,file:`專案圖示檔案`,reset:`重設圖示`},createProject:{title:`新增專案`,inWorkspace:`位於 {{name}}`,everyoneLabel:`此工作區中的所有人`,name:`專案名稱`,namePlaceholder:`行動應用程式`,create:`建立專案`},workspace:{title:`工作區設定`,owner:`擁有者`,noOwner:`沒有擁有者。執行個體管理員可以指定一位。`,transfer:`轉移擁有權`,transferring:`轉移中…`,transferred:`{{name}} 現在擁有 {{workspace}}`,newOwner:`新擁有者`,noEligible:`擁有權只能轉移給使用中的工作區管理員。請先將另一位成員設為管理員。`,chosenCan:`之後 {{name}} 可以轉移或刪除此工作區。`,newOwnerCan:`新擁有者可以轉移或刪除此工作區。`,ownerStays:`{{name}} 仍為管理員。`,unavailable:`工作區無法使用`,backToProjects:`返回專案`,name:`工作區名稱`,edit:`編輯工作區`,copyKey:`複製工作區代碼`,saved:`工作區已儲存`,projects:`專案`,projectCount_other:`您在此工作區中可以存取 {{count, number}} 個專案。`,viewProjects:`檢視專案`,members:`成員`,membersDescription:`工作區角色適用於使用工作區存取權的專案。`,manageMembers:`管理成員`},members:{title:`成員`,customRole:`自訂角色`,search:`搜尋成員`,searchPlaceholder:`名稱或電子郵件…`,add:`新增成員`,added:`成員已新增`,workspaceAccess:`工作區存取權`,owner:`擁有者`,roleFor:`{{name}} 的角色`,useWorkspaceAccess:`使用工作區存取權`,remove:`移除成員`,removeFromWorkspace:`要將 {{name}} 從此工作區及其所有專案中移除嗎?`,restoreAccess:`要為 {{name}} 恢復{{role}}工作區存取權嗎?`,removeFromProject:`要移除 {{name}} 的專案存取權嗎?`,noMatches:`沒有相符的成員`,empty:`尚無成員`,projectRole:`專案角色`,workspaceRole:`工作區角色`},invitations:{title:`邀請`,invite:`透過電子郵件邀請`,removedProjects:`已移除的專案`,wholeWorkspace:`整個工作區`,selectedProjects:`選取的專案`,expired:`已過期`,expires:`<time/>到期`,resend:`重新傳送邀請給 {{email}}`,revoke:`撤銷邀請`,revokeDescription:`傳送給 {{email}} 的連結將失效。`,revoked:`邀請已撤銷`,empty:`沒有待處理的邀請`,email:`電子郵件`,role:`角色`,access:`存取權`,projects:`專案`,noProjects:`此工作區沒有專案。`,linkExpires:`連結有效期限`,inDays_other:`{{count, number}} 天`,submit:`邀請`,link:`邀請連結`,copyLink:`複製邀請連結`,emailed:`已寄送電子郵件給 {{email}}。此連結僅限該地址使用,且只能使用一次。`,notEmailed:`此執行個體未設定電子郵件。請將此連結傳送給 {{email}};此連結僅限該地址使用,且只能使用一次。`,notEmailedUnknown:`此執行個體未設定電子郵件。請將此連結傳送給收件者;此連結僅限該地址使用,且只能使用一次。`},workspaces:{title:`工作區`,search:`搜尋工作區`,searchPlaceholder:`名稱或 URL 代碼…`,noMatches:`沒有相符的工作區`,empty:`尚無工作區`,workspace:`工作區`,administrators:`管理員`,members:`成員`,projects:`專案`,created:`建立時間`,actions:`動作`,noAdministrator:`沒有管理員`,manage:`管理 {{name}}`,pagerNoun:`工作區`,private:`私人`,noProjects:`沒有專案`},roles:{title:`角色`,new:`新增角色`,edit:`編輯角色`,builtIn:`內建`,readOnly:`僅可讀取`,memberships_other:`{{count, number}} 個成員資格`,editNamed:`編輯角色 {{name}}`,deleteNamed:`刪除角色 {{name}}`,deleteUnused:`要刪除未使用的角色 {{name}} 嗎?`,deleted:`角色已刪除`,empty:`尚無角色`,saved:`角色已儲存`,name:`角色名稱`,actions:`動作`,everyoneReads:`所有成員都可以讀取其可存取的專案。`,changesApply_other:`變更會立即套用至持有此角色的 {{count, number}} 個成員資格。`,save:`儲存角色`,delete:`刪除角色`,reassignDescription_other:`有 {{count, number}} 個成員資格持有 {{name}}。請選擇要改用的角色。`,moveTo:`將成員改為`},theme:{mode:{light:`淺色`,dark:`深色`,system:`跟隨系統`},aligned:`已對齊`,hexColor:`請使用六位數的十六進位色碼`,contrast:{light:`請將淺色主題的文字對比提高到至少 4.5:1。`,dark:`請將深色主題的文字對比提高到至少 4.5:1。`},fileTooLarge:`主題檔案必須小於 100 KB。`,tooMany:`最多保留 50 個自訂主題。`},appearance:{colorScheme:`配色`,theme:`主題`,themes:`主題`,colorTheme:`色彩主題`,search:`搜尋主題`,searchPlaceholder:`搜尋主題…`,create:`建立主題`,add:`新增主題`,importFile:`匯入主題檔案`,useMode:{light:`在淺色模式使用 {{name}}`,dark:`在深色模式使用 {{name}}`},modeOnly:{light:`僅用於淺色模式`,dark:`僅用於深色模式`},copyName:`{{name}} 副本`,duplicate:`建立 {{name}} 的副本`,edit:`編輯 {{name}}`,export:`匯出 {{name}}`,delete:`刪除 {{name}}`,deleteDescription:`要從此瀏覽器移除 {{name}} 嗎?使用此主題的淺色或深色外觀將改回您的基礎主題。`,noMatches:`沒有相符的主題`,customTheme:`自訂主題`,name:`主題名稱`,paletteColor:`{{mode}}{{color}}`,color:{background:`背景`,foreground:`文字`,sidebar:`側邊欄`,primary:`強調色`,primaryForeground:`強調色文字`},save:`儲存主題`},color:{swatch:`{{color}}{{label}}`,removeCustom:`從自訂色彩中移除 {{hex}}`,custom:`自訂色彩`,default:`預設`,red:`紅色`,orange:`橘色`,amber:`琥珀色`,yellow:`黃色`,lime:`萊姆綠`,green:`綠色`,emerald:`翡翠綠`,teal:`藍綠色`,cyan:`青色`,sky:`天藍色`,blue:`藍色`,indigo:`靛藍色`,violet:`紫羅蘭色`,purple:`紫色`,fuchsia:`桃紅色`,pink:`粉紅色`,rose:`玫瑰色`,gray:`灰色`}},admin:{instance:{title:`管理`,heading:`執行個體`,edit:`編輯執行個體`,name:`名稱`,description:`說明`,noDescription:`尚無說明`,version:`應用程式版本`,publicUrl:`公開 URL`,provider:{local:`本機 PostgreSQL 儲存空間`,s3:`S3 相容物件儲存空間`,postgres:`內建 PostgreSQL`,elasticsearch:`Elasticsearch 相容搜尋`},section:{storage:`檔案儲存空間`,search:`工單搜尋`,events:`事件傳遞`},viewConfiguration:`檢視設定`,configure:{storage:`設定儲存空間`,search:`設定搜尋`,events:`設定事件傳遞`},providerTitle:`提供者`,deploymentManaged:`由部署管理`,instanceManaged:`由執行個體管理`,transfers:`傳輸`,indexStatus:`索引狀態`,connection:`連線`,transfersS3:`檔案在本機儲存後會移至物件儲存空間。`,transfersLocal:`新檔案會儲存在 PostgreSQL 中。`,searchBuiltIn:`搜尋直接在 PostgreSQL 中執行。`,searchIndexed:`工單變更會在背景建立索引。`,eventsConnection:`通知用於喚醒取用者;PostgreSQL 保存重播歷程。`,builtIn:`內建`,connected:`已連線`,upToDate:`已是最新`,status:{ready:`就緒`,transferring:`傳輸中`,indexing:`建立索引中`,connecting:`連線中`,unavailable:`無法使用`},pendingTransfers:`待傳輸`,pendingRemain:`傳輸完成前,這些檔案仍可從 PostgreSQL 讀取。`,noPending:`沒有等待傳輸的檔案。`,bucket:`儲存貯體`,storageEndpoint:`儲存空間端點`,backup:`備份`,backupBuckets:`請備份 PostgreSQL 及所有參照的物件儲存貯體。`,backupDatabase:`檔案內容包含在 PostgreSQL 備份中。`,fallback:`備援`,fallbackDescription:`外部索引落後或離線時,可使用 PostgreSQL 搜尋。`,searchEndpoint:`搜尋端點`,index:`索引`,delivery:`傳遞`,deliveryDescription:`持久事件和重播保存在 PostgreSQL 中。選用的訊息代理程式負責分送喚醒訊號。`,brokerEndpoint:`訊息代理程式端點`,exchange:`交換器`},configure:{storageTitle:`設定檔案儲存空間`,searchTitle:`設定工單搜尋`,eventsTitle:`設定事件傳遞`,saved:`執行個體設定已儲存`,instanceName:`執行個體名稱`,instanceDescription:`執行個體說明`,publicUrlSource:`公開 URL 由部署中的 KANVIL_URL 設定。`,storageIntro:`附件和個人資料相片使用此提供者。現有檔案會保留在原本的儲存位置。`,storageProvider:`儲存空間提供者`,localDescription:`檔案內容儲存在本機資料庫中,並包含在 PostgreSQL 備份內。`,s3Endpoint:`S3 端點`,s3EndpointHint:`使用 Amazon S3 時請將端點留空。`,region:`區域`,prefix:`物件索引鍵前綴`,accessKeyId:`存取金鑰 ID`,secretAccessKey:`秘密存取金鑰`,savedSecret:`已儲存。留空即可保留。`,deploymentCredentials:`省略時使用部署憑證`,pathStyle:`使用路徑樣式的儲存貯體 URL`,storageVerified:`儲存空間連線已驗證`,testStorage:`測試儲存空間連線`,filesWaiting_other:`{{count, number}} 個檔案等待傳輸。待傳輸的檔案仍可從 PostgreSQL 讀取。`,transfersComplete:`所有檔案傳輸已完成。`,storageDeployment:`儲存空間由 KANVIL_STORAGE_PROVIDER 和 S3_* 部署變數設定。`,locations:`現有儲存位置`,locationFiles_other:`{{count, number}} 個檔案`,cleanUp:`清除未使用的檔案`,cleanUpConfirm:`要移除未被參照的檔案和超過一天的失敗上傳嗎?附加至工單或個人資料的檔案會保留。`,cleanUpNote:`清除作業也會自動執行。S3 物件需要在備份資料庫的同時備份儲存貯體。`,searchProvider:`搜尋提供者`,searchBuiltIn:`內建 PostgreSQL 搜尋`,indexName:`Kanvil 索引名稱`,searchAuthentication:`搜尋驗證`,noAuthentication:`不驗證`,apiKey:`API 金鑰`,basic:`使用者名稱和密碼`,searchApiKey:`搜尋 API 金鑰`,optional:`選用`,searchUsername:`搜尋使用者名稱`,searchPassword:`搜尋密碼`,searchVerified:`搜尋連線已驗證`,testSearch:`測試搜尋連線`,rebuildQueued:`搜尋重建已加入佇列`,rebuild:`重建索引`,searchNote:`權限和篩選一律以 PostgreSQL 為準。外部索引同步中或無法使用時,搜尋會改用 PostgreSQL。`,brokerProvider:`事件傳遞提供者`,brokerBuiltIn:`內建 PostgreSQL 通知`,rabbitUrl:`RabbitMQ 連線 URL`,savedBrokerEndpoint:`已儲存的訊息代理程式端點`,rabbitExchange:`RabbitMQ 交換器`,brokerVerified:`RabbitMQ 傳遞已驗證`,testBroker:`測試訊息代理程式連線`,brokerNote:`RabbitMQ 會將已認可事件的喚醒訊號分送給各 Kanvil 執行個體,並喚醒背景工作。PostgreSQL 保存持久事件和待處理工作,並在訊息代理程式無法使用時傳遞喚醒訊號。`,brokerDeployment:`事件傳遞由 KANVIL_BROKER_PROVIDER 和 RABBITMQ_* 部署變數設定。`,save:`儲存執行個體設定`},authentication:{title:`驗證`,saved:`驗證設定已儲存`,emailPassword:`電子郵件和密碼登入`,emailPasswordDescription:`包含電子郵件登入驗證碼和密碼重設。`,ssoProvision:`透過 SSO 自動建立使用者`,ssoProvisionDescription:`新的 SSO 使用者在受邀前沒有任何工作區或專案存取權。`,registration:`公開電子郵件註冊`,registrationClosed:`已關閉`,identityProvider:`身分識別提供者`,notConfigured:`未設定`,linkedProviders:`已連結帳號的提供者`,linkedProvidersDescription:`僅允許已連結的帳號登入;絕不建立帳號。`,save:`儲存驗證設定`,discoveryUrl:`OIDC 探索 URL`,clientId:`OIDC 用戶端 ID`,oidcSetup:`請在部署中設定 OIDC_DISCOVERY_URL、OIDC_CLIENT_ID、OIDC_CLIENT_SECRET 和 OIDC_NAME,或 GITHUB_、GOOGLE_ 或 DISCORD_ 的 CLIENT_ID 與 CLIENT_SECRET 組合,然後重新啟動 Kanvil。提供者密鑰絕不會傳回瀏覽器。`,consumers:`取用者介面`,mcpEndpoint:`MCP 端點`,eventFeed:`即時事件串流`,openApi:`OpenAPI 規格`},users:{title:`使用者`,search:`搜尋使用者`,searchPlaceholder:`名稱或電子郵件…`,new:`新增使用者`,roleFilter:`執行個體角色篩選`,allRoles:`所有角色`,administrators:`管理員`,users:`使用者`,statusFilter:`帳號狀態篩選`,allAccounts:`所有帳號`,active:`使用中`,disabled:`已停用`,noMatches:`沒有相符的使用者`,empty:`尚無使用者`,user:`使用者`,role:`角色`,status:`狀態`,joined:`加入時間`,actions:`動作`,administrator:`管理員`,manage:`管理 {{name}}`,pagerNoun:`使用者`,create:`建立帳號`,creating:`建立中…`,created:`已建立 {{email}}`,name:`名稱`,email:`電子郵件`,initialPassword:`初始密碼`,instanceRole:`執行個體角色`},account:{details:`帳號詳細資料`,joined:`加入於 {{date}}`,edit:`編輯帳號`,instanceRole:`執行個體角色`,adminDescription:`可以管理所有工作區、帳號和部署設定。`,userDescription:`存取權由工作區和專案角色決定。`,changeRole:`變更角色`,changeRoleTitle:`變更執行個體角色`,signInMethods:`登入方式`,setPassword:`設定本機密碼`,activeSessions:`使用中的工作階段`,sessions_other:`{{count, number}} 個工作階段`,revokeSessions:`撤銷工作階段`,revokeConfirm:`要將 {{name}} 從所有裝置登出嗎?API 金鑰仍會有效。`,sessionsRevoked:`工作階段已撤銷`,access:`帳號存取權`,accessDisabled:`登入和 API 存取已停用。`,accessPreserved:`停用帳號會保留工單和活動歷程。`,enable:`啟用帳號`,disable:`停用帳號`,enableConfirm:`要恢復 {{name}} 的登入嗎?`,disableConfirm:`要停用 {{name}} 的登入和 API 存取,並撤銷其工作階段嗎?`,enabled:`帳號已啟用`,disabledToast:`帳號已停用`,updateFailed:`無法更新帳號`,workspaces:`工作區`,addToWorkspace:`新增至工作區`,noWorkspaces:`沒有工作區成員資格`,projectAccess:`直接專案存取權`,updated:`帳號已更新`,roleUpdated:`執行個體角色已更新`,workspaceAdded:`已新增工作區存取權`,passwordMismatch:`兩次輸入的新密碼不一致。`,passwordSet:`已設定本機密碼並撤銷工作階段`,name:`名稱`,email:`電子郵件`,user:`使用者`,administrator:`管理員`,adminsNote:`管理員可以管理所有工作區、使用者和執行個體設定。`,workspace:`工作區`,workspaceRole:`工作區角色`,newPassword:`新密碼`,confirmPassword:`確認新密碼`,passwordNote:`這會設定本機密碼,並將該使用者從所有裝置登出。`,save:`儲存變更`},delivery:{field:{address:`電子郵件地址`,server:`伺服器`,topic:`主題`,accessToken:`存取權杖`,applicationToken:`應用程式權杖`,endpointUrl:`端點 URL`,signingSecret:`簽署密鑰`,incomingWebhookUrl:`傳入 Webhook URL`,webhookUrl:`Webhook URL`,channel:`頻道`,botToken:`機器人權杖`,chatId:`聊天 ID`,chatIdPlaceholder:`-1001234567890 或 @channel`,topicId:`話題 ID`,optional:`{{label}}(選用)`,removeSaved:`移除已儲存的{{label}}`,savedSecret:`已儲存 · 留空即可保留`},failure:{blockedAddress:`不允許使用此地址`,redirectBlocked:`端點進行了重新導向`,dnsError:`找不到主機`,networkError:`無法連線`,timeout:`逾時`,invalidEndpoint:`端點無效`,emailUnavailable:`此伺服器未設定電子郵件`,smtpError:`郵件伺服器無法使用`,accessRevoked:`連線它的管理員已失去存取權;請重新連線`,repositoryMismatch:`收到來自其他儲存庫的傳遞`,http:`遭拒絕,HTTP {{status}}`,smtp:`郵件伺服器回應 {{code}}`},health:{delivery:{done:`<time/>已傳遞`,pending:`傳送中…`,none:`尚未傳送`,failed:`{{reason}}`},access:{done:`<time/>已檢查存取權`,pending:`正在檢查存取權…`,none:`尚未檢查存取權`,failed:`存取權檢查:{{reason}}`},retrying:`重試中`},paused:`已暫停`,failing:`失敗中`,sendTest:`傳送測試至 {{name}}`,testQueued:`測試已加入佇列`,resume:`繼續 {{name}}`,pause:`暫停 {{name}}`,edit:`編輯 {{name}}`,disconnect:`中斷 {{name}} 的連線`,disconnected:`{{name}} 已中斷連線`,name:`名稱`,type:`類型`,channels:`管道`,addChannel:`新增管道`,noChannels:`尚無管道`,editChannel:`編輯管道`,channelSaved:`管道已儲存`,channelRemoval:`要中斷 {{name}} 的連線嗎?其設定和佇列中的通知將被捨棄。`,sendFrom:`通知來源`,routeOff:`無`,routeAll:`所有專案`,routeSelected:`選取的專案`,saveChannel:`儲存管道`,adminsOnly:`由專案管理員管理整合`,destinations:`目的地`,addDestination:`新增目的地`,noDestinations:`尚無目的地`,editDestination:`編輯目的地`,destinationSaved:`目的地已儲存`,destinationRemoval:`要中斷 {{name}} 的連線嗎?其設定和佇列中的訊息將被捨棄。`,send:`傳送`,saveDestination:`儲存目的地`},repository:{setup:{github:`權杖需要儲存庫讀取權限;若要留言或同步議題,還需要議題和提取要求的寫入權限。請新增一個 GitHub Webhook:URL 從此儲存庫所在的列複製,內容類型為 application/json,使用此密鑰,並勾選 Branch or tag creation、Branch or tag deletion、Issues、Issue comments 和 Pull requests 事件。`,gitea:`權杖需要 read:repository 範圍;若要留言或同步議題,還需要 write:issue。請新增一個 Gitea Webhook:URL 從此儲存庫所在的列複製,內容類型為 application/json,使用此密鑰,並勾選 Create、Delete、Issues、Issue Label、Issue Comment 和 Pull Request 事件。`,gitlab:`權杖需要 read_api 範圍;若要留言或同步議題,還需要 api。請新增一個 GitLab Webhook:URL 從此儲存庫所在的列複製,使用此密鑰權杖,並勾選 Push、Issues、Comments 和 Merge request 事件。`},noWebhook:`尚未收到 Webhook`,webhookReceived:`<time/>收到 Webhook`,saved:`儲存庫已儲存`,title:`儲存庫`,connect:`連線儲存庫`,empty:`尚無儲存庫`,checkAccess:`檢查對 {{repository}} 的存取權`,accessQueued:`存取權檢查已加入佇列`,copyWebhook:`複製 {{repository}} 的 Webhook URL`,issueSync:`{{repository}} 的議題同步`,removal:`要中斷 {{repository}} 的連線嗎?其 Webhook 將立即失效。它新增的連結會保留在工單上。`,edit:`編輯儲存庫`,host:`代管平台`,serverUrl:`伺服器 URL`,serverUrlOptional:`伺服器 URL(選用)`,repository:`{{host}} 儲存庫`,nestedPlaceholder:`group/project`,placeholder:`owner/repository`,accessToken:`存取權杖`,webhookSecret:`Webhook 密鑰`,savedSecret:`已儲存 · 留空即可保留`,commentOnLink:{pull_request:`在新連結的議題和提取要求上留言`,merge_request:`在新連結的議題和合併要求上留言`},moveTickets:`移動連結的工單`,noMove:`不移動`,save:`儲存儲存庫`},issueSync:{reason:{labels:`其同步標籤已變更`,conflict:`雙方都有變更`,changedWhilePaused:`儲存庫暫停期間議題有所變更`,unverified:`代管平台的回應未確認此變更`,claimConflict:`工單已被認領`,invalidContent:`議題超過工單允許的長度`},issuesSyncing_other:`{{count, number}} 個議題同步中`,linksStopped_other:`{{count, number}} 個連結已停止`,match:`比對`,matchAny:`具有任一標籤`,matchAll:`具有所有標籤`,preview:{matching:`繼續同步`,export:`要建立的新議題`,pause:`要暫停的連結`,review:`要審查的連結`},applied:`議題同步已套用`,title:`議題同步 · {{repository}}`,outbound:`Kanvil 至 {{host}}`,exportTickets:`將工單匯出為議題`,exportDescription:`未完成的工單會產生新議題;標題、說明、狀態和新留言會保持同步。`,ticketLabels:`僅限具有這些標籤的工單`,everyTicket:`所有未完成的工單`,inbound:`{{host}} 至 Kanvil`,importIssues:`將議題匯入為工單`,importDescription:`未關閉的議題會在 {{host}} 下次回報時匯入。`,issueLabels:`僅限具有這些標籤的議題`,everyIssue:`所有未關閉的議題`,previewLabel:`預覽`,previewAction:`預覽`,stopped:`已停止的連結 · {{repository}}`,review:`審查`,linkRemoved:`連結已移除`,resumed:`同步已繼續`,open:`開啟`,closed:`已關閉`,reviewTitle:`審查 {{key}}`,mayExist:`{{host}} 上可能已有此議題;再次建立前請先確認。`,comparison:`比較`,field:{title:`標題`,description:`說明`,state:`狀態`},notReported:`未回報`,unlink:`取消連結`,keepHost:`保留 {{host}} 的版本`,keepKanvil:`保留 Kanvil 的版本`,createAgain:`重新建立議題`,unlinkTicket:`取消連結 {{key}}`,unlinkConfirm:`要停止同步 {{key}} 嗎?工單和議題都會保留。`},import:{count:{tickets:`工單`,workflow:`工作流程狀態`,fields:`自訂欄位`,labels:`標籤`,relations:`關聯`,comments:`留言`,evidence:`佐證`,links:`連結`},notExport:`此檔案不是 Kanvil 工單匯出檔。`,progress_other:`{{status}} · {{done, number}} / {{count, number}} 筆記錄`,progressFailed_other:`{{status}} · {{done, number}} / {{count, number}} 筆記錄 · {{failed, number}} 筆失敗`,title:`匯入`,description:`從 Kanvil 工單匯出檔或其他追蹤工具(Linear、Jira、Trello、Plane、PLANKA 或 Kaneo)建立專案。`,action:`匯入專案`,detailsOf:`{{name}} 的詳細資料`,details:`詳細資料`,exportFile:`Kanvil 匯出檔`,source:`來源`,file:`匯出檔`,jiraEdition:`Jira 版本`,siteUrl:`網站 URL`,accountEmail:`帳號電子郵件`,apiKey:`API 金鑰`,apiToken:`API 權杖`,personalToken:`個人存取權杖`,token:`權杖`,apiUrl:`API URL`,workspaceSlug:`工作區 URL 代碼`,signInWith:`登入方式`,username:`使用者名稱或電子郵件`,password:`密碼`,checkConnection:`請檢查連線資訊`,noProjects:`這些憑證無法讀取任何{{label}}。`,includeComments:`包含留言`,projectName:`專案名稱`,start:`匯入`,connect:`連線`,preview:`預覽`,starting:`啟動中…`,connecting:`連線中…`,checking:`檢查中…`,matched:`已配對的人員`,unmatched:`未配對的人員`,previewLabel:`匯入預覽`,errorPrefix:`錯誤:`,notePrefix:`注意:`,progressLabel:`匯入進度`,notImported:`未匯入的記錄`,stop:`停止匯入`,openProject:`開啟專案`}},native:{labelValue:`{{label}}:{{value}}`,ui:{dismiss:`關閉`,loading:`載入中`},server:{url:`伺服器 URL`,connect:`連線`,change:`變更伺服器`,invalidOrigin:`請使用 Kanvil 伺服器的 HTTP 或 HTTPS 來源,不要包含路徑或憑證。`,httpsRequired:`請使用 HTTPS 連線至 Kanvil 伺服器。`},login:{setupTitle:`建立擁有者帳號`,signIn:`登入`,signingIn:`正在登入…`,createAccount:`建立帳號`,signInWith:`使用 {{provider}} 登入`,name:`名稱`,setupToken:`設定權杖`,email:`電子郵件`,password:`密碼`},connection:{connecting:`連線中`,connected:`● 即時`,offline:`離線 · 快取資料`,closed:`已關閉`,signedOut:`登入以重新連線`},screen:{projects:`專案`,inbox:`收件匣`,settings:`設定`,tickets:`工單`,ticket:`工單`,newTicket:`新增工單`,editTicket:`編輯工單`},projects:{empty:`尚無專案`,new:`新增專案`,name:`專案名稱`,prefix:`工單前綴`,description:`說明`,create:`建立專案`},tickets:{search:`搜尋工單`,searchPlaceholder:`標題、說明或編號`,allStatuses:`所有狀態`,empty:`沒有相符的工單`,count:`{{shown}} / {{total}}`},ticket:{priority:`{{priority}}優先順序`,noDifficulty:`未指定難度`,version:`v{{version}}`,starts:`{{date}} 開始`,due:`{{date}} 到期`},editor:{stale:`此工單已變更。您儲存的草稿仍以版本 {{version}} 為基礎。`,discardDraft:`捨棄草稿並重新載入`,title:`標題`,noDifficulty:`未指定`,labels:`標籤(以逗號分隔)`,scopes:`範圍(以逗號分隔)`,editMarkdown:`編輯 Markdown`,previewMarkdown:`預覽 Markdown`,description:`說明(Markdown)`,acceptanceCriteria:`驗收條件(Markdown)`,parent:`父工單 ID`,automationReady:`在需求和相依性允許時可供代理程式處理`,holdReason:`暫緩原因`,draftSaved:`草稿已儲存在此裝置上`,save:`儲存工單`},detail:{archived:`已封存`,subscribe:`訂閱`,unsubscribe:`取消訂閱`,share:`分享`,description:`說明`,noDescription:`_尚無說明_`,acceptanceCriteria:`驗收條件`,noAcceptanceCriteria:`_尚無驗收條件_`,fields:`欄位`,archive:{action:`封存工單`,title:`要封存工單嗎?`,message:`要將此工單從使用中的清單移除,並取消代理程式處理資格嗎?`},unarchive:{action:`取消封存工單`,title:`要取消封存工單嗎?`,message:`要將此工單還原至使用中的清單嗎?`}},readiness:{title:`代理程式就緒狀態`,ready:`可以認領`,notReady:`尚不可認領`,hold:`暫緩:{{reason}}`,claimed:`已由 {{worker}} 認領,直到 {{time}}。`,release:{action:`釋出我的認領`,title:`要釋出認領嗎?`,message:`釋出擁有權前,請先停止您的執行者。`,confirm:`釋出`},recover:{action:`收回已過期的認領`,explanation:`請先確認執行者已停止。`,reason:`收回原因`,confirm:`執行者已停止 · 收回`}},relations:{title:`關聯`,parent:`父工單 · {{key}} · {{title}}`,openParent:`開啟父工單`,children:`子工單`,previousChildren:`上一組子工單`,nextChildren:`下一組子工單`,remove:`移除關聯`,removeTitle:`要移除關聯嗎?`,add:`新增關聯`,find:`尋找工單`,type:`關聯類型`,ticket:`工單`,choose:`選擇工單`},discussion:{title:`討論`,previous:`較早的留言`,next:`較新的留言`,field:`留言(Markdown)`,submit:`留言`,you:`您`,edit:`編輯留言`,editField:`留言`,delete:`刪除留言`,deleteTitle:`要刪除留言嗎?`,deleteMessage:`要永久移除此留言嗎?`,save:`儲存留言`},attachments:{title:`附件`,item:`{{name}} · {{size}} kB`,remove:`移除 {{name}}`,removeTitle:`要移除附件嗎?`,working:`處理中…`,add:`新增附件`,tooLarge:`請選擇不超過 10 MB 的檔案。`,sharingUnavailable:`此裝置無法分享檔案。`},evidence:{title:`佐證`,explanation:`結果由提交的帳號回報。`,add:`新增佐證`,name:`標題`,url:`URL`,outcome:`回報的結果`,commit:`提交(完整雜湊)`,note:`備註`,save:`儲存佐證`},activity:{title:`活動`,previous:`較早的活動`,next:`較新的活動`},inbox:{markAllRead:`全部標示為已讀`,empty:`沒有通知`},notifications:{title:`通知`,inApp:`在收件匣接收已訂閱工單的更新`,push:`推播通知`,enableDevice:`在此裝置上啟用推播`,disableDevice:`在此裝置上停用推播`,unconfigured:`此版本需要 Expo 專案 ID 和簽署設定才能推播。`,noProjectId:`此版本需要 Expo 專案 ID 才能接收推播通知。`,permissionDenied:`請在裝置設定中允許通知以啟用推播。`,channel:`工單更新`},settings:{password:{title:`本機密碼`,current:`目前的密碼`,new:`新密碼`,change:`變更密碼`,updated:`密碼已更新;其他工作階段已撤銷`},oidc:{linked:`帳號已連結`,explanation:`使用此提供者登入前,請先連結此帳號。`,link:`連結 {{provider}}`},signOut:{action:`登出`,title:`要登出嗎?`,message:`此帳號的快取工單和草稿將從此裝置中移除。`},changeServer:{title:`要變更伺服器嗎?`,message:`要登出並清除此帳號的本機快取和草稿嗎?`}}}};export{e as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as e,l as t,o as n,s as r}from"./en-US-DKYSujv3.js";import{Dt as i}from"./index-
|
|
1
|
+
import{c as e,l as t,o as n,s as r}from"./en-US-DKYSujv3.js";import{Dt as i}from"./index-Ds3IHeIi.js";var a={lessThanXSeconds:{one:`少於 1 秒`,other:`少於 {{count}} 秒`},xSeconds:{one:`1 秒`,other:`{{count}} 秒`},halfAMinute:`半分鐘`,lessThanXMinutes:{one:`少於 1 分鐘`,other:`少於 {{count}} 分鐘`},xMinutes:{one:`1 分鐘`,other:`{{count}} 分鐘`},xHours:{one:`1 小時`,other:`{{count}} 小時`},aboutXHours:{one:`大約 1 小時`,other:`大約 {{count}} 小時`},xDays:{one:`1 天`,other:`{{count}} 天`},aboutXWeeks:{one:`大約 1 個星期`,other:`大約 {{count}} 個星期`},xWeeks:{one:`1 個星期`,other:`{{count}} 個星期`},aboutXMonths:{one:`大約 1 個月`,other:`大約 {{count}} 個月`},xMonths:{one:`1 個月`,other:`{{count}} 個月`},aboutXYears:{one:`大約 1 年`,other:`大約 {{count}} 年`},xYears:{one:`1 年`,other:`{{count}} 年`},overXYears:{one:`超過 1 年`,other:`超過 {{count}} 年`},almostXYears:{one:`將近 1 年`,other:`將近 {{count}} 年`}},o=(e,t,n)=>{let r,i=a[e];return r=typeof i==`string`?i:t===1?i.one:i.other.replace(`{{count}}`,String(t)),n?.addSuffix?n.comparison&&n.comparison>0?r+`內`:r+`前`:r},s={date:t({formats:{full:`y'年'M'月'd'日' EEEE`,long:`y'年'M'月'd'日'`,medium:`yyyy-MM-dd`,short:`yy-MM-dd`},defaultWidth:`full`}),time:t({formats:{full:`zzzz a h:mm:ss`,long:`z a h:mm:ss`,medium:`a h:mm:ss`,short:`a h:mm`},defaultWidth:`full`}),dateTime:t({formats:{full:`{{date}} {{time}}`,long:`{{date}} {{time}}`,medium:`{{date}} {{time}}`,short:`{{date}} {{time}}`},defaultWidth:`full`})},c={lastWeek:`'上個'eeee p`,yesterday:`'昨天' p`,today:`'今天' p`,tomorrow:`'明天' p`,nextWeek:`'下個'eeee p`,other:`P`},l={code:`zh-TW`,formatDistance:o,formatLong:s,formatRelative:(e,t,n,r)=>c[e],localize:{ordinalNumber:(e,t)=>{let n=Number(e);switch(t?.unit){case`date`:return n+`日`;case`hour`:return n+`時`;case`minute`:return n+`分`;case`second`:return n+`秒`;default:return`第 `+n}},era:e({values:{narrow:[`前`,`公元`],abbreviated:[`前`,`公元`],wide:[`公元前`,`公元`]},defaultWidth:`wide`}),quarter:e({values:{narrow:[`1`,`2`,`3`,`4`],abbreviated:[`第一刻`,`第二刻`,`第三刻`,`第四刻`],wide:[`第一刻鐘`,`第二刻鐘`,`第三刻鐘`,`第四刻鐘`]},defaultWidth:`wide`,argumentCallback:e=>e-1}),month:e({values:{narrow:[`一`,`二`,`三`,`四`,`五`,`六`,`七`,`八`,`九`,`十`,`十一`,`十二`],abbreviated:[`1月`,`2月`,`3月`,`4月`,`5月`,`6月`,`7月`,`8月`,`9月`,`10月`,`11月`,`12月`],wide:[`一月`,`二月`,`三月`,`四月`,`五月`,`六月`,`七月`,`八月`,`九月`,`十月`,`十一月`,`十二月`]},defaultWidth:`wide`}),day:e({values:{narrow:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],short:[`日`,`一`,`二`,`三`,`四`,`五`,`六`],abbreviated:[`週日`,`週一`,`週二`,`週三`,`週四`,`週五`,`週六`],wide:[`星期日`,`星期一`,`星期二`,`星期三`,`星期四`,`星期五`,`星期六`]},defaultWidth:`wide`}),dayPeriod:e({values:{narrow:{am:`上`,pm:`下`,midnight:`凌晨`,noon:`午`,morning:`早`,afternoon:`下午`,evening:`晚`,night:`夜`},abbreviated:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜間`},wide:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜間`}},defaultWidth:`wide`,formattingValues:{narrow:{am:`上`,pm:`下`,midnight:`凌晨`,noon:`午`,morning:`早`,afternoon:`下午`,evening:`晚`,night:`夜`},abbreviated:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜間`},wide:{am:`上午`,pm:`下午`,midnight:`凌晨`,noon:`中午`,morning:`早晨`,afternoon:`中午`,evening:`晚上`,night:`夜間`}},defaultFormattingWidth:`wide`})},match:{ordinalNumber:n({matchPattern:/^(第\s*)?\d+(日|時|分|秒)?/i,parsePattern:/\d+/i,valueCallback:e=>parseInt(e,10)}),era:r({matchPatterns:{narrow:/^(前)/i,abbreviated:/^(前)/i,wide:/^(公元前|公元)/i},defaultMatchWidth:`wide`,parsePatterns:{any:[/^(前)/i,/^(公元)/i]},defaultParseWidth:`any`}),quarter:r({matchPatterns:{narrow:/^[1234]/i,abbreviated:/^第[一二三四]刻/i,wide:/^第[一二三四]刻鐘/i},defaultMatchWidth:`wide`,parsePatterns:{any:[/(1|一)/i,/(2|二)/i,/(3|三)/i,/(4|四)/i]},defaultParseWidth:`any`,valueCallback:e=>e+1}),month:r({matchPatterns:{narrow:/^(一|二|三|四|五|六|七|八|九|十[二一]?)/i,abbreviated:/^(一|二|三|四|五|六|七|八|九|十[二一]?|\d|1[0-2])月/i,wide:/^(一|二|三|四|五|六|七|八|九|十[二一]?)月/i},defaultMatchWidth:`wide`,parsePatterns:{narrow:[/^一/i,/^二/i,/^三/i,/^四/i,/^五/i,/^六/i,/^七/i,/^八/i,/^九/i,/^十(?!(一|二))/i,/^十一/i,/^十二/i],any:[/^(一|1(?!\d))/i,/^(二|2)/i,/^(三|3)/i,/^(四|4)/i,/^(五|5)/i,/^(六|6)/i,/^(七|7)/i,/^(八|8)/i,/^(九|9)/i,/^(十(?!(一|二))|10)/i,/^(十一|11)/i,/^(十二|12)/i]},defaultParseWidth:`any`}),day:r({matchPatterns:{narrow:/^[一二三四五六日]/i,short:/^[一二三四五六日]/i,abbreviated:/^週[一二三四五六日]/i,wide:/^星期[一二三四五六日]/i},defaultMatchWidth:`wide`,parsePatterns:{any:[/日/i,/一/i,/二/i,/三/i,/四/i,/五/i,/六/i]},defaultParseWidth:`any`}),dayPeriod:r({matchPatterns:{any:/^(上午?|下午?|午夜|[中正]午|早上?|下午|晚上?|凌晨)/i},defaultMatchWidth:`any`,parsePatterns:{any:{am:/^上午?/i,pm:/^下午?/i,midnight:/^午夜/i,noon:/^[中正]午/i,morning:/^早上/i,afternoon:/^下午/i,evening:/^晚上?/i,night:/^凌晨/i}},defaultParseWidth:`any`})},options:{weekStartsOn:1,firstWeekContainsDate:4},labels:{labelDayButton:(e,t,n,r)=>{let a=(r??new i(n)).format(e,`PPPP`);return t.today&&(a=`今天,${a}`),t.selected&&(a=`${a},已選取`),a},labelMonthDropdown:`選擇月份`,labelNext:`前往下個月`,labelPrevious:`前往上個月`,labelWeekNumber:e=>`第 ${e} 週`,labelYearDropdown:`選擇年份`,labelGrid:(e,t,n)=>(n??new i(t)).formatMonthYear(e),labelGridcell:(e,t,n,r)=>{let a=(r??new i(n)).format(e,`PPPP`);return t?.today&&(a=`今天,${a}`),a},labelNav:`導覽列`,labelWeekNumberHeader:`週數`,labelWeekday:(e,t,n)=>(n??new i(t)).format(e,`cccc`)}};export{l as zhTW};
|
package/apps/web/dist/index.html
CHANGED
|
@@ -20,20 +20,20 @@
|
|
|
20
20
|
<meta name="twitter:image" content="/brand/social.png" />
|
|
21
21
|
<meta name="description" content="Kanvil — self-hosted ticketing for people and agents." />
|
|
22
22
|
<title>Kanvil</title>
|
|
23
|
-
<script type="module" crossorigin src="/assets/index-
|
|
23
|
+
<script type="module" crossorigin src="/assets/index-Ds3IHeIi.js"></script>
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-COiEfhsA.js">
|
|
25
25
|
<link rel="modulepreload" crossorigin href="/assets/core-jXc03Sty.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/en-EnSRisMz.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/preload-helper-BLdHbfdN.js">
|
|
28
|
-
<link rel="modulepreload" crossorigin href="/assets/default-
|
|
28
|
+
<link rel="modulepreload" crossorigin href="/assets/default-Dnep8Yat.js">
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/toDate-B3HYxwtT.js">
|
|
30
|
-
<link rel="modulepreload" crossorigin href="/assets/search-
|
|
31
|
-
<link rel="modulepreload" crossorigin href="/assets/item-
|
|
32
|
-
<link rel="modulepreload" crossorigin href="/assets/Trans-
|
|
33
|
-
<link rel="modulepreload" crossorigin href="/assets/forms
|
|
34
|
-
<link rel="modulepreload" crossorigin href="/assets/CompositeItem-
|
|
30
|
+
<link rel="modulepreload" crossorigin href="/assets/search-DwsPEC4E.js">
|
|
31
|
+
<link rel="modulepreload" crossorigin href="/assets/item-BD1Poxfs.js">
|
|
32
|
+
<link rel="modulepreload" crossorigin href="/assets/Trans-BgU0eWAX.js">
|
|
33
|
+
<link rel="modulepreload" crossorigin href="/assets/forms--87R7VQW.js">
|
|
34
|
+
<link rel="modulepreload" crossorigin href="/assets/CompositeItem-Tueq2VKR.js">
|
|
35
35
|
<link rel="modulepreload" crossorigin href="/assets/en-US-DKYSujv3.js">
|
|
36
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
36
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bn-MADyj.css">
|
|
37
37
|
</head>
|
|
38
38
|
<body>
|
|
39
39
|
<div id="root"></div>
|
package/docs/api.md
CHANGED
|
@@ -4,11 +4,19 @@ The HTTP API, MCP tools, and apps execute the same ticket operations and permiss
|
|
|
4
4
|
|
|
5
5
|
Runnable MCP and durable realtime examples, plus the worker ownership sequence, are in [examples](../examples/README.md).
|
|
6
6
|
|
|
7
|
+
The typed client's `api.call` and `api.callPublic` accept an optional `AbortSignal` as their third argument. Its query helpers pass React Query cancellation to the transport, so obsolete list reads stop downloading and aborted reads are not retried.
|
|
8
|
+
|
|
7
9
|
## Authentication
|
|
8
10
|
|
|
9
11
|
Browser and native clients use Better Auth sessions with local login or configured OIDC. Public registration is closed. Initial owner setup requires `KANVIL_SETUP_TOKEN` and is available only before any account exists.
|
|
10
12
|
|
|
11
|
-
Unattended consumers use revocable API credentials in `Authorization: Bearer <key>`. Create credentials through an authenticated account at `POST /api/v1/credentials`, with a name, `projectIds`, `read`, `write`, `claim`, or `
|
|
13
|
+
Unattended consumers use revocable API credentials in `Authorization: Bearer <key>`. Create credentials through an authenticated account at `POST /api/v1/credentials`, with a name, `projectIds`, `read`, `write`, `claim`, `admin`, or `intake` scopes, and an optional `expiresIn` in seconds (one hour to one year). An omitted `expiresIn` expires the key after 90 days; `null` creates a key that never expires. Pass a nonempty `projectIds` array for a project-scoped key, or explicit `projectIds: null` for a global key. Omitted or empty project lists are invalid. Global keys follow the owning account's current project and workspace access, including future grants; existing project-scoped keys retain their allowlists. Both key types obey operation scopes and current account roles, and neither can perform actions that require a signed-in session, including credential management and instance administration. A scope never grants more access than the owning account has. The secret is returned only on creation. Delete a credential at `/api/v1/credentials/{id}` to revoke it.
|
|
14
|
+
|
|
15
|
+
The `intake` scope is create-only and cannot be combined with another scope (`400 validation_error`). It allows only `create_ticket` over HTTP or MCP in projects the credential and its owning account can write. It grants no read access, including ticket lookup, search, snapshots, events, or attachment downloads, and cannot update, delete, move, comment on, or attach files to tickets. Other authenticated operations return `403 insufficient_scope`.
|
|
16
|
+
|
|
17
|
+
Intake creation returns only the new ticket's normal payload. The server forces `automationReady: false`, `holdReason: "Untrusted intake: human triage required"`, the first To do state, no assignee or parent, `archived: false`, no dates, empty custom fields (including defaults and required fields), and no uploads. It preserves the supplied title, description, requirements, kind, difficulty, priority, labels, and scopes, and adds the `intake` label. The 30-label limit includes that label. Reusing the same `requestId` returns the original ticket without creating another. A human session or a credential with ordinary write access can triage it later and enable automation. Eligibility inspection includes its explicit not-ready reasons until triage clears the hold and enables automation.
|
|
18
|
+
|
|
19
|
+
Repository issue sync uses the server-forced intake hold as a quarantine marker: neither export planning nor event-driven export includes the ticket until a trusted writer clears that hold. Already queued issue-sync deliveries recheck the hold before contacting the repository. Ordinary tickets without automation readiness still follow their configured sync rules.
|
|
12
20
|
|
|
13
21
|
## Writes and ownership
|
|
14
22
|
|
|
@@ -16,9 +24,9 @@ Use the ticket's current `version` as `expectedVersion` for updates. A stale ver
|
|
|
16
24
|
|
|
17
25
|
Reuse the same UUID `requestId` when retrying a write. Reusing it with a different operation or payload returns `409 idempotency_conflict`. Bulk edits either all commit or all roll back, and publish one `ticket.updated` event per ticket with consecutive IDs in a single batch.
|
|
18
26
|
|
|
19
|
-
Signed-in sessions may make `KANVIL_SESSION_WRITE_LIMIT` write requests per minute (120 by default) to `/api/v1`, and MCP OAuth clients get the same budget for write tools; exceeding it returns `429 rate_limited` with a `Retry-After` header. This counter is per application process. API keys have a separate limit of 600 requests per minute per key across HTTP and MCP. An instance operator can set `KANVIL_API_KEY_RATE_LIMIT_ENABLED=false` and restart the server to disable that limit for existing and new keys. The default is `true`; restoring it enforces the limit again. Session, MCP OAuth, and sign-in limits are unchanged, as are key expiration, revocation, scopes, and account permissions. Authentication rereads each key and skips timestamp-only bookkeeping writes. When a key is unlimited, authentication leaves its stored `last_request` and `updated_at` unchanged; enabled rate limits and usage quotas still consume their allowance synchronously and atomically.
|
|
27
|
+
Signed-in sessions may make `KANVIL_SESSION_WRITE_LIMIT` write requests per minute (120 by default) to `/api/v1`, and MCP OAuth clients get the same budget for write tools; exceeding it returns `429 rate_limited` with a `Retry-After` header. This counter is per application process. API keys have a separate limit of 600 requests per minute per key across HTTP and MCP. An instance operator can set `KANVIL_API_KEY_RATE_LIMIT_ENABLED=false` and restart the server to disable that limit for existing and new keys. The default is `true`; restoring it enforces the limit again. Session, MCP OAuth, and sign-in limits are unchanged, as are key expiration, revocation, scopes, and account permissions. Authentication rereads each key and skips timestamp-only bookkeeping writes. When a key is unlimited, authentication leaves its stored `last_request` and `updated_at` unchanged; enabled rate limits and usage quotas still consume their allowance synchronously and atomically. A request that consumed a key's last use is admitted even when a concurrent request finds the key exhausted and deletes it first.
|
|
20
28
|
|
|
21
|
-
Claims require an eligible ticket and current version. Successful claiming moves it to In progress. Renew an unexpired claim using its ID and the same credential. Once a ticket has been claimed, machine ticket updates and evidence submissions require a current matching claim ID, including after release. Recovery without `reschedule` lifts that requirement and increments the ticket version, publishing `ticket.updated` with `changedFields: ["claimRequired"]`, so a ticket left outside To do (for example, In review after its work merged) accepts ordinary worker writes again; a write that still names the recovered claim ID returns `409 claim_conflict`. Submit completion and evidence before releasing ownership. Human sessions can edit and reschedule work without a machine claim. Release unexpired ownership after the worker stops or completes. The ticket response exposes `canReleaseClaim` for the exact credential or client identity; sharing an account does not confer ownership. An expired claim remains blocking until a caller with `claim` scope and `ticket.write` permission confirms the old worker stopped and explicitly calls `recover_claim` with its ID, a reason, and `confirmedStopped: true`. Administrator access is not required for expired claims. If a worker has stopped while its claim is still live, for example after revoking its credential, a caller with `project.manage` and `admin` scope can explicitly pass `cancelActive: true` to cancel it before expiry. This still requires `confirmedStopped: true` and a reason, and the recovery event records the cancellation. Without that flag, live claims remain protected. Recovery does not automatically reschedule work or grant a replacement claim. To resume without a human session, explicitly pass `reschedule: true` and the ticket's current `expectedVersion` to `recover_claim`; this returns it to To do and increments its version atomically with recovery. Then call `get_ticket` and `claim_ticket` with the new version for a fresh claim ID. A missing version returns `400 validation_error`, and a stale one returns `409 version_conflict`, leaving the claim and status unchanged. Initial claims and renewals accept `leaseSeconds` of at least 30, bounded by `KANVIL_CLAIM_MAX_LEASE_SECONDS`, which defaults to 86400 (24 hours). The default lease remains 300 seconds, or the configured maximum if smaller. A request above the configured maximum returns `400 validation_error`; renew before expiry to keep ownership.
|
|
29
|
+
Claims require an eligible ticket and current version. Successful claiming moves it to In progress. Renew an unexpired claim using its ID and the same credential. Once a ticket has been claimed, machine ticket updates and evidence submissions require a current matching claim ID, including after release. Recovery without `reschedule` lifts that requirement and increments the ticket version, publishing `ticket.updated` with `changedFields: ["claimRequired"]`, so a ticket left outside To do (for example, In review after its work merged) accepts ordinary worker writes again; a write that still names the recovered claim ID returns `409 claim_conflict`. Submit completion and evidence before releasing ownership. Human sessions can edit and reschedule work without a machine claim. Release unexpired ownership after the worker stops or completes. The ticket response exposes `canReleaseClaim` for the exact credential or client identity; sharing an account does not confer ownership. An expired claim remains blocking until a caller with `claim` scope and `ticket.write` permission confirms the old worker stopped and explicitly calls `recover_claim` with its ID, a reason, and `confirmedStopped: true`. Administrator access is not required for expired claims. If a worker has stopped while its claim is still live, for example after revoking its credential, a caller with `project.manage` and `admin` scope can explicitly pass `cancelActive: true` to cancel it before expiry. This still requires `confirmedStopped: true` and a reason, and the recovery event records the cancellation. Without that flag, live claims remain protected. Recovery does not automatically reschedule work or grant a replacement claim. To resume without a human session, explicitly pass `reschedule: true` and the ticket's current `expectedVersion` to `recover_claim`; this returns it to To do and increments its version atomically with recovery. Then call `get_ticket` and `claim_ticket` with the new version for a fresh claim ID. A missing version returns `400 validation_error`, and a stale one returns `409 version_conflict`, leaving the claim and status unchanged. A ticket that still requires ownership but has no claim left, for example one released outside To do or recovered by an older server that kept the requirement, is recovered the same way without a `claimId`: pass a reason, `confirmedStopped: true`, and the ticket's current `expectedVersion`, which this form requires. Without `reschedule` it lifts the requirement so ordinary worker writes pass again; with `reschedule: true` it returns the ticket to To do for a fresh claim. Both increment the version, and the `claim.recovered` event records `claimId: null`. If the ticket has any claim, live or expired, this form returns `409 claim_conflict` with the claim in `details.claimId`; if it does not require ownership, it returns `409 claim_not_required`. Neither changes the ticket. Initial claims and renewals accept `leaseSeconds` of at least 30, bounded by `KANVIL_CLAIM_MAX_LEASE_SECONDS`, which defaults to 86400 (24 hours). The default lease remains 300 seconds, or the configured maximum if smaller. A request above the configured maximum returns `400 validation_error`; renew before expiry to keep ownership.
|
|
22
30
|
|
|
23
31
|
Evidence records identify the submitting account and supplied commit. A reported passing outcome is an assertion by that identity, not independent verification by Kanvil.
|
|
24
32
|
|
|
@@ -36,11 +44,15 @@ The property filters `status`, `priority`, `kind`, `difficulty`, `label`, `assig
|
|
|
36
44
|
|
|
37
45
|
`list_my_work` (`GET /api/v1/my-work`) pages through the open, unarchived tickets assigned to the caller across every project they can read, optionally narrowed to one `workspaceId`. It accepts `q`, `sort`, and `order` like `list_tickets`, plus `limit` and `offset`; `total` counts every match so clients can fetch the remaining pages. Set `activity` (up to 50) to also receive that many of the most recent events from the same accessible projects, newest first. Revoked memberships and project-bound credentials narrow the tickets, totals, and activity alike.
|
|
38
46
|
|
|
39
|
-
`search_tickets` (`GET /api/v1/tickets/search?q=...`) finds unarchived tickets by exact key or text across every project the caller can read, optionally limited to one `workspaceId`, returning each ticket with its project `id`, `key`, and `name`. Exact key matches sort first.
|
|
47
|
+
`search_tickets` (`GET /api/v1/tickets/search?q=...`) finds unarchived tickets by exact key or text across every project the caller can read, optionally limited to one `workspaceId`, returning each ticket with its project `id`, `key`, and `name`. Exact key matches sort first, then the most relevant matches (the external index's score, or on PostgreSQL title matches before matches only in other text), then the most recently updated.
|
|
48
|
+
|
|
49
|
+
Ticket text queries in `list_tickets`, `list_my_work`, `search_tickets`, and `search_workspace` treat each double-quoted segment of `q` as an exact phrase: `q="live activity" acknowledge` needs the phrase `live activity` and the term `acknowledge`. Terms outside quotes keep fuzzy matching on the external index, and on PostgreSQL they must appear together as written. An unpaired quote is an ordinary character. Repeated phrases count once, and only the first 8 distinct phrases count.
|
|
50
|
+
|
|
51
|
+
`list_tickets`, `search_tickets`, and `search_workspace` take `view`: `full` (the default) or `summary`. A summary row has only `id`, `key`, `number`, `projectId`, `title`, `status`, `stateId`, `kind`, `difficulty`, `priority`, `labels`, `scopes`, `archived`, `automationReady`, `updatedAt`, and `version`, so agents can scan many results without every description and acceptance criteria. In summary view each `search_tickets` result and `search_workspace` result also carries `snippet`: up to 240 characters of its body text (a ticket's description and acceptance criteria) around the first literal match of a phrase or term, or empty when only the title, key, or a fuzzy term matched; a `search_workspace` result's `ticket` is its summary row.
|
|
40
52
|
|
|
41
|
-
`search_workspace` (`GET /api/v1/search?q=...`) searches projects, unarchived tickets, comments on unarchived tickets, and activity across every project the caller can read, optionally within one `workspaceId`. Each result has a `type` (`project`, `ticket`, `comment`, or `activity`), a stable `id` (the project, ticket, or comment ID, or the event ID), its `project`, its `ticket` (`id`, `key`, `title`, `status`, or `null`), a display `title`, an `excerpt` around the first literal match, `actorId`, and `at`. Activity matches its event label, its ticket key, or the name of an active (not disabled) actor. Results order by exact key match, then type in the order above, then most recent first. `limit` (1–50, default 20) and `offset` (up to 1000) page through them; `type` narrows the page to one kind while `counts` still reports every kind and `total` counts the page's kind. The property filters above restrict results to matching tickets and their comments and activity, and leave out projects. Ticket text uses the external index when one is configured and current (`engine: "index"`); otherwise PostgreSQL answers (`engine: "postgres"`), and `indexing: true` says an index is configured but behind or unavailable. Projects, comments, and activity always come from PostgreSQL, and every result is filtered by current project access before counts and excerpts are computed.
|
|
53
|
+
`search_workspace` (`GET /api/v1/search?q=...`) searches projects, unarchived tickets, comments on unarchived tickets, and activity across every project the caller can read, optionally within one `workspaceId`. Each result has a `type` (`project`, `ticket`, `comment`, or `activity`), a stable `id` (the project, ticket, or comment ID, or the event ID), its `project`, its `ticket` (`id`, `key`, `title`, `status`, or `null`), a display `title`, an `excerpt` around the first literal match, `actorId`, and `at`. Activity matches its event label, its ticket key, or the name of an active (not disabled) actor. Results order by exact key match, then type in the order above, then ticket relevance as in `search_tickets`, then most recent first. `limit` (1–50, default 20) and `offset` (up to 1000) page through them; `type` narrows the page to one kind while `counts` still reports every kind and `total` counts the page's kind. The property filters above restrict results to matching tickets and their comments and activity, and leave out projects. Ticket text uses the external index when one is configured and current (`engine: "index"`); otherwise PostgreSQL answers (`engine: "postgres"`), and `indexing: true` says an index is configured but behind or unavailable. Projects, comments, and activity always come from PostgreSQL, and every result is filtered by current project access before counts and excerpts are computed.
|
|
42
54
|
|
|
43
|
-
Use `list_tickets` with `parentId` set to a parent UUID or ticket key to page through child tickets. Set `archived: "include"` to include archived children. Set `relatedTo` to a ticket UUID or key to page through the tickets linked to it by any relationship, in either direction. `list_relations` also returns the current source/target keys
|
|
55
|
+
Use `list_tickets` with `parentId` set to a parent UUID or ticket key to page through child tickets. Set `archived: "include"` to include archived children. Set `relatedTo` to a ticket UUID or key to page through the tickets linked to it by any relationship, in either direction. `list_relations` also returns the current source/target keys, full titles, and projects, so consumers can show readable dependency context without separate lookups. `relatedTo` lists only the linked tickets in the current project; `list_relations` includes links to other projects.
|
|
44
56
|
|
|
45
57
|
`duplicate_ticket` (`POST /api/v1/projects/{projectId}/tickets/{ticketId}/duplicate`) creates a new ticket in the same project with the next key, the original title followed by ` (copy)` (shortened to fit 500 characters), and the caller as `createdBy`. It copies the description, acceptance criteria, priority, kind, difficulty, due date, labels, scopes, and parent, and the assignee while they are still a project member. The copy starts in To do, unarchived, with `automationReady` false and no hold reason; the original's claims, comments, evidence, attachments, relationships, and children are not copied. A `clones` relationship links the copy to the original. One transaction writes the ticket, the relationship, a `ticket.created` event carrying `duplicateOf` (the original ID), and `relation.added` events for both tickets. It requires `write`; reuse `requestId` to retry without creating a second copy.
|
|
46
58
|
|
|
@@ -104,7 +116,7 @@ Each project has named workflow states. Every state maps to one of the six canon
|
|
|
104
116
|
|
|
105
117
|
Ticket writes take `status`, `stateId`, or both. A `stateId` must name a state of the ticket's project, and a `status` written with it must equal that state's category; otherwise the write fails with `400 validation_error` instead of choosing a state. A `status` alone keeps the ticket's state when the category is unchanged and otherwise moves it to that category's first state, so clients that predate workflows keep working. Creation without either starts in the first To do state, duplication copies into it, and claiming moves the ticket to the first In progress state. A change to either field reports both `status` and `stateId` in `changedFields`. `list_tickets` takes `stateId` to list one state's tickets; the `status` filter matches categories. Clients reading a server that predates workflows get `stateId: null`. Clients built from this contract ignore ticket fields they do not know, so later additions do not break them.
|
|
106
118
|
|
|
107
|
-
`add_relation` accepts `blocks`, `related`, `duplicate`, `causes`, `clones`, `implements`, or `reviews`. `direction` defaults to `outgoing` for existing clients. Use `incoming` to express the inverse from the current ticket: for example, `{ticketId: "<current>", targetId: "<blocker>", type: "blocks", direction: "incoming"}` means the current ticket is blocked by the selected ticket. The
|
|
119
|
+
`add_relation` accepts `blocks`, `related`, `duplicate`, `causes`, `clones`, `implements`, or `reviews`. `direction` defaults to `outgoing` for existing clients. Use `incoming` to express the inverse from the current ticket: for example, `{ticketId: "<current>", targetId: "<blocker>", type: "blocks", direction: "incoming"}` means the current ticket is blocked by the selected ticket. `targetId` is a ticket ID or key in this project or in any other project the caller can read, in this workspace or another; a target the caller cannot read, including one outside a project-bound API key's projects, returns `404 not_found` and creates nothing. The caller needs `write` on the current ticket's project. An edge that constrains a ticket in another project, as the blocked side of `blocks` or the source of `duplicate`, also needs `write` there, to add it or to remove it, and fails with `403 forbidden` otherwise; reading that project is enough to depend on its tickets or to add an informational link. The server stores a single canonical edge and rejects dependency cycles in either direction, following blockers and parents across every project. Related links are symmetric. Only blocking dependencies add readiness blockers, whichever project the blocker is in; duplicate sources remain ineligible for agent work. Other link types are informational. Adding or removing an edge writes `relation.added` or `relation.removed` for each ticket in that ticket's own project, so realtime subscribers and followers of either project see it. `remove_relation` works from either ticket's project, under the same `write` rule for a constrained ticket elsewhere. Deleting a ticket, or its whole project, removes its relationships and writes `relation.removed` for each surviving ticket in another project, in that ticket's project. `list_relations` returns each side's `sourceKey`/`targetKey`, title, `sourceProjectId`/`targetProjectId`, and `sourceProjectKey`/`targetProjectKey`, plus `restricted`. When the other ticket is in a project the caller cannot read, `restricted` is `true` and that side carries only its ID and key, with no title or project. Relocating a ticket and moving a project to another workspace stay blocked by its relationships, whichever projects they reach. Parent/child hierarchy continues to use `parentId` rather than a second relationship edge.
|
|
108
120
|
|
|
109
121
|
### Custom fields
|
|
110
122
|
|
package/docs/cli.md
CHANGED
|
@@ -63,7 +63,7 @@ JSON
|
|
|
63
63
|
kanvil task delete APP-12 --profile work --expected-version 7 --yes
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Use the version returned by a read or write. The CLI never fetches a newer version to bypass a conflict. Claims retain the server's readiness, ownership and lease rules; include `claimId` in an update when the operation requires it. Once a lease expires, renew and release refuse it; after confirming the old worker stopped, any writer with `claim` scope can run `claim recover` with the claim ID, a reason and `confirmedStopped: true`. Recovery never reschedules or reclaims on its own: pass `reschedule: true` with the ticket's current version to return it to To do, then read the ticket and `claim acquire` at the new version. `cancelActive: true` cancels a live claim only for project managers whose credential has `admin` scope. See [writes and ownership](api.md#writes-and-ownership) for the full policy. Deletion requires an explicit version and `--yes`; noninteractive invocations receive no confirmation prompt.
|
|
66
|
+
Use the version returned by a read or write. The CLI never fetches a newer version to bypass a conflict. Claims retain the server's readiness, ownership and lease rules; include `claimId` in an update when the operation requires it. Once a lease expires, renew and release refuse it; after confirming the old worker stopped, any writer with `claim` scope can run `claim recover` with the claim ID, a reason and `confirmedStopped: true`. Recovery never reschedules or reclaims on its own: pass `reschedule: true` with the ticket's current version to return it to To do, then read the ticket and `claim acquire` at the new version. When a ticket still requires ownership but no claim is left, for example after a release outside To do, omit the claim ID and pass `--expected-version` with the ticket's current version to lift the requirement, or to reschedule it with `reschedule: true`. `cancelActive: true` cancels a live claim only for project managers whose credential has `admin` scope. See [writes and ownership](api.md#writes-and-ownership) for the full policy. Deletion requires an explicit version and `--yes`; noninteractive invocations receive no confirmation prompt.
|
|
67
67
|
|
|
68
68
|
The typed client retries a transport failure once for reads and receipted writes, using the identical request body, request ID and expected version. HTTP errors, including conflicts, are never retried. For a retry across CLI invocations, supply the original `--request-id UUID` (or `requestId` in JSON) and unchanged input. If both transport attempts fail, the outcome may be unknown: reconcile by reading or replaying that same request ID; do not submit a new write identity blindly. Requests have a finite transport timeout.
|
|
69
69
|
|
package/docs/releasing.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
-
PRs and `main` pushes run `.github/workflows/ci.yml`. Independent suites and browser/native/Docker matrices run in parallel; only jobs consuming build artifacts wait for the build. The always-running `Checks` status requires every suite to succeed. Releases start on a `vX.Y.Z` tag and reuse that workflow against the tagged commit. Publishing jobs have registry write permissions; PR checks have read-only repository access.
|
|
3
|
+
PRs and `main` pushes run `.github/workflows/ci.yml`. `main` pushes also publish an [experimental build](#experimental-builds). Independent suites and browser/native/Docker matrices run in parallel; only jobs consuming build artifacts wait for the build. The always-running `Checks` status requires every suite to succeed. Releases start on a `vX.Y.Z` tag and reuse that workflow against the tagged commit. Publishing jobs have registry write permissions; PR checks have read-only repository access.
|
|
4
4
|
|
|
5
5
|
The public npm application package is `@maudecode/kanvil`; the workspace root and internal packages remain private. The release tag supplies the version for both npm and Docker: `v1.2.3` publishes `@maudecode/kanvil@1.2.3` and `ghcr.io/maudecode/kanvil:1.2.3`. The server build embeds that same version for Administration, public instance info, OpenAPI, and MCP. Installed artifacts need no Git checkout or release environment variables. No `package.json` version bump is needed. PR and local builds use the development version in `package.json`; custom Docker builds pass `--build-arg KANVIL_VERSION=X.Y.Z` to stamp the server and image label together. Native mobile app/store versions remain separate.
|
|
6
6
|
|
|
@@ -11,10 +11,24 @@ The public npm application package is `@maudecode/kanvil`; the workspace root an
|
|
|
11
11
|
3. Check the `Release` run. It rejects malformed or prerelease tags, `v0.0.0`, and commits outside `main` history. The full CI workflow must pass on that commit.
|
|
12
12
|
4. Verify `npm view @maudecode/kanvil@X.Y.Z dist.integrity` and `docker buildx imagetools inspect ghcr.io/maudecode/kanvil:X.Y.Z`. The image includes Linux amd64 and arm64. Confirm anonymous Docker pulls work, and check the GitHub Release for the tag and its automatically generated notes.
|
|
13
13
|
|
|
14
|
-
The workflow publishes the exact npm tarball installed by CI to the `latest` dist-tag, while separate amd64/arm64 publishers push the exact image archives tested by CI. Docker builds and restoration tests run on matching native runners; release publishing does not rebuild or emulate either image. A final Docker manifest job joins those immutable digests under `ghcr.io/maudecode/kanvil:X.Y.Z` and `:latest`. Stable releases only; prerelease versions are rejected. Do not tag an older version to move `latest` backward.
|
|
14
|
+
The workflow publishes the exact npm tarball installed by CI to the `latest` dist-tag, while separate amd64/arm64 publishers push the exact image archives tested by CI. Docker builds and restoration tests run on matching native runners; release publishing does not rebuild or emulate either image. A final Docker manifest job joins those immutable digests under `ghcr.io/maudecode/kanvil:X.Y.Z` and `:latest`. Stable and experimental builds share this publisher, `.github/workflows/publish-images.yml`. Stable releases only; prerelease versions are rejected. Do not tag an older version to move `latest` backward.
|
|
15
15
|
|
|
16
16
|
If either registry publication fails, rerun the failed release jobs. npm and Docker publication are independent after validation. The npm step accepts an existing version only when its SHA-512 integrity matches the tested tarball; different bytes fail and require a new version. Registry publication is not atomic across npm and GHCR. Never move a published tag or reuse a version for changed bytes. Avoid rerunning an old release after a newer release, since Docker's `latest` tag would move backward.
|
|
17
17
|
|
|
18
18
|
After npm publication and the Docker manifest job both succeed, the workflow creates a GitHub Release for the existing tag with an automatically generated title and release notes. Only this final job has repository contents write permission. Reruns preserve an existing release and its notes.
|
|
19
19
|
|
|
20
|
+
## Experimental builds
|
|
21
|
+
|
|
22
|
+
Every push to `main` also publishes an experimental build once CI passes on that exact commit. `.github/workflows/experimental.yml` runs when a CI run finishes. It publishes only a successful `push` run of this repository's `main` whose commit is still on `main`; pull requests, forks, other branches, release tags, and failed or cancelled runs publish nothing. Each `main` commit runs CI in its own concurrency group, so a newer push never cancels or replaces an older commit's verification or publication.
|
|
23
|
+
|
|
24
|
+
An experimental build of commit `SHA` (the full 40-character commit) has the version `0.0.0-experimental.gSHA` in Administration, public instance info, OpenAPI, MCP, and the image's `org.opencontainers.image.version` label; `org.opencontainers.image.revision` is `SHA`. CI builds and tests it with that identity (`KANVIL_RELEASE_CHANNEL=experimental` in `scripts/release-version.ts`), and publishing reuses the exact native images CI tested:
|
|
25
|
+
|
|
26
|
+
- `ghcr.io/maudecode/kanvil:experimental-SHA-amd64` and `-arm64`, then the multi-platform `ghcr.io/maudecode/kanvil:experimental-SHA`. These names are immutable.
|
|
27
|
+
- A Git tag and GitHub prerelease named `experimental-SHA`, pinned to `SHA`, created only after both architectures and the multi-platform manifest are published. It is never marked latest; its notes give the source commit, image, and manifest digest after the generated change list.
|
|
28
|
+
- `ghcr.io/maudecode/kanvil:experimental`, the opt-in rolling alias. It moves to the newest announced experimental build on `main`'s first-parent history and never to an older commit, so builds that finish out of order and reruns of old runs cannot move it backward. Only this step is serialized.
|
|
29
|
+
|
|
30
|
+
Experimental tags start with a letter, so tools that read numeric Docker tags as versions, such as Renovate, never treat them as upgrades from a stable release; CI's `Renovate tag selection` job checks this with Renovate itself. Experimental builds never publish npm packages or move npm dist-tags, `latest`, version tags, or the latest GitHub release.
|
|
31
|
+
|
|
32
|
+
Reruns are safe. `scripts/release-publish.ts` keeps an existing image, manifest, tag, or release that names the same commit and version, and fails on one that names anything else rather than replacing it. If one architecture fails to publish, rerun the failed jobs of the `Experimental` run; they reuse the images CI tested and the architecture already published. If only the alias move fails, rerun it, or let the next `main` commit's run move the alias; it always selects the newest published build.
|
|
33
|
+
|
|
20
34
|
Tag creation and package publication are separate from deploying to any running instance. Operators choose when to upgrade using [self-hosting](self-hosting.md).
|
package/docs/self-hosting.md
CHANGED
|
@@ -49,6 +49,12 @@ docker compose up -d --wait
|
|
|
49
49
|
|
|
50
50
|
The image supports Linux amd64 and arm64. Replace `latest` with a release version such as `0.1.0`, or pin `ghcr.io/maudecode/kanvil@sha256:YOUR_DIGEST` for an immutable deployment.
|
|
51
51
|
|
|
52
|
+
To run unreleased code from `main`, opt in explicitly. `ghcr.io/maudecode/kanvil:experimental` follows the newest `main` commit that passed CI; `ghcr.io/maudecode/kanvil:experimental-FULL_COMMIT_SHA` names one such commit and never changes. Experimental builds are not releases: they can carry unfinished features and migrations that a later stable release does not undo, so back up first and do not move an instance from experimental back to an older stable version. Stable tags and `latest` never point to an experimental build, and Renovate's default Docker versioning does not offer experimental tags to stable consumers:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
KANVIL_IMAGE=ghcr.io/maudecode/kanvil:experimental-0123456789abcdef0123456789abcdef01234567
|
|
56
|
+
```
|
|
57
|
+
|
|
52
58
|
The app binds to loopback port 5180; change the host port with `KANVIL_PORT`. Set up the HTTPS reverse proxy below before opening the site. The application runs as the `node` user with a read-only root filesystem and temporary `/tmp`. Its health check verifies database connectivity. PostgreSQL storage is kept in the `postgres-data` volume. `docker compose down` retains that volume; removing volumes deletes instance data.
|
|
53
59
|
|
|
54
60
|
For source builds, use the repository's Node/pnpm versions and run:
|
|
@@ -97,6 +103,8 @@ API keys have a separate limit of 600 requests per minute per key across HTTP an
|
|
|
97
103
|
|
|
98
104
|
Set `KANVIL_CLAIM_MAX_LEASE_SECONDS` in `.env` to change the maximum initial or renewed worker claim lease. The default is `86400` (24 hours); valid values are integers from `30` to `2147483647` seconds. Restart the server after changing it; with Compose, run `docker compose up -d app`. Omitting `leaseSeconds` requests 300 seconds, capped by this setting. Lowering the setting does not shorten existing claims, but their next renewal must use the new maximum. Expired claims can be explicitly recovered by callers with claim scope and ticket write permission after confirming the old worker stopped, without administrator intervention. A worker can pass `reschedule: true` and the current `expectedVersion` to recovery, then acquire a new claim, without needing a human to reset the ticket. Revoking a credential does not itself clear its claim; after stopping the worker, a project manager with admin scope can explicitly pass `cancelActive: true` to cancel live ownership immediately, including alongside version-checked rescheduling.
|
|
99
105
|
|
|
106
|
+
Each application process opens at most `KANVIL_DATABASE_POOL_MAX` (default `10`, minimum `2`) PostgreSQL connections for API, MCP, and authentication work, `KANVIL_INTEGRATION_POOL_MAX` (default `4`) for integration receipts, instance settings, background sync, and account guards, and one realtime `LISTEN` connection: 15 by default. Connections open on demand and close after 30 seconds idle. A request that waits longer than `KANVIL_DATABASE_ACQUIRE_TIMEOUT_MS` (default `30000`) for a free connection fails instead of queueing forever. Keep the sum for every process below PostgreSQL's `max_connections` with room for backups and administration; `pg_stat_activity` lists the connections as `kanvil`, `kanvil-integration`, and `kanvil-realtime`. Restart the server after changing these settings.
|
|
107
|
+
|
|
100
108
|
## PocketID / OIDC
|
|
101
109
|
|
|
102
110
|
Create a confidential OIDC client in PocketID, enable PKCE, and register:
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as e}from"./rolldown-runtime-COiEfhsA.js";import{An as t,Cn as n,Dn as r,G as i,Mn as a,Nn as o,On as s,Rt as c,Sn as l,W as u,_n as d,an as f,bn as p,bt as m,gn as h,it as g,mn as _,mt as v,r as y,wn as b,xn as x,yn as ee,zt as S}from"./default-D6I-Xfqm.js";import{B as te,G as C,Ga as w,Hi as T,J as E,Ji as D,K as O,Oa as k,Pt as A,Q as ne,U as j,Ui as M,V as N,X as P,Y as F,Yi as re,Zi as ie,aa as ae,at as I,cr as L,di as R,ia as oe,it as z,nt as se,ot as ce,po as le,q as B,qa as V,ra as ue,rt as de,tt as fe,vt as H,wo as U}from"./search-DQtwggUH.js";import{g as pe}from"./item-CRS9Ai9p.js";import{l as me,r as he}from"./project-lifecycle-DuV33mlL.js";import{r as ge}from"./CompositeItem-DvEBmVYM.js";import{t as _e}from"./ellipsis-Cx1SIn8a.js";import{t as ve}from"./invitations-Ch02KEAB.js";import{t as W}from"./forms-BwtRqvGj.js";import{A as ye,D as be,E as xe,Ht as G,M as Se,O as Ce,T as we,an as Te,fn as Ee,hn as De,i as K,k as Oe,n as q,r as ke}from"./index-MTE_naHG.js";import{t as Ae}from"./switch-UPVGS5kv.js";import{a as je,i as J,n as Me,o as Y,r as X,t as Ne}from"./table-D8iAxhJH.js";import{t as Pe}from"./member-directory-CoLB-2Oc.js";var Fe={name:`user-plus`,size:24,node:[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`line`,{x1:`19`,x2:`19`,y1:`8`,y2:`14`,key:`1bvyxn`}],[`line`,{x1:`22`,x2:`16`,y1:`11`,y2:`11`,key:`1shjgl`}]]};Fe.node;var Ie=w(Fe),Z=e(o(),1),Q=y(),Le={get local(){return _(`admin.instance.provider.local`)},get s3(){return _(`admin.instance.provider.s3`)},get postgres(){return _(`admin.instance.provider.postgres`)},get elasticsearch(){return _(`admin.instance.provider.elasticsearch`)},rabbitmq:`RabbitMQ`};function Re({data:e,section:t,onConfigure:n}){if(t===`general`)return(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(z,{title:_(`admin.instance.heading`),action:(0,Q.jsx)(E,{label:_(`admin.instance.edit`),onClick:n,children:(0,Q.jsx)(ge,{})})}),(0,Q.jsx)(q,{title:_(`admin.instance.name`),description:e.name}),(0,Q.jsx)(q,{title:_(`admin.instance.description`),description:e.description||_(`admin.instance.noDescription`)}),(0,Q.jsx)(q,{title:_(`admin.instance.version`),children:(0,Q.jsx)(`code`,{children:e.deployment.version})}),(0,Q.jsx)(q,{title:_(`admin.instance.publicUrl`),children:(0,Q.jsx)(j,{label:_(`admin.instance.publicUrl`),value:e.deployment.url,inline:!0})})]});let r=t===`storage`?e.storage:t===`search`?e.search:e.broker,i=_(t===`storage`?`admin.instance.section.storage`:t===`search`?`admin.instance.section.search`:`admin.instance.section.events`),a=r.source===`deployment`;return(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(z,{title:i,action:(0,Q.jsx)(E,{label:_(a?`admin.instance.viewConfiguration`:`admin.instance.configure.${t}`),onClick:n,children:(0,Q.jsx)(Te,{})})}),(0,Q.jsx)(q,{title:_(`admin.instance.providerTitle`),description:Le[r.provider],children:(0,Q.jsx)(H,{variant:`secondary`,children:_(a?`admin.instance.deploymentManaged`:`admin.instance.instanceManaged`)})}),(0,Q.jsx)(q,{title:_(t===`storage`?`admin.instance.transfers`:t===`search`?`admin.instance.indexStatus`:`admin.instance.connection`),description:t===`storage`?e.storage.provider===`s3`?_(`admin.instance.transfersS3`):_(`admin.instance.transfersLocal`):t===`search`?r.status===`builtin`?_(`admin.instance.searchBuiltIn`):_(`admin.instance.searchIndexed`):_(`admin.instance.eventsConnection`),children:(0,Q.jsx)(H,{variant:r.status===`unavailable`?`destructive`:`outline`,children:r.status===`builtin`?_(`admin.instance.builtIn`):r.status===`ready`?_(t===`events`?`admin.instance.connected`:`admin.instance.upToDate`):_(`admin.instance.status.${r.status}`)})}),t===`storage`&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(q,{title:_(`admin.instance.pendingTransfers`),description:e.storage.pending?_(`admin.instance.pendingRemain`):_(`admin.instance.noPending`),children:e.storage.pending}),e.storage.settings&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(q,{title:_(`admin.instance.bucket`),description:e.storage.settings.bucket}),(0,Q.jsx)(j,{label:_(`admin.instance.storageEndpoint`),value:e.storage.settings.endpoint||`Amazon S3`})]}),(0,Q.jsx)(q,{title:_(`admin.instance.backup`),description:e.locations.length?_(`admin.instance.backupBuckets`):_(`admin.instance.backupDatabase`)})]}),t===`search`&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(q,{title:_(`admin.instance.fallback`),description:_(`admin.instance.fallbackDescription`)}),e.search.settings&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(j,{label:_(`admin.instance.searchEndpoint`),value:e.search.settings.url}),(0,Q.jsx)(q,{title:_(`admin.instance.index`),description:e.search.settings.index})]})]}),t===`events`&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(q,{title:_(`admin.instance.delivery`),description:_(`admin.instance.deliveryDescription`)}),e.broker.settings&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(j,{label:_(`admin.instance.brokerEndpoint`),value:e.broker.settings.endpoint}),(0,Q.jsx)(q,{title:_(`admin.instance.exchange`),description:e.broker.settings.exchange})]})]}),(0,Q.jsx)(O,{error:r.error?Error(r.error):void 0})]})}var ze=g.pick({name:!0,email:!0,image:!0}).extend({id:t(),role:t().nullable(),banned:b(),createdAt:x()}),Be=s({q:t().trim().max(200).default(``),role:l([`all`,`user`,`admin`]).default(`all`),status:l([`all`,`active`,`disabled`]).default(`all`),limit:p().int().min(1).max(100).default(25),offset:p().int().min(0).default(0)}),Ve=s({users:n(ze),total:r().int().nonnegative()}),He=ze.extend({workspaces:n(s({id:a(),name:t(),role:l(S)})),projects:n(s({id:a(),name:t(),role:l(S)})),providers:n(t()),sessionCount:r().int().nonnegative()});s({password:t().min(12).max(128)}).strict(),Be.pick({q:!0,limit:!0,offset:!0});var Ue=s({workspaces:n(s({id:a(),name:t(),slug:t(),ownerId:t().nullable().default(null),createdAt:x(),memberCount:r().int().nonnegative(),projectCount:r().int().nonnegative(),administrators:n(ze.pick({id:!0,name:!0,email:!0,image:!0}))})),total:r().int().nonnegative()});function $({children:e,badge:t}){return(0,Q.jsxs)(`div`,{className:`flex items-center justify-between gap-4 border-b py-3`,children:[e,(0,Q.jsx)(H,{variant:`outline`,children:t})]})}function We({userId:e,onClose:t,onChanged:n}){let{shown:r,settled:i}=we(e,e=>e),[a,o]=(0,Z.useState)(!1);return(0,Q.jsx)(xe,{open:!!e,onOpenChange:e=>{!e&&!a&&t()},onOpenChangeComplete:i,children:(0,Q.jsx)(be,{className:`w-full! sm:max-w-lg!`,children:r&&(0,Q.jsx)(Ge,{userId:r,pending:a,setPending:o,onClose:t,onChanged:n},r)})})}function Ge({userId:e,pending:t,setPending:n,onClose:r,onChanged:a}){let o=D(),[s,l]=(0,Z.useState)(null),[d,f]=(0,Z.useState)(),p=ue(),m=ie(`set_workspace_member`),h=V({queryKey:[`kanvil`,`user`,e],queryFn:()=>T.request(`/api/v1/admin/users/${encodeURIComponent(e)}`,He)}),g=h.data;async function y(e,t){n(!0),f(void 0);try{let n=await e();if(n&&typeof n==`object`&&`error`in n&&n.error){let e=n.error;throw Error(typeof e==`object`&&`message`in e?String(e.message):_(`admin.account.updateFailed`))}await a(),l(null),k.success(t)}catch(e){throw f(e),e}finally{n(!1)}}return(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsxs)(Oe,{className:`shrink-0 border-b pr-14`,children:[(0,Q.jsx)(ye,{className:`mb-0`,children:g?.name??_(`admin.account.details`)}),(0,Q.jsx)(Ce,{className:`break-all`,children:g?.email})]}),(0,Q.jsxs)(`div`,{className:`@container min-h-0 overflow-y-auto px-6 pb-8`,children:[(0,Q.jsx)(O,{error:h.error??(s?void 0:d)}),h.isPending?(0,Q.jsx)(P,{}):g&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsxs)(`div`,{className:`mb-7 flex flex-wrap items-center gap-4`,children:[(0,Q.jsx)(oe,{name:g.name,image:g.image,className:`size-16 text-xl`}),(0,Q.jsxs)(`div`,{className:`min-w-40 flex-1`,children:[(0,Q.jsx)(H,{variant:g.banned?`destructive`:`secondary`,children:g.banned?_(`admin.users.disabled`):_(`admin.users.active`)}),(0,Q.jsx)(`p`,{className:`mt-2 mb-0 text-xs text-muted-foreground`,children:_(`admin.account.joined`,{date:u(g.createdAt)})})]}),(0,Q.jsxs)(R,{className:`ml-auto`,onClick:()=>{f(void 0),l(`profile`)},children:[(0,Q.jsx)(ge,{}),_(`admin.account.edit`)]})]}),(0,Q.jsx)(q,{title:_(`admin.account.instanceRole`),description:i(g.role)?_(`admin.account.adminDescription`):_(`admin.account.userDescription`),children:(0,Q.jsx)(R,{onClick:()=>{f(void 0),l(`role`)},children:_(`admin.account.changeRole`)})}),(0,Q.jsx)(q,{title:_(`admin.account.signInMethods`),description:me(g.providers),children:(0,Q.jsxs)(R,{onClick:()=>{f(void 0),l(`password`)},children:[(0,Q.jsx)(Ee,{}),_(`admin.account.setPassword`)]})}),(0,Q.jsx)(q,{title:_(`admin.account.activeSessions`),description:_(`admin.account.sessions`,{count:g.sessionCount}),children:(0,Q.jsx)(N,{label:_(`admin.account.revokeSessions`),description:_(`admin.account.revokeConfirm`,{name:g.name}),onConfirm:()=>y(()=>M.admin.revokeUserSessions({userId:g.id}),_(`admin.account.sessionsRevoked`))})}),g.id!==o.id&&(0,Q.jsx)(q,{title:_(`admin.account.access`),description:g.banned?_(`admin.account.accessDisabled`):_(`admin.account.accessPreserved`),children:(0,Q.jsx)(N,{label:g.banned?_(`admin.account.enable`):_(`admin.account.disable`),danger:!g.banned,description:g.banned?_(`admin.account.enableConfirm`,{name:g.name}):_(`admin.account.disableConfirm`,{name:g.name}),onConfirm:()=>y(()=>g.banned?M.admin.unbanUser({userId:g.id}):M.admin.banUser({userId:g.id,banReason:`Disabled by an instance administrator`}),g.banned?_(`admin.account.enabled`):_(`admin.account.disabledToast`))})}),g.id!==o.id&&(0,Q.jsx)(he,{account:g,onDeleted:async()=>{r(),await a()}}),(0,Q.jsxs)(`section`,{className:`pt-7`,children:[(0,Q.jsxs)(`div`,{className:`mb-3 flex items-center justify-between gap-3`,children:[(0,Q.jsx)(`h3`,{className:`mb-0`,children:_(`admin.account.workspaces`)}),(0,Q.jsxs)(R,{size:`sm`,onClick:()=>{f(void 0),l(`workspace`)},children:[(0,Q.jsx)(pe,{}),_(`admin.account.addToWorkspace`)]})]}),g.workspaces.length?g.workspaces.map(e=>(0,Q.jsx)($,{badge:c[e.role],children:(0,Q.jsx)(G,{to:`/workspaces/$workspaceId/settings`,params:{workspaceId:e.id},search:{section:`members`},onClick:r,className:`text-primary`,dir:`auto`,children:e.name})},e.id)):(0,Q.jsx)(C,{title:_(`admin.account.noWorkspaces`),className:`py-5`})]}),!!g.projects.length&&(0,Q.jsxs)(`section`,{className:`pt-7`,children:[(0,Q.jsx)(`h3`,{children:_(`admin.account.projectAccess`)}),g.projects.map(e=>(0,Q.jsx)($,{badge:c[e.role],children:(0,Q.jsx)(G,{to:`/projects/$projectId/settings`,params:{projectId:e.id},search:{section:`members`},onClick:r,className:`text-primary`,dir:`auto`,children:e.name})},e.id))]})]})]}),g&&(0,Q.jsx)(ne,{title:_(s===`profile`?`admin.account.edit`:s===`role`?`admin.account.changeRoleTitle`:s===`workspace`?`admin.account.addToWorkspace`:`admin.account.setPassword`),open:!!s,onOpenChange:e=>{e||l(null)},busy:t,children:(0,Q.jsxs)(`form`,{onSubmit:e=>{e.preventDefault();let t=new FormData(e.currentTarget),n,r;if(s===`profile`)n=()=>M.admin.updateUser({userId:g.id,data:{name:W(t,`name`),email:W(t,`email`)}}),r=_(`admin.account.updated`);else if(s===`role`)n=()=>M.admin.setRole({userId:g.id,role:t.get(`role`)===`admin`?`admin`:`user`}),r=_(`admin.account.roleUpdated`);else if(s===`workspace`)n=()=>m.mutateAsync({workspaceId:W(t,`workspace`),email:g.email,role:W(t,`role`)===`reader`?`reader`:W(t,`role`)===`admin`?`admin`:`writer`}),r=_(`admin.account.workspaceAdded`);else{let e=W(t,`password`);if(e!==W(t,`confirm`)){f(Error(_(`admin.account.passwordMismatch`)));return}n=()=>T.request(`/api/v1/admin/users/${encodeURIComponent(g.id)}/password`,v,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({password:e})}),r=_(`admin.account.passwordSet`)}y(n,r).catch(()=>{})},children:[s===`profile`?(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(F,{label:_(`admin.account.name`),name:`name`,defaultValue:g.name,required:!0,maxLength:120,autoFocus:!0}),(0,Q.jsx)(F,{label:_(`admin.account.email`),name:`email`,type:`email`,defaultValue:g.email,required:!0})]}):s===`role`?(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsxs)(I,{label:_(`admin.account.instanceRole`),name:`role`,defaultValue:i(g.role)?`admin`:`user`,children:[(0,Q.jsx)(A,{value:`user`,children:_(`admin.account.user`)}),(0,Q.jsx)(A,{value:`admin`,children:_(`admin.account.administrator`)})]}),(0,Q.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:_(`admin.account.adminsNote`)})]}):s===`workspace`?(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(I,{label:_(`admin.account.workspace`),name:`workspace`,required:!0,children:p.data?.filter(e=>!g.workspaces.some(t=>t.id===e.id)).map(e=>(0,Q.jsx)(A,{value:e.id,children:e.name},e.id))}),(0,Q.jsxs)(I,{label:_(`admin.account.workspaceRole`),name:`role`,defaultValue:`writer`,children:[(0,Q.jsx)(A,{value:`reader`,children:c.reader}),(0,Q.jsx)(A,{value:`writer`,children:c.writer}),(0,Q.jsx)(A,{value:`admin`,children:c.admin})]})]}):(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(F,{label:_(`admin.account.newPassword`),name:`password`,type:`password`,autoComplete:`new-password`,required:!0,minLength:12,maxLength:128}),(0,Q.jsx)(F,{label:_(`admin.account.confirmPassword`),name:`confirm`,type:`password`,autoComplete:`new-password`,required:!0,minLength:12,maxLength:128}),(0,Q.jsx)(`p`,{className:`text-sm text-muted-foreground`,children:_(`admin.account.passwordNote`)})]}),(0,Q.jsx)(B,{error:d,pending:t,submitLabel:_(`admin.account.save`),pendingLabel:_(`common.state.saving`),onCancel:()=>l(null)})]},s)})]})}function Ke({onCreated:e,onCancel:t,pending:n,setPending:r}){let i=U(),[a,o]=(0,Z.useState)();async function s(t){t.preventDefault();let n=t.currentTarget,a=new FormData(n);r(!0),o(void 0);try{let t=await M.admin.createUser({name:W(a,`name`),email:W(a,`email`),password:W(a,`password`),role:a.get(`role`)===`admin`?`admin`:`user`});if(t.error)throw Error(t.error.message);await i.invalidateQueries({queryKey:[`kanvil`,`users`]}),e(t.data.user.id,t.data.user.email)}catch(e){o(e)}finally{r(!1)}}return(0,Q.jsx)(`section`,{children:(0,Q.jsxs)(`form`,{onSubmit:e=>{s(e)},children:[(0,Q.jsx)(F,{label:_(`admin.users.name`),name:`name`,required:!0,maxLength:120,autoFocus:!0}),(0,Q.jsx)(F,{label:_(`admin.users.email`),name:`email`,type:`email`,required:!0,autoComplete:`off`}),(0,Q.jsx)(F,{label:_(`admin.users.initialPassword`),name:`password`,type:`password`,autoComplete:`new-password`,minLength:12,maxLength:128,required:!0}),(0,Q.jsxs)(I,{label:_(`admin.users.instanceRole`),name:`role`,children:[(0,Q.jsx)(A,{value:`user`,children:_(`admin.users.user`)}),(0,Q.jsx)(A,{value:`admin`,children:_(`admin.users.administrator`)})]}),(0,Q.jsx)(B,{error:a,pending:n,submitLabel:_(`admin.users.create`),pendingLabel:_(`admin.users.creating`),onCancel:t})]})})}function qe(){let e=U(),t=M.useSession(),[n,r]=(0,Z.useState)(!1),[a,o]=(0,Z.useState)(!1),[s,c]=(0,Z.useState)(``),[l,u]=(0,Z.useState)(`all`),[d,f]=(0,Z.useState)(`all`),[p,m]=(0,Z.useState)(0),[h,g]=(0,Z.useState)(null),v=V({queryKey:[`kanvil`,`users`,s,l,d,p],queryFn:()=>T.request(`/api/v1/admin/users?${new URLSearchParams({q:s,role:l,status:d,offset:String(p),limit:`25`})}`,Ve),placeholderData:le});async function y(){await Promise.all([e.invalidateQueries({queryKey:[`kanvil`]}),t.refetch()])}return(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(z,{title:_(`admin.users.title`),action:(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-center justify-end gap-2`,children:[(0,Q.jsx)(de,{label:_(`admin.users.search`),placeholder:_(`admin.users.searchPlaceholder`),value:s,onChange:e=>{c(e),m(0)}}),(0,Q.jsx)(E,{label:_(`admin.users.new`),variant:`default`,onClick:()=>r(!0),children:(0,Q.jsx)(Ie,{})})]})}),(0,Q.jsxs)(`div`,{className:`mb-5 flex flex-wrap items-center gap-2`,children:[(0,Q.jsxs)(I,{label:_(`admin.users.roleFilter`),labelHidden:!0,className:`mb-0 w-40`,value:l,onValueChange:e=>{e&&u(e),m(0)},children:[(0,Q.jsx)(A,{value:`all`,children:_(`admin.users.allRoles`)}),(0,Q.jsx)(A,{value:`admin`,children:_(`admin.users.administrators`)}),(0,Q.jsx)(A,{value:`user`,children:_(`admin.users.users`)})]}),(0,Q.jsxs)(I,{label:_(`admin.users.statusFilter`),labelHidden:!0,className:`mb-0 w-40`,value:d,onValueChange:e=>{e&&f(e),m(0)},children:[(0,Q.jsx)(A,{value:`all`,children:_(`admin.users.allAccounts`)}),(0,Q.jsx)(A,{value:`active`,children:_(`admin.users.active`)}),(0,Q.jsx)(A,{value:`disabled`,children:_(`admin.users.disabled`)})]})]}),(0,Q.jsx)(O,{error:v.error}),v.isPending?(0,Q.jsx)(P,{}):v.data?.users.length?(0,Q.jsx)(`div`,{"aria-busy":v.isFetching,children:(0,Q.jsxs)(Ne,{mobileLayout:`rows`,"aria-label":_(`admin.users.title`),children:[(0,Q.jsx)(je,{children:(0,Q.jsxs)(Y,{children:[(0,Q.jsx)(J,{children:_(`admin.users.user`)}),(0,Q.jsx)(J,{children:_(`admin.users.role`)}),(0,Q.jsx)(J,{children:_(`admin.users.status`)}),(0,Q.jsx)(J,{children:_(`admin.users.joined`)}),(0,Q.jsx)(J,{children:(0,Q.jsx)(`span`,{className:`sr-only`,children:_(`admin.users.actions`)})})]})}),(0,Q.jsx)(Me,{children:v.data.users.map(e=>(0,Q.jsx)(Se,{name:e.name,email:e.email,onOpen:()=>g(e.id),children:(0,Q.jsxs)(Y,{children:[(0,Q.jsx)(X,{className:`whitespace-normal`,children:(0,Q.jsx)(R,{variant:`ghost`,className:`h-auto max-w-full justify-start p-0 text-left hover:bg-transparent`,onClick:()=>g(e.id),children:(0,Q.jsx)(ae,{name:e.name,email:e.email,image:e.image})})}),(0,Q.jsx)(X,{"data-label":_(`admin.users.role`),children:(0,Q.jsx)(H,{variant:`secondary`,children:i(e.role)?_(`admin.users.administrator`):_(`admin.users.user`)})}),(0,Q.jsx)(X,{"data-label":_(`admin.users.status`),children:(0,Q.jsx)(H,{variant:e.banned?`destructive`:`outline`,children:e.banned?_(`admin.users.disabled`):_(`admin.users.active`)})}),(0,Q.jsx)(X,{"data-label":_(`admin.users.joined`),className:`text-xs text-muted-foreground`,children:(0,Q.jsx)(se,{value:e.createdAt})}),(0,Q.jsx)(X,{children:(0,Q.jsx)(R,{variant:`ghost`,size:`icon`,"aria-label":_(`admin.users.manage`,{name:e.name}),onClick:()=>g(e.id),children:(0,Q.jsx)(_e,{})})})]})},e.id))})]})}):(0,Q.jsx)(C,{title:_(s||l!==`all`||d!==`all`?`admin.users.noMatches`:`admin.users.empty`),className:`py-12`}),(0,Q.jsx)(fe,{noun:_(`admin.users.pagerNoun`),offset:p,total:v.data?.total??0,size:25,onChange:m}),(0,Q.jsx)(ne,{title:_(`admin.users.create`),open:n,onOpenChange:r,busy:a,children:(0,Q.jsx)(Ke,{pending:a,setPending:o,onCancel:()=>r(!1),onCreated:(e,t)=>{r(!1),c(``),u(`all`),f(`all`),m(0),g(e),k.success(_(`admin.users.created`,{email:t}))}})}),(0,Q.jsx)(We,{userId:h,onClose:()=>g(null),onChanged:y})]})}var Je=25;function Ye(){let[e,t]=(0,Z.useState)(``),[n,r]=(0,Z.useState)(0),[i,a]=(0,Z.useState)(null),o=V({queryKey:[`kanvil`,`admin-workspaces`,e,n],queryFn:()=>T.request(`/api/v1/admin/workspaces?${new URLSearchParams({q:e,offset:String(n),limit:String(Je)})}`,Ue),placeholderData:le});return(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(z,{title:_(`settings.workspaces.title`),action:(0,Q.jsx)(`div`,{className:`flex min-w-0 flex-1 items-center justify-end gap-2`,children:(0,Q.jsx)(de,{label:_(`settings.workspaces.search`),placeholder:_(`settings.workspaces.searchPlaceholder`),value:e,onChange:e=>{t(e),r(0)}})})}),(0,Q.jsx)(O,{error:o.error}),o.isPending?(0,Q.jsx)(P,{}):o.data?.workspaces.length?(0,Q.jsx)(`div`,{"aria-busy":o.isFetching,children:(0,Q.jsxs)(Ne,{mobileLayout:`rows`,"aria-label":_(`settings.workspaces.title`),children:[(0,Q.jsx)(je,{children:(0,Q.jsxs)(Y,{children:[(0,Q.jsx)(J,{children:_(`settings.workspaces.workspace`)}),(0,Q.jsx)(J,{children:_(`settings.workspaces.administrators`)}),(0,Q.jsx)(J,{children:_(`settings.workspaces.members`)}),(0,Q.jsx)(J,{children:_(`settings.workspaces.projects`)}),(0,Q.jsx)(J,{children:_(`settings.workspaces.created`)}),(0,Q.jsx)(J,{children:(0,Q.jsx)(`span`,{className:`sr-only`,children:_(`settings.workspaces.actions`)})})]})}),(0,Q.jsx)(Me,{children:o.data.workspaces.map(e=>(0,Q.jsxs)(Y,{children:[(0,Q.jsx)(X,{className:`whitespace-normal`,children:(0,Q.jsxs)(R,{variant:`ghost`,className:`h-auto max-w-full flex-col items-start gap-0.5 p-0 text-left hover:bg-transparent`,onClick:()=>a(e),children:[(0,Q.jsx)(`span`,{dir:`auto`,className:`font-medium wrap-anywhere whitespace-normal`,children:e.name}),(0,Q.jsx)(`span`,{className:`font-mono text-xs font-normal text-muted-foreground`,children:e.slug})]})}),(0,Q.jsx)(X,{"data-label":_(`settings.workspaces.administrators`),className:`text-xs wrap-anywhere whitespace-normal`,children:e.administrators.length?f(e.administrators.map(e=>e.name)):(0,Q.jsx)(H,{variant:`destructive`,children:_(`settings.workspaces.noAdministrator`)})}),(0,Q.jsx)(X,{"data-label":_(`settings.workspaces.members`),className:`tabular-nums`,children:e.memberCount}),(0,Q.jsx)(X,{"data-label":_(`settings.workspaces.projects`),className:`tabular-nums`,children:e.projectCount}),(0,Q.jsx)(X,{"data-label":_(`settings.workspaces.created`),className:`text-xs text-muted-foreground`,children:(0,Q.jsx)(se,{value:e.createdAt})}),(0,Q.jsx)(X,{children:(0,Q.jsx)(R,{variant:`ghost`,size:`icon`,"aria-label":_(`settings.workspaces.manage`,{name:e.name}),onClick:()=>a(e),children:(0,Q.jsx)(_e,{})})})]},e.id))})]})}):(0,Q.jsx)(C,{title:_(e?`settings.workspaces.noMatches`:`settings.workspaces.empty`),className:`py-12`}),(0,Q.jsx)(fe,{noun:_(`settings.workspaces.pagerNoun`),offset:n,total:o.data?.total??0,size:Je,onChange:r}),(0,Q.jsx)(Xe,{workspace:i,onClose:()=>a(null)})]})}function Xe({workspace:e,onClose:t}){let{shown:n,settled:r}=we(e,e=>e.id),i=n?.id??``,a=V({...T.query(`list_workspace_members`,{workspaceId:i}),enabled:!!n}),o=V({...T.query(`list_projects`,{workspaceId:i,archived:`include`}),enabled:!!n}),s=V({...T.query(`list_workspace_roles`,{workspaceId:i}),enabled:!!n}),c=ie(`set_workspace_member`);return(0,Q.jsx)(xe,{open:!!e,onOpenChange:e=>{!e&&!c.isPending&&t()},onOpenChangeComplete:r,children:(0,Q.jsxs)(be,{className:`w-full! sm:max-w-lg!`,children:[(0,Q.jsxs)(Oe,{className:`shrink-0 border-b pr-14`,children:[(0,Q.jsx)(ye,{className:`mb-0`,children:n?.name??_(`settings.workspaces.workspace`)}),(0,Q.jsx)(Ce,{className:`font-mono`,children:n?.slug})]}),(0,Q.jsxs)(`div`,{className:`@container min-h-0 overflow-y-auto px-6 pb-8`,children:[(0,Q.jsx)(Pe,{people:a.data??[],pending:c.isPending,loading:a.isPending,error:a.error,canAdmin:!0,ownerId:n?.ownerId,scope:`workspace`,roles:s.data,authority:{admin:!0,capabilities:m},onChange:(e,t)=>c.mutateAsync({workspaceId:i,email:e,role:t})}),n&&(0,Q.jsx)(ve,{workspaceId:n.id}),(0,Q.jsxs)(`section`,{className:`pt-7`,children:[(0,Q.jsx)(`h3`,{children:_(`settings.workspaces.projects`)}),(0,Q.jsx)(O,{error:o.error}),o.isPending?(0,Q.jsx)(P,{}):o.data?.length?o.data.map(e=>(0,Q.jsx)($,{badge:e.visibility===`private`?_(`settings.workspaces.private`):_(`settings.members.workspaceAccess`),children:(0,Q.jsx)(G,{to:`/projects/$projectId/settings`,params:{projectId:e.id},search:{section:`members`},onClick:t,className:`text-primary`,children:e.name})},e.id)):(0,Q.jsx)(C,{title:_(`settings.workspaces.noProjects`),className:`py-5`})]})]},n?.id)]})})}var Ze={queryKey:[`kanvil`,`instance`],queryFn:()=>T.request(`/api/v1/admin/instance`,h)};function Qe({section:e}){let t=V({...Ze,refetchOnMount:`always`,refetchInterval:[`search`,`storage`,`events`].includes(e)?5e3:!1});return t.data&&t.isFetchedAfterMount?(0,Q.jsx)(et,{data:t.data,section:e},e):t.isPending||t.isFetching?(0,Q.jsx)(P,{}):(0,Q.jsx)(O,{error:t.error})}function $e(e){return{expectedVersion:e.version,authentication:e.authentication,name:e.name,description:e.description,storageProvider:e.storage.provider,s3:{endpoint:``,region:`us-east-1`,bucket:``,prefix:`kanvil/`,forcePathStyle:!0,accessKeyId:``,...e.storage.settings,secretAccessKey:``},brokerProvider:e.broker.provider,rabbit:{url:``,exchange:e.broker.settings?.exchange??`kanvil.events`},searchProvider:e.search.provider,elastic:{url:``,index:`kanvil-${e.id}`,username:``,...e.search.settings,auth:e.search.settings?.auth??`none`,apiKey:``,password:``}}}function et({data:e,section:t}){let n=U(),i=re(),[a,o]=(0,Z.useState)(()=>$e(e)),[c,l]=(0,Z.useState)(!1),[u,p]=(0,Z.useState)(),[m,h]=(0,Z.useState)(``),[g,y]=(0,Z.useState)(!1),b=e.storage.source===`deployment`,x=e.search.source===`deployment`,S=e.broker.source===`deployment`,C=t===`storage`?b:t===`search`?x:t===`events`&&S,w=e=>o(t=>({...t,s3:{...t.s3,...e}})),E=e=>o(t=>({...t,elastic:{...t.elastic,...e}}));async function D(e,t,n=!1){p(void 0),h(``),l(!0);try{await e(),n?k.success(t,{id:`instance-settings`}):h(t)}catch(e){p(e)}finally{l(!1)}}async function M(){let e=d.parse({expectedVersion:a.expectedVersion,...t===`authentication`?{authentication:a.authentication}:{},name:a.name,description:a.description,storage:a.storageProvider===`s3`?{provider:`s3`,...a.s3}:{provider:`local`},broker:a.brokerProvider===`rabbitmq`?{provider:`rabbitmq`,...a.rabbit,url:a.rabbit.url||void 0}:{provider:`postgres`},search:a.searchProvider===`elasticsearch`?{provider:`elasticsearch`,...a.elastic}:{provider:`postgres`}});await T.request(`/api/v1/admin/instance`,v,{method:`PATCH`,headers:{"Content-Type":`application/json`},body:JSON.stringify(e)}),o(e=>({...e,expectedVersion:e.expectedVersion+1,s3:{...e.s3,secretAccessKey:``},elastic:{...e.elastic,apiKey:``,password:``},rabbit:{...e.rabbit,url:``}})),await n.invalidateQueries({queryKey:[`kanvil`,`instance`]}),await n.invalidateQueries({queryKey:[`info`]}),y(!1)}return(0,Q.jsxs)(ke,{title:_(`admin.instance.title`),section:t,children:[[`general`,`storage`,`search`,`events`].map(t=>(0,Q.jsx)(K,{value:t,children:(0,Q.jsx)(Re,{data:e,section:t,onConfigure:()=>{o($e(e)),p(void 0),h(``),y(!0)}})},t)),!g&&t!==`authentication`&&(0,Q.jsx)(O,{error:u}),(0,Q.jsx)(ne,{title:_(t===`general`?`admin.instance.edit`:t===`storage`?`admin.configure.storageTitle`:t===`search`?`admin.configure.searchTitle`:`admin.configure.eventsTitle`),open:g,onOpenChange:y,busy:c,children:(0,Q.jsxs)(`form`,{className:`[&_section]:py-0 [&>[data-slot=field-set]>section:first-child]:pt-0`,onSubmit:e=>{e.preventDefault(),C||D(M,_(`admin.configure.saved`),!0)},children:[(0,Q.jsxs)(L,{disabled:c,className:`gap-0`,children:[t===`general`&&(0,Q.jsx)(Q.Fragment,{children:(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(`h2`,{children:_(`admin.instance.heading`)}),(0,Q.jsx)(F,{label:_(`admin.configure.instanceName`),required:!0,maxLength:100,value:a.name,onChange:e=>o({...a,name:e.target.value})}),(0,Q.jsx)(ce,{label:_(`admin.configure.instanceDescription`),maxLength:500,value:a.description,onChange:e=>o({...a,description:e.target.value})}),(0,Q.jsx)(j,{label:_(`admin.instance.publicUrl`),value:e.deployment.url}),(0,Q.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:_(`admin.configure.publicUrlSource`)})]})}),t===`storage`&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsxs)(`section`,{children:[(0,Q.jsxs)(`div`,{className:`mb-5 flex items-center gap-3`,children:[(0,Q.jsx)(`h2`,{className:`mb-0`,children:_(`admin.instance.section.storage`)}),(0,Q.jsx)(H,{variant:`secondary`,children:_(b?`admin.instance.deploymentManaged`:`admin.instance.instanceManaged`)})]}),(0,Q.jsx)(`p`,{children:_(`admin.configure.storageIntro`)}),(0,Q.jsxs)(I,{label:_(`admin.configure.storageProvider`),disabled:b,value:a.storageProvider,onValueChange:e=>o({...a,storageProvider:e===`s3`?`s3`:`local`}),children:[(0,Q.jsx)(A,{value:`local`,children:_(`admin.instance.provider.local`)}),(0,Q.jsx)(A,{value:`s3`,children:_(`admin.instance.provider.s3`)})]}),a.storageProvider===`local`?(0,Q.jsx)(`p`,{children:_(`admin.configure.localDescription`)}):(0,Q.jsxs)(L,{disabled:b,className:`gap-0`,children:[(0,Q.jsx)(F,{label:_(`admin.configure.s3Endpoint`),type:`url`,placeholder:`https://s3.example.com`,value:a.s3.endpoint,onChange:e=>w({endpoint:e.target.value})}),(0,Q.jsx)(`p`,{className:`-mt-2 text-xs`,children:_(`admin.configure.s3EndpointHint`)}),(0,Q.jsxs)(`div`,{className:`grid grid-cols-1 gap-x-5 sm:grid-cols-2`,children:[(0,Q.jsx)(F,{label:_(`admin.instance.bucket`),value:a.s3.bucket,required:!0,onChange:e=>w({bucket:e.target.value})}),(0,Q.jsx)(F,{label:_(`admin.configure.region`),value:a.s3.region,required:!0,onChange:e=>w({region:e.target.value})})]}),(0,Q.jsx)(F,{label:_(`admin.configure.prefix`),value:a.s3.prefix,onChange:e=>w({prefix:e.target.value})}),(0,Q.jsx)(F,{label:_(`admin.configure.accessKeyId`),autoComplete:`off`,value:a.s3.accessKeyId,onChange:e=>w({accessKeyId:e.target.value})}),(0,Q.jsx)(F,{label:_(`admin.configure.secretAccessKey`),type:`password`,autoComplete:`new-password`,value:a.s3.secretAccessKey,placeholder:e.storage.secretConfigured?_(`admin.configure.savedSecret`):_(`admin.configure.deploymentCredentials`),onChange:e=>w({secretAccessKey:e.target.value})}),(0,Q.jsx)(te,{label:_(`admin.configure.pathStyle`),checked:a.s3.forcePathStyle,onCheckedChange:e=>w({forcePathStyle:e})})]}),a.storageProvider===`s3`&&(0,Q.jsx)(R,{disabled:c,onClick:()=>{D(()=>T.request(`/api/v1/admin/storage/test`,v,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(a.s3)}),_(`admin.configure.storageVerified`))},children:_(`admin.configure.testStorage`)}),(0,Q.jsx)(`p`,{className:`mt-4 text-xs`,role:`status`,children:e.storage.pending?_(`admin.configure.filesWaiting`,{count:e.storage.pending}):_(`admin.configure.transfersComplete`)}),e.storage.error&&(0,Q.jsx)(O,{error:Error(e.storage.error)}),b&&(0,Q.jsx)(`p`,{className:`mt-4 text-xs`,children:_(`admin.configure.storageDeployment`)})]}),e.locations.length>0&&(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(`h2`,{children:_(`admin.configure.locations`)}),e.locations.map(e=>(0,Q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-3 border-b py-2.5 text-xs wrap-anywhere [&>:first-child]:min-w-24 [&>:first-child]:flex-1`,children:[(0,Q.jsxs)(`span`,{className:`grid gap-1`,children:[(0,Q.jsx)(`strong`,{children:e.bucket}),(0,Q.jsxs)(`span`,{className:`text-muted-foreground`,children:[e.endpoint||`Amazon S3`,` / `,e.prefix]})]}),(0,Q.jsx)(`span`,{children:_(`admin.configure.locationFiles`,{count:e.objects})})]},e.id)),(0,Q.jsx)(`div`,{className:`mt-4`,children:(0,Q.jsx)(N,{label:_(`admin.configure.cleanUp`),description:_(`admin.configure.cleanUpConfirm`),onConfirm:async()=>{await T.request(`/api/v1/admin/storage/cleanup`,s({removed:r()}),{method:`POST`}),await n.invalidateQueries({queryKey:[`kanvil`,`instance`]})}})}),(0,Q.jsx)(`p`,{className:`mt-3 text-xs`,children:_(`admin.configure.cleanUpNote`)})]})]}),t===`search`&&(0,Q.jsx)(Q.Fragment,{children:(0,Q.jsxs)(`section`,{children:[(0,Q.jsxs)(`div`,{className:`mb-5 flex items-center gap-3`,children:[(0,Q.jsx)(`h2`,{className:`mb-0`,children:_(`admin.instance.section.search`)}),(0,Q.jsx)(H,{variant:`secondary`,children:e.search.status===`builtin`?`PostgreSQL`:_(`admin.instance.status.${e.search.status}`)})]}),(0,Q.jsxs)(I,{label:_(`admin.configure.searchProvider`),disabled:x,value:a.searchProvider,onValueChange:e=>o({...a,searchProvider:e===`elasticsearch`?`elasticsearch`:`postgres`}),children:[(0,Q.jsx)(A,{value:`postgres`,children:_(`admin.configure.searchBuiltIn`)}),(0,Q.jsx)(A,{value:`elasticsearch`,children:_(`admin.instance.provider.elasticsearch`)})]}),a.searchProvider===`elasticsearch`&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsxs)(L,{disabled:x,className:`gap-0`,children:[(0,Q.jsx)(F,{label:_(`admin.instance.searchEndpoint`),type:`url`,required:!0,value:a.elastic.url,onChange:e=>E({url:e.target.value})}),(0,Q.jsx)(F,{label:_(`admin.configure.indexName`),required:!0,value:a.elastic.index,onChange:e=>E({index:e.target.value})}),(0,Q.jsxs)(I,{label:_(`admin.configure.searchAuthentication`),value:a.elastic.auth,onValueChange:e=>E({auth:ee.parse(e)}),children:[(0,Q.jsx)(A,{value:`none`,children:_(`admin.configure.noAuthentication`)}),(0,Q.jsx)(A,{value:`apiKey`,children:_(`admin.configure.apiKey`)}),(0,Q.jsx)(A,{value:`basic`,children:_(`admin.configure.basic`)})]}),a.elastic.auth===`apiKey`&&(0,Q.jsx)(F,{label:_(`admin.configure.searchApiKey`),type:`password`,autoComplete:`new-password`,placeholder:e.search.secretConfigured?_(`admin.configure.savedSecret`):_(`admin.configure.optional`),value:a.elastic.apiKey,onChange:e=>E({apiKey:e.target.value})}),a.elastic.auth===`basic`&&(0,Q.jsxs)(`div`,{className:`grid grid-cols-1 gap-x-5 sm:grid-cols-2`,children:[(0,Q.jsx)(F,{label:_(`admin.configure.searchUsername`),autoComplete:`off`,value:a.elastic.username,onChange:e=>E({username:e.target.value})}),(0,Q.jsx)(F,{label:_(`admin.configure.searchPassword`),type:`password`,autoComplete:`new-password`,value:a.elastic.password,onChange:e=>E({password:e.target.value})})]})]}),(0,Q.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,Q.jsx)(R,{onClick:()=>{D(()=>T.request(`/api/v1/admin/search/test`,v,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify(a.elastic)}),_(`admin.configure.searchVerified`))},children:_(`admin.configure.testSearch`)}),e.search.provider===`elasticsearch`&&(0,Q.jsx)(R,{onClick:()=>{D(()=>T.request(`/api/v1/admin/search/reindex`,v,{method:`POST`}),_(`admin.configure.rebuildQueued`))},children:_(`admin.configure.rebuild`)})]})]}),(0,Q.jsx)(`p`,{className:`mt-5`,children:_(`admin.configure.searchNote`)}),e.search.error&&(0,Q.jsx)(O,{error:Error(e.search.error)})]})}),t===`events`&&(0,Q.jsx)(Q.Fragment,{children:(0,Q.jsxs)(`section`,{children:[(0,Q.jsxs)(`div`,{className:`mb-5 flex items-center gap-3`,children:[(0,Q.jsx)(`h2`,{className:`mb-0`,children:_(`admin.instance.section.events`)}),(0,Q.jsx)(H,{variant:`secondary`,children:e.broker.status===`builtin`?`PostgreSQL`:_(`admin.instance.status.${e.broker.status}`)})]}),(0,Q.jsxs)(I,{label:_(`admin.configure.brokerProvider`),disabled:S,value:a.brokerProvider,onValueChange:e=>o({...a,brokerProvider:e===`rabbitmq`?`rabbitmq`:`postgres`}),children:[(0,Q.jsx)(A,{value:`postgres`,children:_(`admin.configure.brokerBuiltIn`)}),(0,Q.jsx)(A,{value:`rabbitmq`,children:`RabbitMQ`})]}),a.brokerProvider===`rabbitmq`&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsxs)(L,{disabled:S,className:`gap-0`,children:[(0,Q.jsx)(F,{label:_(`admin.configure.rabbitUrl`),type:`password`,autoComplete:`new-password`,value:a.rabbit.url,placeholder:e.broker.secretConfigured?_(`admin.configure.savedSecret`):`amqps://user:password@host/vhost`,onChange:e=>o({...a,rabbit:{...a.rabbit,url:e.target.value}})}),e.broker.settings&&(0,Q.jsx)(j,{label:_(`admin.configure.savedBrokerEndpoint`),value:e.broker.settings.endpoint}),(0,Q.jsx)(F,{label:_(`admin.configure.rabbitExchange`),required:!0,value:a.rabbit.exchange,onChange:e=>o({...a,rabbit:{...a.rabbit,exchange:e.target.value}})})]}),(0,Q.jsx)(R,{onClick:()=>{D(()=>T.request(`/api/v1/admin/broker/test`,v,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({...a.rabbit,url:a.rabbit.url||void 0})}),_(`admin.configure.brokerVerified`))},children:_(`admin.configure.testBroker`)})]}),(0,Q.jsx)(`p`,{className:`mt-5`,children:_(`admin.configure.brokerNote`)}),S&&(0,Q.jsx)(`p`,{className:`mt-4 text-xs`,children:_(`admin.configure.brokerDeployment`)}),e.broker.error&&(0,Q.jsx)(O,{error:Error(e.broker.error)})]})})]}),[`general`,`storage`,`search`,`events`].includes(t)&&(0,Q.jsxs)(`div`,{className:`sticky bottom-0 mt-6 flex flex-wrap items-center gap-3 border-t bg-background/95 py-4 backdrop-blur`,children:[(0,Q.jsx)(R,{disabled:c,onClick:()=>y(!1),children:_(C?`common.action.done`:`common.action.cancel`)}),!C&&(0,Q.jsx)(R,{variant:`default`,type:`submit`,disabled:c,children:_(c?`common.state.saving`:`admin.configure.save`)}),m&&(0,Q.jsx)(`span`,{role:`status`,className:`text-sm text-muted-foreground`,children:m}),(0,Q.jsx)(O,{error:u})]})]})}),(0,Q.jsx)(K,{value:`users`,children:(0,Q.jsx)(qe,{})}),(0,Q.jsx)(K,{value:`workspaces`,children:(0,Q.jsx)(Ye,{})}),(0,Q.jsx)(K,{value:`authentication`,children:(0,Q.jsxs)(`section`,{children:[(0,Q.jsx)(z,{title:_(`admin.authentication.title`)}),(0,Q.jsxs)(`form`,{className:`mb-6`,onSubmit:e=>{e.preventDefault(),D(M,_(`admin.authentication.saved`),!0)},children:[(0,Q.jsx)(q,{title:_(`admin.authentication.emailPassword`),description:_(`admin.authentication.emailPasswordDescription`),children:(0,Q.jsx)(Ae,{"aria-label":_(`admin.authentication.emailPassword`),checked:a.authentication.emailPasswordEnabled,disabled:c,onCheckedChange:e=>o(t=>({...t,authentication:{...t.authentication,emailPasswordEnabled:e}}))})}),(0,Q.jsx)(q,{title:_(`admin.authentication.ssoProvision`),description:_(`admin.authentication.ssoProvisionDescription`),children:(0,Q.jsx)(Ae,{"aria-label":_(`admin.authentication.ssoProvision`),checked:a.authentication.ssoAutoProvision,disabled:c,onCheckedChange:e=>o(t=>({...t,authentication:{...t.authentication,ssoAutoProvision:e}}))})}),(0,Q.jsx)(q,{title:_(`admin.authentication.registration`),children:_(`admin.authentication.registrationClosed`)}),(0,Q.jsx)(q,{title:_(`admin.authentication.identityProvider`),children:e.deployment.oidc?.name??_(`admin.authentication.notConfigured`)}),(0,Q.jsx)(q,{title:_(`admin.authentication.linkedProviders`),description:_(`admin.authentication.linkedProvidersDescription`),children:f(i.data?.providers.filter(e=>e.id!==`oidc`).map(e=>e.name)??[])||_(`admin.authentication.notConfigured`)}),(0,Q.jsx)(B,{layout:`inline`,error:u,pending:c,submitLabel:_(`admin.authentication.save`),pendingLabel:_(`common.state.saving`),submitDisabled:a.authentication.emailPasswordEnabled===e.authentication.emailPasswordEnabled&&a.authentication.ssoAutoProvision===e.authentication.ssoAutoProvision})]}),e.deployment.oidc&&(0,Q.jsxs)(Q.Fragment,{children:[(0,Q.jsx)(j,{label:_(`admin.authentication.discoveryUrl`),value:e.deployment.oidc.discoveryUrl}),(0,Q.jsx)(j,{label:_(`admin.authentication.clientId`),value:e.deployment.oidc.clientId})]}),(0,Q.jsx)(`p`,{children:_(`admin.authentication.oidcSetup`)}),(0,Q.jsx)(`h3`,{children:_(`admin.authentication.consumers`)}),(0,Q.jsx)(j,{label:`HTTP API`,value:`${e.deployment.url}/api/v1`}),(0,Q.jsx)(j,{label:_(`admin.authentication.mcpEndpoint`),value:`${e.deployment.url}/mcp`}),(0,Q.jsx)(j,{label:_(`admin.authentication.eventFeed`),value:`${e.deployment.url}/api/v1/events`}),(0,Q.jsxs)(`a`,{className:`inline-flex items-center gap-2 text-primary`,href:`/api/openapi.json`,target:`_blank`,rel:`noreferrer`,children:[_(`admin.authentication.openApi`),(0,Q.jsx)(De,{className:`size-4`})]})]})})]})}export{Qe as AdminSettings};
|