const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/Dashboard-DOlOdjSg.js","assets/mui-G83wUovH.js","assets/rolldown-runtime-CRqgUL4m.js","assets/react-Xw0SfO6v.js","assets/query-C7e42IJc.js","assets/tons-DHETtabP.js","assets/ThreadsList-_9U4Anr1.js","assets/ConfirmDialog-CEBDGERF.js","assets/use-sort-0yBtcPey.js","assets/row-open-BZlCkerT.js","assets/GuardrailsBanner-CnpcsuFG.js","assets/StatusChip-p89yfCBf.js","assets/entity-labels-C7UDwy34.js","assets/ThreadDetail-CRhDIpJl.js","assets/code-block-ufhUm_Gb.js","assets/navigation-CGll2Ai_.js","assets/markdown-0xCMWfdq.js","assets/Sites-krIouanZ.js","assets/SiteActivationWizard-CATpuide.js","assets/config-6I525voS.js","assets/PasteTargetToggle-CeCsY82E.js","assets/SiteEdit-CBaS2EEi.js","assets/emplacements-BXlqES-H.js","assets/Notifications-BR_V_ERL.js","assets/useExtensions-xAf5HddT.js","assets/IntegrationDialog-Bjziiwvh.js","assets/MarkdownRenderer-BLRQfopv.js","assets/react-markdown-B14r9zwv.js","assets/lib-C0z8MQMK.js","assets/remark-gfm-Blsdwsuv.js","assets/Connectors-C4587B5M.js","assets/TasksList-DaH4PKZ4.js","assets/ChampRecherche-CCXArAqY.js","assets/CopyButton-I4V-Yrnc.js","assets/TaskDetail-y0hkUr2N.js","assets/SearchResults-CV1kU22f.js","assets/History-DOibMUol.js","assets/LlmTokens-CzVr4WCD.js","assets/AgentSetupDialog-Bw983kFY.js","assets/LlmUsage-Cue3bOb4.js","assets/DocsPage-DVVXACju.js","assets/Members-D5jBjdhR.js","assets/Billing-CjwNK_Ga.js","assets/Reglages-C-B9GoJT.js","assets/Login-CVbuzoBF.js","assets/LiensLegaux-DfSWTPz9.js","assets/Signup-B0-sT1IM.js","assets/NotFound-_j5RqBgA.js","assets/AcceptInvite-DMeccGBj.js","assets/ForgotPassword-D1Wd2zXQ.js","assets/ResetPassword-RzwcXm1M.js","assets/VerifyEmail-Cp6CYZ-F.js"])))=>i.map(i=>d[i]);
import{r as Qt}from"./rolldown-runtime-CRqgUL4m.js";import{$n as Yt,$t as Xt,An as ze,Cn as Ve,Dn as pe,En as he,Fn as Ee,Gn as Zt,Gt as en,Hn as jt,Ht as Ae,In as N,Jn as tn,Jt as nn,Kn as an,Kt as sn,Ln as z,Mn as q,Nn as Pt,Pn as on,Qn as rn,Qt as dn,Rn as se,Sn as te,Tn as ge,Un as ln,Ut as Re,Vn as L,Wn as cn,Wt as un,Xn as mn,Xt as Ge,Yn as pn,Yt as hn,Zn as gn,Zt as qe,_n as ce,an as bn,ar as C,bn as Q,cn as fn,cr as vn,dn as kn,en as yn,er as Et,fn as Cn,fr as wn,gn as $e,hn as Tn,in as xn,ir as At,kn as Ke,ln as Sn,lr as jn,mn as we,mr as Pn,nn as En,nr as An,on as Je,or as Dn,pn as Y,qn as Ln,qt as In,rn as Qe,rr as Dt,sn as _n,sr as Rn,tn as qn,tr as Mn,un as On,ur as Bn,vn as Hn,wn as Un,xn as Lt,yn as Nn,zn as M}from"./mui-G83wUovH.js";import{a as Me,c as Wn,d as W,f as ke,h as Fn,i as _,l as zn,n as oe,o as m,r as Ye,s as Vn,t as Gn}from"./react-Xw0SfO6v.js";import{a as $n,i as Kn,n as G,o as Jn,r as Qn,s as ye,t as It}from"./query-C7e42IJc.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))a(o);new MutationObserver(o=>{for(const r of o)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&a(l)}).observe(document,{childList:!0,subtree:!0});function i(o){const r={};return o.integrity&&(r.integrity=o.integrity),o.referrerPolicy&&(r.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?r.credentials="include":o.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function a(o){if(o.ep)return;o.ep=!0;const r=i(o);fetch(o.href,r)}})();var d=Qt(Pn(),1),Yn=Fn(),Xn="Source UNIQUE des couleurs de BipWire (ADR-0071) : la SPA (app + console) et la vitrine les lisent ici, exposées partout en variables CSS --bw-<jeton-en-kebab>. Thème « Pétrole profond », choisi par Arnaud le 2026-10-07. Contrastes gardés par test/design-tokens.test.js.",Zn="petrole",ei={primary:"#0f6e6e",primaryHover:"#0b5858",onPrimary:"#ffffff",accent:"#d97706",background:"#f5f8f8",paper:"#ffffff",sidebar:"#edf3f3",sidebarSelected:"#d5eaea",sidebarSelectedText:"#0a4b4b",voletHeader:"#d5e3e3",voletHeaderText:"#0d2e2e",voletHeaderTextSecondary:"#4a5d5d",iconTile:"#d5eaea",iconTileFg:"#0b5858",text:"#122222",textSecondary:"#4a5d5d",border:"#dce6e6",success:"#4d7c0f",error:"#b42318",warning:"#b54708"},ti={primary:"#3cc4bc",primaryHover:"#5fd6cf",onPrimary:"#062a28",accent:"#fbbf24",background:"#0b1414",paper:"#111d1d",sidebar:"#0e1818",sidebarSelected:"#163434",sidebarSelectedText:"#a7ece7",voletHeader:"#203535",voletHeaderText:"#e3f1f0",voletHeaderTextSecondary:"#a6bcba",iconTile:"#1a3838",iconTileFg:"#7fe0d9",text:"#e8f2f1",textSecondary:"#9db3b1",border:"#223333",success:"#a3e635",error:"#f97066",warning:"#fdb022"},_t={$commentaire:Xn,nom:Zn,light:ei,dark:ti},Rt=e=>e.replace(/[A-Z]/g,n=>"-"+n.toLowerCase()),me=e=>`var(--bw-${Rt(e)})`;function ni(e){const n={};for(const[i,a]of Object.entries(_t[e]))n[`--bw-${Rt(i)}`]=a;return n}var ii=(e,n)=>_t[e][n],ai={typography:{fontFamily:'Roboto, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',h1:{fontWeight:500},h2:{fontWeight:500},h3:{fontWeight:500},h4:{fontWeight:500},h5:{fontWeight:500},h6:{fontWeight:500}},shape:{borderRadius:8}};function si(e){const n=i=>ii(e,i);return Bn({...ai,palette:{mode:e,primary:{main:n("primary"),dark:n("primaryHover"),contrastText:n("onPrimary")},secondary:{main:n("accent")},success:{main:n("success")},warning:{main:n("warning")},error:{main:n("error")},info:{main:n("primary")},background:{default:n("background"),paper:n("paper")},text:{primary:n("text"),secondary:n("textSecondary")},divider:n("border"),action:{selected:n("sidebarSelected")}},components:{MuiButton:{defaultProps:{disableElevation:!0},styleOverrides:{root:{textTransform:"none",fontWeight:500}}},MuiPaper:{defaultProps:{elevation:0},styleOverrides:{root:{backgroundImage:"none",border:`1px solid ${n("border")}`}}},MuiDialog:{styleOverrides:{container:{"@media (max-width:599.95px)":{alignItems:"flex-start"}},paper:{"@media (max-width:599.95px)":{margin:0,marginTop:"var(--bw-decalage-visible, 0px)",width:"100%",maxWidth:"100%",height:"var(--bw-hauteur-visible, 100dvh)",maxHeight:"var(--bw-hauteur-visible, 100dvh)",borderRadius:0,border:0}}}},MuiDialogActions:{styleOverrides:{root:{"@media (max-width:599.95px)":{flexShrink:0}}}},MuiAppBar:{defaultProps:{elevation:0,color:"default"},styleOverrides:{root:{backgroundColor:n("paper"),borderBottom:`1px solid ${n("border")}`,backgroundImage:"none"}}},MuiDrawer:{styleOverrides:{paper:{backgroundColor:n("sidebar")}}},MuiListItemButton:{styleOverrides:{root:{"&.Mui-selected, &.Mui-selected:hover":{backgroundColor:n("sidebarSelected"),color:n("sidebarSelectedText")}}}},MuiToggleButton:{styleOverrides:{root:{"&.Mui-selected, &.Mui-selected:hover":{backgroundColor:n("primary"),color:n("onPrimary")},"@media (hover: hover)":{"&.Mui-selected:hover":{backgroundColor:n("primaryHover")}}}}},MuiSwitch:{defaultProps:{color:"success"}},MuiCheckbox:{styleOverrides:{root:{"&.MuiCheckbox-sizeSmall .MuiSvgIcon-root":{fontSize:24}}}},MuiCard:{defaultProps:{elevation:0},styleOverrides:{root:{backgroundImage:"none",border:`1px solid ${n("border")}`}}}}})}var t=wn(),be="bipwire-admin-theme-mode",qt=(0,d.createContext)(null);function oi(){if(typeof window>"u")return"dark";let e=null;try{e=window.localStorage.getItem(be)}catch{}return e==="light"||e==="dark"?e:window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark"}function ri(){try{return!!window.localStorage.getItem(be)}catch{return!1}}function di({children:e}){const[n,i]=(0,d.useState)(oi),[a,o]=(0,d.useState)(()=>!ri());(0,d.useEffect)(()=>{if(!a)return;const v=window.matchMedia("(prefers-color-scheme: light)"),b=()=>i(v.matches?"light":"dark");return b(),v.addEventListener("change",b),()=>v.removeEventListener("change",b)},[a]);const r=(0,d.useCallback)(v=>{try{window.localStorage.setItem(be,v)}catch{}o(!1),i(v)},[]),l=(0,d.useCallback)(()=>{try{window.localStorage.removeItem(be)}catch{}o(!0)},[]),h=(0,d.useCallback)(()=>{r(n==="dark"?"light":"dark")},[n,r]),p=(0,d.useMemo)(()=>si(n),[n]),w=(0,d.useMemo)(()=>({mode:n,toggle:h,setMode:r,suitLeSysteme:a,suivreLeSysteme:l}),[n,h,r,a,l]);return(0,t.jsx)(qt.Provider,{value:w,children:(0,t.jsxs)(Rn,{theme:p,children:[(0,t.jsx)(Dn,{}),(0,t.jsx)(vn,{styles:{":root":ni(n)}}),e]})})}function li(){const e=(0,d.useContext)(qt);if(!e)throw new Error("useThemeMode must be used within ThemeModeProvider");return e}var ci={"admin.specs.vikunja.projectId.help":"ID of the Vikunja project where issues are created (not a BipWire project).","admin.specs.vikunja.projectId.label":"Default Vikunja project","admin.specs.vikunja.projectMap.label":"Vikunja project per BipWire project","admin.specs.vikunja.projectMap.help":'Each BipWire project can send its tickets to its own Vikunja project. Left on "Default project", it uses the one above.',"admin.specs.vikunja.apiToken.help":"Vikunja API token (Settings → API Tokens). Required permissions: projects → read_all, tasks → create, tasks_attachments → create (without it, screenshots and attachments are not attached). Recommended: tasks_comments → create, so that a file rejected on upload is reported in the Vikunja task. Optional: other → user, to show which Vikunja account owns the token."},ui={"admin.specs.email.to.help":"Comma-separated. Sent via Resend.","admin.specs.email.to.label":"Recipients"},mi={"admin.specs.telegram.chatId.help":"BipWire's bot must be a member of the chat. Its name could not be read: ask support for it.","admin.specs.telegram.chatId.helpBot":"Add {bot} to the chat or group: the bot must be a member."},pi={"admin.specs.webhook.label":"Generic webhook","admin.specs.webhook.secret.help":"Optional. If set, the X-Bip-Signature header = HMAC-SHA256 of the body.","admin.specs.webhook.secret.label":"Secret (HMAC signature)"},hi={"admin.taskDetail.toFixDialog.confirmWithoutClaude":"Send back without Claude Code","admin.taskDetail.toFixDialog.withoutClaudeHelp":"The ticket goes back to “in progress” without resuming the Claude Code conversation launched on it.","admin.tasks.kanbanQueuedDrop":"Drop a ticket here to launch Claude Code","admin.tasks.kanbanQueuedReserved":"Reserved for the Claude Code queue","admin.errors.invalid_tasks":"Invalid ticket selection.","admin.errors.too_many_tasks":"Too many tickets for one launch (100 at most).","admin.errors.invalid_display_id":"Invalid ticket identifier.","admin.errors.mixed_sites":"A session covers a single project.","admin.errors.task_closed":"A closed ticket cannot be handed to an agent.","admin.errors.task_suspect":"A ticket flagged as suspicious must be cleared by a human before it is handed to an agent.","admin.reglages.tabClaudeCode":"Claude Code","admin.claudeCode.reportBugs":"Bugs found by the agent","admin.claudeCode.reportBugsToggle":"Report bugs found by Claude Code","admin.claudeCode.reportBugsHelp":"When Claude Code fixes a ticket launched from BipWire and runs into another bug, or something a human must decide, it can log it in BipWire itself instead of quietly fixing it or forgetting it. Off, it creates no ticket. This setting belongs to the account and covers every project; it applies to conversations launched after you change it.","admin.claudeCode.reportBugsExample":"The ticket created is a bug in the same project, with a title starting “[Remonté par l’agent — à trancher]” (“Reported by the agent — to decide”): it stands out in the list and waits for a human decision (fix, postpone, dismiss). The project’s notification channels are alerted like for any new ticket, and the note on the fixed ticket mentions what was logged.","admin.claudeCode.connection":"My remote-claude","admin.claudeCode.connectionHelp":"BipWire launches Claude Code through your remote-claude: it opens a Remote Control conversation there, on the chosen machine, in the project folder, with /bip-fix and your message. You then follow and manage it in remote-claude or on claude.ai/code. Create the token in remote-claude, key button “API tokens”.","admin.claudeCode.url":"remote-claude address","admin.claudeCode.token":"API token (rc_ext_…)","admin.claudeCode.tokenKeep":"{prefix} — leave empty to keep it","admin.claudeCode.connected":"remote-claude answers. Machines: {machines}.","admin.claudeCode.disconnect":"Disconnect","admin.claudeCode.folders":"Project folders","admin.claudeCode.foldersHelp":"Where each project lives on each machine. Without a mapping, Claude Code starts from ~/dev/<project>, and the folder chosen at launch is remembered here.","admin.claudeCode.noFolder":"No folder mapped yet.","admin.claudeCode.project":"Project","admin.claudeCode.machine":"Machine","admin.claudeCode.path":"Folder on the machine","admin.claudeCode.launch":"Launch Claude Code","admin.claudeCode.launchTitle":"Launch Claude Code","admin.claudeCode.launchConfirm":"Launch","admin.claudeCode.loadingMachines":"remote-claude is looking for your machines…","admin.claudeCode.noMachine":"remote-claude knows no machine. Add one in remote-claude.","admin.claudeCode.offline":"offline","admin.claudeCode.folder":"“{project}” project folder","admin.claudeCode.folderHelp":"On the chosen machine; ~ is its home folder. Remembered for next time.","admin.claudeCode.command":"Command sent to Claude","admin.claudeCode.message":"Your message (optional)","admin.claudeCode.messagePlaceholder":"An extra instruction: where to start, what not to touch…","admin.claudeCode.launchedTitle":"Claude Code is running","admin.claudeCode.queueHelp":"Tickets become “queued”: one session per ticket, opened by BipWire as soon as a slot frees up ({next}).","admin.claudeCode.andMore_one":"{list} and 1 more","admin.claudeCode.andMore_other":"{list} and {n} more","admin.claudeCode.launchedQueueHelp":"{n} tickets queued on “{machine}”, in {path}. Claude sessions open one at a time, one per ticket: the next one as soon as the current ticket moves to “to test”. To take a ticket out of the queue, change its status.","admin.claudeCode.launchedHelp":"{tasks} on “{machine}”, in {path}. The conversation lives in remote-claude; it will stop when its tickets are resolved.","admin.claudeCode.openClaude":"Open on claude.ai","admin.claudeCode.openRemoteClaude":"Open in remote-claude","admin.claudeCode.openSession":"Ongoing Claude session","admin.claudeCode.sessionHelp":"Launched on “{machine}” on {date} — opens it in a new tab.","admin.claudeCode.resumeSession":"Resume Claude session","admin.claudeCode.sessionStoppedHelp":"Launched on “{machine}” on {date}, stopped on {stopped} when the ticket was done — opens it in remote-claude, where it resumes on the next message.","admin.claudeCode.noRemoteControl":"The Remote Control bridge could not open right away: open the conversation in remote-claude, it turns on in one click there.","admin.claudeCode.oneProjectOnly":"A session covers a single project: select tickets from the same project.","admin.claudeCode.closedTasks":"A closed ticket cannot be handed to Claude Code.","admin.errors.invalid_token":"Invalid token: it starts with rc_ext_.","admin.errors.invalid_machine":"Invalid machine.","admin.errors.invalid_path":"Invalid folder.","admin.errors.message_too_long":"Message too long (4,000 characters at most).","admin.errors.remote_not_configured":"Connect your remote-claude first: Settings → Claude Code.","admin.errors.remote_unauthorized":"remote-claude rejects the token: it was revoked or mistyped.","admin.errors.remote_unreachable":"remote-claude does not answer at this address.","admin.errors.remote_error":"remote-claude refused the request","admin.errors.secrets_unavailable":"Secret encryption is not configured on this server.","admin.claudeCode.model":"Model","admin.claudeCode.modelDefault":"Machine default","admin.claudeCode.humanValidates":"Whatever the option, Claude never closes a ticket: you validate.","admin.errors.invalid_options":"Invalid launch options.","admin.errors.invalid_branch":"Invalid branch name: letters, digits, “.”, “_”, “-” and “/”.","admin.claudeCode.launchedQueueTitle":"Tickets queued","admin.claudeCode.launchedQueueParallelHelp":"{n} tickets queued on “{machine}”, in {path}. Up to {parallel} Claude sessions run at the same time, one per ticket, each in its own worktree. To take a ticket out of the queue, change its status.","admin.claudeCode.parallel":"Parallel sessions","admin.errors.remote_forbidden_address":"This address is not public: remote-claude must be reachable from the Internet.","admin.errors.remote_timeout":"remote-claude did not answer in time.","admin.claudeCode.delivery.confirm":"Launch","admin.claudeCode.delivery.pending":"Opening the session…","admin.claudeCode.delivery.requested":"Last delivery requested on {date}.","admin.errors.task_not_ready":"Only a ticket “to test” can be merged or deployed.","admin.errors.no_delivery":"Nothing to deliver for this ticket: its Claude Code conversation did not leave it on its branch, or its environment no longer exists.","admin.claudeCode.blockWhere":"Where","admin.claudeCode.blockHow":"How","admin.claudeCode.blockDelivery":"Delivery","admin.claudeCode.browse":"Browse the machine’s folders","admin.claudeCode.browseTitle":"Choose the project folder","admin.claudeCode.browseHome":"Home folder","admin.claudeCode.browseUp":"Parent folder","admin.claudeCode.browseFilter":"Filter","admin.claudeCode.browseHidden":"Hidden folders","admin.claudeCode.browseEmpty":"No subfolder.","admin.claudeCode.browseChoose":"Choose this folder","admin.claudeCode.modeDiscussion":"Chat with Claude","admin.claudeCode.modeAutonome":"Full autonomy","admin.claudeCode.modeDiscussionHelp":"The session is followed live: Claude presents its analysis and the proposed fix, then waits for approval before changing anything. It may ask questions and request permissions. Delivery is decided in the conversation.","admin.claudeCode.modeAutonomeHelp":"No question, no permission request: Claude fixes, decides alone and writes its choices on the ticket.","admin.claudeCode.lotAutonomous":"A batch runs autonomously: one Claude session per ticket, with no question or permission request.","admin.claudeCode.sequence":"One after another","admin.claudeCode.parallelMode":"In parallel","admin.claudeCode.environment":"Environment","admin.claudeCode.environmentImplicit":"Production — {branch}","admin.claudeCode.environmentProduction":"Production","admin.claudeCode.mainBranch":"main branch","admin.claudeCode.whenDiscussion":"Delivery to “{env}” is decided in the conversation. Otherwise, once the ticket is “to test”, a button on it starts the delivery.","admin.claudeCode.when.maintenant":"Deliver now","admin.claudeCode.when.plus_tard":"Deliver later","admin.claudeCode.when.maintenantLot":"After each ticket","admin.claudeCode.when.finLot":"At the end of the batch","admin.claudeCode.when.plus_tardLot":"Later","admin.claudeCode.whenParallelNote":"In parallel, no session touches the environment’s branch: delivery happens at the end or later.","admin.claudeCode.whenHelp.maintenant":"Tests green: merge into {branch}, tag, then deploy “{env}” following the project’s procedure.","admin.claudeCode.whenHelp.plus_tard":"The ticket stays on its branch; once “to test”, a button on it delivers it to “{env}”.","admin.claudeCode.whenHelp.maintenantLot":"Each session merges its ticket into {branch}, tags and deploys “{env}” before the next one opens.","admin.claudeCode.whenHelp.finLot":"Each ticket is merged into a batch branch as it goes; at the end, a dedicated session merges it into {branch} and deploys “{env}” once.","admin.claudeCode.whenHelp.plus_tardLot":"Each ticket stays on its branch; once “to test”, a button on it delivers it to “{env}”.","admin.claudeCode.environments":"Delivery environments","admin.claudeCode.environmentsHelp":"The environments a project’s tickets are delivered to: a name (Beta, Staging, Prod…) and the branch a delivered ticket is merged into. Deployment follows the project’s written procedure (AGENTS.md or CLAUDE.md). The first one is the default.","admin.claudeCode.environmentName":"Name","admin.claudeCode.environmentBranch":"Branch","admin.claudeCode.environmentUp":"Move up","admin.claudeCode.environmentAdd":"Add an environment","admin.claudeCode.noEnvironment":"No environment: delivery goes to “Production”, on the repository’s main branch.","admin.claudeCode.environmentsSaved":"Environments saved.","admin.claudeCode.delivery.deliver":"Deliver to {env}","admin.claudeCode.delivery.deliverHelp":"A Claude Code session merges branch {branch} into {target}, tags, then deploys “{env}” following the project’s procedure.","admin.claudeCode.delivery.deliverConfirm":"Deliver the ticket to “{env}”? A Claude Code session opens autonomously on “{machine}”: it merges branch {branch} into {target}, tags and deploys. The ticket’s status does not change.","admin.claudeCode.deliveryState.planned":"Delivery planned · {env}","admin.claudeCode.deliveryState.plannedHelp":"Claude delivers the ticket to “{env}” as soon as tests pass: merge, tag, then deploy.","admin.claudeCode.deliveryState.delivered":"Delivered by AI · {env}","admin.claudeCode.deliveryState.deliveredHelp":"The Claude Code session delivered straight to “{env}”: it merged, tagged and deployed before moving the ticket to “to test”. Its note gives the commit and version; BipWire does not check the deployment.","admin.claudeCode.deliveryState.later":"To deliver · {env}","admin.claudeCode.deliveryState.laterHelp":"“Later” delivery: the fix stays on its branch. “Deliver to {env}”, on the ticket page once it is to test, starts it.","admin.claudeCode.deliveryState.batch":"Delivery at end of batch · {env}","admin.claudeCode.deliveryState.batchHelp":"The whole batch is delivered to “{env}” once all its tickets are done.","admin.claudeCode.deliveryState.requested":"Delivery requested · {env}","admin.claudeCode.deliveryState.requestedHelp":"Delivery to “{env}” requested on {date}: a Claude Code session merges, tags and deploys. Its note on the ticket gives the outcome.","admin.errors.invalid_environment":"Invalid environment: a unique name of 1 to 40 characters and a branch, at most 10 per project.","admin.siteEdit.sectionClaudeCode":"Claude Code","admin.siteEdit.claudeCodeSubtitle":"The project’s folder on each machine and the environments its tickets are delivered to. Saved separately, block by block.","admin.siteEdit.summaryClaudeCode":"Folder: {folders} · Delivery: {environments}","admin.claudeCode.projectFoldersHelp":"Where this project lives on each machine. Without a mapping, Claude Code starts from ~/dev/<project>, and the folder chosen at launch is remembered here.","admin.claudeCode.auto.title":"Automatic fixing","admin.claudeCode.auto.help":"BipWire hands eligible tickets of automated projects to Claude Code on its own, one at a time per project, through the remote-claude of a responsible member. It never pushes to the main branch, never tags and never deploys. Each project's policy is set in its Claude Code panel.","admin.claudeCode.auto.readOnly":"Only owners and administrators of the organization can change these settings.","admin.claudeCode.auto.orgEnabled":"Automatic fixing on for the organization","admin.claudeCode.auto.concurrent":"Simultaneous automatic tickets","admin.claudeCode.auto.concurrentHelp":"Across the whole organization, at most one per project.","admin.claudeCode.auto.business":"Daily report in business","admin.claudeCode.auto.businessHelp":"Every evening at 8 pm (Paris time), the day's summary is posted as a comment on a business.pop.run task. Leave the address empty to send nothing.","admin.claudeCode.auto.businessUrl":"Business address","admin.claudeCode.auto.businessTask":"Task commented on","admin.claudeCode.auto.businessToken":"API key","admin.claudeCode.auto.businessTokenKeep":"Key saved — leave empty to keep it","admin.claudeCode.auto.stop":"Emergency stop","admin.claudeCode.auto.resume":"Lift the emergency stop","admin.claudeCode.auto.stopConfirm":"Stop every automatic session of the organization right now and empty their queue?","admin.claudeCode.auto.stopped":"Emergency stop since {at}: nothing goes out until it is lifted.","admin.claudeCode.auto.stoppedShort":"Emergency stop in progress: nothing goes out.","admin.claudeCode.auto.projectTitle":"Automatic fixing","admin.claudeCode.auto.projectHelp":"This project's policy: which tickets the agent takes, when, and how it delivers. The first save turns on trial mode.","admin.claudeCode.auto.orgDisabled":"Automatic fixing is off for the organization (Settings → Claude Code).","admin.claudeCode.auto.suspended":"Project suspended ({reason}): save the settings to resume.","admin.claudeCode.auto.today":"Today: {taken} taken out of {cap}, {delivered} delivered, {failed} failed, {skipped} skipped, cost ${cost}.","admin.claudeCode.auto.enabled":"Automate this project","admin.claudeCode.auto.dryRun":"Trial mode: push the branch only, merge nothing","admin.claudeCode.auto.responsible":"Responsible member","admin.claudeCode.auto.responsibleHelp":"The member whose remote-claude opens the sessions.","admin.claudeCode.auto.folder":"Machine and folder","admin.claudeCode.auto.folderHelp":"A folder declared above for the responsible member.","admin.claudeCode.auto.triggers":"Triggers","admin.claudeCode.auto.trigger.creation":"When a ticket is created","admin.claudeCode.auto.trigger.planifie":"Continuously (new tickets)","admin.claudeCode.auto.trigger.manuel":'On request ("Hand to the automatic agent")',"admin.claudeCode.auto.triggerShort.creation":"creation","admin.claudeCode.auto.triggerShort.planifie":"scheduled","admin.claudeCode.auto.triggerShort.manuel":"manual","admin.claudeCode.auto.triggerShort.reprise":"resumed","admin.claudeCode.auto.types":"Types","admin.claudeCode.auto.type.bug":"Bugs","admin.claudeCode.auto.type.feature":"Suggestions","admin.claudeCode.auto.minPriority":"Minimum priority","admin.claudeCode.auto.priority.low":"Low","admin.claudeCode.auto.priority.medium":"Medium","admin.claudeCode.auto.priority.high":"High","admin.claudeCode.auto.priority.critical":"Critical","admin.claudeCode.auto.dailyCap":"Tickets per day","admin.claudeCode.auto.maxMinutes":"Maximum duration (min)","admin.claudeCode.auto.maxAttempts":"Attempts per ticket","admin.claudeCode.auto.model":"Model","admin.claudeCode.auto.modelMachine":"The machine's own","admin.claudeCode.auto.retryRejected":'Take back a ticket sent back "to fix"',"admin.claudeCode.auto.testCommand":"Test command","admin.claudeCode.auto.testCommandHelp":"Empty: the tests described by the repository's AGENTS.md.","admin.claudeCode.auto.testCommandInvalid":"Letters, digits, spaces and . _ / : = @ + - only.","admin.claudeCode.auto.baseBranch":"Base branch","admin.claudeCode.auto.baseBranchMain":"main branch","admin.claudeCode.auto.delivery":"Delivery","admin.claudeCode.auto.deliveryBranch":"Push the branch","admin.claudeCode.auto.deliveryMerge":"Merge into a target branch","admin.claudeCode.auto.targetBranch":"Target branch","admin.claudeCode.auto.deliveryHelp":"Never the main branch nor a production environment's branch, never a tag nor a deployment. The merge only happens when the tests pass, without conflict.","admin.claudeCode.auto.hoursAlways":"At any time","admin.claudeCode.auto.hoursFrom":"From","admin.claudeCode.auto.hoursTo":"To","admin.claudeCode.auto.hoursHelp":"Paris time. A ticket handed over manually goes out outside the slot.","admin.claudeCode.auto.day.1":"Mon","admin.claudeCode.auto.day.2":"Tue","admin.claudeCode.auto.day.3":"Wed","admin.claudeCode.auto.day.4":"Thu","admin.claudeCode.auto.day.5":"Fri","admin.claudeCode.auto.day.6":"Sat","admin.claudeCode.auto.day.7":"Sun","admin.claudeCode.auto.notify":"Notifications","admin.claudeCode.auto.notif.start":"Taken","admin.claudeCode.auto.notif.success":'Delivered "to test"',"admin.claudeCode.auto.notif.failure":"Failure","admin.claudeCode.auto.notif.skipped":"Ticket skipped","admin.claudeCode.auto.notif.daily":"Daily report","admin.claudeCode.auto.notifyHelp":"To the organization's notification channels and subscribed members (Settings → Notifications).","admin.claudeCode.auto.saved":"Settings saved.","admin.claudeCode.auto.journal":"Latest runs","admin.claudeCode.auto.journalEmpty":"No run yet.","admin.claudeCode.auto.ticketJournal":"Automatic agent","admin.claudeCode.auto.attempt":"attempt {n}","admin.claudeCode.auto.testsOk":"tests green","admin.claudeCode.auto.testsKo":"tests failing","admin.claudeCode.auto.merged":"merged into {branch}","admin.claudeCode.auto.dryRunShort":"trial","admin.claudeCode.auto.showOutput":"Output","admin.claudeCode.auto.hideOutput":"Hide","admin.claudeCode.auto.noOutput":"No output reported by remote-claude.","admin.claudeCode.auto.state.en_file":"Queued","admin.claudeCode.auto.state.en_cours":"Running","admin.claudeCode.auto.state.livre":"Delivered","admin.claudeCode.auto.state.echec":"Failed","admin.claudeCode.auto.state.timeout":"Timed out","admin.claudeCode.auto.state.saute":"Skipped","admin.claudeCode.auto.state.arrete":"Stopped","admin.claudeCode.auto.reason.suspect":"suspicious content","admin.claudeCode.auto.reason.not_analyzed":"never analysed","admin.claudeCode.auto.reason.max_attempts":"attempts exhausted","admin.claudeCode.auto.reason.max_duration":"maximum duration","admin.claudeCode.auto.reason.not_reported":"result not reported","admin.claudeCode.auto.reason.withdrawn":"withdrawn by a human","admin.claudeCode.auto.reason.emergency_stop":"emergency stop","admin.claudeCode.auto.reason.remote_error":"remote-claude error","admin.claudeCode.auto.reason.remote_unreachable":"remote-claude unreachable","admin.claudeCode.auto.reason.remote_not_configured":"remote-claude not connected","admin.claudeCode.auto.confide":"Hand to the automatic agent","admin.claudeCode.auto.confideHelp":"The ticket joins the automated project's queue and goes out in turn.","admin.claudeCode.auto.confided":"{n} ticket(s) handed over","admin.claudeCode.auto.confided_one":"{n} ticket handed over","admin.claudeCode.auto.confided_other":"{n} tickets handed over","admin.claudeCode.auto.badge":"Auto","admin.claudeCode.auto.badgeHelp":"In the hands of the automatic agent.","admin.errors.auto_disabled":"Automatic fixing is off for this project or organization.","admin.errors.auto_stopped":"Emergency stop in progress: nothing goes out.","admin.errors.auto_forbidden_target":"Forbidden target branch: neither the main one nor a production environment's.","admin.errors.auto_invalid_settings":"Invalid setting.","admin.errors.auto_not_eligible":"Ticket outside the project's policy (status, type, priority, exclusion, queue).","admin.errors.auto_daily_cap":"Daily cap reached.","admin.errors.auto_suspended":"Project suspended.","admin.errors.auto_suspect":"Suspicious content not lifted.","admin.errors.auto_max_attempts":"Attempts exhausted.","admin.errors.auto_not_analyzed":"Content never analysed.","admin.claudeCode.auto.forbiddenMachine":"forbidden by the platform","admin.claudeCode.auto.reason.forbidden_machine":"forbidden machine","admin.claudeCode.auto.reason.responsible_unavailable":"responsible member unavailable","admin.errors.auto_forbidden_machine":"Machine forbidden to automatic fixing by the platform.","admin.claudeCode.auto.businessUrlPlaceholder":"business https address"},gi={"admin.specs.vikunja.projectId.help":"ID du projet côté Vikunja où créer les issues (pas un projet BipWire).","admin.specs.vikunja.projectId.label":"Projet Vikunja par défaut","admin.specs.vikunja.projectMap.label":"Projet Vikunja par projet BipWire","admin.specs.vikunja.projectMap.help":"Chaque projet BipWire peut envoyer ses tickets dans son propre projet Vikunja. Laissé sur « Projet par défaut », il utilise celui du dessus.","admin.specs.vikunja.apiToken.help":"Jeton d'API Vikunja (Paramètres → Jetons d'API). Permissions nécessaires : projects → read_all, tasks → create, tasks_attachments → create (sans elle, captures et pièces jointes ne sont pas jointes). Conseillée : tasks_comments → create, pour qu'un fichier refusé à l'envoi soit signalé dans la tâche Vikunja. Facultatif : other → user, pour afficher à quel compte Vikunja appartient le jeton."},bi={"admin.specs.email.to.help":"Séparés par des virgules. Envoyé via Resend.","admin.specs.email.to.label":"Destinataires"},fi={"admin.specs.telegram.chatId.help":"Le bot de BipWire doit être membre du chat. Son nom n'a pas pu être lu : le demander au support.","admin.specs.telegram.chatId.helpBot":"Ajouter {bot} au chat ou au groupe : le bot doit en être membre."},vi={"admin.specs.webhook.label":"Webhook générique","admin.specs.webhook.secret.help":"Optionnel. Si fourni, header X-Bip-Signature = HMAC-SHA256 du body.","admin.specs.webhook.secret.label":"Secret (signature HMAC)"},ki={"admin.taskDetail.toFixDialog.confirmWithoutClaude":"À corriger sans Claude Code","admin.taskDetail.toFixDialog.withoutClaudeHelp":"Le ticket repasse « en cours » sans reprendre la conversation Claude Code lancée dessus.","admin.tasks.kanbanQueuedDrop":"Déposer un ticket ici pour lancer Claude Code","admin.tasks.kanbanQueuedReserved":"Réservé à la file de Claude Code","admin.errors.invalid_tasks":"Sélection de tickets invalide.","admin.errors.too_many_tasks":"Trop de tickets pour un seul lancement (100 au plus).","admin.errors.invalid_display_id":"Identifiant de ticket invalide.","admin.errors.mixed_sites":"Une session ne couvre qu’un seul projet.","admin.errors.task_closed":"Un ticket clos ne se confie pas à un agent.","admin.errors.task_suspect":"Un ticket marqué suspect doit être levé par un humain avant d’être confié à un agent.","admin.reglages.tabClaudeCode":"Claude Code","admin.claudeCode.reportBugs":"Bugs découverts par l’agent","admin.claudeCode.reportBugsToggle":"Faire remonter les bugs découverts par Claude Code","admin.claudeCode.reportBugsHelp":"Quand Claude Code corrige un ticket lancé depuis BipWire et tombe sur un autre bug, ou sur un point qu’un humain doit trancher, il peut le consigner lui-même dans BipWire au lieu de le corriger en douce ou de l’oublier. Désactivé, il ne crée aucun ticket. Réglage propre au compte, valable pour tous les projets ; il s’applique aux conversations lancées après l’avoir changé.","admin.claudeCode.reportBugsExample":"Le ticket créé est un bug du même projet, dont le titre commence par « [Remonté par l’agent — à trancher] » : il se repère d’un coup d’œil dans la liste et attend une décision humaine (corriger, reporter, écarter). Les canaux de notification du projet sont prévenus comme pour tout nouveau ticket, et la note du ticket corrigé cite ce qui a été consigné.","admin.claudeCode.connection":"Mon remote-claude","admin.claudeCode.connectionHelp":"BipWire lance Claude Code par remote-claude : une conversation Remote Control s’y ouvre, sur la machine choisie, dans le dossier du projet, avec /bip-fix et le message. Elle se suit et se gère ensuite dans remote-claude ou sur claude.ai/code. Créer le jeton dans remote-claude, bouton clé « Jetons d’API ».","admin.claudeCode.url":"Adresse de remote-claude","admin.claudeCode.token":"Jeton d’API (rc_ext_…)","admin.claudeCode.tokenKeep":"{prefix} — laisser vide pour le garder","admin.claudeCode.connected":"remote-claude répond. Machines : {machines}.","admin.claudeCode.disconnect":"Débrancher","admin.claudeCode.folders":"Dossiers des projets","admin.claudeCode.foldersHelp":"Le dossier où vit chaque projet sur chaque machine. Sans association, Claude Code part de ~/dev/<projet>, et le dossier choisi au lancement est retenu ici.","admin.claudeCode.noFolder":"Aucun dossier associé pour l’instant.","admin.claudeCode.project":"Projet","admin.claudeCode.machine":"Machine","admin.claudeCode.path":"Dossier sur la machine","admin.claudeCode.launch":"Lancer Claude Code","admin.claudeCode.launchTitle":"Lancer Claude Code","admin.claudeCode.launchConfirm":"Lancer","admin.claudeCode.loadingMachines":"remote-claude cherche les machines…","admin.claudeCode.noMachine":"remote-claude ne connaît aucune machine. En ajouter une dans remote-claude.","admin.claudeCode.offline":"hors ligne","admin.claudeCode.folder":"Dossier du projet « {project} »","admin.claudeCode.folderHelp":"Sur la machine choisie ; ~ est son dossier personnel. Retenu pour la prochaine fois.","admin.claudeCode.command":"Commande envoyée à Claude","admin.claudeCode.message":"Message (facultatif)","admin.claudeCode.messagePlaceholder":"Une consigne en plus : par où commencer, ce qu’il ne faut pas toucher…","admin.claudeCode.launchedTitle":"Claude Code est lancé","admin.claudeCode.queueHelp":"Les tickets passent « en file » : une session par ticket, ouverte par BipWire dès qu’une place se libère ({next}).","admin.claudeCode.andMore_one":"{list} et 1 autre","admin.claudeCode.andMore_other":"{list} et {n} autres","admin.claudeCode.launchedQueueHelp":"{n} tickets en file sur « {machine} », dans {path}. Les sessions Claude s’ouvrent une à une, une par ticket : la suivante dès que le ticket en cours passe « à tester ». Pour retirer un ticket de la file, changer son statut.","admin.claudeCode.launchedHelp":"{tasks} sur « {machine} », dans {path}. La conversation est dans remote-claude ; elle s’arrêtera quand ses tickets seront résolus.","admin.claudeCode.openClaude":"Ouvrir sur claude.ai","admin.claudeCode.openRemoteClaude":"Ouvrir dans remote-claude","admin.claudeCode.openSession":"Conversation Claude en cours","admin.claudeCode.sessionHelp":"Lancée sur « {machine} » le {date} — l’ouvre dans un nouvel onglet.","admin.claudeCode.resumeSession":"Reprendre la conversation Claude","admin.claudeCode.sessionStoppedHelp":"Lancée sur « {machine} » le {date}, arrêtée le {stopped} quand le ticket a été fini — l’ouvre dans remote-claude, où elle reprend au premier message.","admin.claudeCode.noRemoteControl":"Le pont Remote Control n’a pas pu s’ouvrir tout de suite : ouvre la conversation dans remote-claude, il s’y active d’un clic.","admin.claudeCode.oneProjectOnly":"Une session ne couvre qu’un seul projet : sélectionne des tickets du même projet.","admin.claudeCode.closedTasks":"Un ticket clos ne se confie pas à Claude Code.","admin.errors.invalid_token":"Jeton invalide : il commence par rc_ext_.","admin.errors.invalid_machine":"Machine invalide.","admin.errors.invalid_path":"Dossier invalide.","admin.errors.message_too_long":"Message trop long (4 000 caractères au plus).","admin.errors.remote_not_configured":"Brancher d’abord remote-claude : Réglages → Claude Code.","admin.errors.remote_unauthorized":"remote-claude refuse le jeton : il a été révoqué ou mal recopié.","admin.errors.remote_unreachable":"remote-claude ne répond pas à cette adresse.","admin.errors.remote_error":"remote-claude a refusé la demande","admin.errors.secrets_unavailable":"Le chiffrement des secrets n’est pas configuré sur ce serveur.","admin.claudeCode.model":"Modèle","admin.claudeCode.modelDefault":"Celui de la machine","admin.claudeCode.humanValidates":"Quelle que soit l’option, Claude ne clôt jamais un ticket : la validation reste humaine.","admin.errors.invalid_options":"Options de lancement invalides.","admin.errors.invalid_branch":"Nom de branche invalide : lettres, chiffres, « . », « _ », « - » et « / ».","admin.claudeCode.launchedQueueTitle":"Tickets en file","admin.claudeCode.launchedQueueParallelHelp":"{n} tickets en file sur « {machine} », dans {path}. Jusqu’à {parallel} sessions Claude tournent en même temps, une par ticket, chacune dans son worktree. Pour retirer un ticket de la file, changer son statut.","admin.claudeCode.parallel":"Sessions en parallèle","admin.errors.remote_forbidden_address":"Cette adresse n’est pas publique : remote-claude doit être joignable depuis Internet.","admin.errors.remote_timeout":"remote-claude n’a pas répondu à temps.","admin.claudeCode.delivery.confirm":"Lancer","admin.claudeCode.delivery.pending":"Ouverture de la session…","admin.claudeCode.delivery.requested":"Dernière livraison demandée le {date}.","admin.errors.task_not_ready":"Seul un ticket « à tester » se fusionne ou se déploie.","admin.errors.no_delivery":"Rien à livrer pour ce ticket : sa conversation Claude Code ne l’a pas laissé sur sa branche, ou son environnement n’existe plus.","admin.claudeCode.blockWhere":"Où","admin.claudeCode.blockHow":"Comment","admin.claudeCode.blockDelivery":"Livraison","admin.claudeCode.browse":"Parcourir les dossiers de la machine","admin.claudeCode.browseTitle":"Choisir le dossier du projet","admin.claudeCode.browseHome":"Dossier personnel","admin.claudeCode.browseUp":"Dossier parent","admin.claudeCode.browseFilter":"Filtrer","admin.claudeCode.browseHidden":"Dossiers cachés","admin.claudeCode.browseEmpty":"Aucun sous-dossier.","admin.claudeCode.browseChoose":"Choisir ce dossier","admin.claudeCode.modeDiscussion":"Discuter avec Claude","admin.claudeCode.modeAutonome":"Autonomie totale","admin.claudeCode.modeDiscussionHelp":"La session se suit en direct : Claude présente son analyse et le correctif proposé, puis attend l'accord avant de modifier quoi que ce soit. Il peut poser des questions et demander des permissions. La livraison se décide dans la conversation.","admin.claudeCode.modeAutonomeHelp":"Aucune question, aucune permission demandée : Claude corrige, tranche seul et note ses choix sur le ticket.","admin.claudeCode.lotAutonomous":"Un lot part en autonomie : une session Claude par ticket, sans question ni permission demandée.","admin.claudeCode.sequence":"L’un après l’autre","admin.claudeCode.parallelMode":"En parallèle","admin.claudeCode.environment":"Environnement","admin.claudeCode.environmentImplicit":"Production — {branch}","admin.claudeCode.environmentProduction":"Production","admin.claudeCode.mainBranch":"branche principale","admin.claudeCode.whenDiscussion":"La livraison dans « {env} » se décide dans la conversation. Sinon, une fois le ticket « à tester », un bouton de sa fiche la lance.","admin.claudeCode.when.maintenant":"Livrer maintenant","admin.claudeCode.when.plus_tard":"Livrer plus tard","admin.claudeCode.when.maintenantLot":"À chaque ticket","admin.claudeCode.when.finLot":"À la fin du lot","admin.claudeCode.when.plus_tardLot":"Plus tard","admin.claudeCode.whenParallelNote":"En parallèle, aucune session ne touche la branche de l’environnement : la livraison se fait à la fin ou plus tard.","admin.claudeCode.whenHelp.maintenant":"Tests verts : fusion dans {branch}, tag, puis déploiement de « {env} » selon la procédure du projet.","admin.claudeCode.whenHelp.plus_tard":"Le ticket reste sur sa branche ; une fois « à tester », un bouton de sa fiche le livre dans « {env} ».","admin.claudeCode.whenHelp.maintenantLot":"Chaque session fusionne son ticket dans {branch}, tague et déploie « {env} » avant que la suivante s’ouvre.","admin.claudeCode.whenHelp.finLot":"Chaque ticket est fusionné au fil de l’eau dans une branche de lot ; à la fin, une session dédiée la fusionne dans {branch} et déploie « {env} » une seule fois.","admin.claudeCode.whenHelp.plus_tardLot":"Chaque ticket reste sur sa branche ; une fois « à tester », un bouton de sa fiche le livre dans « {env} ».","admin.claudeCode.environments":"Environnements de livraison","admin.claudeCode.environmentsHelp":"Les environnements où livrer les tickets d’un projet : un nom (Bêta, Recette, Prod…) et la branche où un ticket livré est fusionné. Le déploiement suit la procédure écrite du projet (AGENTS.md ou CLAUDE.md). Le premier est proposé par défaut.","admin.claudeCode.environmentName":"Nom","admin.claudeCode.environmentBranch":"Branche","admin.claudeCode.environmentUp":"Monter","admin.claudeCode.environmentAdd":"Ajouter un environnement","admin.claudeCode.noEnvironment":"Aucun environnement : la livraison va en « Production », sur la branche principale du dépôt.","admin.claudeCode.environmentsSaved":"Environnements enregistrés.","admin.claudeCode.delivery.deliver":"Livrer en {env}","admin.claudeCode.delivery.deliverHelp":"Une session Claude Code fusionne la branche {branch} dans {target}, tague, puis déploie « {env} » selon la procédure du projet.","admin.claudeCode.delivery.deliverConfirm":"Livrer le ticket dans « {env} » ? Une session Claude Code s’ouvre en autonome sur « {machine} » : elle fusionne la branche {branch} dans {target}, tague et déploie. Le statut du ticket ne change pas.","admin.claudeCode.deliveryState.planned":"Livraison prévue · {env}","admin.claudeCode.deliveryState.plannedHelp":"Claude livre le ticket dans « {env} » dès les tests verts : fusion, tag, puis déploiement.","admin.claudeCode.deliveryState.delivered":"Livré par l’IA · {env}","admin.claudeCode.deliveryState.deliveredHelp":"La session Claude Code livrait aussitôt dans « {env} » : elle a fusionné, tagué et déployé avant de passer le ticket « à tester ». Sa note donne le commit et la version ; BipWire ne vérifie pas le déploiement.","admin.claudeCode.deliveryState.later":"À livrer · {env}","admin.claudeCode.deliveryState.laterHelp":"Livraison « plus tard » : le correctif reste sur sa branche. « Livrer en {env} », sur la fiche du ticket à tester, la lance.","admin.claudeCode.deliveryState.batch":"Livraison en fin de lot · {env}","admin.claudeCode.deliveryState.batchHelp":"Le lot entier est livré dans « {env} » une fois tous ses tickets traités.","admin.claudeCode.deliveryState.requested":"Livraison demandée · {env}","admin.claudeCode.deliveryState.requestedHelp":"Livraison dans « {env} » demandée le {date} : une session Claude Code fusionne, tague et déploie. Sa note sur le ticket en donne l’issue.","admin.errors.invalid_environment":"Environnement invalide : un nom unique de 1 à 40 caractères et une branche, 10 au plus par projet.","admin.siteEdit.sectionClaudeCode":"Claude Code","admin.siteEdit.claudeCodeSubtitle":"Le dossier du projet sur chaque machine et les environnements où livrer ses tickets. Enregistré à part, bloc par bloc.","admin.siteEdit.summaryClaudeCode":"Dossier : {folders} · Livraison : {environments}","admin.claudeCode.projectFoldersHelp":"Le dossier où vit ce projet sur chaque machine. Sans association, Claude Code part de ~/dev/<projet>, et le dossier choisi au lancement est retenu ici.","admin.claudeCode.auto.title":"Correction automatique","admin.claudeCode.auto.help":"BipWire confie seul à Claude Code les tickets éligibles des projets automatisés, un à la fois par projet, par le remote-claude d'un membre responsable. Il ne pousse jamais sur la branche principale, ne tague pas et ne déploie pas. La politique de chaque projet se règle dans son volet Claude Code.","admin.claudeCode.auto.readOnly":"Seuls les propriétaires et les administrateurs de l'organisation modifient ces réglages.","admin.claudeCode.auto.orgEnabled":"Correction automatique allumée pour l'organisation","admin.claudeCode.auto.concurrent":"Tickets automatiques simultanés","admin.claudeCode.auto.concurrentHelp":"Dans toute l'organisation, un par projet au plus.","admin.claudeCode.auto.business":"Rapport du jour dans business","admin.claudeCode.auto.businessHelp":"Chaque soir à 20 h (heure de Paris), le résumé du jour est commenté sur une tâche de business.pop.run. Laisser l'adresse vide pour ne rien envoyer.","admin.claudeCode.auto.businessUrl":"Adresse de business","admin.claudeCode.auto.businessTask":"Tâche commentée","admin.claudeCode.auto.businessToken":"Clé d'API","admin.claudeCode.auto.businessTokenKeep":"Clé enregistrée — laisser vide pour la garder","admin.claudeCode.auto.stop":"Arrêt d'urgence","admin.claudeCode.auto.resume":"Lever l'arrêt d'urgence","admin.claudeCode.auto.stopConfirm":"Arrêter tout de suite toutes les sessions automatiques de l'organisation et vider leur file ?","admin.claudeCode.auto.stopped":"Arrêt d'urgence en cours depuis le {at} : rien ne part jusqu'à sa levée.","admin.claudeCode.auto.stoppedShort":"Arrêt d'urgence en cours : rien ne part.","admin.claudeCode.auto.projectTitle":"Correction automatique","admin.claudeCode.auto.projectHelp":"La politique de ce projet : quels tickets l'agent prend, quand, et comment il livre. Le premier enregistrement active le mode essai.","admin.claudeCode.auto.orgDisabled":"La correction automatique est éteinte pour l'organisation (Réglages → Claude Code).","admin.claudeCode.auto.suspended":"Projet suspendu ({reason}) : enregistrer les réglages pour reprendre.","admin.claudeCode.auto.today":"Aujourd'hui : {taken} pris sur {cap}, {delivered} livrés, {failed} en échec, {skipped} sautés, coût {cost} $.","admin.claudeCode.auto.enabled":"Automatiser ce projet","admin.claudeCode.auto.dryRun":"Mode essai : pousser la branche seulement, ne rien fusionner","admin.claudeCode.auto.responsible":"Responsable","admin.claudeCode.auto.responsibleHelp":"Le membre dont le remote-claude ouvre les sessions.","admin.claudeCode.auto.folder":"Machine et dossier","admin.claudeCode.auto.folderHelp":"Un dossier déclaré plus haut pour le responsable.","admin.claudeCode.auto.triggers":"Déclencheurs","admin.claudeCode.auto.trigger.creation":"À la création d'un ticket","admin.claudeCode.auto.trigger.planifie":"En continu (tickets nouveaux)","admin.claudeCode.auto.trigger.manuel":"Sur demande (« Confier à l'agent automatique »)","admin.claudeCode.auto.triggerShort.creation":"création","admin.claudeCode.auto.triggerShort.planifie":"planifié","admin.claudeCode.auto.triggerShort.manuel":"manuel","admin.claudeCode.auto.triggerShort.reprise":"reprise","admin.claudeCode.auto.types":"Types","admin.claudeCode.auto.type.bug":"Bugs","admin.claudeCode.auto.type.feature":"Suggestions","admin.claudeCode.auto.minPriority":"Priorité minimale","admin.claudeCode.auto.priority.low":"Basse","admin.claudeCode.auto.priority.medium":"Moyenne","admin.claudeCode.auto.priority.high":"Haute","admin.claudeCode.auto.priority.critical":"Critique","admin.claudeCode.auto.dailyCap":"Tickets par jour","admin.claudeCode.auto.maxMinutes":"Durée maximale (min)","admin.claudeCode.auto.maxAttempts":"Tentatives par ticket","admin.claudeCode.auto.model":"Modèle","admin.claudeCode.auto.modelMachine":"Celui de la machine","admin.claudeCode.auto.retryRejected":"Reprendre un ticket renvoyé « à corriger »","admin.claudeCode.auto.testCommand":"Commande de tests","admin.claudeCode.auto.testCommandHelp":"Vide : les tests décrits par l'AGENTS.md du dépôt.","admin.claudeCode.auto.testCommandInvalid":"Lettres, chiffres, espaces et . _ / : = @ + - seulement.","admin.claudeCode.auto.baseBranch":"Branche de base","admin.claudeCode.auto.baseBranchMain":"branche principale","admin.claudeCode.auto.delivery":"Livraison","admin.claudeCode.auto.deliveryBranch":"Pousser la branche","admin.claudeCode.auto.deliveryMerge":"Fusionner dans une branche cible","admin.claudeCode.auto.targetBranch":"Branche cible","admin.claudeCode.auto.deliveryHelp":"Jamais la branche principale ni celle d'un environnement de production, jamais de tag ni de déploiement. La fusion n'a lieu que si les tests passent, sans conflit.","admin.claudeCode.auto.hoursAlways":"À toute heure","admin.claudeCode.auto.hoursFrom":"De","admin.claudeCode.auto.hoursTo":"À","admin.claudeCode.auto.hoursHelp":"Heure de Paris. Un ticket confié à la main part hors créneau.","admin.claudeCode.auto.day.1":"Lun","admin.claudeCode.auto.day.2":"Mar","admin.claudeCode.auto.day.3":"Mer","admin.claudeCode.auto.day.4":"Jeu","admin.claudeCode.auto.day.5":"Ven","admin.claudeCode.auto.day.6":"Sam","admin.claudeCode.auto.day.7":"Dim","admin.claudeCode.auto.notify":"Notifications","admin.claudeCode.auto.notif.start":"Prise en charge","admin.claudeCode.auto.notif.success":"Livré « à tester »","admin.claudeCode.auto.notif.failure":"Échec","admin.claudeCode.auto.notif.skipped":"Ticket sauté","admin.claudeCode.auto.notif.daily":"Rapport du jour","admin.claudeCode.auto.notifyHelp":"Vers les canaux de notification de l'organisation et les membres abonnés (Réglages → Notifications).","admin.claudeCode.auto.saved":"Réglages enregistrés.","admin.claudeCode.auto.journal":"Dernières exécutions","admin.claudeCode.auto.journalEmpty":"Aucune exécution pour l'instant.","admin.claudeCode.auto.ticketJournal":"Agent automatique","admin.claudeCode.auto.attempt":"tentative {n}","admin.claudeCode.auto.testsOk":"tests verts","admin.claudeCode.auto.testsKo":"tests en échec","admin.claudeCode.auto.merged":"fusionné dans {branch}","admin.claudeCode.auto.dryRunShort":"essai","admin.claudeCode.auto.showOutput":"Sortie","admin.claudeCode.auto.hideOutput":"Masquer","admin.claudeCode.auto.noOutput":"Aucune sortie rapportée par remote-claude.","admin.claudeCode.auto.state.en_file":"En file","admin.claudeCode.auto.state.en_cours":"En cours","admin.claudeCode.auto.state.livre":"Livré","admin.claudeCode.auto.state.echec":"Échec","admin.claudeCode.auto.state.timeout":"Durée dépassée","admin.claudeCode.auto.state.saute":"Sauté","admin.claudeCode.auto.state.arrete":"Arrêté","admin.claudeCode.auto.reason.suspect":"contenu suspect","admin.claudeCode.auto.reason.not_analyzed":"jamais analysé","admin.claudeCode.auto.reason.max_attempts":"tentatives épuisées","admin.claudeCode.auto.reason.max_duration":"durée maximale","admin.claudeCode.auto.reason.not_reported":"résultat non rapporté","admin.claudeCode.auto.reason.withdrawn":"retiré par un humain","admin.claudeCode.auto.reason.emergency_stop":"arrêt d'urgence","admin.claudeCode.auto.reason.remote_error":"erreur de remote-claude","admin.claudeCode.auto.reason.remote_unreachable":"remote-claude injoignable","admin.claudeCode.auto.reason.remote_not_configured":"remote-claude non branché","admin.claudeCode.auto.confide":"Confier à l'agent automatique","admin.claudeCode.auto.confideHelp":"Le ticket rejoint la file du projet automatisé et part à son tour.","admin.claudeCode.auto.confided":"{n} ticket(s) confié(s)","admin.claudeCode.auto.confided_one":"{n} ticket confié","admin.claudeCode.auto.confided_other":"{n} tickets confiés","admin.claudeCode.auto.badge":"Auto","admin.claudeCode.auto.badgeHelp":"Entre les mains de l'agent automatique.","admin.errors.auto_disabled":"La correction automatique est éteinte pour ce projet ou cette organisation.","admin.errors.auto_stopped":"Arrêt d'urgence en cours : rien ne part.","admin.errors.auto_forbidden_target":"Branche cible interdite : ni la principale, ni celle d'un environnement de production.","admin.errors.auto_invalid_settings":"Réglage invalide.","admin.errors.auto_not_eligible":"Ticket hors de la politique du projet (statut, type, priorité, exclusion, file).","admin.errors.auto_daily_cap":"Plafond du jour atteint.","admin.errors.auto_suspended":"Projet suspendu.","admin.errors.auto_suspect":"Contenu suspect non levé.","admin.errors.auto_max_attempts":"Tentatives épuisées.","admin.errors.auto_not_analyzed":"Contenu jamais analysé.","admin.claudeCode.auto.forbiddenMachine":"interdite par la plateforme","admin.claudeCode.auto.reason.forbidden_machine":"machine interdite","admin.claudeCode.auto.reason.responsible_unavailable":"responsable indisponible","admin.errors.auto_forbidden_machine":"Machine interdite à la correction automatique par la plateforme.","admin.claudeCode.auto.businessUrlPlaceholder":"Adresse https de business"},yi={"admin.activation.allInOne.desc":"One prompt, to paste into Claude Code, Codex, Cursor or OpenCode opened at the root of the site’s repo: the agent integrates the widget, installs the /bip-* commands, connects the MCP server and creates .bip.json. The rest of this wizard details the same steps, to do by hand.","admin.activation.allInOne.mcpFile":`Add this block to {path} (create the file if it does not exist; otherwise merge it with the existing content, removing nothing):

{block}`,"admin.activation.allInOne.mcpRun":`Run this shell command (user scope, valid for every project; if a “bip” server already exists, remove it first with “claude mcp remove --scope user bip”):

{command}`,"admin.activation.allInOne.needToken":"The prompt carries the personal token, which gives the agent access to tickets. It does not exist yet: create it in one click.","admin.activation.allInOne.placeholder":"No token with a retrievable key: the prompt contains a placeholder, to replace with an LLM token key (“API keys” page).","admin.activation.allInOne.prepare":"Prepare the prompt","admin.activation.allInOne.prompt":`Set up BipWire in this repo, end to end. Four tasks, in order:

1. Widget. {widget}

2. /bip-* commands. Run this shell command (it installs four text files in {commandsPath}, no secret):

{install}

3. MCP server. {mcp}

4. Project. Create a .bip.json file at the root of the repo containing exactly:

{bipJson}

Never copy the token into the repo: it only lives in the personal config ({mcpPath}). Finish with a summary of what was done, and remind me to restart the agent so it loads the MCP server, then to run /bip-tasks.`,"admin.activation.allInOne.secretHint":"This prompt contains the token key: paste it into the agent only, never into a ticket or a file of the repo.","admin.activation.allInOne.title":"Hand it all to an AI agent","admin.activation.back":"Back","admin.activation.finish":"Finish","admin.activation.next":"Next","admin.activation.step1.agentDesc":"Copy this prompt into Claude Code, Codex, Cursor or OpenCode: it integrates the widget for you.","admin.activation.step1.agentPrompt":`Integrate the BipWire support widget on my website. Add this tag to the HTML, preferably in the <head> (otherwise before </body>):

{snippet}

If the site uses a framework (Next.js, Vite, PHP…) or environment variables, wire it in cleanly. If the site defines a Content-Security-Policy (HTTP header, framework or hosting config, or <meta http-equiv>), add to its existing directives, removing nothing: {clientUrl} in script-src and connect-src, 'unsafe-inline' in style-src, data: and blob: in img-src, 'self' and blob: in worker-src. Full paste-ready docs: {docUrl}`,"admin.activation.step1.agentTitle":"Or let your AI agent install it","admin.activation.step1.cspDesc":"Then the browser silently blocks the widget: no bubble, or no screenshot, and nothing ever reaches us. BipWire has to be added to your site’s CSP — not the other way around.","admin.activation.step1.cspDocLink":"Why each directive, nonces, other security headers","admin.activation.step1.cspHint":"Add them to your existing directives, removing nothing. Images served from a CDN? Add its domain to connect-src too, or they come out blank in screenshots.","admin.activation.step1.cspTitle":"Does your site have a Content-Security-Policy?","admin.activation.step1.cspToggle":"Show the values to add","admin.activation.step1.docLink":"See the integration docs","admin.activation.step1.identifyToggle":"Identify your users (optional)","admin.activation.step1.siteLine":"Project: {name} — id {id}","admin.activation.step2.createToken":"Create a token","admin.activation.step2.creating":"Creating your token…","admin.activation.step2.extendToken":"Extend this token to this project","admin.activation.step2.extending":"Extending…","admin.activation.step2.intro":"Now that the widget is in place, connect your CLI agent (Claude Code, Codex, Cursor, OpenCode): it will fix reported bugs straight from your editor.","admin.activation.step2.otherToken":"Use another token","admin.activation.step2.personalHint":"This is your personal token: the same one for all your projects, offered here every time.","admin.activation.step2.tokenLabel":"Agent — {user}","admin.activation.step2.tokenNotOnSite":"Your token “{label}” does not cover this project.","admin.activation.step2.tokenReady":"Your token “{label}” is ready: its key is inserted into the commands below.","admin.activation.step2.tokenRequired":"The MCP connection needs a BipWire LLM token. Pick an existing token, or create one.","admin.activation.step3.detected":"BipWire is loading fine on {origin} 🎉","admin.activation.step3.detectedHint":"The widget responds. You’re all set.","admin.activation.step3.openSite":"Open my site","admin.activation.step3.stuckCsp":"a “violates the following Content Security Policy directive” message: your CSP blocks the widget. Add the values from step 1 (Back).","admin.activation.step3.stuckNothing":"no message: the tag isn’t in the page being served. Check the page source (Ctrl+U), purge your CDN cache, and exclude the script from JS optimizers (Rocket Loader, WP Rocket…).","admin.activation.step3.stuckTitle":"Still nothing? Open your site with the browser console (F12):","admin.activation.step3.waiting":"Waiting for the widget’s first load…","admin.activation.step3.waitingHint":"Open a page of your site where the script is installed to trigger detection.","admin.activation.stepAgent":"Connect your agent","admin.activation.stepInstall":"Install the widget","admin.activation.stepVerify":"Verify","admin.activation.title":"Activate BipWire on your site","admin.agentSetup.cmd.bip-fix":"fixes a ticket end to end, then moves it to “to test” with a note","admin.agentSetup.cmd.bip-next":"picks the highest-priority ticket and fixes it","admin.agentSetup.cmd.bip-sort":"prioritizes urgent tickets across all your projects and says where to start (read-only)","admin.agentSetup.cmd.bip-tasks":"lists the project’s open tickets, by priority (read-only)","admin.agentSetup.footerLegacyPost":"in that repo: a project copy takes precedence over the global one, and it never gets updated.","admin.agentSetup.footerLegacyPre":"Installed them inside a repo with an older version? Delete","admin.agentSetup.footerNote":"The commands are the same for all three agents: only the file location differs. Rerun the step 1 command after a BipWire update to get the latest versions.","admin.agentSetup.introPost":") in your agent. The token only lives in the MCP config below — the commands themselves contain no secret.","admin.agentSetup.introPre":"Install the BipWire slash commands (","admin.agentSetup.machineHint":"These two steps cover all your projects. Already done on this machine? Skip to “Once per project”.","admin.agentSetup.machineTitle":"Once per machine","admin.agentSetup.noSelection":"Without a selection, the snippet contains a placeholder to replace by hand.","admin.agentSetup.noToken":"No token with a recoverable key — create or regenerate a token.","admin.agentSetup.pasteAgent":"my agent’s prompt","admin.agentSetup.pasteAgentHint":"The “!” prefix runs the command from Claude Code (or any agent that supports it), without leaving the session.","admin.agentSetup.pasteTarget":"I’m pasting into:","admin.agentSetup.pasteTerminal":"a terminal","admin.agentSetup.projectHint":"On your next project, this is the only step to redo.","admin.agentSetup.projectTitle":"Once per project","admin.agentSetup.site":"Project","admin.agentSetup.step1":"1. Install the /bip-* commands","admin.agentSetup.step1Desc":"Four text files, with no secret at all, stored in {path}. Run the command from any folder:","admin.agentSetup.step2":"2. Connect the MCP server","admin.agentSetup.step2Desc":"Choose a token: its key is inserted automatically.","admin.agentSetup.step2DescClaude":"It’s your agent’s connection to BipWire. The token is stored in your user config, outside any repo.","admin.agentSetup.step2DescOtherPre":"It’s your agent’s connection to BipWire. It holds your token: paste this block into your personal config,","admin.agentSetup.step3":"3. Point this repo at its project","admin.agentSetup.step3DescPost":"at the repo root so the commands know which project to query.","admin.agentSetup.step3DescPre":"Create a","admin.agentSetup.title":"Connect a CLI agent","admin.agentSetup.token":"Token","admin.auth.backToLogin":"Back to sign in","admin.auth.confirmPassword":"Confirm the password","admin.auth.createMyAccount":"Create my account","admin.auth.emailAddress":"Email address","admin.auth.errPasswordMismatch":"The passwords do not match.","admin.auth.errPasswordTooShort":"The password must be at least 8 characters.","admin.auth.forgotPassword":"Forgot password?","admin.auth.forgotSubtitle":"We’ll send you a reset link","admin.auth.forgotSuccess":"If an account exists, an email has been sent.","admin.auth.forgotTitle":"Forgot password","admin.auth.haveAccount":"Already have an account? Sign in","admin.auth.hidePassword":"Hide the password","admin.auth.inviteCurrentPassword":"Your current password","admin.auth.inviteExistingAccount":"You already have a BipWire account — this organisation will be added to your space.","admin.auth.inviteFlash":"Account created, sign in.","admin.auth.inviteInvalidLink":"Invalid or incomplete invitation link.","admin.auth.inviteJoining":"You're joining {org}.","admin.auth.inviteOrgName":"Organisation name","admin.auth.inviteOrgSubtitle":"Name your space and choose your password","admin.auth.inviteOrgTitle":"Create your organisation","admin.auth.inviteSubtitle":"Choose a password to activate your account","admin.auth.inviteTitle":"Join the space","admin.auth.loginSubtitle":"Sign in to the admin area","admin.auth.loginTitle":"BipWire — Admin","admin.auth.noAccount":"No account yet? Create an account","admin.auth.orgName":"Organisation name","admin.auth.password":"Password","admin.auth.reset":"Reset","admin.auth.resetFlash":"Password updated, sign in.","admin.auth.resetInvalidLink":"Invalid or incomplete reset link.","admin.auth.resetSubtitle":"Choose a new password for your account","admin.auth.resetTitle":"New password","admin.auth.sendLink":"Send the link","admin.auth.showPassword":"Show the password","admin.auth.signIn":"Sign in","admin.auth.signingIn":"Signing in…","admin.auth.signupSubtitle":"Create your BipWire space in seconds","admin.auth.signupTitle":"Create an account","admin.auth.joinWaitlist":"Join the waitlist","admin.auth.invite.checking":"Checking the invitation…","admin.auth.invite.banner":"Invitation: {plan} plan","admin.auth.invite.invalid.unknown.title":"Unknown invitation link","admin.auth.invite.invalid.unknown.text":"This link doesn’t match any invitation. Check that it was copied in full, or join the waitlist.","admin.auth.invite.invalid.revoked.title":"Invitation withdrawn","admin.auth.invite.invalid.revoked.text":"This invitation link has been disabled. Join the waitlist to hear when we open.","admin.auth.invite.invalid.expired.title":"Invitation expired","admin.auth.invite.invalid.expired.text":"This invitation link is no longer valid. Join the waitlist to get a new one.","admin.auth.invite.invalid.full.title":"All seats are taken","admin.auth.invite.invalid.full.text":"This invitation link has already been used as many times as planned. Join the waitlist to hear about the next wave.","admin.auth.signupsClosedText":"Signups are not open yet. Come back soon!","admin.auth.signupsClosedTitle":"Signups opening soon","admin.auth.updating":"Updating…","admin.auth.verifyInvalidLink":"Invalid or incomplete verification link.","admin.auth.verifyPending":"Verifying your email address…","admin.auth.verifySuccess":"Your email address is verified.","admin.auth.verifyTitle":"Email verification","admin.billing.changePlan":"Change plan","admin.billing.company":"Company","admin.billing.companyNote":"This information will appear on your invoices.","admin.billing.conversationsGauge":"{used} / {total} conversations","admin.billing.conversationsUnlimited":"{used} conversations this month (unlimited)","admin.billing.currentPlan":"Current plan","admin.billing.loadError":"Unable to load billing.","admin.billing.manageNote":"Update your payment method, download your invoices or cancel your subscription.","admin.billing.manageSubscription":"Manage my subscription","admin.billing.noSubscription":"No active subscription. Choose a plan to unlock more capacity.","admin.billing.overBanner":"You've exceeded your plan. Service keeps running — upgrade for more conversations.","admin.billing.periodUntil":"Current period until {date}.","admin.billing.planPro":"Pro plan","admin.billing.planScale":"Scale plan","admin.billing.resetsMonthly":"resets on the 1st","admin.billing.stripeNote":"Payment and subscription management are handled by Stripe.","admin.billing.subStatus.active":"active","admin.billing.subStatus.canceled":"canceled","admin.billing.subStatus.incomplete":"incomplete","admin.billing.subStatus.incomplete_expired":"expired","admin.billing.subStatus.past_due":"payment overdue","admin.billing.subStatus.paused":"paused","admin.billing.subStatus.trialing":"trial","admin.billing.subStatus.unpaid":"unpaid","admin.billing.subStatusLabel":"Subscription status: {status}","admin.billing.subscribe":"Subscribe","admin.billing.tier.free":"Free","admin.billing.tier.pro":"Pro","admin.billing.tier.scale":"Scale","admin.billing.tier.unlimited":"Unlimited","admin.billing.title":"Billing","admin.billing.toastProfileSaved":"Company profile saved.","admin.billing.toastSubscribed":"Subscription taken into account.","admin.billing.upgradeTo":"{tier} — {price}/mo · {conversations} conversations","admin.billing.usageTitle":"This month's usage","admin.billingProfile.address":"Address","admin.billingProfile.addressComplement":"Complement","admin.billingProfile.billingAddress":"Billing address","admin.billingProfile.billingContact":"Billing contact","admin.billingProfile.billingEmail":"Billing email","admin.billingProfile.city":"City","admin.billingProfile.contactName":"Contact name","admin.billingProfile.country":"Country","admin.billingProfile.countryPlaceholder":"FR","admin.billingProfile.legalForm":"Legal form","admin.billingProfile.legalFormPlaceholder":"Ltd, LLC, Inc…","admin.billingProfile.legalIdentity":"Legal identity","admin.billingProfile.legalName":"Legal name","admin.billingProfile.phone":"Phone","admin.billingProfile.postalCode":"Postal code","admin.billingProfile.rcs":"RCS","admin.billingProfile.siren":"SIREN","admin.billingProfile.siret":"SIRET","admin.billingProfile.vatCountry":"VAT country","admin.billingProfile.vatLiable":"VAT liable","admin.billingProfile.vatNumber":"Intra-EU VAT number","admin.billingProfile.vatNumberPlaceholder":"FR12345678901","admin.billingProfile.vatTax":"VAT / tax","admin.common.add":"Add","admin.common.adding":"Adding…","admin.common.all":"All","admin.common.anonymous":"anonymous","admin.common.archive":"Archive","admin.common.archived":"Archived","admin.common.archivedTag":"Archived","admin.common.archiving":"Archiving…","admin.common.back":"Back","admin.common.cancel":"Cancel","admin.common.clearSearch":"Clear search","admin.common.close":"Close","admin.common.confirm":"Confirm","admin.common.copied":"Copied","admin.common.copy":"Copy","admin.common.copyId":"Copy ID","admin.common.create":"Create","admin.common.creating":"Creating…","admin.common.dash":"—","admin.common.delete":"Delete","admin.common.deleting":"Deleting…","admin.common.disabled":"Disabled","admin.common.edit":"Edit","admin.common.enabled":"Enabled","admin.common.error":"Error","admin.common.genericError":"Something went wrong. Please try again in a moment.","admin.common.hide":"Hide","admin.common.noSitesConfigured":"No project configured.","admin.common.retry":"Retry","admin.common.save":"Save","admin.common.saving":"Saving…","admin.common.send":"Send","admin.common.sending":"Sending…","admin.common.show":"Show","admin.common.showActive":"Show active","admin.common.unarchive":"Unarchive","admin.common.unknownError":"Unknown error","admin.connectors.add":"Add a connector","admin.connectors.colActions":"Actions","admin.connectors.colAutoCreate":"Auto-create","admin.connectors.colEnabled":"Enabled","admin.connectors.colName":"Name","admin.connectors.colScope":"Scope","admin.connectors.colType":"Type","admin.connectors.deleteMessage":"The connector will be removed. New bugs will no longer create an issue in this tool. Existing links are kept.","admin.connectors.deleteTitle":"Delete this connector?","admin.connectors.description":"Automatically create an issue in an external tool (Vikunja, webhook) for every new reported bug. The link to the issue appears in the ticket detail.","admin.connectors.empty":"No connector configured.","admin.connectors.loadError":"Unable to load connectors.","admin.connectors.title":"Connectors","admin.connectors.toastCreated":"Connector created","admin.connectors.toastDeleted":"Connector deleted","admin.connectors.toastUpdated":"Connector updated","admin.createTask.attach":"Attach files","admin.createTask.attachFailed":"Ticket created, but these files could not be attached:","admin.createTask.attachHelper":"Images (PNG, JPEG, WebP, 5 MB) and files (PDF, Office, text, zip, 10 MB) — 15 at most. An image can also be pasted into the description.","admin.createTask.descIdea":"Describe the idea","admin.createTask.descProblem":"Describe the problem","admin.createTask.description":"Description","admin.createTask.llmHelper":"Title, context, and type will be refined automatically by the LLM.","admin.createTask.openTicket":"Open the ticket","admin.createTask.priority":"Priority","admin.createTask.removeFile":"Remove","admin.createTask.site":"Project","admin.createTask.title":"Create a ticket","admin.createTask.uploading":"Uploading files…","admin.createTask.urlHelper":"Helps to infer the functional context.","admin.createTask.urlOptional":"URL (optional)","admin.dashboard.ai":"AI","admin.dashboard.awaiting":"Awaiting","admin.dashboard.bugReports":"Tickets","admin.dashboard.chatThreads":"Conversations","admin.dashboard.colId":"ID","admin.dashboard.colPriority":"Priority","admin.dashboard.colStatus":"Status","admin.dashboard.colTitle":"Title","admin.dashboard.emptyTasks":"No ticket to handle — you can breathe.","admin.dashboard.emptyThreads":"No visitor waiting.","admin.dashboard.inProgress":"In progress","admin.dashboard.new":"New","admin.dashboard.seeAll":"See all →","admin.dashboard.sites":"Projects","admin.dashboard.tasksToHandle":"Tickets to handle","admin.dashboard.threadsAwaiting":"Conversations awaiting","admin.dashboard.title":"Dashboard","admin.dashboard.toTest":"To test","admin.dashboard.visitorFallback":"Anonymous","admin.designer.open":"Customize the widget","admin.designer.title":"Appearance studio","admin.designer.subtitle":"Pick a role, choose a color: the preview shows the widget as your visitors will see it.","admin.designer.rolePanel":"Panel background","admin.designer.rolePanelHint":"Background of the chat and the report form","admin.designer.rolePanelText":"Panel text","admin.designer.rolePanelTextHint":"Texts, fields, labels in the panel; borders and greys derive from it","admin.designer.roleAi":"Assistant messages","admin.designer.roleAiHint":"Background of replies; their text stays readable on its own","admin.designer.panelTheme":"Panel theme","admin.designer.themeLight":"Light","admin.designer.themeDark":"Dark","admin.designer.themeSite":"Match the site","admin.designer.groupAccent":"Accent","admin.designer.groupPanel":"Panel","admin.designer.reset":"Reset everything","admin.designer.mine":"My colors","admin.designer.duplicate":"Duplicate & adjust","admin.designer.mineEmpty":"Duplicate a color to redefine it however you like: it joins your palette, saved with the project.","admin.designer.deleteColor":"Remove from my colors","admin.designer.hsl.h":"Hue","admin.designer.hsl.s":"Saturation","admin.designer.hsl.l":"Lightness","admin.designer.presets":"Suggested pairings","admin.designer.noPalette":"No colors picked up from the site yet: the widget will collect them on its next visit.","admin.designer.roles":"Roles","admin.designer.roleBg":"Accent","admin.designer.roleBgHint":"Bubble, header, send button, visitor messages","admin.designer.roleFg":"Text on accent","admin.designer.roleFgHint":"Bubble icon, header title, labels","admin.designer.pickFor":"Color for “{role}”","admin.designer.fromSite":"Found on the site","admin.designer.neutrals":"Neutrals","admin.designer.fixContrast":"Make it readable","admin.designer.custom":"Another color…","admin.designer.readability":"Readability","admin.designer.contrast.aaa":"excellent (AAA)","admin.designer.contrast.aa":"good (AA)","admin.designer.contrast.large":"borderline — large text only","admin.designer.contrast.low":"insufficient","admin.designer.apply":"Apply","admin.designer.previewTitle":"Support","admin.designer.previewSubtitle":"Replies within minutes","admin.designer.previewWelcome":"Hi! How can we help?","admin.designer.previewVisitor":"Payment fails on mobile.","admin.designer.previewPlaceholder":"Write a message…","admin.designer.previewSend":"Send","admin.designer.siteColors":"Site colors","admin.designer.siteColorsHint":"Fetch the site's CSS, or paste it: we extract its brand, background and text colors and suggest complete themes.","admin.designer.fetchSite":"Fetch from the site","admin.designer.fetching":"Reading the site…","admin.designer.fetchError":"Couldn't read the site: {message}","admin.designer.fetchFrom":"Read from {origin} ({count} stylesheets, {scripts} scripts)","admin.designer.pasteCss":"Paste CSS","admin.designer.pastePlaceholder":`:root { --primary: #6d28d9; }
body { background: #faf5ff; color: #1f2937; }
.btn { background: var(--primary); }`,"admin.designer.analyze":"Analyze","admin.designer.clearCss":"Clear","admin.designer.analyzeEmpty":"No color found in this CSS.","admin.designer.analyzeSummary":"{count} colors found","admin.designer.groupBrand":"Brand","admin.designer.groupBackground":"Background","admin.designer.groupText":"Text","admin.designer.sourceCss":"From the analyzed CSS","admin.designer.sourceDetected":"From the colors sampled by the widget","admin.designer.themes":"Suggested themes","admin.designer.themesHint":"One click applies all five colors, all readable; everything stays adjustable afterwards.","admin.designer.themesEmpty":"Fetch or paste the site's CSS to get themes in its colors.","admin.designer.theme.assorti":"Matches the site","admin.designer.theme.clair":"Brand on light","admin.designer.theme.sombre":"Brand on dark","admin.designer.theme.teinte":"Brand tint","admin.designer.theme.second":"Second color","admin.designer.theme.sobre":"Understated","admin.docs.notFound":"Doc not found: {slug}","admin.docs.toc":"On this page","admin.embedSnippet.basicHint1":"Paste into the","admin.embedSnippet.basicHint2":"or before","admin.embedSnippet.basicHint3":"of your site. The URL is tied to this project — if you regenerate the key, the current embed stops working and you'll need to paste the new one. The “Widget enabled” toggle in the admin turns the widget off without touching the site's HTML.","admin.embedSnippet.basicTitle":"Basic embed","admin.embedSnippet.envHint":"Avoid hardcoding the URL in the HTML. Rotating the key = changing one variable, not hunting through templates.","admin.embedSnippet.envTitle":"Recommended — store the URL in an environment variable","admin.embedSnippet.identifyBefore":"before","admin.embedSnippet.identifyComment1":"Place this BEFORE the embed script. Identifies the logged-in user.","admin.embedSnippet.identifyCommentId":"recommended: your stable user ID","admin.embedSnippet.identifyCommentMeta":"free-form: put whatever you want","admin.embedSnippet.identifyCommentName":"optional: name shown in the admin","admin.embedSnippet.identifyHint1":"ties the conversation to your site's logged-in user: you see who's writing (and their context) right in the admin conversation, and the AI can use it to reply. Call it after login (or again if the user changes). Place it","admin.embedSnippet.identifyHint2":"the embed script.","admin.errors.file_too_large":"File too large: 5 MB for an image, 10 MB for any other file.","admin.errors.invalid_color":"Invalid color: #rrggbb format expected.","admin.errors.invalid_file":"Unreadable file.","admin.errors.invalid_file_type":"Format not accepted: PNG, JPEG, WebP, PDF, Office, text or zip.","admin.errors.invalid_icon_svg":"This SVG could not be read: it must be made of simple shapes (path, circle, rect…) and weigh 10 KB at most once cleaned.","admin.errors.already_invited":"An invitation is already pending for this email.","admin.errors.already_member":"This user is already a member of this space.","admin.errors.billing_unavailable":"Billing is not configured.","admin.errors.cannot_change_self":"You cannot change your own role.","admin.errors.cannot_delete_self":"You cannot delete your own account.","admin.errors.cannot_revoke_self":"You cannot remove yourself.","admin.errors.cannot_set_self":"Change your own password from your profile.","admin.errors.default":"Something went wrong. Please try again in a moment.","admin.errors.email_mismatch":"The email you typed does not match the account.","admin.errors.email_taken":"An account already exists with this email.","admin.errors.forbidden":"You don’t have the rights for this action.","admin.errors.invalid_credentials":"Incorrect password. Please check it and try again.","admin.errors.internal_error":"Something went wrong on our side. Please try again in a moment; if it keeps happening, let us know.","admin.errors.invalid_input":"A field is not valid: please check it and try again.","admin.errors.lecture_seule":"This session entered through the platform console and is read-only: nothing can be changed here. Go back to the home organization, or open the link in a private window.","admin.errors.invalid_or_expired":"This link is no longer valid: it has already been used or has expired. Just ask for a new one.","admin.errors.last_owner":"Not possible: this is the last owner of this space.","admin.errors.name_mismatch":"The name you typed does not match the organisation’s name.","admin.errors.network_error":"Network error. Check your connection.","admin.errors.no_customer":"No subscription to manage for now.","admin.errors.no_secret":"Key unavailable for this token (created before v1.22) — regenerate it.","admin.errors.not_archivable":"Only resolved items can be archived.","admin.errors.not_archived":"The organisation must be archived before it can be purged.","admin.errors.org_disk_quota_exceeded":"The organization's disk quota is reached.","admin.errors.org_is_default":"The default organisation can be neither archived nor deleted.","admin.errors.org_not_found":"This organisation does not exist any more.","admin.errors.price_not_configured":"This plan is not available for subscription.","admin.errors.signups_closed":"Signups are not open yet.","admin.errors.token_revoked":"This token is revoked — create a new token instead.","admin.errors.too_many_attempts":"Too many attempts. Try again in a few minutes.","admin.errors.too_many_files":"15 files per ticket at most.","admin.errors.unauthorized":"Session expired. Please sign in again.","admin.errors.site_css_no_origin":"This project has no declared origin: add the site's address under “Origins”.","admin.errors.site_css_forbidden_address":"This address can't be read from BipWire (private network or non-standard port).","admin.errors.site_css_unreachable":"The site didn't respond, or didn't return a readable page.","admin.errors.site_css_timeout":"The site took too long to respond.","admin.errors.site_css_rate_limited":"Too many reads of the site in a row: try again in a minute.","admin.events.task.created":"New bug / suggestion","admin.events.task.status_changed":"Status change (to test, resolved…)","admin.events.thread.escalated":"Escalation to a human","admin.guardrails.analyze":"Analyze","admin.guardrails.analyzedBy":"Analyzed by: {by}","admin.guardrails.banner":"The detector found, in the description or an attachment, text that tries to manipulate an AI assistant. The ticket stays normal, but automated agents skip it and its priority is capped. Read it, then clear the flag if it is a false positive.","admin.guardrails.bannerLifted":"Flag cleared by {by} on {date}. The AI-inferred priority has been restored.","admin.guardrails.bannerLiftedTitle":"Suspicious content, reviewed by a human","admin.guardrails.bannerTitle":"Suspicious content — possible prompt injection","admin.guardrails.by.aucun":"nobody","admin.guardrails.by.heuristique":"heuristics only","admin.guardrails.by.llm":"heuristics + AI second opinion","admin.guardrails.hideReasons":"Hide reasons","admin.guardrails.lift":"Clear the flag","admin.guardrails.liftedHint":"Flagged as suspicious, then reviewed and cleared by a human.","admin.guardrails.noReason":"No detailed reason.","admin.guardrails.notAnalyzed":"Not analyzed","admin.guardrails.notAnalyzedHint":"This content was not analyzed by the injection detector: it predates the feature, or its format has no text extractor.","admin.guardrails.reanalyze":"Re-analyze","admin.guardrails.reason.bloc_base64":"Base64-encoded block","admin.guardrails.reason.commande_exfiltration":"Exfiltration command","admin.guardrails.reason.demande_secret":"Request for a secret or sensitive file","admin.guardrails.reason.instruction_agent":"Instruction addressed to an agent","admin.guardrails.reason.llm":"AI second opinion","admin.guardrails.reason.marqueur_role":"Role or system-prompt marker","admin.guardrails.reason.texte_cache":"Hidden text (invisible characters)","admin.guardrails.reason.url_ip_brute":"URL to a raw IP address","admin.guardrails.showReasons":"Show reasons","admin.guardrails.suspect":"Suspicious content","admin.guardrails.suspectHint":"The prompt-injection detector found content that tries to manipulate an AI.","admin.guardrails.suspectLifted":"Suspicious · cleared","admin.history.activeSessions":"Active admin sessions","admin.history.adminSessions":"Admin sessions","admin.history.colAction":"Action","admin.history.colCreated":"Created","admin.history.colExpires":"Expires","admin.history.colIp":"IP","admin.history.colUserAgent":"User-Agent","admin.history.conversationCleanupHint":"Also deletes linked messages (cascade). Unresolved conversations are preserved.","admin.history.counters":"Counters","admin.history.emptySessions":"No active session.","admin.history.olderThanDays":"Older than (days)","admin.history.purgeConversationsMessage":"The conversations (and all their messages) will be deleted. This cannot be undone.","admin.history.purgeConversationsTitle":"Delete resolved conversations older than {days} days?","admin.history.purgeExpired":"Purge expired / revoked","admin.history.purgeSessionsMessage":"Deletes from the DB all expired or revoked sessions. No effect on active sessions.","admin.history.purgeSessionsTitle":"Purge dead sessions?","admin.history.purgeTasksMessage":"The DB rows AND the screenshot files on disk will be deleted. This cannot be undone.","admin.history.purgeTasksTitle":"Delete tickets older than {days} days?","admin.history.resolvedOverDays":"Resolved > N days","admin.history.revokeMessage":"The user will be logged out immediately.","admin.history.revokeSession":"Revoke the session","admin.history.revokeTitle":"Revoke this session?","admin.history.screenshotsCount_one":"{n} screenshot","admin.history.screenshotsCount_other":"{n} screenshots","admin.history.sessionsHint":"active · {dead} dead","admin.history.subtitle":"Overview + cleanup of old sessions, tickets and conversations.","admin.history.taskCleanup":"Ticket cleanup","admin.history.tasks":"Tickets","admin.history.tasksHint":"{old} > 90d","admin.history.threadCleanup":"Resolved conversation cleanup","admin.history.threads":"Conversations","admin.history.threadsHint":"{resolved} resolved · {old} > 30d","admin.history.title":"History & maintenance","admin.history.toastConversationsPurged_one":"{n} resolved conversation deleted.","admin.history.toastConversationsPurged_other":"{n} resolved conversations deleted.","admin.history.toastSessionRevoked":"Session revoked.","admin.history.toastSessionsPurged_one":"{n} session deleted.","admin.history.toastSessionsPurged_other":"{n} sessions deleted.","admin.history.toastTasksPurged_one":"{n} ticket deleted ({s}).","admin.history.toastTasksPurged_other":"{n} tickets deleted ({s}).","admin.integrationDialog.autoCreate":"Automatic issue creation","admin.integrationDialog.autoCreateCaption":"For every new bug, create an issue in the target tool.","admin.integrationDialog.disabled":"Disabled","admin.integrationDialog.editTitle":"Edit — {name}","admin.integrationDialog.enabled":"Enabled","admin.integrationDialog.events":"Events","admin.integrationDialog.eventsAll":"All events","admin.integrationDialog.eventsCaption":"None selected = triggered on all events.","admin.integrationDialog.fieldGuide":"{label} guide: where to find each field ↗","admin.integrationDialog.guideShow":"Show the {label} guide","admin.integrationDialog.guideHide":"Hide the {label} guide","admin.integrationDialog.name":"Name","admin.integrationDialog.namePlaceholderConnector":"e.g. Vikunja prod","admin.integrationDialog.namePlaceholderNotification":"e.g. Team alerts","admin.integrationDialog.newConnector":"New connector","admin.integrationDialog.newNotification":"New notification","admin.integrationDialog.sites":"Projects concerned","admin.integrationDialog.sitesAll":"All projects","admin.integrationDialog.sitesCaption":"None selected = applied to all projects.","admin.integrationDialog.test":"Test","admin.integrationDialog.testErrorUnknown":"unknown error","admin.integrationDialog.testFailed":"Test failed: {error}","admin.integrationDialog.testMissingFields":"Missing required fields: {fields}","admin.integrationDialog.testSuccess":"Complete configuration — no message sent (the test only checks required fields).","admin.integrationDialog.testValid":"Valid configuration — no issue created (the test only checks required fields).","admin.integrationDialog.testing":"Testing…","admin.integrationDialog.sourceLoad":"Load {label} projects","admin.integrationDialog.sourceLoading":"Loading…","admin.integrationDialog.sourceCaption":"After the URL and token: load the list to pick projects instead of copying their ID.","admin.integrationDialog.sourceLoaded":"{count} project(s) loaded — pick below.","admin.integrationDialog.sourceLoadedAccount":"{count} project(s) loaded, those the {label} account “{account}” can see — pick below.","admin.integrationDialog.sourceUnknownValue":"#{value} (not in the list)","admin.integrationDialog.siteMapDefault":"Default project","admin.integrationDialog.sourceError.generic":"Could not load ({code}).","admin.integrationDialog.sourceError.upstream_status":'The tool refused the request ({code}): check the URL and the token ("Projects: read" permission).',"admin.integrationDialog.sourceError.upstream_down":"The tool is not answering ({code}): the instance seems to be down. Restart it, or check the URL.","admin.integrationDialog.sourceError.upstream_forbidden_address":"Address refused: only an instance reachable from the Internet can be queried.","admin.integrationDialog.sourceError.upstream_unreachable":"Instance unreachable: check the URL.","admin.integrationDialog.sourceError.upstream_timeout":"The instance did not answer in time.","admin.integrationDialog.sourceError.upstream_invalid_url":"Invalid URL.","admin.integrationDialog.sourceError.config_incomplete":"Fill in the URL and the token first.","admin.integrationDialog.sourceError.vikunja_invalid_response":"The response does not look like a Vikunja instance: check the URL.","admin.integrationDialog.type":"Type","admin.integrationDialog.typeLocked":"The type cannot be changed after creation. ","admin.integrations.error.module_disabled":"This integration type is disabled by the platform. You can keep the entry, but not enable it.","admin.integrations.error.extension_unavailable":"This integration type is not available to this organization. You can keep the entry, but not enable it.","admin.integrations.error.type_not_implemented":"This integration type is not served by this platform.","admin.integrations.error.server_not_configured":"This channel cannot send anything: the service is not configured on the platform yet. Contact support.","admin.integrations.moduleDisabled":"disabled by the platform","admin.integrations.scopeSites_one":"{n} project","admin.integrations.scopeSites_other":"{n} projects","admin.integrations.typeDisabledByPlatform":"Disabled by the platform","admin.integrations.typeUnavailable":"Unavailable to this organization","admin.knowledgeDocHelp.avoid1":"Pasting the whole landing page (too verbose, too marketing).","admin.knowledgeDocHelp.avoid2":"Pasting the code's technical docs (the end user doesn't care).","admin.knowledgeDocHelp.avoid3":"Inventing features that don't exist yet.","admin.knowledgeDocHelp.avoid4a":"Relative URLs","admin.knowledgeDocHelp.avoid4b":"absolute URLs only, otherwise the link breaks from the embedded widget.","admin.knowledgeDocHelp.avoid5":"Rewording the same info across several sections.","admin.knowledgeDocHelp.avoidTitle":"Avoid","admin.knowledgeDocHelp.claudePrompt":`You'll write the "knowledge base" that will be injected into the system prompt of a support chatbot (an AI chat widget that answers questions from this site's visitors). Target: 5 to 10 KB of Markdown, **no more**. Every token counts — the doc is reloaded on every conversation.

## Method

1. Explore the codebase: read the README, package.json, the public pages (landing, pricing, features, existing FAQ), the public API routes, the major UI components. Run \`git log --oneline -50\` to spot what shipped recently.
2. If the site is live and you have WebFetch, go read the home page, the pricing page, the features page, the footer (terms, legal notices, contact).
3. Note every fact that is *verifiable* from the code or the site. **Make nothing up.** If something is ambiguous, leave it as an HTML comment \`<!-- TO VERIFY: ... -->\` at the end of the relevant section and move on.

## What the chatbot must be able to answer

Cover AT LEAST:

- **What the product does, in 1-2 sentences**
- **Who it's for** (target user)
- **How it works** (main workflow, 3-5 steps max)
- **Pricing** (plans, free tier, trial, what's included in each)
- **Key features** (short list, not exhaustive)
- **Limits / quotas** (per plan, per account, per request)
- **Account and auth** (how to sign up, social login, forgotten password, account deletion)
- **Payment** (accepted methods, currency, invoice, refund)
- **Data & privacy** (where it's hosted, retention, GDPR if relevant)
- **Known bugs / frequent technical FAQs** (if you find recurring tickets in git/issues)
- **How to reach human support** (email, response time)

Add any domain-specific sections you find relevant.

## Required format

- Flat Markdown, **H2 sections** (\`##\`) — no H1, no H3.
- Under each section: **short bullets**, ideally one line per fact.
- **No narrative**, no intro sentences, no "Welcome to…", no "Our team is thrilled to…".
- No nested bullets (1 level max).
- No Markdown tables (the LLM gets confused by them). Use bullet lists instead.
- No "etc.", "and more", "and others" — be concrete or drop it.
- No cross-section repetition.
- When a fact depends on a condition, say it in 1 line:
  "Free → 5 exports/month. Pro (€9/month) → unlimited."

## Absolutely avoid

- The site's marketing copywriting ("revolutionary solution", "unique experience"). We want **facts**, not a sales pitch.
- Non-factual promises ("the best on the market").
- Tech implementation details (stack, framework) — the end user doesn't care.
- Old features that no longer exist.

## Links

Include the **absolute URLs** of key pages (pricing, login, contact, app, public docs…) in Markdown format \`[text](https://…)\`. The bot can surface them as clickable links in its replies — only include URLs you actually want suggested. No relative URLs like \`/pricing\`.

## Deliverable

Write the complete file to \`KNOWLEDGE.md\` at the root. When you're done, also print the character count and the size in KB so I can check we're under the limit.

Go ahead — explore first, then write.`,"admin.knowledgeDocHelp.compressPrompt":"Re-read KNOWLEDGE.md and cut everything redundant or vague. Goal: 30% shorter without losing actionable info. Keep the sections in order of visitor-question frequency (pricing > features > account > technical).","admin.knowledgeDocHelp.copyPrompt":"Copy the prompt","admin.knowledgeDocHelp.costLabel":"Ballpark cost (Gemini Flash Lite, 100 chats/day, 10 KB):","admin.knowledgeDocHelp.costSuffix":"(caching is enabled automatically).","admin.knowledgeDocHelp.costValue":"~22¢/month without cache, ~3¢/month with cache","admin.knowledgeDocHelp.intro1":"The doc is injected into the chatbot's system prompt","admin.knowledgeDocHelp.intro2":". Aim for","admin.knowledgeDocHelp.intro3":"of dense Markdown (≈ 2,500 tokens). Beyond that, the bill climbs fast and the model loses focus.","admin.knowledgeDocHelp.intro5to10":"5–10 KB max","admin.knowledgeDocHelp.introEachConv":"on every conversation","admin.knowledgeDocHelp.step1Caption1":"Claude explores the codebase + visits the live site if possible, then writes","admin.knowledgeDocHelp.step1Caption2":"at the root. It will print the final size.","admin.knowledgeDocHelp.step1Desc1":"Open a Claude Code session","admin.knowledgeDocHelp.step1Desc2":"of the site you want to generate the doc for (not in BipWire). Paste this prompt:","admin.knowledgeDocHelp.step1DescRoot":"at the repo root","admin.knowledgeDocHelp.step1Title":"1. Generate the doc with Claude Code","admin.knowledgeDocHelp.step2Desc":"Re-run Claude Code in the same session with this short prompt:","admin.knowledgeDocHelp.step2Title":"2. If you go over 10 KB, iterate","admin.knowledgeDocHelp.step3Desc1":"Copy the final contents of","admin.knowledgeDocHelp.step3Desc2":"and paste it into the “Reference doc” field of this form. Save the project. That is it — the next chat will use the new doc.","admin.knowledgeDocHelp.step3Title":"3. Paste into the field","admin.knowledgeDocHelp.title":"How to write the reference doc","admin.layout.activeSpace":"Active space","admin.layout.collapseMenu":"Collapse menu","admin.layout.createTask":"Create a ticket","admin.layout.currentOrg":"Current organisation","admin.layout.expandMenu":"Expand menu","admin.layout.logout":"Logout","admin.layout.myAccount":"My account","admin.layout.myProfile":"My profile","admin.layout.openMenu":"Open menu","admin.layout.themeDark":"Switch to dark theme","admin.layout.themeLight":"Switch to light theme","admin.layout.userMenu":"User menu","admin.layout.versionTitle":"Deployed version — used to verify the deployment","admin.llmScopes.sitesRead":"Read project config","admin.llmScopes.tasksRead":"Read tickets (bugs and features)","admin.llmScopes.tasksWrite":"Mark a ticket “to test”","admin.llmScopes.threadsRead":"Read chat conversations","admin.llmTokens.active":"Active","admin.llmTokens.allowedSites":"Allowed projects","admin.llmTokens.colActions":"Actions","admin.llmTokens.colCreated":"Created","admin.llmTokens.colLabel":"Label","admin.llmTokens.colLastUsed":"Last used","admin.llmTokens.colPrefix":"Prefix","admin.llmTokens.colScopes":"Scopes","admin.llmTokens.colSites":"Projects","admin.llmTokens.colStatus":"Status","admin.llmTokens.connect.agentPrompt":`Connect to BipWire (my bug-report + user-chat tool) via its MCP server, then use it.

1. Add the MCP server by running this command at the project root:
   claude mcp add --transport http bip {mcpUrl} --header "Authorization: Bearer {token}"
   (the token is on BipWire’s API Keys page, it starts with bipwire_llm_ — token "{label}", prefix {prefix})

2. Once connected, first call the get_my_permissions tool to learn your scopes, then use the available BipWire tools:
   {tools}

Then, when I say "look at the latest bugs", "what did the user say in conversation X" or "are there recurring complaints about feature Y", fetch the tasks/threads via these tools before answering.`,"admin.llmTokens.connect.fetchingKey":"Fetching the key…","admin.llmTokens.connect.introEmph":"prompt below","admin.llmTokens.connect.introPost":"and paste it into Claude Code — it will add the BipWire MCP server on its own and know what to do with it.","admin.llmTokens.connect.introPre":"Simplest: copy the","admin.llmTokens.connect.keyInserted":"The token key is already inserted in the snippets.","admin.llmTokens.connect.keyReplace":"Replace {placeholder} with your key.","admin.llmTokens.connect.neverCommitPost":"with the token in clear text.","admin.llmTokens.connect.neverCommitPre":"Never commit","admin.llmTokens.connect.noSecret":"This token’s key is not recoverable (created before v1.22) — the snippets contain a placeholder. Regenerate this token (↻ icon in the list) to get an automatically insertable key.","admin.llmTokens.connect.orDirect":"Or directly — command to run yourself","admin.llmTokens.connect.promptHeading":"Prompt to paste into Claude Code","admin.llmTokens.connect.rightsInfo":"This token’s scopes: {scopes}. Claude Code will only see the matching tools ({tools}).","admin.llmTokens.connect.sitesAll":"Access to all projects.","admin.llmTokens.connect.sitesLimited":"Scope limited to projects: {sites}.","admin.llmTokens.connect.title":"Connect to Claude Code — {label}","admin.llmTokens.connect.warnKeyInserted":"The snippets above contain the key in clear text.","admin.llmTokens.connect.warnKeyReplace":"Replace {placeholder} with this token’s key.","admin.llmTokens.connectAgent":"Connect a CLI agent","admin.llmTokens.connectHint":"Connect to Claude Code (command + prompt)","admin.llmTokens.copyFullKey":"Copy the full key","admin.llmTokens.createSitesHelper":"Limit the token to specific projects. No box checked = access to all projects.","admin.llmTokens.created.addToClaudeCode":"Add to Claude Code (MCP server)","admin.llmTokens.created.doneClose":"I copied it, close","admin.llmTokens.created.intro":"Copy this token to wire it up right away. You can also copy it again later from the API Keys list (copy button on the token’s row).","admin.llmTokens.created.mcpDescPost":"will be available in Claude Code (read tools for tickets / threads / projects depending on the token’s scopes).","admin.llmTokens.created.mcpDescPre":"Token already inserted. Run this command at the root of your project — the MCP server","admin.llmTokens.created.testLabel":"Test","admin.llmTokens.created.title":"Token created — copy it now","admin.llmTokens.created.tokenLabel":"Token","admin.llmTokens.created.warnMid":"command stores the token locally (private, not committed). If you use","admin.llmTokens.created.warnPost":", don’t commit it — it contains the token in clear text.","admin.llmTokens.created.warnPre":"The","admin.llmTokens.deleteHint":"Delete permanently (revokes then removes the row)","admin.llmTokens.deleteMessage":"The token will be revoked then removed from the DB — it disappears from the list, no trace left (lastUsedAt, etc.). Prefer “Revoke” if you want to keep the usage history for auditing.","admin.llmTokens.deleteTitle":"Delete “{label}”?","admin.llmTokens.editScopesHint":"Edit scopes","admin.llmTokens.editScopesInfo":"Applied immediately server-side — the tool using this token sees the change on its next call without having to regenerate.","admin.llmTokens.editScopesTitle":"Edit scopes — {label}","admin.llmTokens.editSitesHelper":"No box checked = access to all projects.","admin.llmTokens.editSitesHint":"Edit allowed projects","admin.llmTokens.empty":"No token. Generate the first one to give an LLM agent access.","admin.llmTokens.grantedScopes":"Granted scopes","admin.llmTokens.howTo.claudeMdBlock":`## BipWire — user bugs and conversations

BipWire is my internal chat + bug-report tool. You can read the reported bugs
and chat conversations for this project (\`siteId={siteId}\`).

**Auth**: \`Authorization: Bearer $BIPWIRE_LLM_TOKEN\` header (token in my env,
never hard-code it).

**Full docs**: {docUrl} — fetch it before asking questions, it
describes the markdown format of the responses and the available filters.

**Useful endpoints**:
- Open tickets: \`GET {adminOrigin}/api/llm/tasks?siteId=<SITE_ID>&status=new&limit=10\`
- Bugs only: add \`&type=bug\` (or \`&type=feature\` for suggestions)
- Ticket detail: \`GET {adminOrigin}/api/llm/tasks/{id}\`
- Pending threads: \`GET {adminOrigin}/api/llm/threads?siteId=<SITE_ID>&status=awaiting_human\`
- Thread detail: \`GET {adminOrigin}/api/llm/threads/{id}\`

When I say "look at the latest ticket" / "what did the user say in conversation X"
/ "are there recurring complaints about feature Y", automatically fetch
the tasks/threads before answering.`,"admin.llmTokens.howTo.intro":"Three steps so Claude Code (or any WebFetch-capable agent) can read the bugs and conversations of your projects from any repo.","admin.llmTokens.howTo.mcpBodyPost":", same tokens as the ones created here. Full config + available tools:","admin.llmTokens.howTo.mcpBodyPre":"The MCP server is exposed at","admin.llmTokens.howTo.mcpServerPage":"MCP Server page","admin.llmTokens.howTo.mcpTitle":"Using Claude Desktop or an MCP client?","admin.llmTokens.howTo.methodADescEnd":"into the env of the Bash/WebFetch tools it launches, without touching your shell.","admin.llmTokens.howTo.methodADescMid":"(global) or","admin.llmTokens.howTo.methodADescPost":"(per-project). Claude Code automatically injects","admin.llmTokens.howTo.methodADescPre":"Add the block to","admin.llmTokens.howTo.methodAPost":"(recommended)","admin.llmTokens.howTo.methodAPre":"Method A — Claude Code","admin.llmTokens.howTo.methodB":"Method B — Classic shell variable","admin.llmTokens.howTo.methodBAltPost":"gitignored at the repo root.","admin.llmTokens.howTo.methodBAltPre":"Per-project alternative: a","admin.llmTokens.howTo.methodBDescPost":"in the terminal to also have access to the token (outside Claude Code).","admin.llmTokens.howTo.methodBDescPre":"If you want","admin.llmTokens.howTo.multiBody":"Generate one token per project/machine with a meaningful label (“Claude Code shotcv MacBook”). On a leak: you just revoke that one without breaking the others. The bipwire_llm_xxxx… prefix visible in the list lets you quickly identify which token is affected.","admin.llmTokens.howTo.multiTitle":"Multi-project, multi-token","admin.llmTokens.howTo.securityBody":"— the API is read-only by design. Even if leaked, a token cannot modify BipWire’s state. lastUsedAt in the list shows you the zombie tokens to revoke.","admin.llmTokens.howTo.securityLabel":"Security","admin.llmTokens.howTo.shellComment1":"# Once and for all (~/.zshrc or ~/.bashrc)","admin.llmTokens.howTo.shellComment2":"# then reload: source ~/.zshrc","admin.llmTokens.howTo.siteIdPlaceholder":"<PUT_THE_SITE_ID_HERE>","admin.llmTokens.howTo.step1":"1. Generate a token + expose it as an env var","admin.llmTokens.howTo.step1Desc":"Click “New token” on this page, copy the clear text shown, then one of the two methods below depending on your setup.","admin.llmTokens.howTo.step2DescPost":"with the id of the relevant BipWire project (shotcv, hypnalis…).","admin.llmTokens.howTo.step2DescPre":"Replace","admin.llmTokens.howTo.step2Post":"of the consuming project","admin.llmTokens.howTo.step2Pre":"2. Add this block to the","admin.llmTokens.howTo.step3":"3. Test from your shell before launching Claude Code","admin.llmTokens.howTo.step3Desc":"Should return markdown listing the 2 latest bugs. If you see 401, the token is mispasted or revoked. If nothing, check the DNS of the admin host.","admin.llmTokens.howTo.title":"Connect Claude Code (or another agent) to the BipWire API","admin.llmTokens.howToConnect":"How to connect Claude Code?","admin.llmTokens.keyTitle":"Key — {label}","admin.llmTokens.label":"Label","admin.llmTokens.labelHelper":"Free-form description to identify this token later (usually the tool + the machine).","admin.llmTokens.labelPlaceholder":"e.g. Claude Code shotcv MacBook","admin.llmTokens.newToken":"New token","admin.llmTokens.newTokenTitle":"New API token","admin.llmTokens.orManualPost":"(repo root):","admin.llmTokens.orManualPre":"Or equivalent manual config in","admin.llmTokens.personal":"Personal","admin.llmTokens.personalMine":"Mine","admin.llmTokens.quickTest":"Quick test","admin.llmTokens.regenConfirm":"Regenerate","admin.llmTokens.regenHint":"Regenerate the key (the old one stops working)","admin.llmTokens.regenMessage":"A new key is generated on this token (same scopes, same projects). The old key stops working IMMEDIATELY — every tool wired to it will need the new one.","admin.llmTokens.regenPending":"Regenerating…","admin.llmTokens.regenTitle":"Regenerate the key for “{label}”?","admin.llmTokens.renameHint":"Click to rename","admin.llmTokens.revokeConfirm":"Revoke","admin.llmTokens.revokeHint":"Revoke this token (keeps the record in the list)","admin.llmTokens.revokeMessage":"The token will be invalidated immediately. Any tool using it will get a 401 on its next call. Irreversible (you’ll have to generate a new token).","admin.llmTokens.revokePending":"Revoking…","admin.llmTokens.revokeTitle":"Revoke “{label}”?","admin.llmTokens.revoked":"Revoked","admin.llmTokens.scopesHelper":"The token will only reach the endpoints / MCP tools matching these scopes. Check only what’s needed — you can generate several tokens with different scopes if necessary.","admin.llmTokens.scopesNone":"none","admin.llmTokens.sitesAll":"all","admin.llmTokens.subtitleDocLink":"auto-discoverable docs","admin.llmTokens.subtitleMcpLink":"MCP server","admin.llmTokens.subtitleP1":"Long-lived bearer tokens for agents (Claude Code, Cursor…). Same tokens for the REST API","admin.llmTokens.subtitleP2":"and the","admin.llmTokens.subtitleP3":". See the","admin.llmTokens.subtitleP4":"for details.","admin.llmTokens.title":"API Keys","admin.llmTokens.toastCopyFailed":"Copy failed — key unavailable","admin.llmTokens.toastDeleted":"Token deleted","admin.llmTokens.toastKeyCopied":"Key copied to clipboard","admin.llmTokens.toastRevoked":"Token revoked","admin.llmTokens.toastScopesUpdated":"Scopes updated","admin.llmTokens.tokenPlaceholder":"PASTE_YOUR_BIPWIRE_TOKEN","admin.llmTokens.viewKey":"View the key in clear text","admin.llmUsage.blocked":"blocked","admin.llmUsage.calls":"Calls","admin.llmUsage.callsThisMonth":"LLM calls this month","admin.llmUsage.colCostEur":"Cost €","admin.llmUsage.colDay":"Day","admin.llmUsage.colInput":"Input","admin.llmUsage.colOutput":"Output","admin.llmUsage.colTotalTokens":"Total tokens","admin.llmUsage.cost":"Cost","admin.llmUsage.detailTitle":"{name} — {days}-day detail","admin.llmUsage.disk":"Disk","admin.llmUsage.emptyPeriod":"No activity in the period.","admin.llmUsage.overviewSummary":"{month} — {tokens} tokens · {eur} · {calls} calls (all projects)","admin.llmUsage.planQuotas":"Plan & quotas (organisation)","admin.llmUsage.sites":"Projects","admin.llmUsage.title":"LLM consumption","admin.llmUsage.tokens":"Tokens","admin.llmUsage.unlimited":"unlimited","admin.members.changeRole":"Change role","admin.members.colActions":"Actions","admin.members.colAddedAt":"Added on","admin.members.colEmail":"Email","admin.members.colExpires":"Expires","admin.members.colInvitedBy":"Invited by","admin.members.colRole":"Role","admin.members.emailAddress":"Email address","admin.members.empty":"No member yet.","admin.members.invite":"Invite","admin.members.inviteTitle":"Invite a member","admin.members.lastOwnerTooltip":"Last owner of the space — action unavailable","admin.members.loadError":"Unable to load members.","admin.members.ownerOnlyTooltip":"Only an owner can act on an owner","admin.members.pendingEmpty":"No pending invitation.","admin.members.pendingTitle":"Pending invitations","admin.members.remove":"Remove","admin.members.removeFromSpace":"Remove from the space","admin.members.removing":"Removing…","admin.members.resendInvite":"Resend invitation","admin.members.revokeInvite":"Revoke invitation","admin.members.revokeMessage":"{email} will lose access to this space.","admin.members.revokeTitle":"Remove this member?","admin.members.role":"Role","admin.members.self":"(you)","admin.members.sendInvite":"Send the invitation","admin.members.setPassword":"Set the password","admin.members.title":"Members","admin.members.toastInviteResent":"Invitation resent — new link, the old one no longer works.","admin.members.toastInviteRevoked":"Invitation revoked","admin.members.toastInviteSent":"Invitation sent to {email}","admin.members.toastPasswordUpdated":"Password updated","admin.members.toastRemoved":"Member removed","admin.members.toastRoleUpdated":"Role updated","admin.messages.awaitingVisitor":"Awaiting the visitor’s reply · {date}","admin.messages.captureReceived":"✅ Capture received","admin.messages.captureRequested":"📸 Capture requested","admin.messages.roleBot":"🤖 BipWire","admin.messages.roleSupport":"👤 Support","admin.messages.viewCapture":"View the capture","admin.nav.aiModels":"AI models","admin.nav.apiKeys":"API Keys","admin.nav.backToApp":"Back to app","admin.nav.billing":"Billing","admin.nav.connectors":"Connectors","admin.nav.dashboard":"Dashboard","admin.nav.documentation":"Documentation","admin.nav.history":"History","admin.nav.llmUsage":"LLM usage","admin.nav.members":"Members","admin.nav.modules":"Modules","admin.nav.notifications":"Notifications","admin.nav.organisations":"Organisations","admin.nav.platformAdmin":"Platform admin","admin.nav.sectionAide":"Help","admin.nav.sectionOrganisation":"Organisation","admin.nav.sectionPlateforme":"Platform","admin.nav.sectionSupport":"Support","admin.nav.settings":"Settings","admin.nav.sites":"Projects","admin.nav.tasks":"Tickets","admin.nav.threads":"Conversations","admin.nav.users":"Users","admin.notFound.backToDashboard":"Back to dashboard","admin.notFound.code":"404","admin.notFound.message":"This page does not exist.","admin.notifications.add":"Add a notification","admin.notifications.colActions":"Actions","admin.notifications.colEnabled":"Enabled","admin.notifications.colEvents":"Events","admin.notifications.colName":"Name","admin.notifications.colScope":"Scope","admin.notifications.colType":"Type","admin.notifications.deleteMessage":"The channel will be removed. Events will no longer be sent to this destination.","admin.notifications.deleteTitle":"Delete this notification?","admin.notifications.description":"Send a message to an external channel (email, Telegram, webhook) when an event occurs. Scope and events are configurable per channel.","admin.notifications.empty":"No notification configured.","admin.notifications.loadError":"Unable to load notifications.","admin.notifications.title":"Notifications","admin.notifications.toastCreated":"Notification created","admin.notifications.toastDeleted":"Notification deleted","admin.notifications.toastUpdated":"Notification updated","admin.orgBillingProfile.subtitle":"Legal and billing information. Used on invoices (ADR-0013).","admin.orgBillingProfile.title":"Company — {orgName}","admin.orgMembers.addHint":"If the email matches an existing account, it is linked directly; otherwise an invitation is sent.","admin.orgMembers.emailLabel":"User email","admin.orgMembers.empty":"No member.","admin.orgMembers.subtitle":"Add, change the role of, or remove a user from this space. Removing a member does not delete their account.","admin.orgMembers.title":"Members — {orgName}","admin.orgMembers.toastAdded":"Member added.","admin.orgMembers.toastInvited":"Invitation sent to {email}.","admin.orgMembers.toastPasswordUpdated":"Password updated.","admin.orgMembers.toastRemoved":"Member removed.","admin.orgMembers.toastRoleUpdated":"Role updated.","admin.pagination.perPage":"Per page:","admin.pagination.range":"{from}–{to} of {count}","admin.platform.invitations.colActions":"Actions","admin.platform.invitations.colEmail":"Email","admin.platform.invitations.colExpires":"Expires","admin.platform.invitations.colInvitedBy":"Invited by","admin.platform.invitations.colTarget":"Destination","admin.platform.invitations.newOrg":"New organisation","admin.profile.activeOrg":"Active org: {name}","admin.profile.agentToken":"My agent token","admin.profile.agentTokenConnect":"Connect an agent","admin.profile.agentTokenCreate":"Create my token","admin.profile.agentTokenDesc":"One token, yours: it connects Claude Code, Cursor, Codex or OpenCode to every project of the organisation, from each of your machines. The project is picked by the repo’s .bip.json.","admin.profile.agentTokenFresh":"New key: paste it into the MCP config of each of your machines.","admin.profile.agentTokenNone":"You don’t have a token yet.","admin.profile.agentTokenRegen":"Regenerate","admin.profile.agentTokenRegenConfirm":"The old key stops working immediately, on all your machines. You will have to paste the new one into each MCP config.","admin.profile.agentTokenShow":"Show the key","admin.profile.changePassword":"Change my password","admin.profile.confirmNewPassword":"Confirm the new password","admin.profile.currentPassword":"Current password","admin.profile.errFailed":"Password change failed.","admin.profile.errMismatch":"The confirmation does not match.","admin.profile.errTooShort":"The new password must be at least 8 characters.","admin.profile.identity":"Identity","admin.profile.langFailed":"Failed to change the language.","admin.profile.langUpdated":"Language updated.","admin.profile.myOrgs":"My organisations","admin.profile.newPassword":"New password","admin.profile.newPasswordHelper":"At least 8 characters","admin.profile.title":"My profile","admin.profile.toastUpdated":"Password updated.","admin.profile.update":"Update","admin.projet.aucunResultat":"No matching project","admin.projet.entrer":"Open project","admin.projet.introuvable":"Project not found","admin.projet.introuvableAide":"Project “{id}” does not exist in this organization, or it was deleted.","admin.projet.porteeTous":"all projects","admin.projet.recents":"Recent","admin.projet.rechercher":"Search a project","admin.projet.reglages":"Project settings","admin.projet.retourGlobal":"All projects","admin.projet.selecteur":"Project","admin.projet.tous":"All projects","admin.projet.tousLesProjets":"Projects","admin.reglages.notifEmailOff":"Email is turned off by the platform: your choice is kept, but nothing is sent for now.","admin.reglages.notifHelp":"Get emails at {email} about what happens in this organization. Nothing is checked by default.","admin.reglages.notifSaved":"Notifications saved","admin.reglages.notifScope":"Projects","admin.reglages.notifScopeAll":"All projects","admin.reglages.notifScopeSome":"Only some projects","admin.reglages.notifTaskCreated":"A new ticket comes in","admin.reglages.notifTaskStatus":"A ticket changes status (except when I change it)","admin.reglages.notifThreadEscalated":"A conversation is waiting for a human reply","admin.reglages.notifTitle":"My notifications","admin.reglages.orgId":"Identifier","admin.reglages.orgIdentity":"Organization","admin.reglages.orgManagePlan":"Manage subscription →","admin.reglages.orgName":"Organization name","admin.reglages.orgNameReadOnly":"Only the owner and admins can change it.","admin.reglages.orgPlan":"Plan","admin.reglages.orgPlanOwnerOnly":"The owner manages the subscription.","admin.reglages.orgRenamed":"Organization renamed","admin.reglages.tabCompte":"Account","admin.reglages.tabNotifications":"Notifications","admin.reglages.tabOrganisation":"Organization","admin.reglages.tabPreferences":"Preferences","admin.reglages.theme":"Theme","admin.reglages.themeDark":"Dark","admin.reglages.themeHint":"“System” follows your computer’s setting. The choice is kept on this device.","admin.reglages.themeLight":"Light","admin.reglages.themeSystem":"System","admin.reglages.title":"Settings","admin.report.fields":"Report content","admin.report.dates":"Dates","admin.report.fixReport":"Fix report (AI)","admin.report.fixReportHeading":"Fix report","admin.report.context":"Context","admin.report.html":"HTML","admin.report.markdown":"Markdown","admin.report.plainText":"Plain text","admin.report.priority":"Priority","admin.report.receivedOn":"Received on","admin.report.reportHeading":"Report — {date}","admin.report.request":"Request","admin.report.siteHeading_one":"{site} ({n} ticket)","admin.report.siteHeading_other":"{site} ({n} tickets)","admin.report.status":"Status","admin.report.title_one":"Report ({n} ticket)","admin.report.title_other":"Report ({n} tickets)","admin.report.toastCopied":"Report copied to clipboard","admin.report.toastCopiedFormatted":"Report copied (formatted for Teams / mail)","admin.report.toastCopyFailed":"Copy failed — select the text manually","admin.report.type":"Type","admin.report.untitled":"(untitled)","admin.report.updated":"Updated","admin.report.url":"URL","admin.report.visitor":"Visitor","admin.roles.admin":"Administrator","admin.roles.member":"Member","admin.roles.owner":"Owner","admin.roles.superAdmin":"Super admin","admin.search.empty":"No ticket matches “{q}”.","admin.search.error":"Search error: {msg}","admin.search.placeholder":"Search a ticket (id, title, description)…","admin.search.prompt":"Type a term to search a ticket.","admin.search.resultCount_one":"{n} result","admin.search.resultCount_other":"{n} results","admin.search.resultsShown":"({limit} shown)","admin.search.subtitle":"All projects, active and archived tickets.","admin.search.title":"Search","admin.setPassword.field":"New password","admin.setPassword.helper":"At least 8 characters","admin.setPassword.set":"Set","admin.setPassword.text":"New password for {email}. It applies immediately.","admin.setPassword.title":"Set a password","admin.settings.language":"Language","admin.shortcut.invalidHint":"Select at least ⌘/Ctrl or {mod}, and a key.","admin.shortcut.keyPlaceholder":"Key","admin.shortcut.shift":"Shift","admin.shortcut.validHint":"⌘ (Mac) and Ctrl (Windows/Linux) both trigger the shortcut — either works.","admin.siteEdit.aiChatOffHint":"Human chat only: every message goes straight to a human (Telegram notification).","admin.siteEdit.aiChatOnHint":"An LLM (Gemini + Claude fallback) replies, with escalation to a human.","admin.siteEdit.appearanceSubtitle":"Icon, colors, size and position of the bubble on the visitor side. Baked into the project's loader — no visual flash.","admin.siteEdit.backToList":"Back to the list","admin.siteEdit.bubbleColors":"Bubble colors","admin.siteEdit.bubbleColorsHint":"They color the widget's bubble and panel, and take precedence over the tag's data-color attribute. Left on default, the tag still decides.","admin.siteEdit.bubbleColor":"Background","admin.siteEdit.bubbleIconColor":"Icon","admin.siteEdit.colorDefault":"Default","admin.siteEdit.bubbleIconCustom":"Custom","admin.siteEdit.iconUpload":"Upload an SVG","admin.siteEdit.iconUploadHint":"Preferably a single-color SVG: it will take the icon color. Simple shapes (path, circle, rect…), 10 KB at most once cleaned.","admin.siteEdit.iconRemove":"Remove icon","admin.siteEdit.iconTooBig":"File too large: 64 KB at most.","admin.siteEdit.iconMulticolor":"Multicolor icon: it keeps its own colors, the icon color does not apply.","admin.siteEdit.paletteWaiting":"The site just opened in a new tab: the widget is picking up its colors there. You can close the tab as soon as they show up here.","admin.siteEdit.paletteDone":"Colors picked up.","admin.siteEdit.paletteTimeout":"No colors received. Check that the widget is installed on that page, then try again.","admin.siteEdit.paletteEmpty":"No colors picked up yet: “Detect again” opens the site and collects them.","admin.siteEdit.paletteChooseTitle":"Which address should the colors be picked up from?","admin.siteEdit.paletteChooseHint":"The site opens in a new tab; the installed widget picks up the palette there, then you can close the tab.","admin.siteEdit.paletteTitle":"Colors found on the site","admin.siteEdit.paletteHint":"Click: background color · Shift+click or right-click: icon color","admin.siteEdit.paletteDetectedAt":"found on {date}","admin.siteEdit.paletteRefresh":"Detect again","admin.siteEdit.paletteRequested":"The widget will pick up the site's colors on its next visit.","admin.siteEdit.paletteRefreshPending":"New detection requested: it will happen on the site's next visit.","admin.siteEdit.paletteRole.theme-color":"theme color","admin.siteEdit.paletteRole.css-var":"CSS variable","admin.siteEdit.paletteRole.background":"page background","admin.siteEdit.paletteRole.text":"text","admin.siteEdit.paletteRole.button":"buttons","admin.siteEdit.paletteRole.link":"links","admin.siteEdit.paletteRole.header":"header","admin.siteEdit.panelColors":"Panel colors","admin.siteEdit.panelColorsHint":"Background, text and assistant messages in the widget panel. Left on default, the panel keeps its original light theme.","admin.siteEdit.bubbleCornerBottomLeft":"Bottom-left","admin.siteEdit.bubbleCornerBottomRight":"Bottom-right","admin.siteEdit.bubbleCornerTopLeft":"Top-left","admin.siteEdit.bubbleCornerTopRight":"Top-right","admin.siteEdit.bubbleDraggable":"Draggable bubble (drag & drop)","admin.siteEdit.bubbleDraggableHint":"The visitor can reposition the bubble; its position is remembered in their browser. Dragging lives in the loader (active from page-load) without bloating the bundle.","admin.siteEdit.bubbleIconBug":"Bug","admin.siteEdit.bubbleIconChat":"Chat","admin.siteEdit.bubbleIconHelp":"Help ( ? )","admin.siteEdit.bubbleOffsetX":"Horizontal offset","admin.siteEdit.bubbleOffsetY":"Vertical offset","admin.siteEdit.bubbleMode":"Button placement","admin.siteEdit.bubbleModeHint":"A floating bubble in a corner of the screen, a bubble placed inside an element of the page (a toolbar, for example), or no button: the site then opens the widget with its own button.","admin.siteEdit.bubbleModeDoc":"See the docs","admin.siteEdit.bubbleModeFloating":"Floating","admin.siteEdit.bubbleModeEmbedded":"Embedded in the page","admin.siteEdit.bubbleModeManual":"None — site's own button","admin.siteEdit.bubbleSelector":"CSS selector of the element","admin.siteEdit.bubbleSelectorHint":"Example: #bipwire-button or .header-actions. The bubble is added inside it, even if the element appears after page load or is re-rendered. Missing element: no button, and a warning in the browser console.","admin.siteEdit.bubbleSelectorInvalid":"A valid CSS selector is required (200 characters at most).","admin.siteEdit.bubbleManualHint":"Add this to the page, alongside the widget's <script> tag:","admin.siteEdit.panelPosition":"Where the panel opens","admin.siteEdit.panelPositionHint":"Corner of the screen where the panel opens, and its margins from the edges. On mobile, the panel takes the full screen.","admin.siteEdit.bubblePosition":"Bubble position","admin.siteEdit.bubblePositionHint":"Anchor corner and offsets from the edges of the visitor's screen. The panel and notifications follow the same corner. If the bubble is draggable, this position is only the starting point.","admin.siteEdit.bubbleSize":"Bubble size","admin.siteEdit.bubbleSizeLarge":"Large","admin.siteEdit.bubbleSizeMedium":"Medium","admin.siteEdit.bubbleSizeSmall":"Small","admin.siteEdit.capture":"Capture","admin.siteEdit.captureBodies":"Request contents (bodies)","admin.siteEdit.captureBodiesHintMid":"or","admin.siteEdit.captureBodiesHintPost":"enabled.","admin.siteEdit.captureBodiesHintPre":"Includes request / response bodies (truncated, tokens masked). ⚠️ may contain PII. Requires","admin.siteEdit.captureConsole":"Console","admin.siteEdit.captureConsoleHintPost":"+ JS errors + CSP violations.","admin.siteEdit.captureConsoleHintPre":"All","admin.siteEdit.captureFetchHint":"Method + URL + status + duration. Disable if the site only uses XHR.","admin.siteEdit.capturePageImage":"Capture an image of the page","admin.siteEdit.capturePageImageHint":"When the visitor sends their report, the widget can take a full-page screenshot (SnapDOM).","admin.siteEdit.captureShortcut":"Capture keyboard shortcut","admin.siteEdit.captureShortcutHint":"When enabled, this shortcut captures the viewport, opens the annotation editor, then (on confirm) the report form with the capture inside.","admin.siteEdit.captureStorage":"Storage values","admin.siteEdit.captureStorageHint":"Captures localStorage / sessionStorage contents (values truncated to 2 KB). ⚠️ may contain tokens / personal data. Without it, only key names are visible.","admin.siteEdit.captureXhr":"XMLHttpRequest","admin.siteEdit.captureXhrHint":"For legacy sites / libs that haven't migrated to fetch.","admin.siteEdit.chatSubtitle":"Conversation bubble. Enabled alone = messages go straight to a human; with “AI chat”, an LLM replies first and escalates if needed.","admin.siteEdit.clientLinkHelperFooter":"If a value is missing, the link doesn’t appear.","admin.siteEdit.clientLinkHelperId":"(the id passed to identify)","admin.siteEdit.clientLinkHelperLabel":"Available placeholders","admin.siteEdit.clientLinkHelperVisitorId":"(BipWire’s internal visitor id).","admin.siteEdit.clientLinkSubtitle":"Know who is writing: your site passes on the signed-in user, and each ticket links to their profile in your back office.","admin.siteEdit.createSite":"Create the project","admin.siteEdit.defaultSystemPrompt":`You are the assistant for {site}.

Mission: answer visitors' questions about the product, how it works, pricing and the support on offer. Warm, direct tone. Short replies (2–4 sentences max unless explicitly asked otherwise).

Escape hatches — hand off to a human IMMEDIATELY (call the escalate tool) whenever:
- You're unsure of the answer — never guess, never make up any fact about the product.
- The user brings up payment, a refund, an invoice, or a dispute.
- The user shows frustration or explicitly asks to talk to someone.
- Medical, legal, HR, or sensitive personal topics.
- A non-trivial technical bug beyond the FAQ.

If the visitor has already left their email and you escalate, tell them they'll be contacted back within the day.`,"admin.siteEdit.defaultSystemPromptSiteFallback":"this site","admin.siteEdit.displayPagesSubtitle":"Restrict the widget display to certain pages of the site.","admin.siteEdit.edit":"Editing","admin.siteEdit.enableAiChat":"Enable AI chat","admin.siteEdit.fieldId":"ID","admin.siteEdit.fieldName":"Name","admin.siteEdit.fieldOrigins":"Origins (one URL per line)","admin.siteEdit.howToWriteIt":"How to write it?","admin.siteEdit.idHelperCreate":"Short readable identifier, e.g. shotcv. Lowercase, digits, hyphens. ⚠️ Permanent — cannot be changed after creation.","admin.siteEdit.idHelperEdit":"Set at creation — cannot be changed.","admin.siteEdit.identitySubtitle":"The project’s identifier and name, and the CORS-allowed origins.","admin.siteEdit.insertDefaultTemplate":"Insert the default template","admin.siteEdit.knowledgeDocCount":"{count} / {max} characters","admin.siteEdit.knowledgeDocHelper":"Injected into the system prompt on every chat — aim for dense Markdown (5–10 KB).","admin.siteEdit.knowledgeDocPlaceholder":"FAQ, pricing, features, limits, support contact…","admin.siteEdit.loadError":"Couldn't load the project: {msg}","admin.siteEdit.newSite":"New project","admin.siteEdit.originsErrorHelper":"Each line must be an absolute URL (https://… or http://localhost…).","admin.siteEdit.originsHelper":"One URL per line. Every listed origin is allowed by CORS.","admin.siteEdit.pathPatternsErrorHelper":"Each line must start with /, !/ or be *.","admin.siteEdit.pathPatternsHelper":"Example: “/hub” activates on /hub and all its sub-paths.","admin.siteEdit.pathPatternsLabel":"Path patterns","admin.siteEdit.patternExampleAllButAdmin":"everywhere except /admin and its sub-paths","admin.siteEdit.patternExampleAllButHome":"everywhere except the home page (“/” only targets / exactly)","admin.siteEdit.patternExampleSectionButOne":"on /app and its sub-paths, except /app/internal","admin.siteEdit.patternExclude":"excludes","admin.siteEdit.patternExclusionExamples":"Exclusion examples","admin.siteEdit.patternExclusionRule":"An exclusion always wins; with exclusions only, the widget shows on every other page.","admin.siteEdit.patternMatchAll":"matches everything","admin.siteEdit.patternMidWildcard":"wildcard in the middle","admin.siteEdit.patternOneSegment":"one segment","admin.siteEdit.patternRecursive":"recursive","admin.siteEdit.patternSmartPrefix":"smart prefix (matches /hub, /hub/x, …)","admin.siteEdit.patternsHeading":"Accepted patterns","admin.siteEdit.patternsHeadingHint":"(one per line) — empty = active everywhere:","admin.siteEdit.referenceDoc":"Reference doc (optional)","admin.siteEdit.reportSubtitle":"Enables the report button in the widget and sets what the widget captures on the visitor side. The more you capture, the richer the report — but the heavier the loader.","admin.siteEdit.screenshotAlways":"Always","admin.siteEdit.screenshotAlwaysHint":"Always captures, even if the visitor provided one","admin.siteEdit.screenshotIfMissing":"If not provided","admin.siteEdit.screenshotIfMissingHint":"Captures only if the visitor didn't provide one","admin.siteEdit.screenshotNever":"Never","admin.siteEdit.screenshotNeverHint":"No auto-capture — the visitor provides one or nothing","admin.siteEdit.sectionActivation":"Activation","admin.siteEdit.sectionAppearance":"Appearance","admin.siteEdit.sectionChat":"Chat","admin.siteEdit.sectionClientLink":"Customer profile link","admin.siteEdit.sectionDisplayPages":"Display pages","admin.siteEdit.sectionIdentity":"Identity","admin.siteEdit.sectionReport":"Report a bug / feature","admin.siteEdit.showClientLink":"Show the link on tickets and conversations","admin.siteEdit.showClientLinkHint":"Disabled = template stored but link hidden in the admin.","admin.siteEdit.systemPrompt":"System prompt","admin.siteEdit.systemPromptHelper":"Context given to the LLM. Includes the escape hatches: conditions under which to hand off to a human.","admin.siteEdit.thanksHelper":"Shown to the visitor right after they submit a ticket. Empty = default text.","admin.siteEdit.thanksMessage":"Thank-you message (optional)","admin.siteEdit.thanksPlaceholder":"Thanks, we’re on it!","admin.siteEdit.toastUpdated":"Project updated","admin.siteEdit.urlTemplate":"URL template","admin.siteEdit.visitorUpload":"Visitor file upload","admin.siteEdit.visitorUploadHint":"Shows an “Attach a file” button next to the capture, in the form. The visitor attaches as many files as they want (images, PDFs, logs, exports…); they are sent with the screenshots.","admin.siteEdit.welcomeHelper":"First message shown in the bubble. Empty = default text matching the mode.","admin.siteEdit.welcomeMessage":"Welcome message (optional)","admin.siteEdit.welcomePlaceholderAi":"Hi 👋 Ask your question, I'll reply in a few seconds.","admin.siteEdit.welcomePlaceholderHuman":"Hi 👋 Write your message, we'll get back to you shortly.","admin.siteEdit.widgetActive":"Widget active","admin.siteEdit.widgetDisabled":"Widget disabled","admin.sites.activationWizard":"Activation assistant","admin.sites.aiChatEnabled":"AI chat enabled","admin.sites.captureRequestBodiesEnabled":"HTTP request capture enabled","admin.sites.colActions":"Actions","admin.sites.colCreatedAt":"Created on","admin.sites.colFeatures":"Features","admin.sites.colId":"ID","admin.sites.colName":"Name","admin.sites.colOrigin":"Origin","admin.sites.colPublicKey":"Public key","admin.sites.deleteMessage":"This will also delete every associated conversation and ticket.","admin.sites.deleteTitle":"Delete this project?","admin.sites.empty":"No project yet.","admin.sites.externalUserLinkEnabled":"External client link enabled","admin.sites.loadError":"Unable to load projects.","admin.sites.newSite":"New project","admin.sites.rebuildFailed":"Rebuild failed: {error}","admin.sites.rebuildScripts":"Rebuild scripts","admin.sites.rebuildScriptsTooltip":"Triggers `bun run build` for widget assets (b.js / panel / bug / loader) and resyncs all project loaders. Run after editing the widget in production.","admin.sites.rebuildSuccess_one":"Scripts rebuild OK in {duration}s — {loaders} loader resynced, {size} KB total","admin.sites.rebuildSuccess_other":"Scripts rebuild OK in {duration}s — {loaders} loaders resynced, {size} KB total","admin.sites.rebuilding":"Rebuilding…","admin.sites.regenKey":"Regenerate the key","admin.sites.regenKeyMessage":"The old one will stop working immediately. Every current embed will break until you update the new key.","admin.sites.regenKeyTitle":"Regenerate the key?","admin.sites.regenerating":"Regenerating…","admin.sites.taskReportEnabled":"Ticket reporting enabled","admin.sites.title":"Projects","admin.sites.toastDeleted":"Project deleted","admin.sites.toastNewKey":"New key generated","admin.sites.widgetDisabled":"Widget disabled: no feature runs on this project","admin.task.typeBug":"Bug","admin.task.typeFeature":"Feature","admin.taskDetail.actionNote":"Action note (status “to test”)","admin.taskDetail.attachmentPurged":"File purged on {date} — the plan’s retention period has passed","admin.taskDetail.attachments":"Attachments ({n})","admin.taskDetail.backToTasks":"Back to tickets","admin.taskDetail.bipVisitorId":"BipWire visitor ID","admin.taskDetail.captureAlt":"Screenshot {idx}","admin.taskDetail.captureTooltip":"Screenshot {idx} — {size} — click to enlarge","admin.taskDetail.captures":"Captures ({n})","admin.taskDetail.clientId":"Client ID","admin.taskDetail.contextHint":"Context inferred by the LLM from the description and URL","admin.taskDetail.ctxUrl":"URL","admin.taskDetail.deleteAttachment":"Delete this file","admin.taskDetail.deleteCapture":"Delete this capture","admin.taskDetail.deleteTaskMessage":"The ticket, its captures, attachments and notes are permanently deleted (disk files included). Irreversible.","admin.taskDetail.deleteTaskTitle":"Delete {displayId}?","admin.taskDetail.descEditHint":"Double-click (or “e”) to edit","admin.taskDetail.description":"Description","admin.taskDetail.displayIdHint":"Human ID — usable in the URL and to reference the ticket","admin.taskDetail.download":"Download","admin.taskDetail.editDescription":"Edit the description (E)","admin.taskDetail.editShortcutsHint":"⌘↵ save · Esc cancel","admin.taskDetail.email":"Email","admin.taskDetail.language":"Language","admin.taskDetail.languagePrefs":"(preferences: {list})","admin.taskDetail.linkedIssues":"Connector deliveries","admin.taskDetail.linkDeletedConnector":"Deleted connector","admin.taskDetail.linkFailed":"failed: {error}","admin.taskDetail.linkPartial":"missing files","admin.taskDetail.linkSent":"sent","admin.taskDetail.linkedThread":"Linked conversation","admin.taskDetail.localStorage_one":"localStorage ({n} key)","admin.taskDetail.localStorage_other":"localStorage ({n} keys)","admin.taskDetail.nextToTest":"Next to test","admin.taskDetail.nextToTestPending":"Next to test…","admin.taskDetail.noMoreToTest":"No more bug to test","admin.taskDetail.noTitle":"(no title)","admin.taskDetail.notFound":"Ticket not found","admin.taskDetail.openConversation":"Open the conversation →","admin.taskDetail.pageContext":"Page context (at bug time)","admin.taskDetail.pageTitle":"Page title","admin.taskDetail.priorityValue":"Priority · {label}","admin.taskDetail.received":"Received {date}","admin.taskDetail.referrer":"Referrer","admin.taskDetail.regenTitleHint":"Regenerate title + context via the LLM (from description + URL)","admin.taskDetail.removeAttMessage":"The file on disk is unlinked and the DB row deleted. Irreversible. The other attachments remain.","admin.taskDetail.removeAttTitle":"Delete this attachment?","admin.taskDetail.removeShotMessage":"The file on disk is unlinked and the DB row deleted. Irreversible. The ticket’s other captures remain.","admin.taskDetail.removeShotTitle":"Delete this capture?","admin.taskDetail.saveError":"Save failed","admin.taskDetail.sessionStorage_one":"sessionStorage ({n} key)","admin.taskDetail.sessionStorage_other":"sessionStorage ({n} keys)","admin.taskDetail.storageFormatted":"Formatted","admin.taskDetail.storageRaw":"Raw","admin.taskDetail.storageTitle":"Browser storage (local / session)","admin.taskDetail.storageValuesOff":"Values not captured. Enable “Storage values” in the project config to see key contents.","admin.taskDetail.timelineTitle":"Timeline (console + fetch/XHR — all events)","admin.taskDetail.timelineUncompressed":" — uncompressed","admin.taskDetail.toFix":"To fix","admin.taskDetail.toFixDialog.confirm":"Send back to in progress","admin.taskDetail.toFixDialog.confirmWithNote":"Comment and send back","admin.taskDetail.toFixDialog.help":"The ticket goes back to “in progress”. Your comment is added to the notes, where whoever picks it up will read it.","admin.taskDetail.toFixDialog.placeholder":"What still doesn't work… (optional — Ctrl/Cmd+Enter to confirm)","admin.taskDetail.toFixDialog.title":"Why does it need fixing?","admin.taskDetail.total":"Total: {size}","admin.taskDetail.updateError":"Update failed","admin.taskDetail.urlAtBug":"URL at bug time","admin.taskDetail.userAgent":"User-Agent","admin.taskDetail.viewPage":"View the page","admin.taskDetail.viewProfile":"View profile","admin.taskDetail.viewport":"Viewport","admin.taskDetail.viewportValue":"{width} × {height} ({dpr}× DPR)","admin.taskDetail.visitorAnonymous":"Anonymous visitor","admin.taskNotes.addError":"Failed — try again","admin.taskNotes.deleteMessage":"This action is irreversible.","admin.taskNotes.deleteNote":"Delete the note","admin.taskNotes.deleteTitle":"Delete this note?","admin.taskNotes.empty":"No note yet. Add the first one below.","admin.taskNotes.header":"Notes & comments ({n})","admin.taskNotes.headerEmpty":"Notes & comments","admin.taskNotes.loadError":"Error loading notes: {msg}","admin.taskNotes.placeholder":"Add a note (Ctrl/Cmd+Enter to send)…","admin.tasks.archivableHint":"Only resolved/wontfix tickets can be archived","admin.tasks.archiveResolved":"Archive resolved ({n})","admin.tasks.archiveResolvedMessage":"Resolved tickets will be moved to the archives. Reversible: you can unarchive them from the “Archived” view.","admin.tasks.archiveResolvedTitle_one":"Archive {n} resolved ticket?","admin.tasks.archiveResolvedTitle_other":"Archive {n} resolved tickets?","admin.tasks.bulkDeleteMessage":"The DB rows AND the screenshot files will be deleted. Irreversible.","admin.tasks.bulkDeleteTitle_one":"Delete {n} ticket?","admin.tasks.bulkDeleteTitle_other":"Delete {n} tickets?","admin.tasks.changePriority":"Change priority","admin.tasks.colActions":"Actions","admin.tasks.colId":"ID","admin.tasks.colPriority":"Priority","admin.tasks.colReceived":"Received","admin.tasks.colSite":"Project","admin.tasks.colStatus":"Status","admin.tasks.colTitle":"Title","admin.tasks.colType":"Type","admin.tasks.colVisitor":"Visitor","admin.tasks.copyIds":"Copy IDs","admin.tasks.createBug":"Create a ticket","admin.tasks.deleteMessage":"The DB row AND the screenshot file will be deleted. Irreversible.","admin.tasks.deleteTitle":"Delete this ticket?","admin.tasks.deselect":"Deselect","admin.tasks.emptyArchived":"No archived ticket.","admin.tasks.emptyFiltered":"No ticket matches the filters.","admin.tasks.filterPriority":"Priority","admin.tasks.filterPriorityAll":"All priorities","admin.tasks.filterSite":"Project","admin.tasks.filterSiteAll":"All projects","admin.tasks.filterStatus":"Status","admin.tasks.filterType":"Type","admin.tasks.filterTypeAll":"All types","admin.tasks.filters":"Filters","admin.tasks.kanbanEmptyColumn":"Drag a card here","admin.tasks.loadError":"Loading error: {msg}","admin.tasks.markResolved":"Mark resolved","admin.tasks.otherStatuses":"Other statuses","admin.tasks.priority.critical":"Critical","admin.tasks.priority.high":"High","admin.tasks.priority.low":"Low","admin.tasks.priority.medium":"Medium","admin.tasks.priorityMenu":"Priority ▾","admin.tasks.report":"Report","admin.tasks.resetFilters":"Reset filters","admin.tasks.searchPlaceholder":"Search (id, title, description)…","admin.tasks.selectAllPage":"Select all on the page","admin.tasks.sortAsc":"Sort ascending","admin.tasks.sortBy":"Sort by","admin.tasks.sortDesc":"Sort descending","admin.tasks.status.in_progress":"In progress","admin.tasks.status.new":"New","admin.tasks.status.queued":"Queued","admin.tasks.status.resolved":"Resolved","admin.tasks.status.to_test":"To test","admin.tasks.status.wontfix":"Won’t fix","admin.tasks.statusMenu":"Status ▾","admin.tasks.subtitle_one":"{n} ticket","admin.tasks.subtitle_other":"{n} tickets","admin.tasks.title":"Tickets","admin.tasks.titleActive":"Active tickets","admin.tasks.titleArchived":"Archived tickets","admin.tasks.type.bug":"Bug","admin.tasks.type.feature":"Feature","admin.tasks.locked.label":"Locked ticket","admin.tasks.locked.hint":"Your plan's open-ticket limit was reached when this ticket arrived: it is kept, but its content can only be read after upgrading.","admin.tasks.locked.banner":"{count} locked ticket(s) waiting: your plan stops at {total} open tickets. Upgrade to Pro to read them.","admin.tasks.locked.near":"{open} / {total} open tickets: beyond that, new tickets will arrive locked. Close the ones you handled, or upgrade to Pro.","admin.tasks.locked.cta":"See plans","admin.tasks.locked.receivedAt":"Received on {date}","admin.tasks.locked.page":"Page: {url}","admin.billing.ticketsTitle":"Open tickets","admin.billing.ticketsGauge":"{used} / {total} open tickets","admin.billing.ticketsUnlimited":"{used} open tickets — unlimited on your plan","admin.billing.ticketsLocked":"{count} locked ticket(s) waiting. Upgrade to Pro to read them: they will all unlock.","admin.tasks.viewKanban":"Kanban view","admin.tasks.viewList":"List view","admin.threadDetail.backToConversations":"Back to conversations","admin.threadDetail.charCount":"{len} / {max}","admin.threadDetail.gdpr":"GDPR","admin.threadDetail.gdprBody":"GDPR right to erasure: {parts}{sites} will be deleted (DB + disk).{emails} Irreversible.","admin.threadDetail.gdprComputing":"Computing scope…","admin.threadDetail.gdprConfirmLabel":"Erase permanently","admin.threadDetail.gdprConfirmTitle":"Erase all data for this visitor?","admin.threadDetail.gdprEmails":" Associated emails: {emails}.","admin.threadDetail.gdprNoData":"No data associated with this visitor.","admin.threadDetail.gdprNoPreview":"No preview available.","admin.threadDetail.gdprPartConversations_one":"{n} conversation","admin.threadDetail.gdprPartConversations_other":"{n} conversations","admin.threadDetail.gdprPartMessages_one":"{n} message","admin.threadDetail.gdprPartMessages_other":"{n} messages","admin.threadDetail.gdprPartScreenshots_one":"{n} screenshot","admin.threadDetail.gdprPartScreenshots_other":"{n} screenshots","admin.threadDetail.gdprPartTasks_one":"{n} ticket","admin.threadDetail.gdprPartTasks_other":"{n} tickets","admin.threadDetail.gdprSites":" on {sites}","admin.threadDetail.gdprTitle":"GDPR right to erasure — deletes all of the visitor’s data across every project","admin.threadDetail.noMessages":"No message.","admin.threadDetail.notFoundMessage":"This conversation does not exist anymore.","admin.threadDetail.notFoundTitle":"Conversation not found","admin.threadDetail.reopenToReply":"Reopen to reply.","admin.threadDetail.replyPlaceholder":"Write a reply…","admin.threadDetail.replyPlaceholderResolved":"Resolved conversation","admin.threadDetail.requestCapture":"Request capture","admin.threadDetail.requestCaptureTitle":"Ask the visitor to capture the page","admin.threadDetail.resolved":"Resolved","admin.threadDetail.sendError":"Could not send.","admin.threadDetail.sidebarConversation":"Conversation","admin.threadDetail.sidebarCreated":"Created","admin.threadDetail.sidebarLastActivity":"Last activity","admin.threadDetail.sidebarMessages":"Messages","admin.threadDetail.sidebarThreadId":"Conversation ID","admin.threadDetail.siteUnknown":"Unknown project","admin.threadDetail.toastArchived":"Conversation archived","admin.threadDetail.toastCaptureRequested":"Capture request sent to the visitor","admin.threadDetail.toastPurged":"Visitor purged: {threads} conversations, {messages} messages, {tasks} tickets.","admin.threadDetail.toastRequestError":"Error during the request.","admin.threadDetail.toastResolved":"Conversation marked as resolved","admin.threadDetail.toastUnarchived":"Conversation unarchived","admin.threadDetail.tooLong":"Too long ({len} / {max})","admin.threadDetail.visitorFallback":"Anonymous visitor","admin.threads.archivableHint":"Only resolved conversations can be archived","admin.threads.bulkDeleteMessage":"The selected conversations and all their messages will be deleted. Irreversible.","admin.threads.bulkDeleteTitle_one":"Delete {n} conversation?","admin.threads.bulkDeleteTitle_other":"Delete {n} conversations?","admin.threads.clearSelection":"Clear selection","admin.threads.colActions":"Actions","admin.threads.colMsg":"Msg","admin.threads.colPreview":"Preview","admin.threads.colSite":"Project","admin.threads.colStatus":"Status","admin.threads.colUpdated":"Updated","admin.threads.colVisitor":"Visitor","admin.threads.deleteMessage":"The conversation and all its messages will be deleted. Irreversible.","admin.threads.deleteTitle":"Delete this conversation?","admin.threads.empty":"No conversation yet.","admin.threads.emptyArchived":"No archived conversation.","admin.threads.filterSite":"Project","admin.threads.filterStatus":"Status","admin.threads.filterStatusAi":"AI","admin.threads.filterStatusAll":"All","admin.threads.filterStatusAwaiting":"To handle","admin.threads.filterStatusResolved":"Resolved","admin.threads.loadError":"Unable to load conversations.","admin.threads.markResolved":"Mark resolved","admin.threads.selectAll":"Select all","admin.threads.selectRow":"Select {id}","admin.threads.selected_one":"{n} selected","admin.threads.selected_other":"{n} selected","admin.threads.status.ai_handling":"AI","admin.threads.status.awaiting_human":"To handle","admin.threads.status.resolved":"Resolved","admin.threads.title":"Conversations","admin.ticketSearch.archived":"archived","admin.ticketSearch.noTicket":"No ticket","admin.ticketSearch.placeholder":"Search a ticket…","admin.ticketSearch.searching":"Searching…","admin.userPanel.anonymous":"Anonymous visitor (no host identification).","admin.userPanel.clientId":"Client ID","admin.userPanel.email":"Email","admin.userPanel.identified":"Identified user","admin.userPanel.metadata":"Metadata","admin.userPanel.name":"Name","admin.userPanel.viewProfile":"View profile","admin.pwa.updateAvailable":"A new version of BipWire is available.","admin.pwa.reload":"Reload","admin.pwa.installPrompt":"Install BipWire on this device to open it like an app.","admin.pwa.installIos":"To install BipWire: tap Share, then “Add to Home Screen”.","admin.pwa.install":"Install","admin.pwa.later":"Later","admin.siteEdit.expandAll":"Expand all","admin.siteEdit.collapseAll":"Collapse all","admin.siteEdit.sectionHasError":"Needs fixing","admin.siteEdit.summaryActive":"The widget is served on the site","admin.siteEdit.summaryInactive":"The widget is no longer served","admin.siteEdit.summaryNoOrigin":"no origin","admin.siteEdit.summaryEverywhere":"Everywhere on the site","admin.siteEdit.summaryChatAi":"AI answers, a human takes over if needed","admin.siteEdit.summaryChatHuman":"A human answers","admin.siteEdit.summaryCapture":"Captures: {list}","admin.siteEdit.summaryBodies":"request bodies","admin.siteEdit.summaryNothing":"nothing","admin.siteEdit.summaryNoLink":"No link","admin.siteEdit.summaryDraggable":"Draggable by the visitor","admin.siteEdit.summaryFixed":"Fixed","admin.siteEdit.summaryPromptMissing":"System prompt to write","admin.siteEdit.summaryDefaultText":"Default text","admin.siteEdit.subIcon":"Bubble icon","admin.siteEdit.subColors":"Colors","admin.siteEdit.subColorsHint":"Colors are set in the appearance studio, and only there: your site's colors, suggested themes, checked readability. Click a color to open it.","admin.siteEdit.subPlacement":"Size and position","admin.siteEdit.subBehavior":"Behavior","admin.siteEdit.subChatReply":"Who answers","admin.siteEdit.subChatPrompt":"System prompt and reference document","admin.siteEdit.appearanceWidgetFirst":"The widget hasn't been seen on your site yet: the appearance can wait. Save the project, install the widget (“Activation assistant” button at the top), then come back here — “Sample on the site” will open your site and the widget will sample its colors. You can also fetch or paste the site's CSS right now, in the studio.","admin.siteEdit.appearanceCreateFirst":"Start by creating the project and installing the widget: the appearance comes next, from your site's colors.","admin.designer.releveWidget":"Sample on the site","admin.designer.releveWidgetHint":"Opens your site in a tab: the widget samples the colors on screen, even those of a JavaScript app.","admin.designer.releveWidgetNotSeen":"Available once the widget is installed on your site and seen at least once.","admin.designer.releveWidgetNote":"The widget must already be installed on your site. A tab opens, the widget samples the colors once the page is displayed, then the tab closes by itself.","admin.designer.releveWaiting":"Your site opened in a tab: the widget is sampling its colors… The tab will close by itself.","admin.designer.releveTimeout":"No colors received from the widget. Check the tag is on the home page, then try again.","admin.designer.widgetFirst":"The widget isn't on your site yet: install it, then “Sample on the site” will read its colors as they're displayed.","admin.siteEdit.colorsReset":"Reset all to default","admin.nav.waitlist":"Waitlist","admin.nav.inviteLinks":"Invite links","admin.legal.notice":"Legal notice","admin.legal.privacy":"Privacy","admin.legal.terms":"Terms","admin.errors.task_not_found":"Ticket not found.","admin.errors.task_locked":"Locked ticket: the plan's open-ticket limit is reached. Upgrade to read it.","admin.errors.status_reserved":"“Queued” is reserved for Claude Code: a ticket enters it through “Launch Claude Code”.","admin.errors.invalid_url":"Invalid address: https:// required.","admin.errors.invalid_site":"Invalid project.","admin.siteEdit.clientLinkIntro":"By default, a ticket is anonymous: BipWire only knows the widget visitor. To know WHO sent it, your site has to identify the signed-in user. Their id, email and name then travel with every ticket and every conversation, show on the ticket page, and build the link to their customer profile.","admin.siteEdit.clientLinkStep1":"1. Identify the signed-in user, on your site","admin.siteEdit.clientLinkStep1Hint":"On the pages where the user is signed in, add this script — before or after the widget tag, the call is queued. Replace the sample values with your user’s (usually written into the page by your server). In a single-page app, call it after sign-in: window.bip.identify({ … }); on sign-out, window.bip.reset().","admin.siteEdit.clientLinkStep1Static":"Or, more simply, as attributes on the widget tag:","admin.siteEdit.clientLinkStep2":"2. The link to their profile in your back office (optional)","admin.siteEdit.clientLinkHelperEmail":"(the email passed to identify, or typed by the visitor)","admin.siteEdit.clientLinkPreview":"Sample generated link","admin.siteEdit.clientLinkSnippetComment":"The user signed in on your site: they will follow every ticket and conversation.","admin.errors.invalid_url_template":"The URL template must start with https:// (or http://) and contain no spaces.","admin.taskDetail.move":"Change project","admin.taskDetail.moveTitle":"Move {displayId} to another project","admin.taskDetail.moveMessage":"The ticket keeps its captures, files, notes and history, and gets a new identifier in the chosen project. The former identifier ({displayId}) no longer leads to it.","admin.taskDetail.moveTarget":"Destination project","admin.taskDetail.moveConfirm":"Move","admin.taskDetail.movePending":"Moving…","admin.siteEdit.panelTheme":"Panel theme","admin.siteEdit.panelThemeHint":"Light by default. Dark: always the dark mode colors. Automatic: the widget follows the site’s theme and switches with it, without reloading (“dark” class or “data-theme” attribute… on the page, else the displayed background color, else the visitor’s device). To try it: open the site, then change its theme.","admin.siteEdit.panelThemeLight":"Light","admin.siteEdit.panelThemeDark":"Dark","admin.siteEdit.panelThemeAuto":"Automatic (follows the site)","admin.siteEdit.openSite":"Open the site","admin.siteEdit.colorsTitle":"Colors","admin.siteEdit.colorsLight":"Light mode colors","admin.siteEdit.colorsLightHint":"Applied when the site is light.","admin.siteEdit.colorsDark":"Dark mode colors","admin.siteEdit.colorsDarkHint":"Applied when the site is dark. In the studio, “Fetch from the site” keeps only the dark styles of its CSS (prefers-color-scheme: dark block, “dark” class, “data-theme” attribute…). A role left to default takes the standard dark theme, and the accent the light mode one.","admin.designer.titleLight":"Appearance studio — light mode","admin.designer.titleDark":"Appearance studio — dark mode","admin.designer.subtitleDark":"The widget colors when the site is dark. “Fetch from the site” keeps only the dark styles of its CSS.","admin.designer.analyzeNoDark":"No dark style in this CSS: no prefers-color-scheme: dark block, no rule scoped by a “dark” class or a “data-theme” attribute. The dark themes offered are generic.","admin.errors.invalid_action":"Unknown delivery action.","admin.siteEdit.unsavedTitle":"Unsaved changes","admin.siteEdit.unsavedMessage":"Changes to this project have not been saved. Leaving the page discards them.","admin.siteEdit.unsavedStay":"Stay on the page","admin.siteEdit.unsavedLeave":"Leave without saving","admin.siteEdit.unsavedBadge":"Unsaved","admin.reglages.notifAutofixStarted":"The automatic agent takes a ticket","admin.reglages.notifAutofixDelivered":'The automatic agent delivers a ticket "to test"',"admin.reglages.notifAutofixFailed":"The automatic agent fails","admin.reglages.notifAutofixSkipped":"The automatic agent skips a ticket","admin.reglages.notifAutofixDaily":"Daily report of the automatic agent","admin.events.autofix.started":"Automatic agent: ticket taken","admin.events.autofix.delivered":"Automatic agent: ticket delivered","admin.events.autofix.failed":"Automatic agent: failure","admin.events.autofix.skipped":"Automatic agent: ticket skipped","admin.events.autofix.daily":"Automatic agent: daily report"},Ci={"admin.activation.allInOne.desc":"Un seul prompt, à coller dans Claude Code, Codex, Cursor ou OpenCode ouvert à la racine du repo du site : l’agent intègre le widget, installe les commandes /bip-*, branche le serveur MCP et crée .bip.json. La suite de l’assistant détaille les mêmes étapes, à faire à la main.","admin.activation.allInOne.mcpFile":`Ajoute ce bloc à {path} (crée le fichier s’il n’existe pas ; sinon fusionne-le avec le contenu existant, sans rien retirer) :

{block}`,"admin.activation.allInOne.mcpRun":`Lance cette commande shell (portée utilisateur, valable pour tous les projets ; si un serveur « bip » existe déjà, retire-le d’abord avec « claude mcp remove --scope user bip ») :

{command}`,"admin.activation.allInOne.needToken":"Le prompt porte le token personnel, qui donne à l’agent l’accès aux tickets. Il n’existe pas encore : le créer d’un clic.","admin.activation.allInOne.placeholder":"Aucun token à clé récupérable : le prompt contient un placeholder, à remplacer par la clé d’un token LLM (page « Clés API »).","admin.activation.allInOne.prepare":"Préparer le prompt","admin.activation.allInOne.prompt":`Configure BipWire dans ce repo, de A à Z. Quatre tâches, dans l’ordre :

1. Widget. {widget}

2. Commandes /bip-*. Lance cette commande shell (elle installe quatre fichiers texte dans {commandsPath}, sans aucun secret) :

{install}

3. Serveur MCP. {mcp}

4. Projet. Crée à la racine du repo un fichier .bip.json qui contient exactement :

{bipJson}

Ne recopie jamais le token dans le repo : il ne vit que dans la config personnelle ({mcpPath}). Termine par un récapitulatif de ce qui a été fait, et rappelle-moi de redémarrer l’agent pour qu’il charge le serveur MCP, puis de lancer /bip-tasks.`,"admin.activation.allInOne.secretHint":"Ce prompt contient la clé du token : le coller seulement dans l’agent, jamais dans un ticket ni dans un fichier du repo.","admin.activation.allInOne.title":"Tout confier à un agent IA","admin.activation.back":"Retour","admin.activation.finish":"Terminer","admin.activation.next":"Suivant","admin.activation.step1.agentDesc":"Copier ce prompt dans Claude Code, Codex, Cursor ou OpenCode : l’agent intègre le widget.","admin.activation.step1.agentPrompt":`Intègre le widget de support BipWire sur mon site web. Ajoute cette balise dans le HTML, de préférence dans le <head> (sinon avant </body>) :

{snippet}

Si le site utilise un framework (Next.js, Vite, PHP…) ou des variables d’environnement, câble-le proprement. Si le site définit une Content-Security-Policy (en-tête HTTP, config du framework ou de l’hébergeur, ou <meta http-equiv>), ajoute à ses directives existantes, sans rien retirer : {clientUrl} dans script-src et connect-src, 'unsafe-inline' dans style-src, data: et blob: dans img-src, 'self' et blob: dans worker-src. Doc complète, collable telle quelle : {docUrl}`,"admin.activation.step1.agentTitle":"Ou laisser un agent IA l’installer","admin.activation.step1.cspDesc":"Alors le navigateur bloque le widget en silence : pas de bulle, ou pas de capture d’écran, et rien ne nous parvient. Il faut ajouter BipWire à la CSP du site — pas l’inverse.","admin.activation.step1.cspDocLink":"Pourquoi chaque directive, nonces, autres en-têtes de sécurité","admin.activation.step1.cspHint":"À ajouter aux directives existantes, sans rien retirer. Les images viennent d’un CDN ? Ajouter aussi son domaine à connect-src, sinon elles sortent blanches dans les captures.","admin.activation.step1.cspTitle":"Le site a une Content-Security-Policy ?","admin.activation.step1.cspToggle":"Voir les valeurs à ajouter","admin.activation.step1.docLink":"Voir la doc d’intégration","admin.activation.step1.identifyToggle":"Identifier les utilisateurs (optionnel)","admin.activation.step1.siteLine":"Projet : {name} — id {id}","admin.activation.step2.createToken":"Créer un token","admin.activation.step2.creating":"Création du token…","admin.activation.step2.extendToken":"Étendre ce token à ce projet","admin.activation.step2.extending":"Extension…","admin.activation.step2.intro":"Maintenant que le widget est en place, brancher un agent CLI (Claude Code, Codex, Cursor, OpenCode) : il corrigera les bugs remontés directement depuis l’éditeur.","admin.activation.step2.otherToken":"Utiliser un autre token","admin.activation.step2.personalHint":"C’est le token personnel : le même pour tous les projets, reproposé ici à chaque fois.","admin.activation.step2.tokenLabel":"Agent — {user}","admin.activation.step2.tokenNotOnSite":"Le token « {label} » ne couvre pas ce projet.","admin.activation.step2.tokenReady":"Le token « {label} » est prêt : sa clé est insérée dans les commandes ci-dessous.","admin.activation.step2.tokenRequired":"Le branchement MCP a besoin d’un token LLM BipWire. Sélectionner un token existant, ou en créer un.","admin.activation.step3.detected":"BipWire est bien chargé sur {origin} 🎉","admin.activation.step3.detectedHint":"Le widget répond. Tout est prêt.","admin.activation.step3.openSite":"Ouvrir mon site","admin.activation.step3.stuckCsp":"un message « violates the following Content Security Policy directive » : la CSP bloque le widget. Ajouter les valeurs de l’étape 1 (Retour).","admin.activation.step3.stuckNothing":"aucun message : la balise n’est pas dans la page servie. Vérifier le code source (Ctrl+U), vider le cache du CDN, et exclure le script des optimiseurs JS (Rocket Loader, WP Rocket…).","admin.activation.step3.stuckTitle":"Toujours rien ? Ouvrir le site avec la console du navigateur (F12) :","admin.activation.step3.waiting":"En attente du premier chargement du widget…","admin.activation.step3.waitingHint":"Ouvrir une page du site où le script est installé pour déclencher la détection.","admin.activation.stepAgent":"Brancher un agent","admin.activation.stepInstall":"Installer le widget","admin.activation.stepVerify":"Vérifier","admin.activation.title":"Activer BipWire sur le site","admin.agentSetup.cmd.bip-fix":"corrige un ticket de bout en bout, puis le passe « à tester » avec une note","admin.agentSetup.cmd.bip-next":"prend le ticket le plus prioritaire et le corrige","admin.agentSetup.cmd.bip-sort":"priorise les tickets urgents de tous les projets, et dit par quoi commencer (lecture seule)","admin.agentSetup.cmd.bip-tasks":"liste les tickets ouverts du projet, par priorité (lecture seule)","admin.agentSetup.footerLegacyPost":"dans ce repo : une copie de projet passe avant la version globale, et elle ne se met jamais à jour.","admin.agentSetup.footerLegacyPre":"Commandes installées dans un repo avec une ancienne version ? Supprimer","admin.agentSetup.footerNote":"Les commandes sont les mêmes pour les trois agents : seul l’emplacement des fichiers diffère. Relancer la commande de l’étape 1 après une mise à jour de BipWire pour récupérer les dernières versions.","admin.agentSetup.introPost":") dans l’agent. Le token ne vit que dans la config MCP ci-dessous — les commandes, elles, ne contiennent aucun secret.","admin.agentSetup.introPre":"Installer les slash commands BipWire (","admin.agentSetup.machineHint":"Ces deux étapes valent pour tous les projets. Déjà faites sur ce poste ? Passer directement à « Une fois par projet ».","admin.agentSetup.machineTitle":"Une fois par poste","admin.agentSetup.noSelection":"Sans sélection, le snippet contient un placeholder à remplacer à la main.","admin.agentSetup.noToken":"Aucun token avec clé récupérable — crée ou régénère un token.","admin.agentSetup.pasteAgent":"l’invite de mon agent","admin.agentSetup.pasteAgentHint":"Le préfixe « ! » lance la commande depuis Claude Code (ou tout agent qui le connaît), sans quitter la session.","admin.agentSetup.pasteTarget":"Je colle dans :","admin.agentSetup.pasteTerminal":"un terminal","admin.agentSetup.projectHint":"Au projet suivant, c’est la seule étape à refaire.","admin.agentSetup.projectTitle":"Une fois par projet","admin.agentSetup.site":"Projet","admin.agentSetup.step1":"1. Installer les commandes /bip-*","admin.agentSetup.step1Desc":"Quatre fichiers texte, sans aucun secret, rangés dans {path}. Lancer la commande depuis n’importe quel dossier :","admin.agentSetup.step2":"2. Brancher le serveur MCP","admin.agentSetup.step2Desc":"Choisir un token : sa clé est insérée automatiquement.","admin.agentSetup.step2DescClaude":"C’est la connexion de l’agent à BipWire. Le token est rangé dans la config utilisateur, hors de tout repo.","admin.agentSetup.step2DescOtherPre":"C’est la connexion de l’agent à BipWire. Elle porte le token : coller ce bloc dans la config personnelle,","admin.agentSetup.step3":"3. Désigner le projet de ce repo","admin.agentSetup.step3DescPost":"à la racine du repo pour que les commandes sachent quel projet interroger.","admin.agentSetup.step3DescPre":"Créer un","admin.agentSetup.title":"Brancher un agent CLI","admin.agentSetup.token":"Token","admin.auth.backToLogin":"Retour à la connexion","admin.auth.confirmPassword":"Confirmer le mot de passe","admin.auth.createMyAccount":"Créer mon compte","admin.auth.emailAddress":"Adresse email","admin.auth.errPasswordMismatch":"Les mots de passe ne correspondent pas.","admin.auth.errPasswordTooShort":"Le mot de passe doit faire au moins 8 caractères.","admin.auth.forgotPassword":"Mot de passe oublié ?","admin.auth.forgotSubtitle":"On t’envoie un lien de réinitialisation","admin.auth.forgotSuccess":"Si un compte existe, un email a été envoyé.","admin.auth.forgotTitle":"Mot de passe oublié","admin.auth.haveAccount":"Déjà un compte ? Se connecter","admin.auth.hidePassword":"Masquer le mot de passe","admin.auth.inviteCurrentPassword":"Mot de passe actuel","admin.auth.inviteExistingAccount":"Compte BipWire existant — cette organisation s’ajoutera à l’espace.","admin.auth.inviteFlash":"Compte créé, se connecter.","admin.auth.inviteInvalidLink":"Lien d’invitation invalide ou incomplet.","admin.auth.inviteJoining":"Invitation à rejoindre {org}.","admin.auth.inviteOrgName":"Nom de l’organisation","admin.auth.inviteOrgSubtitle":"Nommer l’espace et choisir un mot de passe","admin.auth.inviteOrgTitle":"Créer l’organisation","admin.auth.inviteSubtitle":"Choisir un mot de passe pour activer le compte","admin.auth.inviteTitle":"Rejoindre l’espace","admin.auth.loginSubtitle":"Connexion à l’espace administrateur","admin.auth.loginTitle":"BipWire — Admin","admin.auth.noAccount":"Pas encore de compte ? Créer un compte","admin.auth.orgName":"Nom de l’organisation","admin.auth.password":"Mot de passe","admin.auth.reset":"Réinitialiser","admin.auth.resetFlash":"Mot de passe mis à jour, se connecter.","admin.auth.resetInvalidLink":"Lien de réinitialisation invalide ou incomplet.","admin.auth.resetSubtitle":"Choisir un nouveau mot de passe pour le compte","admin.auth.resetTitle":"Nouveau mot de passe","admin.auth.sendLink":"Envoyer le lien","admin.auth.showPassword":"Afficher le mot de passe","admin.auth.signIn":"Se connecter","admin.auth.signingIn":"Connexion…","admin.auth.signupSubtitle":"Créer un espace BipWire en quelques secondes","admin.auth.signupTitle":"Créer un compte","admin.auth.joinWaitlist":"Rejoindre la liste d’attente","admin.auth.invite.checking":"Vérification de l’invitation…","admin.auth.invite.banner":"Invitation : offre {plan}","admin.auth.invite.invalid.unknown.title":"Lien d’invitation inconnu","admin.auth.invite.invalid.unknown.text":"Ce lien ne correspond à aucune invitation. Vérifier qu’il a été copié en entier, ou rejoindre la liste d’attente.","admin.auth.invite.invalid.revoked.title":"Invitation retirée","admin.auth.invite.invalid.revoked.text":"Ce lien d’invitation a été désactivé. Rejoindre la liste d’attente pour être prévenu de l’ouverture.","admin.auth.invite.invalid.expired.title":"Invitation expirée","admin.auth.invite.invalid.expired.text":"Ce lien d’invitation n’est plus valable. Rejoindre la liste d’attente pour en recevoir un nouveau.","admin.auth.invite.invalid.full.title":"Toutes les places sont prises","admin.auth.invite.invalid.full.text":"Ce lien d’invitation a déjà servi autant de fois que prévu. Rejoindre la liste d’attente pour être prévenu de la prochaine vague.","admin.auth.signupsClosedText":"Les inscriptions ne sont pas encore ouvertes. À bientôt !","admin.auth.signupsClosedTitle":"Inscriptions bientôt ouvertes","admin.auth.updating":"Mise à jour…","admin.auth.verifyInvalidLink":"Lien de vérification invalide ou incomplet.","admin.auth.verifyPending":"Vérification de l’adresse email…","admin.auth.verifySuccess":"Adresse email vérifiée.","admin.auth.verifyTitle":"Vérification de l’email","admin.billing.changePlan":"Changer de plan","admin.billing.company":"Société","admin.billing.companyNote":"Ces informations apparaîtront sur les factures.","admin.billing.conversationsGauge":"{used} / {total} conversations","admin.billing.conversationsUnlimited":"{used} conversations ce mois-ci (illimité)","admin.billing.currentPlan":"Plan actuel","admin.billing.loadError":"Impossible de charger la facturation.","admin.billing.manageNote":"Mettre à jour le moyen de paiement, télécharger les factures ou annuler l’abonnement.","admin.billing.manageSubscription":"Gérer mon abonnement","admin.billing.noSubscription":"Aucun abonnement actif. Choisir un plan pour débloquer plus de capacité.","admin.billing.overBanner":"Forfait dépassé. Le service continue — passer à un plan supérieur pour plus de conversations.","admin.billing.periodUntil":"Période en cours jusqu’au {date}.","admin.billing.planPro":"Plan Pro","admin.billing.planScale":"Plan Scale","admin.billing.resetsMonthly":"remise à zéro le 1er","admin.billing.stripeNote":"Le paiement et la gestion de l’abonnement sont assurés par Stripe.","admin.billing.subStatus.active":"actif","admin.billing.subStatus.canceled":"annulé","admin.billing.subStatus.incomplete":"incomplet","admin.billing.subStatus.incomplete_expired":"expiré","admin.billing.subStatus.past_due":"paiement en retard","admin.billing.subStatus.paused":"en pause","admin.billing.subStatus.trialing":"essai","admin.billing.subStatus.unpaid":"impayé","admin.billing.subStatusLabel":"Statut de l’abonnement : {status}","admin.billing.subscribe":"S’abonner","admin.billing.tier.free":"Gratuit","admin.billing.tier.pro":"Pro","admin.billing.tier.scale":"Scale","admin.billing.tier.unlimited":"Illimité","admin.billing.title":"Facturation","admin.billing.toastProfileSaved":"Profil société enregistré.","admin.billing.toastSubscribed":"Abonnement pris en compte.","admin.billing.upgradeTo":"{tier} — {price}/mois · {conversations} conversations","admin.billing.usageTitle":"Consommation ce mois-ci","admin.billingProfile.address":"Adresse","admin.billingProfile.addressComplement":"Complément","admin.billingProfile.billingAddress":"Adresse de facturation","admin.billingProfile.billingContact":"Contact facturation","admin.billingProfile.billingEmail":"Email de facturation","admin.billingProfile.city":"Ville","admin.billingProfile.contactName":"Nom du contact","admin.billingProfile.country":"Pays","admin.billingProfile.countryPlaceholder":"FR","admin.billingProfile.legalForm":"Forme juridique","admin.billingProfile.legalFormPlaceholder":"SAS, SARL, EI…","admin.billingProfile.legalIdentity":"Identité légale","admin.billingProfile.legalName":"Raison sociale","admin.billingProfile.phone":"Téléphone","admin.billingProfile.postalCode":"Code postal","admin.billingProfile.rcs":"RCS","admin.billingProfile.siren":"SIREN","admin.billingProfile.siret":"SIRET","admin.billingProfile.vatCountry":"Pays TVA","admin.billingProfile.vatLiable":"Assujetti TVA","admin.billingProfile.vatNumber":"N° TVA intracom","admin.billingProfile.vatNumberPlaceholder":"FR12345678901","admin.billingProfile.vatTax":"TVA / fiscal","admin.common.add":"Ajouter","admin.common.adding":"Ajout…","admin.common.all":"Tous","admin.common.anonymous":"anonyme","admin.common.archive":"Archiver","admin.common.archived":"Archivés","admin.common.archivedTag":"Archivé","admin.common.archiving":"Archivage…","admin.common.back":"Retour","admin.common.cancel":"Annuler","admin.common.clearSearch":"Effacer la recherche","admin.common.close":"Fermer","admin.common.confirm":"Confirmer","admin.common.copied":"Copié","admin.common.copy":"Copier","admin.common.copyId":"Copier l’ID","admin.common.create":"Créer","admin.common.creating":"Création…","admin.common.dash":"—","admin.common.delete":"Supprimer","admin.common.deleting":"Suppression…","admin.common.disabled":"Désactivé","admin.common.edit":"Modifier","admin.common.enabled":"Activé","admin.common.error":"Erreur","admin.common.genericError":"Quelque chose n’a pas fonctionné. Réessayer dans un instant.","admin.common.hide":"Masquer","admin.common.noSitesConfigured":"Aucun projet configuré.","admin.common.retry":"Réessayer","admin.common.save":"Enregistrer","admin.common.saving":"Enregistrement…","admin.common.send":"Envoyer","admin.common.sending":"Envoi…","admin.common.show":"Afficher","admin.common.showActive":"Voir les actifs","admin.common.unarchive":"Désarchiver","admin.common.unknownError":"Erreur inconnue","admin.connectors.add":"Ajouter un connecteur","admin.connectors.colActions":"Actions","admin.connectors.colAutoCreate":"Auto-création","admin.connectors.colEnabled":"Activé","admin.connectors.colName":"Nom","admin.connectors.colScope":"Portée","admin.connectors.colType":"Type","admin.connectors.deleteMessage":"Le connecteur sera retiré. Les nouveaux bugs ne créeront plus d’issue dans cet outil. Les liens existants sont conservés.","admin.connectors.deleteTitle":"Supprimer ce connecteur ?","admin.connectors.description":"Crée automatiquement une issue dans un outil externe (Vikunja, webhook) à chaque nouveau bug signalé. Le lien vers l’issue apparaît dans le détail du ticket.","admin.connectors.empty":"Aucun connecteur configuré.","admin.connectors.loadError":"Impossible de charger les connecteurs.","admin.connectors.title":"Connecteurs","admin.connectors.toastCreated":"Connecteur créé","admin.connectors.toastDeleted":"Connecteur supprimé","admin.connectors.toastUpdated":"Connecteur mis à jour","admin.createTask.attach":"Joindre des fichiers","admin.createTask.attachFailed":"Ticket créé, mais ces fichiers n’ont pas pu être joints :","admin.createTask.attachHelper":"Images (PNG, JPEG, WebP, 5 Mo) et fichiers (PDF, Office, texte, zip, 10 Mo) — 15 au plus. Une image peut aussi se coller dans la description.","admin.createTask.descIdea":"Décrire l’idée","admin.createTask.descProblem":"Décrire le problème","admin.createTask.description":"Description","admin.createTask.llmHelper":"Le titre, le contexte et le type seront affinés automatiquement par le LLM.","admin.createTask.openTicket":"Ouvrir le ticket","admin.createTask.priority":"Priorité","admin.createTask.removeFile":"Retirer","admin.createTask.site":"Projet","admin.createTask.title":"Créer un ticket","admin.createTask.uploading":"Envoi des fichiers…","admin.createTask.urlHelper":"Aide à inférer le contexte fonctionnel.","admin.createTask.urlOptional":"URL (optionnel)","admin.dashboard.ai":"IA","admin.dashboard.awaiting":"En attente","admin.dashboard.bugReports":"Tickets","admin.dashboard.chatThreads":"Conversations","admin.dashboard.colId":"ID","admin.dashboard.colPriority":"Priorité","admin.dashboard.colStatus":"Statut","admin.dashboard.colTitle":"Titre","admin.dashboard.emptyTasks":"Aucun ticket à traiter.","admin.dashboard.emptyThreads":"Aucun visiteur en attente.","admin.dashboard.inProgress":"En cours","admin.dashboard.new":"Nouveaux","admin.dashboard.seeAll":"Tout voir →","admin.dashboard.sites":"Projets","admin.dashboard.tasksToHandle":"Tickets à traiter","admin.dashboard.threadsAwaiting":"Conversations en attente","admin.dashboard.title":"Dashboard","admin.dashboard.toTest":"À tester","admin.dashboard.visitorFallback":"Anonyme","admin.designer.open":"Personnaliser le widget","admin.designer.title":"Studio d'apparence","admin.designer.subtitle":"Choisir un rôle, piocher une couleur : l'aperçu montre le widget tel que le verront les visiteurs.","admin.designer.rolePanel":"Fond du panneau","admin.designer.rolePanelHint":"Le fond du chat et du formulaire de signalement","admin.designer.rolePanelText":"Texte du panneau","admin.designer.rolePanelTextHint":"Textes, champs, libellés du panneau ; bordures et gris en sont déduits","admin.designer.roleAi":"Messages de l'assistant","admin.designer.roleAiHint":"Le fond des réponses ; leur texte reste lisible tout seul","admin.designer.panelTheme":"Thème du panneau","admin.designer.themeLight":"Clair","admin.designer.themeDark":"Sombre","admin.designer.themeSite":"Assorti au site","admin.designer.groupAccent":"Accent","admin.designer.groupPanel":"Panneau","admin.designer.reset":"Tout remettre par défaut","admin.designer.mine":"Mes couleurs","admin.designer.duplicate":"Dupliquer et ajuster","admin.designer.mineEmpty":"Dupliquer une couleur pour la redéfinir à volonté : elle rejoint la palette, enregistrée avec le projet.","admin.designer.deleteColor":"Retirer de mes couleurs","admin.designer.hsl.h":"Teinte","admin.designer.hsl.s":"Saturation","admin.designer.hsl.l":"Luminosité","admin.designer.presets":"Harmonies proposées","admin.designer.noPalette":"Aucune couleur relevée sur le site pour l'instant : le widget les relèvera à sa prochaine visite.","admin.designer.roles":"Rôles","admin.designer.roleBg":"Accent","admin.designer.roleBgHint":"Bulle, bandeau, bouton d'envoi, messages du visiteur","admin.designer.roleFg":"Texte sur l'accent","admin.designer.roleFgHint":"Icône de la bulle, titre du bandeau, libellés","admin.designer.pickFor":"Couleur du rôle « {role} »","admin.designer.fromSite":"Repérées sur le site","admin.designer.neutrals":"Neutres","admin.designer.fixContrast":"Rendre lisible","admin.designer.custom":"Autre couleur…","admin.designer.readability":"Lisibilité","admin.designer.contrast.aaa":"excellente (AAA)","admin.designer.contrast.aa":"bonne (AA)","admin.designer.contrast.large":"juste — gros texte seulement","admin.designer.contrast.low":"insuffisante","admin.designer.apply":"Appliquer","admin.designer.previewTitle":"Support","admin.designer.previewSubtitle":"Réponse en quelques minutes","admin.designer.previewWelcome":"Bonjour 👋 Une question ?","admin.designer.previewVisitor":"Le paiement ne passe pas sur mobile.","admin.designer.previewPlaceholder":"Écrire un message…","admin.designer.previewSend":"Envoyer","admin.designer.siteColors":"Couleurs du site","admin.designer.siteColorsHint":"Récupérer la CSS du site, ou la coller : on en extrait les couleurs de marque, de fond et de texte, et on propose des thèmes complets.","admin.designer.fetchSite":"Récupérer depuis le site","admin.designer.fetching":"Lecture du site…","admin.designer.fetchError":"Impossible de lire le site : {message}","admin.designer.fetchFrom":"Lu sur {origin} ({count} feuilles de style, {scripts} scripts)","admin.designer.pasteCss":"Coller la CSS","admin.designer.pastePlaceholder":`:root { --primary: #6d28d9; }
body { background: #faf5ff; color: #1f2937; }
.btn { background: var(--primary); }`,"admin.designer.analyze":"Analyser","admin.designer.clearCss":"Effacer","admin.designer.analyzeEmpty":"Aucune couleur trouvée dans cette CSS.","admin.designer.analyzeSummary":"{count} couleurs trouvées","admin.designer.groupBrand":"Marque","admin.designer.groupBackground":"Fond","admin.designer.groupText":"Texte","admin.designer.sourceCss":"D'après la CSS analysée","admin.designer.sourceDetected":"D'après les couleurs relevées par le widget","admin.designer.themes":"Thèmes proposés","admin.designer.themesHint":"Un clic applique les cinq couleurs, toutes lisibles ; tout reste ajustable ensuite.","admin.designer.themesEmpty":"Récupérer ou coller la CSS du site pour obtenir des thèmes à ses couleurs.","admin.designer.theme.assorti":"Assorti au site","admin.designer.theme.clair":"Marque sur clair","admin.designer.theme.sombre":"Marque sur sombre","admin.designer.theme.teinte":"Teinte de la marque","admin.designer.theme.second":"Deuxième couleur","admin.designer.theme.sobre":"Sobre","admin.docs.notFound":"Doc introuvable : {slug}","admin.docs.toc":"Sommaire","admin.embedSnippet.basicHint1":"À coller dans le","admin.embedSnippet.basicHint2":"ou avant","admin.embedSnippet.basicHint3":"du site. L’URL est liée à ce projet — régénérer la clé rend l’embed actuel inopérant, et il faudra coller le nouveau. Le toggle « Widget activé » dans l’admin coupe le widget sans toucher au HTML du site.","admin.embedSnippet.basicTitle":"Embed de base","admin.embedSnippet.envHint":"Éviter de coller l’URL en dur dans le HTML. Rotation de clé = changement d’une variable, pas de chasse aux templates.","admin.embedSnippet.envTitle":"Recommandé — stocker l’URL en variable d’environnement","admin.embedSnippet.identifyBefore":"avant","admin.embedSnippet.identifyComment1":"À placer AVANT le script d’embed. Identifie l’utilisateur connecté.","admin.embedSnippet.identifyCommentId":"recommandé : ID utilisateur stable","admin.embedSnippet.identifyCommentMeta":"libre : n’importe quelle donnée utile","admin.embedSnippet.identifyCommentName":"optionnel : nom affiché dans l’admin","admin.embedSnippet.identifyHint1":"rattache la conversation à l’utilisateur connecté du site : on voit qui écrit (et son contexte) directement dans la conversation admin, et l’IA peut s’en servir pour répondre. L’appeler après le login (ou de nouveau si l’utilisateur change). Le placer","admin.embedSnippet.identifyHint2":"le script d’embed.","admin.errors.file_too_large":"Fichier trop gros : 5 Mo pour une image, 10 Mo pour un autre fichier.","admin.errors.invalid_color":"Couleur invalide : format #rrggbb attendu.","admin.errors.invalid_file":"Fichier illisible.","admin.errors.invalid_file_type":"Format non accepté : PNG, JPEG, WebP, PDF, Office, texte ou zip.","admin.errors.invalid_icon_svg":"Ce SVG n'a pas pu être lu : il doit être fait de formes simples (path, circle, rect…) et peser 10 Ko au plus une fois nettoyé.","admin.errors.already_invited":"Une invitation est déjà en attente pour cet email.","admin.errors.already_member":"Cet utilisateur est déjà membre de cet espace.","admin.errors.billing_unavailable":"La facturation n’est pas configurée.","admin.errors.cannot_change_self":"Impossible de modifier son propre rôle.","admin.errors.cannot_delete_self":"Impossible de supprimer son propre compte.","admin.errors.cannot_revoke_self":"Impossible de se retirer soi-même.","admin.errors.cannot_set_self":"Le mot de passe de son propre compte se change depuis le profil.","admin.errors.default":"Quelque chose n’a pas fonctionné. Réessayer dans un instant.","admin.errors.email_mismatch":"L’email saisi ne correspond pas à celui du compte.","admin.errors.email_taken":"Un compte existe déjà avec cet email.","admin.errors.forbidden":"Droits insuffisants pour cette action.","admin.errors.invalid_credentials":"Mot de passe incorrect. Le vérifier et réessayer.","admin.errors.internal_error":"Un souci est survenu de notre côté. Réessayer dans un instant ; s’il persiste, nous le signaler.","admin.errors.invalid_input":"Un champ n’est pas valide : le vérifier et réessayer.","admin.errors.lecture_seule":"Cette session est entrée par la console plateforme, en lecture seule : rien ne peut être modifié ici. Revenir à son organisation, ou ouvrir le lien dans une fenêtre privée.","admin.errors.invalid_or_expired":"Ce lien n’est plus valable : il a déjà servi ou il a expiré. Il suffit d’en demander un nouveau.","admin.errors.last_owner":"Impossible : c’est le dernier propriétaire de cet espace.","admin.errors.name_mismatch":"Le nom saisi ne correspond pas à celui de l’organisation.","admin.errors.network_error":"Erreur réseau. Vérifier la connexion.","admin.errors.no_customer":"Aucun abonnement à gérer pour le moment.","admin.errors.no_secret":"Clé indisponible pour ce token (créé avant la v1.22) — régénère-le.","admin.errors.not_archivable":"Seuls les éléments résolus peuvent être archivés.","admin.errors.not_archived":"Il faut d’abord archiver l’organisation avant de la purger.","admin.errors.org_disk_quota_exceeded":"Quota disque de l’organisation atteint.","admin.errors.org_is_default":"L’organisation par défaut ne peut être ni archivée ni supprimée.","admin.errors.org_not_found":"Cette organisation n’existe pas ou plus.","admin.errors.price_not_configured":"Ce plan n’est pas disponible à l’abonnement.","admin.errors.signups_closed":"Les inscriptions ne sont pas encore ouvertes.","admin.errors.token_revoked":"Ce token est révoqué — crée un nouveau token plutôt.","admin.errors.too_many_attempts":"Trop de tentatives. Réessayer dans quelques minutes.","admin.errors.too_many_files":"15 fichiers par ticket au plus.","admin.errors.unauthorized":"Session expirée. Se reconnecter.","admin.errors.site_css_no_origin":"Ce projet n'a aucune origine déclarée : ajoute l'adresse du site dans « Origines ».","admin.errors.site_css_forbidden_address":"Cette adresse n'est pas lisible depuis BipWire (réseau privé ou port non standard).","admin.errors.site_css_unreachable":"Le site n'a pas répondu, ou n'a pas renvoyé de page lisible.","admin.errors.site_css_timeout":"Le site a mis trop de temps à répondre.","admin.errors.site_css_rate_limited":"Trop de lectures du site d'affilée : réessaie dans une minute.","admin.events.task.created":"Nouveau bug / suggestion","admin.events.task.status_changed":"Changement de statut (à tester, résolu…)","admin.events.thread.escalated":"Escalade vers un humain","admin.guardrails.analyze":"Analyser","admin.guardrails.analyzedBy":"Analysé par : {by}","admin.guardrails.banner":"Le détecteur a relevé, dans la description ou une pièce jointe, un texte qui tente de manipuler un assistant IA. Le ticket reste normal, mais les agents automatiques le sautent et sa priorité est plafonnée. Lisez-le, puis levez le drapeau si c’est un faux positif.","admin.guardrails.bannerLifted":"Drapeau levé par {by} le {date}. La priorité déduite par l’IA a été restaurée.","admin.guardrails.bannerLiftedTitle":"Contenu suspect, revu par un humain","admin.guardrails.bannerTitle":"Contenu suspect — injection de prompt possible","admin.guardrails.by.aucun":"personne","admin.guardrails.by.heuristique":"heuristiques seules","admin.guardrails.by.llm":"heuristiques + second avis IA","admin.guardrails.hideReasons":"Masquer les motifs","admin.guardrails.lift":"Lever le drapeau","admin.guardrails.liftedHint":"Marqué suspect, puis revu et levé par un humain.","admin.guardrails.noReason":"Aucun motif détaillé.","admin.guardrails.notAnalyzed":"Non analysé","admin.guardrails.notAnalyzedHint":"Ce contenu n’a pas été analysé par le détecteur d’injection : antérieur à la fonctionnalité, ou format sans extracteur de texte.","admin.guardrails.reanalyze":"Réanalyser","admin.guardrails.reason.bloc_base64":"Bloc encodé en base64","admin.guardrails.reason.commande_exfiltration":"Commande d’exfiltration","admin.guardrails.reason.demande_secret":"Demande de secret ou de fichier sensible","admin.guardrails.reason.instruction_agent":"Instruction adressée à un agent","admin.guardrails.reason.llm":"Second avis de l’IA","admin.guardrails.reason.marqueur_role":"Marqueur de rôle ou de prompt système","admin.guardrails.reason.texte_cache":"Texte caché (caractères invisibles)","admin.guardrails.reason.url_ip_brute":"URL vers une adresse IP brute","admin.guardrails.showReasons":"Voir les motifs","admin.guardrails.suspect":"Contenu suspect","admin.guardrails.suspectHint":"Le détecteur d’injection de prompt a relevé un contenu qui tente de manipuler une IA.","admin.guardrails.suspectLifted":"Suspect · levé","admin.history.activeSessions":"Sessions admin actives","admin.history.adminSessions":"Sessions admin","admin.history.colAction":"Action","admin.history.colCreated":"Créée","admin.history.colExpires":"Expire","admin.history.colIp":"IP","admin.history.colUserAgent":"User-Agent","admin.history.conversationCleanupHint":"Supprime aussi les messages liés (cascade). Les conversations non résolues sont préservées.","admin.history.counters":"Compteurs","admin.history.emptySessions":"Aucune session active.","admin.history.olderThanDays":"Plus vieux que (jours)","admin.history.purgeConversationsMessage":"Les conversations (+ tous leurs messages) seront supprimées. Action irréversible.","admin.history.purgeConversationsTitle":"Supprimer les conversations résolues > {days} jours ?","admin.history.purgeExpired":"Purger expirées / révoquées","admin.history.purgeSessionsMessage":"Supprime de la DB toutes les sessions expirées ou révoquées. Sans effet sur les sessions actives.","admin.history.purgeSessionsTitle":"Purger les sessions mortes ?","admin.history.purgeTasksMessage":"Les rows DB ET les fichiers screenshot sur disque seront supprimés. Action irréversible.","admin.history.purgeTasksTitle":"Supprimer les tickets > {days} jours ?","admin.history.resolvedOverDays":"Résolus > N jours","admin.history.revokeMessage":"L’utilisateur sera déconnecté immédiatement.","admin.history.revokeSession":"Révoquer la session","admin.history.revokeTitle":"Révoquer cette session ?","admin.history.screenshotsCount_one":"{n} capture","admin.history.screenshotsCount_other":"{n} captures","admin.history.sessionsHint":"actives · {dead} mortes","admin.history.subtitle":"Vue d’ensemble + nettoyage des sessions, tickets et conversations anciennes.","admin.history.taskCleanup":"Nettoyage des tickets","admin.history.tasks":"Tickets","admin.history.tasksHint":"{old} > 90 j","admin.history.threadCleanup":"Nettoyage des conversations résolues","admin.history.threads":"Conversations","admin.history.threadsHint":"{resolved} résolues · {old} > 30 j","admin.history.title":"Historique & maintenance","admin.history.toastConversationsPurged_one":"{n} conversation résolue supprimée.","admin.history.toastConversationsPurged_other":"{n} conversations résolues supprimées.","admin.history.toastSessionRevoked":"Session révoquée.","admin.history.toastSessionsPurged_one":"{n} session supprimée.","admin.history.toastSessionsPurged_other":"{n} sessions supprimées.","admin.history.toastTasksPurged_one":"{n} ticket supprimé ({s}).","admin.history.toastTasksPurged_other":"{n} tickets supprimés ({s}).","admin.integrationDialog.autoCreate":"Création automatique d’issue","admin.integrationDialog.autoCreateCaption":"À chaque nouveau bug, crée une issue dans l’outil cible.","admin.integrationDialog.disabled":"Désactivé","admin.integrationDialog.editTitle":"Modifier — {name}","admin.integrationDialog.enabled":"Activé","admin.integrationDialog.events":"Événements","admin.integrationDialog.eventsAll":"Tous les événements","admin.integrationDialog.eventsCaption":"Aucun sélectionné = déclenché sur tous les événements.","admin.integrationDialog.fieldGuide":"Guide {label} : où trouver chaque champ ↗","admin.integrationDialog.guideShow":"Afficher le guide {label}","admin.integrationDialog.guideHide":"Masquer le guide {label}","admin.integrationDialog.name":"Nom","admin.integrationDialog.namePlaceholderConnector":"Ex : Vikunja prod","admin.integrationDialog.namePlaceholderNotification":"Ex : Alertes équipe","admin.integrationDialog.newConnector":"Nouveau connecteur","admin.integrationDialog.newNotification":"Nouvelle notification","admin.integrationDialog.sites":"Projets concernés","admin.integrationDialog.sitesAll":"Tous les projets","admin.integrationDialog.sitesCaption":"Aucun sélectionné = appliqué à tous les projets.","admin.integrationDialog.test":"Tester","admin.integrationDialog.testErrorUnknown":"erreur inconnue","admin.integrationDialog.testFailed":"Test échoué : {error}","admin.integrationDialog.testMissingFields":"Champs requis manquants : {fields}","admin.integrationDialog.testSuccess":"Configuration complète — aucun message envoyé (le test ne fait que vérifier les champs requis).","admin.integrationDialog.testValid":"Configuration valide — aucune issue créée (le test ne fait que vérifier les champs requis).","admin.integrationDialog.testing":"Test en cours…","admin.integrationDialog.sourceLoad":"Charger les projets {label}","admin.integrationDialog.sourceLoading":"Chargement…","admin.integrationDialog.sourceCaption":"Après l'URL et le token : charger la liste pour choisir les projets au lieu de recopier leur ID.","admin.integrationDialog.sourceLoaded":"{count} projet(s) chargé(s) — choisir ci-dessous.","admin.integrationDialog.sourceLoadedAccount":"{count} projet(s) chargé(s), ceux que voit le compte {label} « {account} » — choisir ci-dessous.","admin.integrationDialog.sourceUnknownValue":"#{value} (absent de la liste)","admin.integrationDialog.siteMapDefault":"Projet par défaut","admin.integrationDialog.sourceError.generic":"Chargement impossible ({code}).","admin.integrationDialog.sourceError.upstream_status":"L'outil a refusé la demande ({code}) : vérifier l'URL et le token (droit « Projets : lire »).","admin.integrationDialog.sourceError.upstream_down":"L'outil ne répond pas ({code}) : l'instance semble arrêtée. La relancer, ou vérifier l'URL.","admin.integrationDialog.sourceError.upstream_forbidden_address":"Adresse refusée : seule une instance joignable depuis Internet peut être interrogée.","admin.integrationDialog.sourceError.upstream_unreachable":"Instance injoignable : vérifier l'URL.","admin.integrationDialog.sourceError.upstream_timeout":"L'instance n'a pas répondu à temps.","admin.integrationDialog.sourceError.upstream_invalid_url":"URL invalide.","admin.integrationDialog.sourceError.config_incomplete":"Renseigner d'abord l'URL et le token.","admin.integrationDialog.sourceError.vikunja_invalid_response":"La réponse ne ressemble pas à une instance Vikunja : vérifier l'URL.","admin.integrationDialog.type":"Type","admin.integrationDialog.typeLocked":"Le type n’est pas modifiable après création. ","admin.integrations.error.module_disabled":"Ce type d’intégration est désactivé par la plateforme. La ligne peut être gardée, mais pas activée.","admin.integrations.error.extension_unavailable":"Ce type d’intégration n’est pas disponible pour cette organisation. La ligne peut être gardée, mais pas activée.","admin.integrations.error.type_not_implemented":"Ce type d’intégration n’est pas servi par cette plateforme.","admin.integrations.error.server_not_configured":"Ce canal ne peut rien envoyer : le service n’est pas encore configuré sur la plateforme. Contacter le support.","admin.integrations.moduleDisabled":"désactivé par la plateforme","admin.integrations.scopeSites_one":"{n} projet","admin.integrations.scopeSites_other":"{n} projets","admin.integrations.typeDisabledByPlatform":"Désactivé par la plateforme","admin.integrations.typeUnavailable":"Indisponible pour cette organisation","admin.knowledgeDocHelp.avoid1":"Coller la landing page entière (trop verbeux, trop marketing).","admin.knowledgeDocHelp.avoid2":"Coller la doc technique du code (l’utilisateur final s’en fout).","admin.knowledgeDocHelp.avoid3":"Inventer des features qui n’existent pas encore.","admin.knowledgeDocHelp.avoid4a":"URLs relatives","admin.knowledgeDocHelp.avoid4b":"uniquement des URLs absolues, sinon le lien casse depuis le widget embarqué.","admin.knowledgeDocHelp.avoid5":"Reformuler la même info dans plusieurs sections.","admin.knowledgeDocHelp.avoidTitle":"À éviter","admin.knowledgeDocHelp.claudePrompt":`Tu vas écrire la "knowledge base" qui sera injectée dans le system prompt d’un chatbot de support (un widget de chat IA qui répond aux questions des visiteurs de ce site). Cible : 5 à 10 KB de Markdown, **pas plus**. Chaque token compte — le doc est rechargé à chaque conversation.

## Méthode

1. Explore le codebase : lis le README, le package.json, les pages publiques (landing, pricing, features, FAQ existante), les routes API publiques, les composants UI majeurs. Ouvre \`git log --oneline -50\` pour repérer ce qui a été shipé récemment.
2. Si le site est en ligne et que tu as WebFetch, va lire la home, la page pricing, la page features, le footer (CGV, mentions, contact).
3. Note tout fait *vérifiable* depuis le code ou le site. **N’invente rien.** Si un truc est ambigu, mets-le en commentaire HTML \`<!-- À VÉRIFIER: ... -->\` à la fin de la section concernée et continue.

## Ce que le chatbot doit pouvoir répondre

Couvre AU MINIMUM :

- **Qu’est-ce que le produit fait, en 1-2 phrases**
- **À qui ça s’adresse** (cible utilisateur)
- **Comment ça marche** (workflow principal, 3-5 étapes max)
- **Prix** (plans, free tier, essai, ce qui est inclus dans chaque)
- **Features clés** (liste courte, pas exhaustif)
- **Limites / quotas** (par plan, par compte, par requête)
- **Compte et auth** (comment s’inscrire, login social, mot de passe oublié, suppression de compte)
- **Paiement** (méthodes acceptées, devise, facture, remboursement)
- **Données & confidentialité** (où c’est hébergé, rétention, RGPD si pertinent)
- **Bugs connus / FAQ techniques fréquentes** (si tu trouves trace de tickets récurrents dans git/issues)
- **Comment contacter le support humain** (email, délai de réponse)

Ajoute les sections spécifiques au métier que tu trouves pertinentes.

## Format obligatoire

- Markdown plat, **sections H2** (\`##\`) — pas de H1, pas de H3.
- Sous chaque section : **bullets courtes**, idéalement une ligne par fait.
- **Pas de narratif**, pas de phrases d’intro, pas de "Bienvenue sur…", pas de "Notre équipe est ravie de…".
- Pas de bullets imbriquées (1 niveau max).
- Pas de tableaux Markdown (le LLM s’embrouille dessus). Liste à puces à la place.
- Pas de "etc.", "et plus encore", "et d’autres" — soit concret, soit jette.
- Pas de répétitions inter-sections.
- Quand un fait dépend d’une condition, dis-le en 1 ligne :
  "Free → 5 exports/mois. Pro (€9/mois) → illimité."

## À éviter absolument

- Le copywriting marketing du site ("solution révolutionnaire", "expérience unique"). On veut des **faits**, pas de la vente.
- Les promesses non factuelles ("le meilleur du marché").
- Les détails d’implémentation tech (stack, framework) — l’utilisateur final s’en fout.
- Les anciens features qui n’existent plus.

## Liens

Inclus les **URLs absolues** des pages clés (pricing, login, contact, app, docs publiques…) au format Markdown \`[texte](https://…)\`. Le bot peut les ressortir cliquables dans ses réponses — ne mets que des URLs que tu veux réellement voir proposées. Pas d’URLs relatives type \`/pricing\`.

## Livrable

Écris le fichier complet dans \`KNOWLEDGE.md\` à la racine. Quand tu as fini, affiche aussi le compte de caractères et la taille en KB pour que je vérifie qu’on est sous la limite.

Vas-y, explore d’abord puis rédige.`,"admin.knowledgeDocHelp.compressPrompt":"Relis KNOWLEDGE.md et coupe tout ce qui est redondant ou vague. Objectif : 30% plus court sans perdre d’info actionnable. Garde les sections par ordre de fréquence des questions visiteur (prix > features > compte > technique).","admin.knowledgeDocHelp.copyPrompt":"Copier le prompt","admin.knowledgeDocHelp.costLabel":"Coût indicatif (Gemini Flash Lite, 100 chats/jour, 10 KB) :","admin.knowledgeDocHelp.costSuffix":"(le caching est actif automatiquement).","admin.knowledgeDocHelp.costValue":"~22 ¢/mois sans cache, ~3 ¢/mois avec cache","admin.knowledgeDocHelp.intro1":"La doc est injectée dans le system prompt du chatbot","admin.knowledgeDocHelp.intro2":". Viser","admin.knowledgeDocHelp.intro3":"de Markdown dense (≈ 2 500 tokens). Au-delà, la facture monte vite et le modèle se disperse.","admin.knowledgeDocHelp.intro5to10":"5 à 10 KB max","admin.knowledgeDocHelp.introEachConv":"à chaque conversation","admin.knowledgeDocHelp.step1Caption1":"Claude explore le codebase + visite le site live si possible, puis écrit","admin.knowledgeDocHelp.step1Caption2":"à la racine. Il affichera la taille finale.","admin.knowledgeDocHelp.step1Desc1":"Ouvrir une session Claude Code","admin.knowledgeDocHelp.step1Desc2":"du site dont la doc est à générer (pas dans BipWire). Coller ce prompt :","admin.knowledgeDocHelp.step1DescRoot":"à la racine du repo","admin.knowledgeDocHelp.step1Title":"1. Générer la doc avec Claude Code","admin.knowledgeDocHelp.step2Desc":"Relancer Claude Code dans la même session avec ce prompt court :","admin.knowledgeDocHelp.step2Title":"2. Au-delà de 10 KB, itérer","admin.knowledgeDocHelp.step3Desc1":"Copier le contenu final de","admin.knowledgeDocHelp.step3Desc2":"et le coller dans le champ « Doc de référence » de ce formulaire. Sauvegarder le projet. C’est tout — le prochain chat utilisera la nouvelle doc.","admin.knowledgeDocHelp.step3Title":"3. Coller dans le champ","admin.knowledgeDocHelp.title":"Comment écrire la doc de référence","admin.layout.activeSpace":"Espace actif","admin.layout.collapseMenu":"Réduire le menu","admin.layout.createTask":"Créer un ticket","admin.layout.currentOrg":"Organisation en cours","admin.layout.expandMenu":"Déplier le menu","admin.layout.logout":"Déconnexion","admin.layout.myAccount":"Mon compte","admin.layout.myProfile":"Mon profil","admin.layout.openMenu":"Ouvrir le menu","admin.layout.themeDark":"Passer en thème sombre","admin.layout.themeLight":"Passer en thème clair","admin.layout.userMenu":"Menu utilisateur","admin.layout.versionTitle":"Version déployée — sert à vérifier le déploiement","admin.llmScopes.sitesRead":"Lire la config des projets","admin.llmScopes.tasksRead":"Lire les tickets (bugs et fonctionnalités)","admin.llmScopes.tasksWrite":"Marquer un ticket « à tester »","admin.llmScopes.threadsRead":"Lire les conversations chat","admin.llmTokens.active":"Actif","admin.llmTokens.allowedSites":"Projets autorisés","admin.llmTokens.colActions":"Actions","admin.llmTokens.colCreated":"Créé","admin.llmTokens.colLabel":"Libellé","admin.llmTokens.colLastUsed":"Dernière utilisation","admin.llmTokens.colPrefix":"Préfixe","admin.llmTokens.colScopes":"Droits","admin.llmTokens.colSites":"Projets","admin.llmTokens.colStatus":"Statut","admin.llmTokens.connect.agentPrompt":`Connecte-toi à BipWire (mon outil de bug-report + chat utilisateurs) via son serveur MCP, puis sers-t’en.

1. Ajoute le serveur MCP en lançant cette commande à la racine du projet :
   claude mcp add --transport http bip {mcpUrl} --header "Authorization: Bearer {token}"
   (le token est sur la page Clés API de BipWire, il commence par bipwire_llm_ — token « {label} », prefix {prefix})

2. Une fois connecté, appelle d’abord le tool get_my_permissions pour connaître tes droits, puis utilise les tools BipWire disponibles :
   {tools}

Ensuite, quand je te dis « regarde les derniers bugs », « qu’a dit l’utilisateur dans la conversation X » ou « y a-t-il des plaintes récurrentes sur la feature Y », récupère les tickets/threads via ces tools avant de répondre.`,"admin.llmTokens.connect.fetchingKey":"Récupération de la clé…","admin.llmTokens.connect.introEmph":"prompt ci-dessous","admin.llmTokens.connect.introPost":"et le coller dans Claude Code — il ajoutera le serveur MCP BipWire tout seul et saura quoi en faire.","admin.llmTokens.connect.introPre":"Le plus simple : copier le","admin.llmTokens.connect.keyInserted":"La clé du token est déjà insérée dans les snippets.","admin.llmTokens.connect.keyReplace":"Remplacer {placeholder} par la clé.","admin.llmTokens.connect.neverCommitPost":"avec le token en clair.","admin.llmTokens.connect.neverCommitPre":"Ne jamais commiter","admin.llmTokens.connect.noSecret":"La clé de ce token n’est pas récupérable (créé avant la v1.22) — les snippets contiennent un placeholder. Régénérer ce token (icône ↻ dans la liste) pour obtenir une clé insérable automatiquement.","admin.llmTokens.connect.orDirect":"Ou en direct — commande à lancer soi-même","admin.llmTokens.connect.promptHeading":"Prompt à coller dans Claude Code","admin.llmTokens.connect.rightsInfo":"Droits de ce token : {scopes}. Claude Code ne verra que les tools correspondants ({tools}).","admin.llmTokens.connect.sitesAll":"Accès à tous les projets.","admin.llmTokens.connect.sitesLimited":"Périmètre limité aux projets : {sites}.","admin.llmTokens.connect.title":"Connecter à Claude Code — {label}","admin.llmTokens.connect.warnKeyInserted":"Les snippets ci-dessus contiennent la clé en clair.","admin.llmTokens.connect.warnKeyReplace":"Remplacer {placeholder} par la clé de ce token.","admin.llmTokens.connectAgent":"Brancher un agent CLI","admin.llmTokens.connectHint":"Connecter à Claude Code (commande + prompt)","admin.llmTokens.copyFullKey":"Copier la clé complète","admin.llmTokens.createSitesHelper":"Limite le token à certains projets. Aucune case cochée = accès à tous les projets.","admin.llmTokens.created.addToClaudeCode":"Ajouter à Claude Code (serveur MCP)","admin.llmTokens.created.doneClose":"J’ai copié, fermer","admin.llmTokens.created.intro":"Copier ce token pour le brancher tout de suite. Il reste recopiable plus tard depuis la liste des Clés API (bouton copier sur la ligne du token).","admin.llmTokens.created.mcpDescPost":"sera dispo dans Claude Code (tools de lecture tickets / threads / projets selon les droits du token).","admin.llmTokens.created.mcpDescPre":"Token déjà inséré. Lancer cette commande à la racine du projet — le serveur MCP","admin.llmTokens.created.testLabel":"Test","admin.llmTokens.created.title":"Token créé — copie-le maintenant","admin.llmTokens.created.tokenLabel":"Token","admin.llmTokens.created.warnMid":"stocke le token en local (privé, non commité). Avec","admin.llmTokens.created.warnPost":", ne le commite pas — il contient le token en clair.","admin.llmTokens.created.warnPre":"La commande","admin.llmTokens.deleteHint":"Supprimer définitivement (révoque puis supprime la ligne)","admin.llmTokens.deleteMessage":"Le token sera révoqué puis supprimé de la DB — il disparaît de la liste, plus aucune trace (lastUsedAt etc.) ne reste. Préférer « Révoquer » pour garder l’historique d’utilisation pour audit.","admin.llmTokens.deleteTitle":"Supprimer « {label} » ?","admin.llmTokens.editScopesHint":"Modifier les droits","admin.llmTokens.editScopesInfo":"Modification immédiate côté serveur — l’outil qui utilise ce token voit le changement au prochain appel sans avoir à régénérer.","admin.llmTokens.editScopesTitle":"Modifier les droits — {label}","admin.llmTokens.editSitesHelper":"Aucune case cochée = accès à tous les projets.","admin.llmTokens.editSitesHint":"Modifier les projets autorisés","admin.llmTokens.empty":"Aucun token. Générer le premier pour donner accès à un agent LLM.","admin.llmTokens.grantedScopes":"Droits accordés","admin.llmTokens.howTo.claudeMdBlock":`## BipWire — bugs et conversations utilisateurs

BipWire est mon outil interne de chat + bug-report. Tu peux y lire les bugs reportés
et les conversations chat pour ce projet (\`siteId={siteId}\`).

**Auth** : header \`Authorization: Bearer $BIPWIRE_LLM_TOKEN\` (token dans mon env,
ne le mets jamais en dur dans le code).

**Doc complète** : {docUrl} — fetch-la avant de poser des questions, elle
décrit le format markdown des réponses et les filtres dispos.

**Endpoints utiles** :
- Tickets ouverts : \`GET {adminOrigin}/api/llm/tasks?siteId=<SITE_ID>&status=new&limit=10\`
- Bugs uniquement : ajouter \`&type=bug\` (ou \`&type=feature\` pour les suggestions)
- Détail ticket : \`GET {adminOrigin}/api/llm/tasks/{id}\`
- Threads en attente : \`GET {adminOrigin}/api/llm/threads?siteId=<SITE_ID>&status=awaiting_human\`
- Détail thread : \`GET {adminOrigin}/api/llm/threads/{id}\`

Quand je te dis "regarde le dernier ticket" / "qu’a dit le user dans la conversation X"
/ "y a-t-il des plaintes récurrentes sur la feature Y", fetch automatiquement
les tickets/threads avant de répondre.`,"admin.llmTokens.howTo.intro":"Trois étapes pour que Claude Code (ou n’importe quel agent compatible WebFetch) puisse lire les bugs et conversations des projets depuis n’importe quel repo.","admin.llmTokens.howTo.mcpBodyPost":", mêmes tokens que ceux créés ici. Config complète + tools disponibles :","admin.llmTokens.howTo.mcpBodyPre":"Le serveur MCP est exposé sur","admin.llmTokens.howTo.mcpServerPage":"page Serveur MCP","admin.llmTokens.howTo.mcpTitle":"Avec Claude Desktop ou un client MCP ?","admin.llmTokens.howTo.methodADescEnd":"dans l’env des outils Bash/WebFetch qu’il lance, sans toucher au shell.","admin.llmTokens.howTo.methodADescMid":"(global) ou","admin.llmTokens.howTo.methodADescPost":"(per-projet). Claude Code injecte automatiquement","admin.llmTokens.howTo.methodADescPre":"Ajouter le bloc dans","admin.llmTokens.howTo.methodAPost":"(recommandée)","admin.llmTokens.howTo.methodAPre":"Méthode A — Claude Code","admin.llmTokens.howTo.methodB":"Méthode B — Variable shell classique","admin.llmTokens.howTo.methodBAltPost":"gitignoré à la racine du repo.","admin.llmTokens.howTo.methodBAltPre":"Alternative par-projet : un","admin.llmTokens.howTo.methodBDescPost":"dans le terminal ait aussi accès au token (hors Claude Code).","admin.llmTokens.howTo.methodBDescPre":"Pour que","admin.llmTokens.howTo.multiBody":"Générer un token par projet/machine avec un label parlant (« Claude Code shotcv MacBook »). En cas de fuite : révoquer celui-là seul, sans casser les autres. Le préfixe bipwire_llm_xxxx… visible dans la liste identifie rapidement le token concerné.","admin.llmTokens.howTo.multiTitle":"Multi-projet, multi-token","admin.llmTokens.howTo.securityBody":"— l’API est read-only par construction. Même fuité, un token ne peut pas modifier l’état de BipWire. lastUsedAt dans la liste montre les tokens zombies à révoquer.","admin.llmTokens.howTo.securityLabel":"Sécurité","admin.llmTokens.howTo.shellComment1":"# Une fois pour toutes (~/.zshrc ou ~/.bashrc)","admin.llmTokens.howTo.shellComment2":"# puis reload : source ~/.zshrc","admin.llmTokens.howTo.siteIdPlaceholder":"<METS_LE_SITE_ID_ICI>","admin.llmTokens.howTo.step1":"1. Générer un token + l’exposer en env var","admin.llmTokens.howTo.step1Desc":"Cliquer « Nouveau token » sur cette page, copier le clear-text affiché, puis suivre une des deux méthodes ci-dessous selon le setup.","admin.llmTokens.howTo.step2DescPost":"par l’id du projet BipWire concerné (shotcv, hypnalis…).","admin.llmTokens.howTo.step2DescPre":"Remplacer","admin.llmTokens.howTo.step2Post":"du projet consommateur","admin.llmTokens.howTo.step2Pre":"2. Ajouter ce bloc dans le","admin.llmTokens.howTo.step3":"3. Tester depuis le shell avant de lancer Claude Code","admin.llmTokens.howTo.step3Desc":"Doit renvoyer du markdown listant les 2 derniers bugs. Un 401 : le token est mal collé ou révoqué. Rien : vérifier le DNS du host admin.","admin.llmTokens.howTo.title":"Connecter Claude Code (ou un autre agent) à l’API BipWire","admin.llmTokens.howToConnect":"Comment connecter Claude Code ?","admin.llmTokens.keyTitle":"Clé — {label}","admin.llmTokens.label":"Libellé","admin.llmTokens.labelHelper":"Description libre pour identifier ce token plus tard (l’outil + la machine, en général).","admin.llmTokens.labelPlaceholder":"ex : Claude Code shotcv MacBook","admin.llmTokens.newToken":"Nouveau token","admin.llmTokens.newTokenTitle":"Nouveau token API","admin.llmTokens.orManualPost":"(racine du repo) :","admin.llmTokens.orManualPre":"Ou config manuelle équivalente dans","admin.llmTokens.personal":"Personnel","admin.llmTokens.personalMine":"Le mien","admin.llmTokens.quickTest":"Test rapide","admin.llmTokens.regenConfirm":"Régénérer","admin.llmTokens.regenHint":"Régénérer la clé (l’ancienne cesse de fonctionner)","admin.llmTokens.regenMessage":"Une nouvelle clé est générée sur ce token (mêmes droits, mêmes projets). L’ancienne clé cesse IMMÉDIATEMENT de fonctionner — tous les outils branchés dessus devront prendre la nouvelle.","admin.llmTokens.regenPending":"Régénération…","admin.llmTokens.regenTitle":"Régénérer la clé de « {label} » ?","admin.llmTokens.renameHint":"Cliquer pour renommer","admin.llmTokens.revokeConfirm":"Révoquer","admin.llmTokens.revokeHint":"Révoquer ce token (garde la trace dans la liste)","admin.llmTokens.revokeMessage":"Le token sera invalidé immédiatement. Tout outil qui l’utilise verra 401 au prochain appel. Irréversible (il faudra régénérer un nouveau token).","admin.llmTokens.revokePending":"Révocation…","admin.llmTokens.revokeTitle":"Révoquer « {label} » ?","admin.llmTokens.revoked":"Révoqué","admin.llmTokens.scopesHelper":"Le token n’aura accès qu’aux endpoints / tools MCP qui correspondent à ces droits. Cocher le strict nécessaire — on peut générer plusieurs tokens avec des droits différents si besoin.","admin.llmTokens.scopesNone":"aucun","admin.llmTokens.sitesAll":"tous","admin.llmTokens.subtitleDocLink":"doc auto-discoverable","admin.llmTokens.subtitleMcpLink":"serveur MCP","admin.llmTokens.subtitleP1":"Bearer tokens longue durée pour les agents (Claude Code, Cursor…). Mêmes tokens pour l’API REST","admin.llmTokens.subtitleP2":"et le","admin.llmTokens.subtitleP3":". Voir la","admin.llmTokens.subtitleP4":"pour le détail.","admin.llmTokens.title":"Clés API","admin.llmTokens.toastCopyFailed":"Copie impossible — clé indisponible","admin.llmTokens.toastDeleted":"Token supprimé","admin.llmTokens.toastKeyCopied":"Clé copiée dans le presse-papier","admin.llmTokens.toastRevoked":"Token révoqué","admin.llmTokens.toastScopesUpdated":"Droits mis à jour","admin.llmTokens.tokenPlaceholder":"COLLE_TON_TOKEN_BIPWIRE","admin.llmTokens.viewKey":"Voir la clé en clair","admin.llmUsage.blocked":"bloqué","admin.llmUsage.calls":"Appels","admin.llmUsage.callsThisMonth":"Appels LLM ce mois","admin.llmUsage.colCostEur":"Coût €","admin.llmUsage.colDay":"Jour","admin.llmUsage.colInput":"Entrée","admin.llmUsage.colOutput":"Sortie","admin.llmUsage.colTotalTokens":"Total tokens","admin.llmUsage.cost":"Coût","admin.llmUsage.detailTitle":"{name} — détail {days} jours","admin.llmUsage.disk":"Disque","admin.llmUsage.emptyPeriod":"Aucune activité sur la période.","admin.llmUsage.overviewSummary":"{month} — {tokens} tokens · {eur} · {calls} appels (tous projets)","admin.llmUsage.planQuotas":"Plan & quotas (organisation)","admin.llmUsage.sites":"Projets","admin.llmUsage.title":"Consommation LLM","admin.llmUsage.tokens":"Tokens","admin.llmUsage.unlimited":"illimité","admin.members.changeRole":"Changer le rôle","admin.members.colActions":"Actions","admin.members.colAddedAt":"Ajouté le","admin.members.colEmail":"Email","admin.members.colExpires":"Expire","admin.members.colInvitedBy":"Invité par","admin.members.colRole":"Rôle","admin.members.emailAddress":"Adresse email","admin.members.empty":"Aucun membre pour le moment.","admin.members.invite":"Inviter","admin.members.inviteTitle":"Inviter un membre","admin.members.lastOwnerTooltip":"Dernier propriétaire de l’espace — action impossible","admin.members.loadError":"Impossible de charger les membres.","admin.members.ownerOnlyTooltip":"Seul un propriétaire peut agir sur un propriétaire","admin.members.pendingEmpty":"Aucune invitation en attente.","admin.members.pendingTitle":"Invitations en attente","admin.members.remove":"Retirer","admin.members.removeFromSpace":"Retirer de l’espace","admin.members.removing":"Retrait…","admin.members.resendInvite":"Renvoyer l’invitation","admin.members.revokeInvite":"Révoquer l’invitation","admin.members.revokeMessage":"{email} perdra l’accès à cet espace.","admin.members.revokeTitle":"Retirer ce membre ?","admin.members.role":"Rôle","admin.members.self":"(moi)","admin.members.sendInvite":"Envoyer l’invitation","admin.members.setPassword":"Définir le mot de passe","admin.members.title":"Membres","admin.members.toastInviteResent":"Invitation renvoyée — nouveau lien, l’ancien ne marche plus.","admin.members.toastInviteRevoked":"Invitation révoquée","admin.members.toastInviteSent":"Invitation envoyée à {email}","admin.members.toastPasswordUpdated":"Mot de passe mis à jour","admin.members.toastRemoved":"Membre retiré","admin.members.toastRoleUpdated":"Rôle mis à jour","admin.messages.awaitingVisitor":"En attente de la réponse du visiteur · {date}","admin.messages.captureReceived":"✅ Capture reçue","admin.messages.captureRequested":"📸 Capture demandée","admin.messages.roleBot":"🤖 BipWire","admin.messages.roleSupport":"👤 Support","admin.messages.viewCapture":"Voir la capture","admin.nav.aiModels":"Modèles IA","admin.nav.apiKeys":"Clés API","admin.nav.backToApp":"Retour à l’app","admin.nav.billing":"Facturation","admin.nav.connectors":"Connecteurs","admin.nav.dashboard":"Dashboard","admin.nav.documentation":"Documentation","admin.nav.history":"Historique","admin.nav.llmUsage":"Conso LLM","admin.nav.members":"Membres","admin.nav.modules":"Modules","admin.nav.notifications":"Notifications","admin.nav.organisations":"Organisations","admin.nav.platformAdmin":"Admin plateforme","admin.nav.sectionAide":"Aide","admin.nav.sectionOrganisation":"Organisation","admin.nav.sectionPlateforme":"Plateforme","admin.nav.sectionSupport":"Support","admin.nav.settings":"Réglages","admin.nav.sites":"Projets","admin.nav.tasks":"Tickets","admin.nav.threads":"Conversations","admin.nav.users":"Utilisateurs","admin.notFound.backToDashboard":"Retour au dashboard","admin.notFound.code":"404","admin.notFound.message":"Cette page n’existe pas.","admin.notifications.add":"Ajouter une notification","admin.notifications.colActions":"Actions","admin.notifications.colEnabled":"Activé","admin.notifications.colEvents":"Événements","admin.notifications.colName":"Nom","admin.notifications.colScope":"Portée","admin.notifications.colType":"Type","admin.notifications.deleteMessage":"Le canal sera retiré. Les événements ne seront plus envoyés vers cette destination.","admin.notifications.deleteTitle":"Supprimer cette notification ?","admin.notifications.description":"Envoie un message vers un canal externe (e-mail, Telegram, webhook) quand un événement survient. La portée et les événements sont configurables par canal.","admin.notifications.empty":"Aucune notification configurée.","admin.notifications.loadError":"Impossible de charger les notifications.","admin.notifications.title":"Notifications","admin.notifications.toastCreated":"Notification créée","admin.notifications.toastDeleted":"Notification supprimée","admin.notifications.toastUpdated":"Notification mise à jour","admin.orgBillingProfile.subtitle":"Informations légales et de facturation. Utilisées sur les factures (ADR-0013).","admin.orgBillingProfile.title":"Société — {orgName}","admin.orgMembers.addHint":"Si l’email correspond à un compte existant, il est rattaché directement ; sinon une invitation lui est envoyée.","admin.orgMembers.emailLabel":"Email de l’utilisateur","admin.orgMembers.empty":"Aucun membre.","admin.orgMembers.subtitle":"Ajouter, changer le rôle ou retirer un utilisateur de cet espace. Retirer un membre ne supprime pas son compte.","admin.orgMembers.title":"Membres — {orgName}","admin.orgMembers.toastAdded":"Membre ajouté.","admin.orgMembers.toastInvited":"Invitation envoyée à {email}.","admin.orgMembers.toastPasswordUpdated":"Mot de passe mis à jour.","admin.orgMembers.toastRemoved":"Membre retiré.","admin.orgMembers.toastRoleUpdated":"Rôle mis à jour.","admin.pagination.perPage":"Par page :","admin.pagination.range":"{from}–{to} sur {count}","admin.platform.invitations.colActions":"Actions","admin.platform.invitations.colEmail":"Email","admin.platform.invitations.colExpires":"Expire","admin.platform.invitations.colInvitedBy":"Invité par","admin.platform.invitations.colTarget":"Destination","admin.platform.invitations.newOrg":"Nouvelle organisation","admin.profile.activeOrg":"Org active : {name}","admin.profile.agentToken":"Mon token d’agent","admin.profile.agentTokenConnect":"Brancher un agent","admin.profile.agentTokenCreate":"Créer mon token","admin.profile.agentTokenDesc":"Un seul token, personnel : il branche Claude Code, Cursor, Codex ou OpenCode sur tous les projets de l’organisation, depuis chaque poste. Le projet est désigné par le .bip.json du repo.","admin.profile.agentTokenFresh":"Nouvelle clé : la copier dans la config MCP de chaque poste.","admin.profile.agentTokenNone":"Aucun token pour l’instant.","admin.profile.agentTokenRegen":"Régénérer","admin.profile.agentTokenRegenConfirm":"L’ancienne clé cessera de fonctionner immédiatement, sur tous les postes. Il faudra recoller la nouvelle dans chaque config MCP.","admin.profile.agentTokenShow":"Afficher la clé","admin.profile.changePassword":"Changer mon mot de passe","admin.profile.confirmNewPassword":"Confirmer le nouveau mot de passe","admin.profile.currentPassword":"Mot de passe actuel","admin.profile.errFailed":"Échec du changement de mot de passe.","admin.profile.errMismatch":"La confirmation ne correspond pas.","admin.profile.errTooShort":"Le nouveau mot de passe doit faire au moins 8 caractères.","admin.profile.identity":"Identité","admin.profile.langFailed":"Échec du changement de langue.","admin.profile.langUpdated":"Langue mise à jour.","admin.profile.myOrgs":"Mes organisations","admin.profile.newPassword":"Nouveau mot de passe","admin.profile.newPasswordHelper":"Au moins 8 caractères","admin.profile.title":"Mon profil","admin.profile.toastUpdated":"Mot de passe mis à jour.","admin.profile.update":"Mettre à jour","admin.projet.aucunResultat":"Aucun projet ne correspond","admin.projet.entrer":"Ouvrir le projet","admin.projet.introuvable":"Projet introuvable","admin.projet.introuvableAide":"Le projet « {id} » n’existe pas dans cette organisation, ou il a été supprimé.","admin.projet.porteeTous":"tous les projets","admin.projet.recents":"Récents","admin.projet.rechercher":"Rechercher un projet","admin.projet.reglages":"Réglages du projet","admin.projet.retourGlobal":"Tous les projets","admin.projet.selecteur":"Projet","admin.projet.tous":"Tous les projets","admin.projet.tousLesProjets":"Projets","admin.reglages.notifEmailOff":"Le courriel est éteint par la plateforme : le choix est gardé, mais rien ne part pour l’instant.","admin.reglages.notifHelp":"Recevoir par courriel, à {email}, ce qui se passe dans cette organisation. Rien n’est coché au départ.","admin.reglages.notifSaved":"Notifications enregistrées","admin.reglages.notifScope":"Projets","admin.reglages.notifScopeAll":"Tous les projets","admin.reglages.notifScopeSome":"Seulement certains projets","admin.reglages.notifTaskCreated":"Un nouveau ticket arrive","admin.reglages.notifTaskStatus":"Un ticket change de statut (sauf quand c’est moi)","admin.reglages.notifThreadEscalated":"Une conversation attend une réponse humaine","admin.reglages.notifTitle":"Mes notifications","admin.reglages.orgId":"Identifiant","admin.reglages.orgIdentity":"Organisation","admin.reglages.orgManagePlan":"Gérer l’abonnement →","admin.reglages.orgName":"Nom de l’organisation","admin.reglages.orgNameReadOnly":"Seuls le propriétaire et les administrateurs peuvent le changer.","admin.reglages.orgPlan":"Plan","admin.reglages.orgPlanOwnerOnly":"Le propriétaire gère l’abonnement.","admin.reglages.orgRenamed":"Organisation renommée","admin.reglages.tabCompte":"Compte","admin.reglages.tabNotifications":"Notifications","admin.reglages.tabOrganisation":"Organisation","admin.reglages.tabPreferences":"Préférences","admin.reglages.theme":"Thème","admin.reglages.themeDark":"Sombre","admin.reglages.themeHint":"« Système » suit le réglage de l’ordinateur. Le choix est gardé sur cet appareil.","admin.reglages.themeLight":"Clair","admin.reglages.themeSystem":"Système","admin.reglages.title":"Réglages","admin.report.fields":"Contenu du rapport","admin.report.dates":"Dates","admin.report.fixReport":"Rapport de correction (IA)","admin.report.fixReportHeading":"Rapport de correction","admin.report.context":"Contexte","admin.report.html":"HTML","admin.report.markdown":"Markdown","admin.report.plainText":"Texte brut","admin.report.priority":"Priorité","admin.report.receivedOn":"Reçu le","admin.report.reportHeading":"Rapport — {date}","admin.report.request":"Demande","admin.report.siteHeading_one":"{site} ({n} ticket)","admin.report.siteHeading_other":"{site} ({n} tickets)","admin.report.status":"Statut","admin.report.title_one":"Rapport ({n} ticket)","admin.report.title_other":"Rapport ({n} tickets)","admin.report.toastCopied":"Rapport copié dans le presse-papier","admin.report.toastCopiedFormatted":"Rapport copié (formaté pour Teams / mail)","admin.report.toastCopyFailed":"Copie impossible — sélectionner le texte manuellement","admin.report.type":"Type","admin.report.untitled":"(sans titre)","admin.report.updated":"Mis à jour","admin.report.url":"URL","admin.report.visitor":"Visiteur","admin.roles.admin":"Administrateur","admin.roles.member":"Membre","admin.roles.owner":"Propriétaire","admin.roles.superAdmin":"Super admin","admin.search.empty":"Aucun ticket ne correspond à « {q} ».","admin.search.error":"Erreur de recherche : {msg}","admin.search.placeholder":"Rechercher un ticket (id, titre, description)…","admin.search.prompt":"Saisir un terme pour rechercher un ticket.","admin.search.resultCount_one":"{n} résultat","admin.search.resultCount_other":"{n} résultats","admin.search.resultsShown":"({limit} affichés)","admin.search.subtitle":"Tous les projets, tickets actifs et archivés.","admin.search.title":"Recherche","admin.setPassword.field":"Nouveau mot de passe","admin.setPassword.helper":"8 caractères minimum","admin.setPassword.set":"Définir","admin.setPassword.text":"Nouveau mot de passe pour {email}. Il s’applique immédiatement.","admin.setPassword.title":"Définir un mot de passe","admin.settings.language":"Langue","admin.shortcut.invalidHint":"Sélectionner au moins ⌘/Ctrl ou {mod}, et une touche.","admin.shortcut.keyPlaceholder":"Touche","admin.shortcut.shift":"Maj","admin.shortcut.validHint":"⌘ (Mac) comme Ctrl (Windows/Linux) déclenchent le raccourci — les deux marchent.","admin.siteEdit.aiChatOffHint":"Chat humain seul : chaque message part directement à un humain (notification Telegram).","admin.siteEdit.aiChatOnHint":"Un LLM (Gemini + fallback Claude) répond, avec escalade vers un humain.","admin.siteEdit.appearanceSubtitle":"Icône, couleurs, taille et position de la bulle côté visiteur. Inscrites dans le loader du projet — pas de flash visuel.","admin.siteEdit.backToList":"Retour à la liste","admin.siteEdit.bubbleColors":"Couleurs de la bulle","admin.siteEdit.bubbleColorsHint":"Elles colorent la bulle et le panneau du widget, et priment sur l'attribut data-color de la balise. Laissées par défaut, la balise continue de décider.","admin.siteEdit.bubbleColor":"Fond","admin.siteEdit.bubbleIconColor":"Icône","admin.siteEdit.colorDefault":"Par défaut","admin.siteEdit.bubbleIconCustom":"Personnalisée","admin.siteEdit.iconUpload":"Téléverser un SVG","admin.siteEdit.iconUploadHint":"De préférence un SVG d'une seule couleur : il prendra la couleur d'icône. Formes simples (path, circle, rect…), 10 Ko au plus une fois nettoyé.","admin.siteEdit.iconRemove":"Retirer l'icône","admin.siteEdit.iconTooBig":"Fichier trop lourd : 64 Ko au plus.","admin.siteEdit.iconMulticolor":"Icône multicolore : elle garde ses couleurs, la couleur d'icône ne s'y applique pas.","admin.siteEdit.paletteWaiting":"Le site vient de s'ouvrir dans un nouvel onglet : le widget y relève les couleurs. L'onglet peut être fermé dès qu'elles apparaissent ici.","admin.siteEdit.paletteDone":"Couleurs relevées.","admin.siteEdit.paletteTimeout":"Aucune couleur reçue. Vérifier que le widget est bien installé sur cette page, puis relancer le relevé.","admin.siteEdit.paletteEmpty":"Aucune couleur relevée pour l'instant : « Relever à nouveau » ouvre le site et les récupère.","admin.siteEdit.paletteChooseTitle":"Sur quelle adresse relever les couleurs ?","admin.siteEdit.paletteChooseHint":"Le site s'ouvre dans un nouvel onglet ; le widget installé y relève la palette, puis l'onglet peut être fermé.","admin.siteEdit.paletteTitle":"Couleurs repérées sur le site","admin.siteEdit.paletteHint":"Clic : couleur de fond · Maj+clic ou clic droit : couleur d'icône","admin.siteEdit.paletteDetectedAt":"relevées le {date}","admin.siteEdit.paletteRefresh":"Relever à nouveau","admin.siteEdit.paletteRequested":"Le widget relèvera les couleurs du site à sa prochaine visite.","admin.siteEdit.paletteRefreshPending":"Nouveau relevé demandé : il aura lieu à la prochaine visite du site.","admin.siteEdit.paletteRole.theme-color":"couleur de thème","admin.siteEdit.paletteRole.css-var":"variable CSS","admin.siteEdit.paletteRole.background":"fond de page","admin.siteEdit.paletteRole.text":"texte","admin.siteEdit.paletteRole.button":"boutons","admin.siteEdit.paletteRole.link":"liens","admin.siteEdit.paletteRole.header":"en-tête","admin.siteEdit.panelColors":"Couleurs du panneau","admin.siteEdit.panelColorsHint":"Fond, texte et messages de l'assistant dans le panneau du widget. Laissées par défaut, le panneau garde son thème clair d'origine.","admin.siteEdit.bubbleCornerBottomLeft":"Bas-gauche","admin.siteEdit.bubbleCornerBottomRight":"Bas-droite","admin.siteEdit.bubbleCornerTopLeft":"Haut-gauche","admin.siteEdit.bubbleCornerTopRight":"Haut-droite","admin.siteEdit.bubbleDraggable":"Bulle déplaçable (drag & drop)","admin.siteEdit.bubbleDraggableHint":"Le visiteur peut repositionner la bulle ; sa position est mémorisée dans son navigateur. Le drag vit dans le loader (actif dès le page-load), sans alourdir le bundle.","admin.siteEdit.bubbleIconBug":"Insecte","admin.siteEdit.bubbleIconChat":"Discussion","admin.siteEdit.bubbleIconHelp":"Aide ( ? )","admin.siteEdit.bubbleOffsetX":"Décalage horizontal","admin.siteEdit.bubbleOffsetY":"Décalage vertical","admin.siteEdit.bubbleMode":"Emplacement du bouton","admin.siteEdit.bubbleModeHint":"Bulle flottante dans un coin de l’écran, bulle posée dans un élément de la page (une barre d’outils, par exemple), ou aucun bouton : le site ouvre alors le widget par son propre bouton.","admin.siteEdit.bubbleModeDoc":"Voir la doc","admin.siteEdit.bubbleModeFloating":"Flottant","admin.siteEdit.bubbleModeEmbedded":"Intégré dans la page","admin.siteEdit.bubbleModeManual":"Aucun — bouton du site","admin.siteEdit.bubbleSelector":"Sélecteur CSS de l’élément","admin.siteEdit.bubbleSelectorHint":"Exemple : #bipwire-bouton ou .header-actions. La bulle s’y ajoute, même si l’élément apparaît après le chargement ou est redessiné. Élément absent : aucun bouton, un avertissement dans la console du navigateur.","admin.siteEdit.bubbleSelectorInvalid":"Sélecteur CSS requis et valide (200 caractères au plus).","admin.siteEdit.bubbleManualHint":"À poser dans la page, en plus de la balise <script> du widget :","admin.siteEdit.panelPosition":"Où s’ouvre le panneau","admin.siteEdit.panelPositionHint":"Coin de l’écran où s’ouvre le panneau, et ses marges depuis les bords. Sur mobile, le panneau occupe tout l’écran.","admin.siteEdit.bubblePosition":"Position de la bulle","admin.siteEdit.bubblePositionHint":"Coin d'ancrage et décalages depuis les bords de l'écran du visiteur. Le panneau et les notifications suivent le même coin. Si la bulle est déplaçable, cette position n'est que le point de départ.","admin.siteEdit.bubbleSize":"Taille de la bulle","admin.siteEdit.bubbleSizeLarge":"Grande","admin.siteEdit.bubbleSizeMedium":"Moyenne","admin.siteEdit.bubbleSizeSmall":"Petite","admin.siteEdit.capture":"Capturer","admin.siteEdit.captureBodies":"Contenu des requêtes (bodies)","admin.siteEdit.captureBodiesHintMid":"ou","admin.siteEdit.captureBodiesHintPost":"activé.","admin.siteEdit.captureBodiesHintPre":"Inclut request / response bodies (tronqués, tokens masqués). ⚠️ peut contenir des PII. Nécessite","admin.siteEdit.captureConsole":"Console","admin.siteEdit.captureConsoleHintPost":"+ erreurs JS + violations CSP.","admin.siteEdit.captureConsoleHintPre":"Tous les","admin.siteEdit.captureFetchHint":"Méthode + URL + status + durée. Désactiver si le site n’utilise que XHR.","admin.siteEdit.capturePageImage":"Capturer une image de la page","admin.siteEdit.capturePageImageHint":"Au moment où le visiteur envoie son rapport, le widget peut prendre un screenshot pleine page (SnapDOM).","admin.siteEdit.captureShortcut":"Raccourci clavier de capture","admin.siteEdit.captureShortcutHint":"Quand activé, ce raccourci capture le viewport, ouvre l’éditeur d’annotation, puis (à la validation) le formulaire de signalement avec la capture dedans.","admin.siteEdit.captureStorage":"Valeurs de storage","admin.siteEdit.captureStorageHint":"Capture le contenu de localStorage / sessionStorage (valeurs tronquées à 2 Ko). ⚠️ peut contenir des tokens / données perso. Sans ça, seuls les noms de clés sont visibles.","admin.siteEdit.captureXhr":"XMLHttpRequest","admin.siteEdit.captureXhrHint":"Pour les sites legacy / libs qui n’ont pas migré vers fetch.","admin.siteEdit.chatSubtitle":"Bulle de conversation. Activé seul = messages directs à un humain ; avec « chat IA », un LLM répond d’abord et escalade si besoin.","admin.siteEdit.clientLinkHelperFooter":"Si une valeur manque, le lien n’apparaît pas.","admin.siteEdit.clientLinkHelperId":"(l’id transmis par identify)","admin.siteEdit.clientLinkHelperLabel":"Placeholders disponibles","admin.siteEdit.clientLinkHelperVisitorId":"(identifiant interne BipWire du visiteur).","admin.siteEdit.clientLinkSubtitle":"Savoir qui écrit : le site transmet l’utilisateur connecté, et chaque ticket mène à sa fiche dans le back-office.","admin.siteEdit.createSite":"Créer le projet","admin.siteEdit.defaultSystemPrompt":`Tu es l’assistant de {site}.

Mission : répondre aux questions des visiteurs sur le produit, son fonctionnement, les tarifs et l’accompagnement proposé. Ton chaleureux, direct, tutoiement. Réponses courtes (2-4 phrases max sauf demande explicite).

Escape hatches — passe IMMÉDIATEMENT la main à un humain (appelle l’outil escalate) dès que :
- Tu n’es pas sûr de la réponse — ne devine jamais, n’invente aucun fait sur le produit.
- L’utilisateur parle de paiement, remboursement, facture, ou litige.
- L’utilisateur exprime de la frustration ou demande explicitement à parler à quelqu’un.
- Sujet médical, légal, RH, ou personnel sensible.
- Bug technique non-trivial qui dépasse les FAQ.

Si le visiteur a déjà laissé son email et que tu escalades, dis-lui qu’il sera recontacté dans la journée.`,"admin.siteEdit.defaultSystemPromptSiteFallback":"ce site","admin.siteEdit.displayPagesSubtitle":"Restreindre l’affichage du widget à certaines pages du site.","admin.siteEdit.edit":"Édition","admin.siteEdit.enableAiChat":"Activer le chat IA","admin.siteEdit.fieldId":"ID","admin.siteEdit.fieldName":"Nom","admin.siteEdit.fieldOrigins":"Origins (une URL par ligne)","admin.siteEdit.howToWriteIt":"Comment l’écrire ?","admin.siteEdit.idHelperCreate":"Identifiant court lisible, ex : shotcv. Minuscules, chiffres, tirets. ⚠️ Définitif — non modifiable après création.","admin.siteEdit.idHelperEdit":"Définitif depuis la création — non modifiable.","admin.siteEdit.identitySubtitle":"L’identifiant et le nom du projet, et les origines autorisées par le CORS.","admin.siteEdit.insertDefaultTemplate":"Insérer le template par défaut","admin.siteEdit.knowledgeDocCount":"{count} / {max} caractères","admin.siteEdit.knowledgeDocHelper":"Injectée dans le system prompt à chaque chat — vise du Markdown dense (5-10 KB).","admin.siteEdit.knowledgeDocPlaceholder":"FAQ, prix, features, limites, contact support…","admin.siteEdit.loadError":"Impossible de charger le projet : {msg}","admin.siteEdit.newSite":"Nouveau projet","admin.siteEdit.originsErrorHelper":"Chaque ligne doit être une URL absolue (https://… ou http://localhost…).","admin.siteEdit.originsHelper":"Une URL par ligne. Toutes les origines listées sont autorisées par le CORS.","admin.siteEdit.pathPatternsErrorHelper":"Chaque ligne doit commencer par /, !/ ou être *.","admin.siteEdit.pathPatternsHelper":"Exemple : « /hub » active sur /hub et tous ses sous-chemins.","admin.siteEdit.pathPatternsLabel":"Patterns de path","admin.siteEdit.patternExampleAllButAdmin":"partout sauf /admin et ses sous-chemins","admin.siteEdit.patternExampleAllButHome":"partout sauf la page d'accueil (« / » ne vise que / exact)","admin.siteEdit.patternExampleSectionButOne":"sur /app et ses sous-chemins, sauf /app/internal","admin.siteEdit.patternExclude":"exclut","admin.siteEdit.patternExclusionExamples":"Exemples d'exclusion","admin.siteEdit.patternExclusionRule":"Une exclusion l'emporte toujours ; avec des exclusions seules, le widget s'affiche sur toutes les autres pages.","admin.siteEdit.patternMatchAll":"match tout","admin.siteEdit.patternMidWildcard":"wildcard au milieu","admin.siteEdit.patternOneSegment":"un segment","admin.siteEdit.patternRecursive":"récursif","admin.siteEdit.patternSmartPrefix":"smart prefix (match /hub, /hub/x, …)","admin.siteEdit.patternsHeading":"Patterns acceptés","admin.siteEdit.patternsHeadingHint":"(un par ligne) — vide = actif partout :","admin.siteEdit.referenceDoc":"Doc de référence (optionnel)","admin.siteEdit.reportSubtitle":"Activer le bouton de signalement dans le widget et régler ce que le widget capture côté visiteur. Plus on capture, plus le rapport est riche — mais aussi plus le loader pèse.","admin.siteEdit.screenshotAlways":"Toujours","admin.siteEdit.screenshotAlwaysHint":"Capture systématique, même si le visiteur en a mis","admin.siteEdit.screenshotIfMissing":"Si pas fournie","admin.siteEdit.screenshotIfMissingHint":"Capture uniquement si le visiteur n’en a pas mise","admin.siteEdit.screenshotNever":"Jamais","admin.siteEdit.screenshotNeverHint":"Pas de capture auto — le visiteur fournit ou rien","admin.siteEdit.sectionActivation":"Activation","admin.siteEdit.sectionAppearance":"Apparence","admin.siteEdit.sectionChat":"Chat","admin.siteEdit.sectionClientLink":"Lien fiche client","admin.siteEdit.sectionDisplayPages":"Pages d’affichage","admin.siteEdit.sectionIdentity":"Identité","admin.siteEdit.sectionReport":"Signaler un bug / fonctionnalité","admin.siteEdit.showClientLink":"Afficher le lien sur les tickets et les conversations","admin.siteEdit.showClientLinkHint":"Désactivé = template stocké mais lien masqué dans l’admin.","admin.siteEdit.systemPrompt":"System prompt","admin.siteEdit.systemPromptHelper":"Contexte donné au LLM. Inclut les escape hatches : conditions sous lesquelles passer la main à un humain.","admin.siteEdit.thanksHelper":"Message affiché au visiteur juste après l’envoi de son ticket. Vide = texte par défaut.","admin.siteEdit.thanksMessage":"Message de remerciement (optionnel)","admin.siteEdit.thanksPlaceholder":"Merci, on regarde ça !","admin.siteEdit.toastUpdated":"Projet mis à jour","admin.siteEdit.urlTemplate":"Template d’URL","admin.siteEdit.visitorUpload":"Upload de fichier par le visiteur","admin.siteEdit.visitorUploadHint":"Affiche un bouton « Joindre un fichier » à côté de la capture, dans le formulaire. Le visiteur joint autant de fichiers qu’il veut (images, PDF, logs, exports…) ; ils partent avec les screenshots.","admin.siteEdit.welcomeHelper":"Premier message affiché dans la bulle. Vide = texte par défaut adapté au mode.","admin.siteEdit.welcomeMessage":"Message d’accueil (optionnel)","admin.siteEdit.welcomePlaceholderAi":"Bonjour 👋 Une question ? Réponse en quelques secondes.","admin.siteEdit.welcomePlaceholderHuman":"Bonjour 👋 Un message ? Réponse au plus vite.","admin.siteEdit.widgetActive":"Widget actif","admin.siteEdit.widgetDisabled":"Widget désactivé","admin.sites.activationWizard":"Assistant d’activation","admin.sites.aiChatEnabled":"Chat IA activé","admin.sites.captureRequestBodiesEnabled":"Capture des bodies HTTP activée","admin.sites.colActions":"Actions","admin.sites.colCreatedAt":"Créé le","admin.sites.colFeatures":"Fonctionnalités","admin.sites.colId":"ID","admin.sites.colName":"Nom","admin.sites.colOrigin":"Origin","admin.sites.colPublicKey":"Clé publique","admin.sites.deleteMessage":"Cela supprimera aussi toutes les conversations et tous les tickets associés.","admin.sites.deleteTitle":"Supprimer ce projet ?","admin.sites.empty":"Aucun projet pour le moment.","admin.sites.externalUserLinkEnabled":"Lien client externe activé","admin.sites.loadError":"Impossible de charger les projets.","admin.sites.newSite":"Nouveau projet","admin.sites.rebuildFailed":"Reconstruction échouée : {error}","admin.sites.rebuildScripts":"Reconstruire les scripts","admin.sites.rebuildScriptsTooltip":"Relancer `bun run build` côté widget (b.js / panel / bug / loader) et resynchroniser tous les loaders par projet. À déclencher après une modification du widget en prod.","admin.sites.rebuildSuccess_one":"Scripts reconstruits en {duration}s — {loaders} loader resync, {size} KB au total","admin.sites.rebuildSuccess_other":"Scripts reconstruits en {duration}s — {loaders} loaders resync, {size} KB au total","admin.sites.rebuilding":"Reconstruction en cours…","admin.sites.regenKey":"Régénérer la clé","admin.sites.regenKeyMessage":"L’ancienne arrêtera de fonctionner immédiatement. Tous les embeds en cours resteront cassés tant que la nouvelle clé n’est pas posée.","admin.sites.regenKeyTitle":"Régénérer la clé ?","admin.sites.regenerating":"Régénération…","admin.sites.taskReportEnabled":"Signalement de tickets activé","admin.sites.title":"Projets","admin.sites.toastDeleted":"Projet supprimé","admin.sites.toastNewKey":"Nouvelle clé générée","admin.sites.widgetDisabled":"Widget désactivé : aucune feature ne tourne sur ce projet","admin.task.typeBug":"Bug","admin.task.typeFeature":"Fonctionnalité","admin.taskDetail.actionNote":"Note d’action (statut « à tester »)","admin.taskDetail.attachmentPurged":"Fichier purgé le {date} — la garde du plan est passée","admin.taskDetail.attachments":"Pièces jointes ({n})","admin.taskDetail.backToTasks":"Retour aux tickets","admin.taskDetail.bipVisitorId":"Visitor ID BipWire","admin.taskDetail.captureAlt":"Capture {idx}","admin.taskDetail.captureTooltip":"Capture {idx} — {size} — cliquer pour agrandir","admin.taskDetail.captures":"Captures ({n})","admin.taskDetail.clientId":"ID Client","admin.taskDetail.contextHint":"Contexte inféré par le LLM depuis la description et l’URL","admin.taskDetail.ctxUrl":"URL","admin.taskDetail.deleteAttachment":"Supprimer ce fichier","admin.taskDetail.deleteCapture":"Supprimer cette capture","admin.taskDetail.deleteTaskMessage":"Le ticket, ses captures, fichiers joints et notes sont supprimés définitivement (fichiers disque inclus). Irréversible.","admin.taskDetail.deleteTaskTitle":"Supprimer {displayId} ?","admin.taskDetail.descEditHint":"Double-cliquer (ou « e ») pour éditer","admin.taskDetail.description":"Description","admin.taskDetail.displayIdHint":"ID humain — utilisable dans l’URL et pour référencer le ticket","admin.taskDetail.download":"Télécharger","admin.taskDetail.editDescription":"Modifier la description (E)","admin.taskDetail.editShortcutsHint":"⌘↵ enregistrer · Échap annuler","admin.taskDetail.email":"Email","admin.taskDetail.language":"Langue","admin.taskDetail.languagePrefs":"(préférences : {list})","admin.taskDetail.linkedIssues":"Envois des connecteurs","admin.taskDetail.linkDeletedConnector":"Connecteur supprimé","admin.taskDetail.linkFailed":"échec : {error}","admin.taskDetail.linkPartial":"fichiers manquants","admin.taskDetail.linkSent":"envoyé","admin.taskDetail.linkedThread":"Conversation liée","admin.taskDetail.localStorage_one":"localStorage ({n} clé)","admin.taskDetail.localStorage_other":"localStorage ({n} clés)","admin.taskDetail.nextToTest":"Suivant à tester","admin.taskDetail.nextToTestPending":"Suivant à tester…","admin.taskDetail.noMoreToTest":"Plus de bug à tester","admin.taskDetail.noTitle":"(pas de titre)","admin.taskDetail.notFound":"Ticket introuvable","admin.taskDetail.openConversation":"Ouvrir la conversation →","admin.taskDetail.pageContext":"Contexte page (au moment du bug)","admin.taskDetail.pageTitle":"Titre page","admin.taskDetail.priorityValue":"Priorité · {label}","admin.taskDetail.received":"Reçu {date}","admin.taskDetail.referrer":"Referrer","admin.taskDetail.regenTitleHint":"Régénérer titre + contexte via le LLM (depuis description + URL)","admin.taskDetail.removeAttMessage":"Le fichier sur disque est unlink, le row DB supprimé. Irréversible. Les autres pièces jointes restent.","admin.taskDetail.removeAttTitle":"Supprimer ce fichier joint ?","admin.taskDetail.removeShotMessage":"Le fichier sur disque est unlink, le row DB supprimé. Irréversible. Les autres captures du ticket restent.","admin.taskDetail.removeShotTitle":"Supprimer cette capture ?","admin.taskDetail.saveError":"Échec de l’enregistrement","admin.taskDetail.sessionStorage_one":"sessionStorage ({n} clé)","admin.taskDetail.sessionStorage_other":"sessionStorage ({n} clés)","admin.taskDetail.storageFormatted":"Formaté","admin.taskDetail.storageRaw":"Brut","admin.taskDetail.storageTitle":"Stockage navigateur (local / session)","admin.taskDetail.storageValuesOff":"Valeurs non capturées. Activer « Valeurs de storage » dans la config du projet pour voir le contenu des clés.","admin.taskDetail.timelineTitle":"Timeline (console + fetch/XHR — tous les événements)","admin.taskDetail.timelineUncompressed":" — non compressé","admin.taskDetail.toFix":"À corriger","admin.taskDetail.toFixDialog.confirm":"Renvoyer en cours","admin.taskDetail.toFixDialog.confirmWithNote":"Commenter et renvoyer en cours","admin.taskDetail.toFixDialog.help":"Le ticket repasse « en cours ». Le commentaire est ajouté aux notes, là où celui qui le reprend le lira.","admin.taskDetail.toFixDialog.placeholder":"Ce qui ne va pas encore… (facultatif — Ctrl/Cmd+Entrée pour valider)","admin.taskDetail.toFixDialog.title":"Pourquoi faut-il corriger ?","admin.taskDetail.total":"Total : {size}","admin.taskDetail.updateError":"Échec de la mise à jour","admin.taskDetail.urlAtBug":"URL au moment du bug","admin.taskDetail.userAgent":"User-Agent","admin.taskDetail.viewPage":"Voir la page","admin.taskDetail.viewProfile":"Voir la fiche","admin.taskDetail.viewport":"Viewport","admin.taskDetail.viewportValue":"{width} × {height} ({dpr}× DPR)","admin.taskDetail.visitorAnonymous":"Visiteur anonyme","admin.taskNotes.addError":"Échec — réessaie","admin.taskNotes.deleteMessage":"Cette action est irréversible.","admin.taskNotes.deleteNote":"Supprimer la note","admin.taskNotes.deleteTitle":"Supprimer cette note ?","admin.taskNotes.empty":"Aucune note pour l’instant. Ajouter la première ci-dessous.","admin.taskNotes.header":"Notes & commentaires ({n})","admin.taskNotes.headerEmpty":"Notes & commentaires","admin.taskNotes.loadError":"Erreur de chargement des notes : {msg}","admin.taskNotes.placeholder":"Ajouter une note (Ctrl/Cmd+Entrée pour envoyer)…","admin.tasks.archivableHint":"Seuls les tickets résolus/abandonnés sont archivables","admin.tasks.archiveResolved":"Archiver les résolus ({n})","admin.tasks.archiveResolvedMessage":"Les tickets résolus seront déplacés dans les archives. Réversible : ils se désarchivent depuis la vue « Archivés ».","admin.tasks.archiveResolvedTitle_one":"Archiver {n} ticket résolu ?","admin.tasks.archiveResolvedTitle_other":"Archiver {n} tickets résolus ?","admin.tasks.bulkDeleteMessage":"Les DB rows ET les fichiers screenshots seront supprimés. Irréversible.","admin.tasks.bulkDeleteTitle_one":"Supprimer {n} ticket ?","admin.tasks.bulkDeleteTitle_other":"Supprimer {n} tickets ?","admin.tasks.changePriority":"Changer la priorité","admin.tasks.colActions":"Actions","admin.tasks.colId":"ID","admin.tasks.colPriority":"Priorité","admin.tasks.colReceived":"Reçu","admin.tasks.colSite":"Projet","admin.tasks.colStatus":"Statut","admin.tasks.colTitle":"Titre","admin.tasks.colType":"Type","admin.tasks.colVisitor":"Visiteur","admin.tasks.copyIds":"Copier les IDs","admin.tasks.createBug":"Créer un ticket","admin.tasks.deleteMessage":"La DB row ET le fichier screenshot seront supprimés. Irréversible.","admin.tasks.deleteTitle":"Supprimer ce ticket ?","admin.tasks.deselect":"Désélectionner","admin.tasks.emptyArchived":"Aucun ticket archivé.","admin.tasks.emptyFiltered":"Aucun ticket ne correspond aux filtres.","admin.tasks.filterPriority":"Priorité","admin.tasks.filterPriorityAll":"Toutes priorités","admin.tasks.filterSite":"Projet","admin.tasks.filterSiteAll":"Tous les projets","admin.tasks.filterStatus":"Statut","admin.tasks.filterType":"Type","admin.tasks.filterTypeAll":"Tous types","admin.tasks.filters":"Filtres","admin.tasks.kanbanEmptyColumn":"Glisser une carte ici","admin.tasks.loadError":"Erreur de chargement : {msg}","admin.tasks.markResolved":"Marquer résolu","admin.tasks.otherStatuses":"Autres statuts","admin.tasks.priority.critical":"Critique","admin.tasks.priority.high":"Haute","admin.tasks.priority.low":"Faible","admin.tasks.priority.medium":"Moyenne","admin.tasks.priorityMenu":"Priorité ▾","admin.tasks.report":"Rapport","admin.tasks.resetFilters":"Réinitialiser les filtres","admin.tasks.searchPlaceholder":"Rechercher (id, titre, description)…","admin.tasks.selectAllPage":"Tout sélectionner sur la page","admin.tasks.sortAsc":"Tri croissant","admin.tasks.sortBy":"Trier par","admin.tasks.sortDesc":"Tri décroissant","admin.tasks.status.in_progress":"En cours","admin.tasks.status.new":"Nouveau","admin.tasks.status.queued":"En file","admin.tasks.status.resolved":"Résolu","admin.tasks.status.to_test":"À tester","admin.tasks.status.wontfix":"Abandonné","admin.tasks.statusMenu":"Statut ▾","admin.tasks.subtitle_one":"{n} ticket","admin.tasks.subtitle_other":"{n} tickets","admin.tasks.title":"Tickets","admin.tasks.titleActive":"Tickets en cours","admin.tasks.titleArchived":"Tickets archivés","admin.tasks.type.bug":"Bug","admin.tasks.type.feature":"Fonctionnalité","admin.tasks.locked.label":"Ticket verrouillé","admin.tasks.locked.hint":"La limite de tickets ouverts du plan était atteinte quand ce ticket est arrivé : il est gardé, mais son contenu ne se lit qu’après passage à un plan supérieur.","admin.tasks.locked.banner":"{count} ticket(s) verrouillé(s) attendent : le plan s’arrête à {total} tickets ouverts. Passer au Pro pour les lire.","admin.tasks.locked.near":"{open} / {total} tickets ouverts : au-delà, les nouveaux tickets arriveront verrouillés. Clôture ceux qui sont traités, ou passe au Pro.","admin.tasks.locked.cta":"Voir les plans","admin.tasks.locked.receivedAt":"Reçu le {date}","admin.tasks.locked.page":"Page : {url}","admin.billing.ticketsTitle":"Tickets ouverts","admin.billing.ticketsGauge":"{used} / {total} tickets ouverts","admin.billing.ticketsUnlimited":"{used} tickets ouverts — illimités sur ce plan","admin.billing.ticketsLocked":"{count} ticket(s) verrouillé(s) attendent. Passer au Pro pour les lire : ils s’ouvriront tous.","admin.tasks.viewKanban":"Vue kanban","admin.tasks.viewList":"Vue liste","admin.threadDetail.backToConversations":"Retour aux conversations","admin.threadDetail.charCount":"{len} / {max}","admin.threadDetail.gdpr":"RGPD","admin.threadDetail.gdprBody":"Droit à l’effacement RGPD : {parts}{sites} vont être supprimés (DB + disque).{emails} Irréversible.","admin.threadDetail.gdprComputing":"Calcul de l’étendue…","admin.threadDetail.gdprConfirmLabel":"Effacer définitivement","admin.threadDetail.gdprConfirmTitle":"Effacer toutes les données de ce visiteur ?","admin.threadDetail.gdprEmails":" Emails associés : {emails}.","admin.threadDetail.gdprNoData":"Aucune donnée associée à ce visiteur.","admin.threadDetail.gdprNoPreview":"Aucun aperçu disponible.","admin.threadDetail.gdprPartConversations_one":"{n} conversation","admin.threadDetail.gdprPartConversations_other":"{n} conversations","admin.threadDetail.gdprPartMessages_one":"{n} message","admin.threadDetail.gdprPartMessages_other":"{n} messages","admin.threadDetail.gdprPartScreenshots_one":"{n} capture","admin.threadDetail.gdprPartScreenshots_other":"{n} captures","admin.threadDetail.gdprPartTasks_one":"{n} ticket","admin.threadDetail.gdprPartTasks_other":"{n} tickets","admin.threadDetail.gdprSites":" sur {sites}","admin.threadDetail.gdprTitle":"Droit à l’effacement RGPD — supprime toutes les données du visiteur sur tous les projets","admin.threadDetail.noMessages":"Aucun message.","admin.threadDetail.notFoundMessage":"Cette conversation n’existe pas ou plus.","admin.threadDetail.notFoundTitle":"Conversation introuvable","admin.threadDetail.reopenToReply":"Rouvrir pour répondre.","admin.threadDetail.replyPlaceholder":"Écrire une réponse…","admin.threadDetail.replyPlaceholderResolved":"Conversation résolue","admin.threadDetail.requestCapture":"Demander capture","admin.threadDetail.requestCaptureTitle":"Envoyer au visiteur une demande de capture de la page","admin.threadDetail.resolved":"Résolu","admin.threadDetail.sendError":"Envoi impossible.","admin.threadDetail.sidebarConversation":"Conversation","admin.threadDetail.sidebarCreated":"Créée","admin.threadDetail.sidebarLastActivity":"Dernière activité","admin.threadDetail.sidebarMessages":"Messages","admin.threadDetail.sidebarThreadId":"ID conversation","admin.threadDetail.siteUnknown":"Projet inconnu","admin.threadDetail.toastArchived":"Conversation archivée","admin.threadDetail.toastCaptureRequested":"Demande de capture envoyée au visiteur","admin.threadDetail.toastPurged":"Visiteur purgé : {threads} conversations, {messages} messages, {tasks} tickets.","admin.threadDetail.toastRequestError":"Erreur lors de la demande.","admin.threadDetail.toastResolved":"Conversation marquée comme résolue","admin.threadDetail.toastUnarchived":"Conversation désarchivée","admin.threadDetail.tooLong":"Trop long ({len} / {max})","admin.threadDetail.visitorFallback":"Visiteur anonyme","admin.threads.archivableHint":"Seules les conversations résolues sont archivables","admin.threads.bulkDeleteMessage":"Les conversations sélectionnées et tous leurs messages seront supprimés. Irréversible.","admin.threads.bulkDeleteTitle_one":"Supprimer {n} conversation ?","admin.threads.bulkDeleteTitle_other":"Supprimer {n} conversations ?","admin.threads.clearSelection":"Annuler la sélection","admin.threads.colActions":"Actions","admin.threads.colMsg":"Msg","admin.threads.colPreview":"Aperçu","admin.threads.colSite":"Projet","admin.threads.colStatus":"Statut","admin.threads.colUpdated":"Mis à jour","admin.threads.colVisitor":"Visiteur","admin.threads.deleteMessage":"La conversation et tous ses messages seront supprimés. Irréversible.","admin.threads.deleteTitle":"Supprimer cette conversation ?","admin.threads.empty":"Aucune conversation pour le moment.","admin.threads.emptyArchived":"Aucune conversation archivée.","admin.threads.filterSite":"Projet","admin.threads.filterStatus":"Statut","admin.threads.filterStatusAi":"IA","admin.threads.filterStatusAll":"Tous","admin.threads.filterStatusAwaiting":"À traiter","admin.threads.filterStatusResolved":"Résolus","admin.threads.loadError":"Impossible de charger les conversations.","admin.threads.markResolved":"Marquer résolu","admin.threads.selectAll":"Tout sélectionner","admin.threads.selectRow":"Sélectionner {id}","admin.threads.selected_one":"{n} sélectionné","admin.threads.selected_other":"{n} sélectionnés","admin.threads.status.ai_handling":"IA","admin.threads.status.awaiting_human":"À traiter","admin.threads.status.resolved":"Résolu","admin.threads.title":"Conversations","admin.ticketSearch.archived":"archivé","admin.ticketSearch.noTicket":"Aucun ticket","admin.ticketSearch.placeholder":"Rechercher un ticket…","admin.ticketSearch.searching":"Recherche…","admin.userPanel.anonymous":"Visiteur anonyme (pas d’identification host).","admin.userPanel.clientId":"ID Client","admin.userPanel.email":"Email","admin.userPanel.identified":"Utilisateur identifié","admin.userPanel.metadata":"Métadonnées","admin.userPanel.name":"Nom","admin.userPanel.viewProfile":"Voir la fiche","admin.pwa.updateAvailable":"Une nouvelle version de BipWire est disponible.","admin.pwa.reload":"Recharger","admin.pwa.installPrompt":"Installer BipWire sur cet appareil pour l’ouvrir comme une application.","admin.pwa.installIos":"Pour installer BipWire : touche Partager, puis « Sur l’écran d’accueil ».","admin.pwa.install":"Installer","admin.pwa.later":"Plus tard","admin.siteEdit.expandAll":"Tout déplier","admin.siteEdit.collapseAll":"Tout replier","admin.siteEdit.sectionHasError":"À corriger","admin.siteEdit.summaryActive":"Le widget est servi sur le site","admin.siteEdit.summaryInactive":"Le widget n'est plus servi","admin.siteEdit.summaryNoOrigin":"aucune origine","admin.siteEdit.summaryEverywhere":"Partout sur le site","admin.siteEdit.summaryChatAi":"L'IA répond, un humain prend le relais si besoin","admin.siteEdit.summaryChatHuman":"Un humain répond","admin.siteEdit.summaryCapture":"Capture : {list}","admin.siteEdit.summaryBodies":"corps des requêtes","admin.siteEdit.summaryNothing":"rien","admin.siteEdit.summaryNoLink":"Aucun lien","admin.siteEdit.summaryDraggable":"Déplaçable par le visiteur","admin.siteEdit.summaryFixed":"Fixe","admin.siteEdit.summaryPromptMissing":"Invite système à écrire","admin.siteEdit.summaryDefaultText":"Texte par défaut","admin.siteEdit.subIcon":"Icône de la bulle","admin.siteEdit.subColors":"Couleurs","admin.siteEdit.subColorsHint":"Les couleurs se règlent dans le studio d'apparence, et seulement là : couleurs du site, thèmes proposés, lisibilité vérifiée. Cliquer une couleur pour l'ouvrir.","admin.siteEdit.subPlacement":"Taille et position","admin.siteEdit.subBehavior":"Comportement","admin.siteEdit.subChatReply":"Qui répond","admin.siteEdit.subChatPrompt":"Invite système et document de référence","admin.siteEdit.appearanceWidgetFirst":"Le widget n'a pas encore été vu sur le site : l'apparence peut attendre. Enregistrer le projet, intégrer le widget (bouton « Assistant d'activation » en haut), puis revenir ici — « Relever sur le site » ouvrira le site et le widget en relèvera les couleurs. La CSS du site peut aussi être récupérée ou collée dès maintenant, dans le studio.","admin.siteEdit.appearanceCreateFirst":"Commencer par créer le projet et intégrer le widget : l'apparence se règle ensuite, à partir des couleurs du site.","admin.designer.releveWidget":"Relever sur le site","admin.designer.releveWidgetHint":"Ouvrir le site dans un onglet : le widget relève les couleurs affichées, même celles d'une application JavaScript.","admin.designer.releveWidgetNotSeen":"Disponible une fois le widget intégré au site et vu au moins une fois.","admin.designer.releveWidgetNote":"Le widget doit déjà être installé sur le site. Un onglet s'ouvre, le widget relève les couleurs une fois la page affichée, puis l'onglet se referme tout seul.","admin.designer.releveWaiting":"Le site s'est ouvert dans un onglet : le widget relève ses couleurs… L'onglet se refermera tout seul.","admin.designer.releveTimeout":"Aucune couleur reçue du widget. Vérifier que la balise est bien sur la page d'accueil, puis réessayer.","admin.designer.widgetFirst":"Le widget n'est pas encore sur le site : l'intégrer, puis « Relever sur le site » lira ses couleurs telles qu'elles s'affichent.","admin.siteEdit.colorsReset":"Tout remettre par défaut","admin.nav.waitlist":"Liste d’attente","admin.nav.inviteLinks":"Liens d’invitation","admin.legal.notice":"Mentions légales","admin.legal.privacy":"Confidentialité","admin.legal.terms":"Conditions","admin.errors.task_not_found":"Ticket introuvable.","admin.errors.task_locked":"Ticket verrouillé : la limite de tickets ouverts du plan est atteinte. Passer à un plan supérieur pour le lire.","admin.errors.status_reserved":"« En file » est réservé à Claude Code : un ticket y entre par « Lancer Claude Code ».","admin.errors.invalid_url":"Adresse invalide : https:// obligatoire.","admin.errors.invalid_site":"Projet invalide.","admin.siteEdit.clientLinkIntro":"Par défaut, un ticket est anonyme : BipWire ne connaît que le visiteur du widget. Pour savoir QUI l’a envoyé, le site doit identifier l’utilisateur connecté. Son identifiant, son e-mail et son nom arrivent alors avec chaque ticket et chaque conversation, s’affichent sur la fiche du ticket, et servent à composer le lien vers sa fiche client.","admin.siteEdit.clientLinkStep1":"1. Identifier l’utilisateur connecté, sur le site","admin.siteEdit.clientLinkStep1Hint":"Sur les pages où l’utilisateur est connecté, ajouter ce script — avant ou après la balise du widget, l’appel est mis en file d’attente. Remplacer les valeurs d’exemple par celles de l’utilisateur (généralement écrites par le serveur dans la page). En application monopage, l’appeler après la connexion : window.bip.identify({ … }) ; à la déconnexion, window.bip.reset().","admin.siteEdit.clientLinkStep1Static":"Ou, plus simplement, en attributs sur la balise du widget :","admin.siteEdit.clientLinkStep2":"2. Le lien vers sa fiche dans le back-office (optionnel)","admin.siteEdit.clientLinkHelperEmail":"(l’e-mail transmis par identify, ou saisi par le visiteur)","admin.siteEdit.clientLinkPreview":"Exemple de lien produit","admin.siteEdit.clientLinkSnippetComment":"L’utilisateur connecté sur le site : il suivra chaque ticket et chaque conversation.","admin.errors.invalid_url_template":"Le modèle d’URL doit commencer par https:// (ou http://) et ne pas contenir d’espace.","admin.taskDetail.move":"Changer de projet","admin.taskDetail.moveTitle":"Déplacer {displayId} vers un autre projet","admin.taskDetail.moveMessage":"Le ticket garde ses captures, fichiers, notes et son historique, et prend un nouvel identifiant dans le projet choisi. L’ancien identifiant ({displayId}) ne mène plus à lui.","admin.taskDetail.moveTarget":"Projet d’arrivée","admin.taskDetail.moveConfirm":"Déplacer","admin.taskDetail.movePending":"Déplacement…","admin.siteEdit.panelTheme":"Thème du panneau","admin.siteEdit.panelThemeHint":"Clair par défaut. Sombre : toujours les couleurs du mode sombre. Automatique : le widget suit le thème du site et bascule avec lui, sans rechargement (classe « dark » ou attribut « data-theme »… sur la page, sinon la couleur de fond affichée, sinon l'appareil du visiteur). Pour l'essayer : ouvrir le site, puis changer son thème.","admin.siteEdit.panelThemeLight":"Clair","admin.siteEdit.panelThemeDark":"Sombre","admin.siteEdit.panelThemeAuto":"Automatique (suit le site)","admin.siteEdit.openSite":"Ouvrir le site","admin.siteEdit.colorsTitle":"Couleurs","admin.siteEdit.colorsLight":"Couleurs du mode clair","admin.siteEdit.colorsLightHint":"Appliquées quand le site est en clair.","admin.siteEdit.colorsDark":"Couleurs du mode sombre","admin.siteEdit.colorsDarkHint":"Appliquées quand le site est en sombre. Dans le studio, « Récupérer depuis le site » ne retient que les styles sombres de sa CSS (bloc prefers-color-scheme: dark, classe « dark », attribut « data-theme »…). Un rôle laissé par défaut prend le thème sombre standard, et l'accent celui du mode clair.","admin.designer.titleLight":"Studio d'apparence — mode clair","admin.designer.titleDark":"Studio d'apparence — mode sombre","admin.designer.subtitleDark":"Les couleurs du widget quand le site est en sombre. « Récupérer depuis le site » ne retient que les styles sombres de sa CSS.","admin.designer.analyzeNoDark":"Aucun style sombre dans cette CSS : ni bloc prefers-color-scheme: dark, ni règle portée par une classe « dark » ou un attribut « data-theme ». Les thèmes sombres proposés sont génériques.","admin.errors.invalid_action":"Action de livraison inconnue.","admin.siteEdit.unsavedTitle":"Modifications non enregistrées","admin.siteEdit.unsavedMessage":"Des modifications de ce projet n’ont pas été enregistrées. Quitter la page les perd.","admin.siteEdit.unsavedStay":"Rester sur la page","admin.siteEdit.unsavedLeave":"Quitter sans enregistrer","admin.siteEdit.unsavedBadge":"Non enregistré","admin.reglages.notifAutofixStarted":"L'agent automatique prend un ticket","admin.reglages.notifAutofixDelivered":"L'agent automatique livre un ticket « à tester »","admin.reglages.notifAutofixFailed":"L'agent automatique échoue","admin.reglages.notifAutofixSkipped":"L'agent automatique saute un ticket","admin.reglages.notifAutofixDaily":"Rapport du jour de l'agent automatique","admin.events.autofix.started":"Agent automatique : ticket pris","admin.events.autofix.delivered":"Agent automatique : ticket livré","admin.events.autofix.failed":"Agent automatique : échec","admin.events.autofix.skipped":"Agent automatique : ticket sauté","admin.events.autofix.daily":"Agent automatique : rapport du jour"},wi=Object.assign({"../../../../extensions/connecteur-vikunja/i18n/en.json":ci,"../../../../extensions/notification-email/i18n/en.json":ui,"../../../../extensions/notification-telegram/i18n/en.json":mi,"../../../../extensions/notification-webhook/i18n/en.json":pi,"../../../../extensions/remote-claude/i18n/en.json":hi}),Ti=Object.assign({"../../../../extensions/connecteur-vikunja/i18n/fr.json":gi,"../../../../extensions/notification-email/i18n/fr.json":bi,"../../../../extensions/notification-telegram/i18n/fr.json":fi,"../../../../extensions/notification-webhook/i18n/fr.json":vi,"../../../../extensions/remote-claude/i18n/fr.json":ki});function Xe(e,n){return Object.assign({},e,...Object.values(n))}var xi="admin.platform.";function Ze(e){const n={};for(const i of Object.keys(e))i.startsWith(xi)||(n[i]=e[i]);return n}function De(e){const n={en:Ze(Xe(yi,wi)),fr:Ze(Xe(Ci,Ti))};return n[e]||n.en}function et(e,n){return n?e.replace(/\{(\w+)\}/g,(i,a)=>a in n?String(n[a]):i):e}function Si(e,n){return(n==="fr"?e<=1:e===1)?"one":"other"}function Oe(e,n){return(i,a)=>{if(a&&a.n!==void 0&&a.n!==null&&!Number.isNaN(Number(a.n))){const r=e[`${i}_${Si(Number(a.n),n)}`];if(r!==void 0)return et(r,a)}const o=e[i];return o!==void 0?et(o,a):i}}var Mt=Oe(De("en"),"en");function ji(e,n){Mt=Oe(e,n)}function Le(e,n){return Mt(e,n)}var Pi="",Ie=class extends Error{status;code;details;constructor(e,n,i,a={}){super(i),this.name="ApiError",this.status=e,this.code=n,this.details=a}};async function s(e,n={}){const i=new Headers(n.headers);n.body&&!i.has("Content-Type")&&i.set("Content-Type","application/json");let a;try{a=await fetch(`${Pi}${e}`,{...n,headers:i,credentials:"include"})}catch(o){throw new Ie(0,"network_error",o.message)}if(!a.ok){let o={};try{o=await a.json()}catch{}const r=o.error??`http_${a.status}`,l=o.message??Ei(r);throw new Ie(a.status,r,l,o)}if(a.status!==204)try{return await a.json()}catch{return}}function Ei(e){const n=`admin.errors.${e}`,i=Le(n);return i===n?Le("admin.errors.default"):i}var qs={small:44,medium:56,large:68};function Te(e){if(!e)return"";const n=new URLSearchParams;for(const[a,o]of Object.entries(e))o==null||o===""||n.set(a,String(o));const i=n.toString();return i?`?${i}`:""}var D={login(e,n){return s("/api/admin/auth/login",{method:"POST",body:JSON.stringify({email:e,password:n})})},logout(){return s("/api/admin/auth/logout",{method:"POST"})},me(){return s("/api/admin/auth/me",{method:"GET"})},switchOrg(e){return s("/api/admin/auth/switch-org",{method:"POST",body:JSON.stringify({orgId:e})})},changePassword(e,n){return s("/api/admin/auth/change-password",{method:"POST",body:JSON.stringify({currentPassword:e,newPassword:n})})},updateLanguage(e){return s("/api/admin/user/language",{method:"PATCH",body:JSON.stringify({language:e})})},i18n:{get(e){return s(`/api/admin/i18n/${encodeURIComponent(e)}`,{method:"GET"})}},signup(e,n,i,a){return s("/api/admin/account/signup",{method:"POST",body:JSON.stringify(a?{email:e,password:n,orgName:i,invite:a}:{email:e,password:n,orgName:i})})},checkInvite(e){return s(`/api/admin/account/invite/${encodeURIComponent(e)}`,{method:"GET"})},members:{list(){return s("/api/admin/members",{method:"GET"})},invite(e,n){return s("/api/admin/members/invite",{method:"POST",body:JSON.stringify({email:e,role:n})})},revoke(e){return s("/api/admin/members/revoke",{method:"POST",body:JSON.stringify({userId:e})})},setPassword(e,n){return s("/api/admin/members/password",{method:"POST",body:JSON.stringify({userId:e,password:n})})},setRole(e,n){return s("/api/admin/members/role",{method:"POST",body:JSON.stringify({userId:e,role:n})})},invitations(){return s("/api/admin/members/invitations",{method:"GET"})},resendInvitation(e){return s(`/api/admin/members/invitations/${encodeURIComponent(e)}/resend`,{method:"POST"})},revokeInvitation(e){return s(`/api/admin/members/invitations/${encodeURIComponent(e)}`,{method:"DELETE"})}},platform:{orgs(e=!1){return s(`/api/admin/platform/orgs${e?"?includeArchived=true":""}`,{method:"GET"})},createOrg(e){return s("/api/admin/platform/orgs",{method:"POST",body:JSON.stringify({name:e})})},renameOrg(e,n){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/rename`,{method:"POST",body:JSON.stringify({name:n})})},getLimits(e){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/limits`,{method:"GET"})},setLimits(e,n){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/limits`,{method:"PATCH",body:JSON.stringify(n)})},setPlan(e,n){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/plan`,{method:"POST",body:JSON.stringify({tier:n})})},archiveOrg(e){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/archive`,{method:"POST"})},unarchiveOrg(e){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/unarchive`,{method:"POST"})},deleteOrg(e,n){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}`,{method:"DELETE",body:JSON.stringify({confirmName:n})})},getSettings(){return s("/api/admin/platform/settings",{method:"GET"})},setSettings(e){return s("/api/admin/platform/settings",{method:"POST",body:JSON.stringify({signupsEnabled:e})})},getLlm(){return s("/api/admin/platform/llm",{method:"GET"})},getStats(){return s("/api/admin/platform/stats",{method:"GET"})},setStats(e){return s("/api/admin/platform/stats",{method:"PATCH",body:JSON.stringify(e)})},getInboundEmail(){return s("/api/admin/platform/inbound-email",{method:"GET"})},setInboundEmail(e){return s("/api/admin/platform/inbound-email",{method:"PUT",body:JSON.stringify(e)})},extensions:{list(){return s("/api/admin/platform/extensions",{method:"GET"})},set(e,n){return s(`/api/admin/platform/extensions/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(n)})},grant(e,n){return s(`/api/admin/platform/extensions/${encodeURIComponent(e)}/grants`,{method:"PUT",body:JSON.stringify(n)})},revoke(e,n){const i=`?kind=${encodeURIComponent(n.kind)}&id=${encodeURIComponent(n.id)}`;return s(`/api/admin/platform/extensions/${encodeURIComponent(e)}/grants${i}`,{method:"DELETE"})}},inviteLinks:{list(){return s("/api/admin/platform/invite-links",{method:"GET"})},create(e){return s("/api/admin/platform/invite-links",{method:"POST",body:JSON.stringify(e)})},revoke(e){return s(`/api/admin/platform/invite-links/${encodeURIComponent(e)}/revoke`,{method:"POST"})}},waitlist:{list(){return s("/api/admin/platform/waitlist",{method:"GET"})},invite(e){return s(`/api/admin/platform/waitlist/${encodeURIComponent(e)}/invite`,{method:"POST"})},remove(e){return s(`/api/admin/platform/waitlist/${encodeURIComponent(e)}`,{method:"DELETE"})}},setLlm(e,n){return s("/api/admin/platform/llm",{method:"PATCH",body:JSON.stringify({besoin:e,chaine:n})})},users:{list(){return s("/api/admin/platform/users",{method:"GET"})},setPlatformAdmin(e,n){return s(`/api/admin/platform/users/${encodeURIComponent(e)}/platform-admin`,{method:"POST",body:JSON.stringify({value:n})})},remove(e,n){return s(`/api/admin/platform/users/${encodeURIComponent(e)}`,{method:"DELETE",body:JSON.stringify({confirmEmail:n})})}},invitations:{list(){return s("/api/admin/platform/invitations",{method:"GET"})},inviteOrg(e,n){return s("/api/admin/platform/invitations",{method:"POST",body:JSON.stringify({email:e,planTier:n})})},resend(e){return s(`/api/admin/platform/invitations/${encodeURIComponent(e)}/resend`,{method:"POST"})},revoke(e){return s(`/api/admin/platform/invitations/${encodeURIComponent(e)}`,{method:"DELETE"})}},orgMembers:{list(e){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/members`,{method:"GET"})},add(e,n,i){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/members`,{method:"POST",body:JSON.stringify({email:n,role:i})})},setRole(e,n,i){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/members/${encodeURIComponent(n)}/role`,{method:"POST",body:JSON.stringify({role:i})})},remove(e,n){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/members/${encodeURIComponent(n)}`,{method:"DELETE"})},setPassword(e,n,i){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/members/${encodeURIComponent(n)}/password`,{method:"POST",body:JSON.stringify({password:i})})}},orgBillingProfile:{get(e){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/billing-profile`,{method:"GET"})},save(e,n){return s(`/api/admin/platform/orgs/${encodeURIComponent(e)}/billing-profile`,{method:"PUT",body:JSON.stringify(n)})}}},account:{inviteInfo(e){return s(`/api/admin/account/invite?token=${encodeURIComponent(e)}`,{method:"GET"})},acceptInvite(e,n,i){return s("/api/admin/account/accept-invite",{method:"POST",body:JSON.stringify({token:e,password:n,orgName:i})})},forgotPassword(e){return s("/api/admin/account/forgot-password",{method:"POST",body:JSON.stringify({email:e})})},resetPassword(e,n){return s("/api/admin/account/reset-password",{method:"POST",body:JSON.stringify({token:e,password:n})})},verifyEmail(e){return s("/api/admin/account/verify-email",{method:"POST",body:JSON.stringify({token:e})})}},threads:{list(e){return s(`/api/admin/threads${Te(e)}`,{method:"GET"})},get(e){return s(`/api/admin/threads/${encodeURIComponent(e)}`,{method:"GET"})},reply(e,n){return s(`/api/admin/threads/${encodeURIComponent(e)}/reply`,{method:"POST",body:JSON.stringify({content:n})})},liftGuardrails(e){return s(`/api/admin/threads/${encodeURIComponent(e)}/guardrails`,{method:"PATCH",body:JSON.stringify({lifted:!0})})},requestCapture(e){return s(`/api/admin/threads/${encodeURIComponent(e)}/request-capture`,{method:"POST",body:JSON.stringify({})})},setStatus(e,n){return s(`/api/admin/threads/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({status:n})})},setArchived(e,n){return s(`/api/admin/threads/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({archived:n})})},remove(e){return s(`/api/admin/threads/${encodeURIComponent(e)}`,{method:"DELETE"})},bulk(e,n){return s("/api/admin/threads/bulk",{method:"POST",body:JSON.stringify({action:e,ids:n})})}},maintenance:{stats(){return s("/api/admin/maintenance/stats",{method:"GET"})},sessions(){return s("/api/admin/maintenance/sessions",{method:"GET"})},revokeSession(e){return s(`/api/admin/maintenance/sessions/${encodeURIComponent(e)}/revoke`,{method:"POST"})},purgeDeadSessions(){return s("/api/admin/maintenance/sessions/dead",{method:"DELETE"})},purgeTasks(e){return s(`/api/admin/maintenance/tasks?days=${e}`,{method:"DELETE"})},purgeThreads(e){return s(`/api/admin/maintenance/threads?days=${e}`,{method:"DELETE"})},rebuildScripts(){return s("/api/admin/maintenance/rebuild-scripts",{method:"POST"})}},tasks:{list(e){return s(`/api/admin/tasks${Te(e)}`,{method:"GET"})},search(e){return s(`/api/admin/tasks/search${Te({q:e.q,limit:e.limit,siteId:e.siteId})}`,{method:"GET"})},create(e){return s("/api/admin/tasks",{method:"POST",body:JSON.stringify(e)})},addFile(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}/files`,{method:"POST",body:JSON.stringify(n)})},get(e){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"GET"})},setStatus(e,n,i){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(i?.resumeClaude===!1?{status:n,resumeClaude:!1}:{status:n})})},setPriority(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({priority:n})})},setType(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({type:n})})},moveToSite(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({siteId:n})})},setArchived(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({archived:n})})},updateDescription(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({description:n})})},regenerateTitle(e){return s(`/api/admin/tasks/${encodeURIComponent(e)}/regenerate-title`,{method:"POST"})},liftGuardrails(e){return s(`/api/admin/tasks/${encodeURIComponent(e)}/guardrails`,{method:"PATCH",body:JSON.stringify({lifted:!0})})},reanalyzeGuardrails(e){return s(`/api/admin/tasks/${encodeURIComponent(e)}/guardrails/reanalyze`,{method:"POST",body:JSON.stringify({})})},removeScreenshot(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}/screenshot/${n}`,{method:"DELETE"})},removeAttachment(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}/attachment/${n}`,{method:"DELETE"})},remove(e){return s(`/api/admin/tasks/${encodeURIComponent(e)}`,{method:"DELETE"})},removeByStatus(e){return s(`/api/admin/tasks?status=${encodeURIComponent(e)}`,{method:"DELETE"})},notes:{list(e){return s(`/api/admin/tasks/${encodeURIComponent(e)}/notes`,{method:"GET"})},create(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}/notes`,{method:"POST",body:JSON.stringify({body:n})})},remove(e,n){return s(`/api/admin/tasks/${encodeURIComponent(e)}/notes/${encodeURIComponent(n)}`,{method:"DELETE"})}}},sites:{list(){return s("/api/admin/sites",{method:"GET"})},get(e){return s(`/api/admin/sites/${encodeURIComponent(e)}`,{method:"GET"})},create(e){return s("/api/admin/sites",{method:"POST",body:JSON.stringify(e)})},siteCss(e){return s(`/api/admin/sites/${encodeURIComponent(e)}/site-css`,{method:"GET"})},update(e,n){return s(`/api/admin/sites/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(n)})},remove(e){return s(`/api/admin/sites/${encodeURIComponent(e)}`,{method:"DELETE"})},regenerateKey(e){return s(`/api/admin/sites/${encodeURIComponent(e)}/regenerate-key`,{method:"POST"})}},notifications:{list(){return s("/api/admin/notifications",{method:"GET"})},create(e){return s("/api/admin/notifications",{method:"POST",body:JSON.stringify(e)})},update(e,n){return s(`/api/admin/notifications/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(n)})},remove(e){return s(`/api/admin/notifications/${encodeURIComponent(e)}`,{method:"DELETE"})},test(e){return s(`/api/admin/notifications/${encodeURIComponent(e)}/test`,{method:"POST"})}},integrationExtensions:{list(){return s("/api/admin/integrations/extensions",{method:"GET"})}},connectors:{list(){return s("/api/admin/connectors",{method:"GET"})},create(e){return s("/api/admin/connectors",{method:"POST",body:JSON.stringify(e)})},update(e,n){return s(`/api/admin/connectors/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(n)})},remove(e){return s(`/api/admin/connectors/${encodeURIComponent(e)}`,{method:"DELETE"})},test(e){return s(`/api/admin/connectors/${encodeURIComponent(e)}/test`,{method:"POST"})},source(e){return s("/api/admin/connectors/source",{method:"POST",body:JSON.stringify(e)})}},org:{usage(){return s("/api/admin/org/usage",{method:"GET"})},rename(e){return s("/api/admin/org",{method:"PATCH",body:JSON.stringify({name:e})})}},extensionsActives(){return s("/api/admin/extensions",{method:"GET"})},mesNotifications:{get(){return s("/api/admin/me/notifications",{method:"GET"})},put(e){return s("/api/admin/me/notifications",{method:"PUT",body:JSON.stringify(e)})}},billing:{get(){return s("/api/admin/billing",{method:"GET"})},checkout(e){return s("/api/admin/billing/checkout",{method:"POST",body:JSON.stringify({tier:e})})},portal(){return s("/api/admin/billing/portal",{method:"POST"})},getProfile(){return s("/api/admin/billing/profile",{method:"GET"})},saveProfile(e){return s("/api/admin/billing/profile",{method:"PUT",body:JSON.stringify(e)})}},llmUsage:{overview(){return s("/api/admin/llm-usage",{method:"GET"})},detail(e,n=30){const i=new URLSearchParams({days:String(n)});return s(`/api/admin/llm-usage/${encodeURIComponent(e)}?${i.toString()}`,{method:"GET"})}},visitors:{preview(e){return s(`/api/admin/visitors/${encodeURIComponent(e)}`,{method:"GET"})},purge(e){return s(`/api/admin/visitors/${encodeURIComponent(e)}/purge`,{method:"POST",body:JSON.stringify({confirm:!0})})}},llmTokens:{list(){return s("/api/admin/llm-tokens",{method:"GET"})},create(e,n,i,a){return s("/api/admin/llm-tokens",{method:"POST",body:JSON.stringify({label:e,scopes:n,siteIds:i,personal:a?.personal===!0})})},reveal(e){return s(`/api/admin/llm-tokens/${encodeURIComponent(e)}/reveal`,{method:"POST"})},update(e,n){return s(`/api/admin/llm-tokens/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify(n)})},regenerate(e){return s(`/api/admin/llm-tokens/${encodeURIComponent(e)}/regenerate`,{method:"POST"})},revoke(e){return s(`/api/admin/llm-tokens/${encodeURIComponent(e)}/revoke`,{method:"POST"})},remove(e){return s(`/api/admin/llm-tokens/${encodeURIComponent(e)}`,{method:"DELETE"})}}},Ms=["tasks:read","tasks:write","threads:read","sites:read"],Os={"tasks:read":"admin.llmScopes.tasksRead","tasks:write":"admin.llmScopes.tasksWrite","threads:read":"admin.llmScopes.threadsRead","sites:read":"admin.llmScopes.sitesRead"};function Be(){return G({queryKey:["me"],queryFn:()=>D.me(),staleTime:3e4})}var Ot=(0,d.createContext)(void 0);function Ai({children:e}){const[n,i]=(0,d.useState)("loading"),a=(0,d.useCallback)(async()=>{try{await D.me(),i("authed")}catch(h){h instanceof Ie&&h.status,i("anon")}},[]);(0,d.useEffect)(()=>{a()},[a]);const o=(0,d.useCallback)(async(h,p)=>{await D.login(h,p),i("authed")},[]),r=(0,d.useCallback)(async()=>{try{await D.logout()}finally{i("anon")}},[]),l=(0,d.useMemo)(()=>({status:n,login:o,logout:r,refresh:a}),[n,o,r,a]);return(0,t.jsx)(Ot.Provider,{value:l,children:e})}function Bt(){const e=(0,d.useContext)(Ot);if(!e)throw new Error("useAuth must be used within an AuthProvider");return e}function Di(){const{status:e}=Bt(),n=W();return e==="loading"?(0,t.jsx)(C,{sx:{minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center"},children:(0,t.jsx)(At,{})}):e==="anon"?(0,t.jsx)(_,{to:"/login",replace:!0,state:{from:n}}):(0,t.jsx)(Me,{})}function Li(e,n){if(n.kind.startsWith("task.")){e.invalidateQueries({queryKey:["tasks"]}),e.invalidateQueries({queryKey:["task"]});return}if(n.kind.startsWith("thread.")){e.invalidateQueries({queryKey:["threads"]}),e.invalidateQueries({queryKey:["thread"]});return}if(n.kind.startsWith("site.")){e.invalidateQueries({queryKey:["sites"]}),e.invalidateQueries({queryKey:["site",n.id]});return}}function Ii(){const e=ye();return(0,d.useEffect)(()=>{const n=new EventSource("/api/admin/events/stream",{withCredentials:!0});n.addEventListener("admin",a=>{try{const o=JSON.parse(a.data);Li(e,o)}catch{}}),n.addEventListener("ready",()=>{console.debug("[realtime] admin stream connected")});let i=!1;return n.addEventListener("open",()=>{i&&e.invalidateQueries(),i=!0}),()=>{n.close()}},[e]),null}var Ht="Europe/Paris",fe="fr";function _i(e){fe=e}var tt=new Map;function Ri(e){let n=tt.get(e);return n||(n=new Intl.RelativeTimeFormat(e,{numeric:"auto",style:"short"}),tt.set(e,n)),n}var nt=new Map;function qi(e){let n=nt.get(e);return n||(n=new Intl.DateTimeFormat(e,{timeZone:Ht,day:"numeric",month:"short"}),nt.set(e,n)),n}var it=new Map;function Mi(e){let n=it.get(e);return n||(n=new Intl.DateTimeFormat(e,{timeZone:Ht,day:"numeric",month:"short",hour:"2-digit",minute:"2-digit",hourCycle:"h23"}),it.set(e,n)),n}function Bs(e,n=Date.now()){if(!Number.isFinite(e))return"";const i=Math.max(0,Math.floor((n-e)/1e3)),a=Ri(fe);if(i<60)return a.format(0,"second");const o=Math.floor(i/60);if(o<60)return a.format(-o,"minute");const r=Math.floor(o/60);if(r<24)return a.format(-r,"hour");const l=Math.floor(r/24);return l<7?a.format(-l,"day"):qi(fe).format(new Date(e))}function Hs(e){return Number.isFinite(e)?Mi(fe).format(new Date(e)):""}var Ut=(0,d.createContext)(null);function Oi({children:e,initialLang:n="en"}){const[i,a]=(0,d.useState)(n);(0,d.useEffect)(()=>{_i(i)},[i]);const[o,r]=(0,d.useState)(()=>De(n));(0,d.useEffect)(()=>{ji(o,i)},[o,i]);const l=Be().data?.user?.language,h=(0,d.useRef)(!1);(0,d.useEffect)(()=>{h.current||!l||(h.current=!0,a(l),D.i18n.get(l).then(r).catch(()=>{}))},[l]);const p=(0,d.useCallback)(async v=>{await D.updateLanguage(v);try{r(await D.i18n.get(v))}catch{r(De(v))}a(v)},[]),w=(0,d.useMemo)(()=>({lang:i,translations:o,setLang:p}),[i,o,p]);return(0,t.jsx)(Ut.Provider,{value:w,children:e})}var Bi="bipwire-umami",at="umami.disabled";function Hi(){const[e,n]=(document.querySelector(`meta[name="${Bi}"]`)?.content??"").split("|");return e&&n?{url:e,siteId:n}:null}var xe=null;function st(e){return xe||(xe=new Promise(n=>{const i=document.createElement("script");i.defer=!0,i.src=`${e.url}/script.js`,i.dataset.websiteId=e.siteId,i.onload=()=>n(),i.onerror=()=>n(),document.head.appendChild(i)})),xe}function ot(e){try{e?localStorage.setItem(at,"1"):localStorage.removeItem(at)}catch{}}function Ui(e){window.umami?.identify(e.id,{org:e.id,nom:e.name})}var Ni=["/login","/signup","/accept-invite","/forgot-password","/reset-password","/verify-email"];function Wi(){const e=Hi(),{pathname:n}=W(),i=Ni.some(o=>n===o||n.startsWith(`${o}/`)),a=G({queryKey:["me"],queryFn:()=>D.me(),staleTime:3e4,enabled:!!e&&!i,retry:!1});return(0,d.useEffect)(()=>{if(!e)return;if(i){st(e);return}if(!a.data)return;if(a.data.platformAdmin){ot(!0);return}ot(!1);const o=a.data.org;st(e).then(()=>{o&&Ui(o)})},[e?.url,e?.siteId,i,a.data]),null}function R(){const e=(0,d.useContext)(Ut);if(!e)throw new Error("useI18n doit être utilisé à l’intérieur d’un <I18nProvider>");const n=(0,d.useMemo)(()=>Oe(e.translations,e.lang),[e.translations,e.lang]);return{lang:e.lang,t:n,setLang:e.setLang}}var Fi="/p/:siteId",zi=["/tasks","/threads","/search","/llm-usage","/connectors","/notifications","/llm-tokens","/settings"],Nt=e=>e==="/"||zi.some(n=>e===n||e.startsWith(n+"/"));function He(e){return zn({path:Fi,end:!1},e)?.params.siteId??null}function Ue(e,n){const i=`/p/${encodeURIComponent(e)}`;return n==="/"?i:i+n}function Vi(e,n){if(!e)return n;const[i,a]=n.split(/(?=[?#])/,2);return Nt(i)?Ue(e,i)+(a??""):n}function Wt(e,n){const i=He(e);let a=i?e.slice(`/p/${encodeURIComponent(i)}`.length)||"/":e;const o=a.match(/^\/(tasks|threads)\/[^/]+/);return o&&(a="/"+o[1]),!i&&/^\/projects\/[^/]+\/edit$/.test(a)&&(a="/settings"),n?Ue(n,Nt(a)?a:"/"):a==="/settings"?"/projects":a}function X(){const{pathname:e}=W(),n=He(e),i=G({queryKey:["sites"],queryFn:()=>D.sites.list(),enabled:n!==null}),a=n?i.data?.items.find(o=>o.id===n):void 0;return{siteId:n,site:a,chargement:n!==null&&i.isPending,introuvable:n!==null&&i.isSuccess&&!a}}function Ne(){const{pathname:e}=W(),n=He(e);return(0,d.useCallback)(i=>Vi(n,i),[n])}var Ft="bipwire:projets-recents",Gi=3;function zt(){try{const e=JSON.parse(localStorage.getItem(Ft)??"[]");return Array.isArray(e)?e.filter(n=>typeof n=="string"):[]}catch{return[]}}function $i(e){try{const n=[e,...zt().filter(i=>i!==e)].slice(0,Gi);localStorage.setItem(Ft,JSON.stringify(n))}catch{}}function Ki(){const{t:e}=R(),n=W(),{siteId:i,site:a,chargement:o,introuvable:r}=X();if((0,d.useEffect)(()=>{a&&$i(a.id)},[a]),i&&i!==i.toLowerCase()){const l=`/p/${encodeURIComponent(i)}`,h=`/p/${encodeURIComponent(i.toLowerCase())}`;return(0,t.jsx)(_,{to:h+n.pathname.slice(l.length)+n.search+n.hash,replace:!0})}return o?(0,t.jsx)(Dt,{sx:{mx:-3,mt:-3}}):r?(0,t.jsx)(C,{sx:{display:"flex",justifyContent:"center",pt:{xs:4,sm:10}},children:(0,t.jsxs)(jt,{variant:"outlined",sx:{p:{xs:3,sm:5},maxWidth:440,textAlign:"center"},children:[(0,t.jsx)(L,{variant:"h5",component:"h1",sx:{fontWeight:700,mb:1},children:e("admin.projet.introuvable")}),(0,t.jsx)(L,{color:"text.secondary",sx:{mb:3},children:e("admin.projet.introuvableAide",{id:i??""})}),(0,t.jsx)(M,{component:oe,to:"/",variant:"contained",children:e("admin.projet.retourGlobal")})]})}):(0,t.jsx)(Me,{})}function Se({vers:e}){return(0,t.jsx)(_,{to:e,replace:!0})}function Ji(){const{siteId:e}=X();return(0,t.jsx)(_,{to:e?Ue(e,"/"):"/",replace:!0})}var Qi=7;function ve({site:e,taille:n=10}){return(0,t.jsx)(C,{component:"span","aria-hidden":!0,sx:{width:n,height:n,borderRadius:"50%",flexShrink:0,bgcolor:e?.bubbleColor??"primary.main",boxShadow:i=>`inset 0 0 0 1px ${i.palette.divider}`}})}function Yi(){const{t:e}=R(),n=ke(),{pathname:i}=W(),{siteId:a,site:o}=X(),[r,l]=(0,d.useState)(null),[h,p]=(0,d.useState)(""),w=(0,d.useRef)(null),v=G({queryKey:["sites"],queryFn:()=>D.sites.list()}),b=(0,d.useMemo)(()=>[...v.data?.items??[]].sort((k,S)=>k.name.localeCompare(S.name)),[v.data]),y=(0,d.useMemo)(()=>{const k=new Map(b.map(S=>[S.id,S]));return zt().filter(S=>S!==a).map(S=>k.get(S)).filter(S=>!!S)},[b,r,a]);if(b.length===0)return null;const c=b.length>Qi,u=h.trim().toLowerCase(),f=u?b.filter(k=>k.name.toLowerCase().includes(u)||k.id.includes(u)):b,P=()=>{l(null),p("")},E=k=>{P(),k!==a&&n(Wt(i,k))},$=(k,S)=>(0,t.jsxs)(q,{selected:k.id===a,onClick:()=>E(k.id),children:[(0,t.jsx)(z,{children:(0,t.jsx)(ve,{site:k})}),(0,t.jsx)(N,{primary:k.name,secondary:k.name!==k.id?k.id:void 0}),k.id===a&&(0,t.jsx)(ge,{fontSize:"small",sx:{ml:1,color:"primary.main"}})]},S);return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(M,{color:"inherit",variant:"outlined",size:"small",onClick:k=>l(k.currentTarget),"aria-haspopup":"menu","aria-expanded":!!r,"aria-label":e("admin.projet.selecteur"),"data-testid":"selecteur-projet",endIcon:(0,t.jsx)(Un,{}),startIcon:a?(0,t.jsx)(ve,{site:o}):(0,t.jsx)(Ve,{fontSize:"small"}),sx:{mr:{xs:.5,md:1.5},maxWidth:{xs:160,md:240},minWidth:0,flexShrink:0,borderColor:"divider",textTransform:"none",fontWeight:500,"&:hover":{borderColor:"text.primary",bgcolor:"action.hover"},"& .MuiButton-startIcon":{ml:0}},children:(0,t.jsx)(L,{component:"span",noWrap:!0,sx:{fontSize:14,fontWeight:"inherit"},children:a?o?.name??a:e("admin.projet.tous")})}),(0,t.jsxs)(Pt,{anchorEl:r,open:!!r,onClose:P,transitionDuration:150,anchorOrigin:{vertical:"bottom",horizontal:"left"},transformOrigin:{vertical:"top",horizontal:"left"},slotProps:{paper:{sx:{minWidth:260,maxHeight:480}},transition:{onEntered:()=>w.current?.focus()}},autoFocus:!c,children:[c&&(0,t.jsx)(C,{sx:{px:1.5,pt:.5,pb:1},onKeyDown:k=>k.stopPropagation(),children:(0,t.jsx)(he,{inputRef:w,size:"small",fullWidth:!0,placeholder:e("admin.projet.rechercher"),value:h,onChange:k=>p(k.target.value),onKeyDown:k=>{k.key==="Enter"&&f.length===1&&E(f[0].id)}})}),!u&&(0,t.jsxs)(q,{selected:!a,onClick:()=>E(null),children:[(0,t.jsx)(z,{children:(0,t.jsx)(Ve,{fontSize:"small"})}),(0,t.jsx)(N,{primary:e("admin.projet.tous")}),!a&&(0,t.jsx)(ge,{fontSize:"small",sx:{ml:1,color:"primary.main"}})]}),!u&&y.length>0&&(0,t.jsx)(se,{}),!u&&y.length>0&&(0,t.jsx)(Ee,{sx:{lineHeight:"32px",bgcolor:"transparent"},children:e("admin.projet.recents")}),!u&&y.map(k=>$(k,"recent-"+k.id)),(0,t.jsx)(se,{}),(0,t.jsx)(Ee,{sx:{lineHeight:"32px",bgcolor:"transparent"},children:e("admin.projet.tousLesProjets")}),f.map(k=>$(k,k.id)),u&&f.length===0&&(0,t.jsx)(q,{disabled:!0,children:(0,t.jsx)(N,{primary:e("admin.projet.aucunResultat")})})]})]})}function Xi({collapsed:e,onItemClick:n}){const{t:i}=R(),{pathname:a}=W(),{siteId:o,site:r}=X();if(!o)return null;const l=Wt(a,null),h=r?.name??o;return e?(0,t.jsx)(C,{sx:{display:"flex",justifyContent:"center",pt:1.5,pb:.5},children:(0,t.jsx)(Q,{title:`${h} — ${i("admin.projet.retourGlobal")}`,placement:"right",arrow:!0,children:(0,t.jsx)(te,{component:oe,to:l,onClick:n,size:"small","aria-label":i("admin.projet.retourGlobal"),children:(0,t.jsx)(ve,{site:r,taille:14})})})}):(0,t.jsxs)(C,{"data-testid":"en-tete-projet",sx:{px:2.5,pt:2,pb:1.5,borderBottom:"1px solid",borderColor:"divider"},children:[(0,t.jsxs)(Lt,{component:oe,to:l,onClick:n,underline:"hover",color:"text.secondary",sx:{display:"inline-flex",alignItems:"center",gap:.5,fontSize:12,mb:1},children:[(0,t.jsx)(Nn,{sx:{fontSize:14}}),i("admin.projet.retourGlobal")]}),(0,t.jsxs)(C,{sx:{display:"flex",alignItems:"center",gap:1,minWidth:0},children:[(0,t.jsx)(ve,{site:r,taille:12}),(0,t.jsx)(L,{variant:"subtitle1",noWrap:!0,sx:{fontWeight:600,lineHeight:1.3},children:h})]})]})}function Zi(){const{data:e}=G({queryKey:["modules-plateforme"],queryFn:async()=>{const n=await fetch("/api/admin/platform/access",{credentials:"same-origin"});return n.ok?(await n.json()).modules??[]:[]},staleTime:6e4,retry:!1});return e??[]}var We=["image/png","image/jpeg","image/webp"],Vt=["pdf","txt","log","md","json","csv","har","zip","docx","xlsx","pptx","doc","xls","ppt"],ea=[...We,...Vt.map(e=>`.${e}`)].join(","),je=15,ta=5242880,na=10485760;function rt(e){const n=We.includes(e.type),i=e.name.split(".").pop()?.toLowerCase()??"";return!n&&!Vt.includes(i)?"invalid_file_type":e.size>(n?ta:na)?"file_too_large":null}function ia(e){return new Promise((n,i)=>{const a=new FileReader;a.onload=()=>n(String(a.result).replace(/^data:[^,]*,/,"")),a.onerror=()=>i(a.error??new Error("read_error")),a.readAsDataURL(e)})}function aa({open:e,onClose:n,defaultSiteId:i,defaultType:a}){const o=ke(),r=ye(),{t:l}=R(),h=X(),p=Ne(),w=i??h.siteId??"",[v,b]=(0,d.useState)(w);(0,d.useEffect)(()=>{e&&w&&b(w)},[e,w]);const[y,c]=(0,d.useState)(a??"bug");(0,d.useEffect)(()=>{e&&c(a??"bug")},[e,a]);const[u,f]=(0,d.useState)(""),[P,E]=(0,d.useState)(""),[$,k]=(0,d.useState)("medium"),[S,K]=(0,d.useState)([]),[re,J]=(0,d.useState)(null),[O,F]=(0,d.useState)(null),j=(0,d.useRef)(null);function Z(g){const A=g.map(rt).find(U=>U!==null),B=g.filter(U=>rt(U)===null),H=[...S,...B];if(H.length>je){J(l("admin.errors.too_many_files")),K(H.slice(0,je));return}J(A?l(`admin.errors.${A}`):null),K(H)}const ne=[{value:"low",label:l("admin.tasks.priority.low")},{value:"medium",label:l("admin.tasks.priority.medium")},{value:"high",label:l("admin.tasks.priority.high")},{value:"critical",label:l("admin.tasks.priority.critical")}],V=G({queryKey:["sites"],queryFn:()=>D.sites.list(),enabled:e}),I=It({mutationFn:async()=>{const g=await D.tasks.create({siteId:v,description:u.trim(),url:P.trim()||void 0,priority:$,type:y}),A=[];for(const B of S)try{await D.tasks.addFile(g.id,{name:B.name,data:await ia(B)})}catch(H){A.push(`${B.name} — ${H.message}`)}return{id:g.id,echecs:A}},onSuccess:g=>{if(r.invalidateQueries({queryKey:["tasks"]}),g.echecs.length>0){F(g);return}ee(g.id)}});function ee(g){de(),n(),o(p(`/tasks/${g}`))}function de(){b(i??""),c(a??"bug"),f(""),E(""),k("medium"),K([]),J(null),F(null),I.reset()}function ie(){if(!I.isPending){if(O)return ee(O.id);de(),n()}}const le=v.length>0&&u.trim().length>0&&!I.isPending&&!O;return(0,t.jsxs)(qn,{open:e,onClose:ie,maxWidth:"sm",fullWidth:!0,children:[(0,t.jsx)(dn,{children:l("admin.createTask.title")}),(0,t.jsxs)(Xt,{children:[I.error&&(0,t.jsx)(Y,{severity:"error",sx:{mb:2},children:I.error.message}),O&&(0,t.jsxs)(Y,{severity:"warning",sx:{mb:2},children:[l("admin.createTask.attachFailed"),(0,t.jsx)(C,{component:"ul",sx:{m:0,pl:2},children:O.echecs.map(g=>(0,t.jsx)("li",{children:g},g))})]}),(0,t.jsxs)(qe,{spacing:2,sx:{mt:1},children:[(0,t.jsxs)(hn,{value:y,exclusive:!0,onChange:(g,A)=>A&&c(A),size:"small",fullWidth:!0,color:"primary",children:[(0,t.jsxs)(Ge,{value:"bug",children:[(0,t.jsx)(Et,{fontSize:"small",sx:{mr:1}})," ",l("admin.task.typeBug")]}),(0,t.jsxs)(Ge,{value:"feature",children:[(0,t.jsx)(nn,{fontSize:"small",sx:{mr:1}})," ",l("admin.task.typeFeature")]})]}),(0,t.jsxs)(Ke,{size:"small",fullWidth:!0,children:[(0,t.jsx)(ze,{children:l("admin.createTask.site")}),(0,t.jsx)(pe,{label:l("admin.createTask.site"),value:v,onChange:g=>b(String(g.target.value)),children:(V.data?.items??[]).map(g=>(0,t.jsx)(q,{value:g.id,children:g.name},g.id))})]}),(0,t.jsx)(he,{label:l("admin.createTask.description"),placeholder:l(y==="feature"?"admin.createTask.descIdea":"admin.createTask.descProblem"),value:u,onChange:g=>f(g.target.value),onPaste:g=>{const A=Array.from(g.clipboardData.files).filter(B=>We.includes(B.type));A.length!==0&&(g.preventDefault(),Z(A))},onKeyDown:g=>{(g.metaKey||g.ctrlKey)&&g.key==="Enter"&&le&&(g.preventDefault(),I.mutate())},multiline:!0,minRows:4,fullWidth:!0,slotProps:{htmlInput:{maxLength:1e4}},helperText:l("admin.createTask.llmHelper")}),(0,t.jsx)(he,{label:l("admin.createTask.urlOptional"),placeholder:"https://example.com/page",value:P,onChange:g=>E(g.target.value),fullWidth:!0,slotProps:{htmlInput:{maxLength:2e3}},helperText:l("admin.createTask.urlHelper")}),(0,t.jsxs)(C,{children:[(0,t.jsx)("input",{ref:j,type:"file",multiple:!0,accept:ea,hidden:!0,onChange:g=>{Z(Array.from(g.target.files??[])),g.target.value=""}}),(0,t.jsx)(M,{size:"small",startIcon:(0,t.jsx)(In,{}),onClick:()=>j.current?.click(),disabled:I.isPending||!!O||S.length>=je,children:l("admin.createTask.attach")}),S.length>0&&(0,t.jsx)(C,{sx:{display:"flex",flexWrap:"wrap",gap:1,mt:1},children:S.map((g,A)=>(0,t.jsx)(En,{label:g.name,size:"small",onDelete:I.isPending||O?void 0:()=>K(S.filter((B,H)=>H!==A)),title:l("admin.createTask.removeFile")},`${g.name}-${A}`))}),(0,t.jsx)(L,{variant:"caption",color:re?"error":"text.secondary",sx:{display:"block",mt:.5},children:re??l("admin.createTask.attachHelper")})]}),(0,t.jsxs)(Ke,{size:"small",fullWidth:!0,children:[(0,t.jsx)(ze,{children:l("admin.createTask.priority")}),(0,t.jsx)(pe,{label:l("admin.createTask.priority"),value:$,onChange:g=>k(g.target.value),children:ne.map(g=>(0,t.jsx)(q,{value:g.value,children:g.label},g.value))})]})]})]}),(0,t.jsx)(yn,{children:O?(0,t.jsx)(M,{variant:"contained",onClick:()=>ee(O.id),children:l("admin.createTask.openTicket")}):(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(M,{onClick:ie,disabled:I.isPending,children:l("admin.common.cancel")}),(0,t.jsx)(M,{variant:"contained",onClick:()=>I.mutate(),disabled:!le,children:I.isPending?l(S.length>0?"admin.createTask.uploading":"admin.common.creating"):l("admin.common.create")})]})})]})}function sa(){const{t:e}=R();return(0,t.jsxs)(C,{component:"span",sx:{display:"inline-flex",alignItems:"center",gap:.5,color:"text.secondary",fontStyle:"italic"},children:[(0,t.jsx)(Re,{sx:{fontSize:15}}),e("admin.tasks.locked.label")]})}function oa(e,n){return e.locked?n("admin.tasks.locked.label"):e.title??e.description}function Us(){const{t:e}=R(),n=G({queryKey:["org-usage"],queryFn:()=>D.org.usage()}),i=n.data?.limits.maxOpenTasks??0,a=n.data?.usage.openTasks??0,o=n.data?.usage.lockedTasks??0;return o===0&&(i===0||a<i*.8)?null:(0,t.jsx)(Y,{severity:o>0?"warning":"info",icon:(0,t.jsx)(Re,{}),sx:{mb:2},action:(0,t.jsx)(M,{component:oe,to:"/billing",color:"inherit",size:"small",children:e("admin.tasks.locked.cta")}),children:o>0?e("admin.tasks.locked.banner",{count:o,total:i}):e("admin.tasks.locked.near",{open:a,total:i})})}function Ns({tache:e}){const{t:n,lang:i}=R();return(0,t.jsx)(jt,{sx:{p:3,maxWidth:720},children:(0,t.jsxs)(qe,{spacing:1.5,children:[(0,t.jsxs)(C,{sx:{display:"flex",alignItems:"center",gap:1},children:[(0,t.jsx)(Re,{color:"warning"}),(0,t.jsxs)(L,{variant:"h6",sx:{fontWeight:600},children:[e.displayId," — ",n("admin.tasks.locked.label")]})]}),(0,t.jsx)(L,{variant:"body2",color:"text.secondary",children:n("admin.tasks.locked.hint")}),(0,t.jsxs)(L,{variant:"body2",children:[n("admin.tasks.locked.receivedAt",{date:new Intl.DateTimeFormat(i,{dateStyle:"long",timeStyle:"short"}).format(new Date(e.createdAt))})," · ",n(`admin.tasks.type.${e.type}`)," · ",e.siteName]}),e.url&&(0,t.jsx)(L,{variant:"body2",sx:{wordBreak:"break-all"},children:n("admin.tasks.locked.page",{url:e.url})}),(0,t.jsx)(C,{children:(0,t.jsx)(M,{component:oe,to:"/billing",variant:"contained",children:n("admin.tasks.locked.cta")})})]})})}var ra=7;function da(){const{t:e}=R(),n=ke(),{siteId:i}=X(),a=Ne(),[o,r]=(0,d.useState)(!1),[l,h]=(0,d.useState)(""),[p,w]=(0,d.useState)(""),v=(0,d.useRef)(null);(0,d.useEffect)(()=>{const u=setTimeout(()=>w(l.trim()),250);return()=>clearTimeout(u)},[l]);const b=G({queryKey:["tasks-search",p,i],queryFn:()=>D.tasks.search({q:p,limit:ra,siteId:i??void 0}),enabled:p.length>0,staleTime:15e3,placeholderData:$n}),y=p.length>0?b.data?.items??[]:[],c=()=>{const u=l.trim();u&&(r(!1),n(a(`/search?q=${encodeURIComponent(u)}`)))};return(0,t.jsx)(sn,{open:o&&l.trim().length>0,onOpen:()=>r(!0),onClose:()=>r(!1),options:y,filterOptions:u=>u,loading:b.isFetching,loadingText:e("admin.ticketSearch.searching"),value:null,inputValue:l,onInputChange:(u,f)=>h(f),onHighlightChange:(u,f)=>{v.current=f},blurOnSelect:!0,clearOnBlur:!0,getOptionLabel:u=>`${u.displayId} ${oa(u,e)}`,isOptionEqualToValue:(u,f)=>u.id===f.id,onChange:(u,f)=>{f&&(h(""),r(!1),n(a(`/tasks/${f.displayId}`)))},onKeyDown:u=>{u.key==="Enter"&&!v.current&&(u.preventDefault(),c())},noOptionsText:b.isFetching?e("admin.ticketSearch.searching"):e("admin.ticketSearch.noTicket"),renderOption:(u,f)=>{const{key:P,...E}=u;return(0,t.jsxs)(C,{component:"li",...E,sx:{display:"flex",gap:1,alignItems:"baseline"},children:[(0,t.jsx)(L,{variant:"caption",sx:{fontFamily:"ui-monospace, monospace",color:"text.secondary",whiteSpace:"nowrap"},children:f.displayId}),(0,t.jsx)(L,{variant:"body2",noWrap:!0,sx:{flex:1},children:f.locked?(0,t.jsx)(sa,{}):f.title??f.description}),f.archivedAt&&(0,t.jsx)(L,{variant:"caption",color:"text.disabled",noWrap:!0,children:e("admin.ticketSearch.archived")}),(0,t.jsx)(L,{variant:"caption",color:"text.secondary",noWrap:!0,children:f.siteName})]},P)},renderInput:u=>(0,t.jsx)(he,{...u,size:"small",placeholder:e("admin.ticketSearch.placeholder"),slotProps:{...u.slotProps,input:{...u.slotProps.input,startAdornment:(0,t.jsx)(en,{position:"start",children:(0,t.jsx)(un,{fontSize:"small",sx:{color:"inherit",opacity:.85}})}),endAdornment:(0,t.jsxs)(t.Fragment,{children:[b.isFetching?(0,t.jsx)(At,{color:"inherit",size:16}):null,u.slotProps.input.endAdornment]})}}}),sx:{width:260,mr:1.5,display:{xs:"none",md:"block"},"& .MuiOutlinedInput-root":{color:"inherit"},"& .MuiOutlinedInput-notchedOutline":{borderColor:"divider"},"&:hover .MuiOutlinedInput-notchedOutline":{borderColor:"text.primary"},"& .MuiSvgIcon-root":{color:"inherit"},"& input::placeholder":{color:"inherit",opacity:.7}}})}function la({size:e=24}){return(0,t.jsxs)("svg",{width:e,height:e,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:2,strokeLinecap:"round",strokeLinejoin:"round","aria-hidden":"true",children:[(0,t.jsx)("path",{d:"M8 18.5A8 8 0 1 0 4.5 15L3.5 19l4.5-0.5Z"}),(0,t.jsx)("circle",{cx:"11",cy:"11.5",r:"1.4",fill:"currentColor",stroke:"none"}),(0,t.jsx)("path",{d:"M14.3 9.2a3.5 3.5 0 0 1 0 4.6"}),(0,t.jsx)("path",{d:"M16.4 7.4a6 6 0 0 1 0 8.2"})]})}function ca(){const e=Be(),{lang:n}=R();if(!e.data?.viaPlateforme)return null;const i=n==="fr",a=e.data.org?.name??"",o=document.querySelector('meta[name="bipwire-zone-autre"]')?.content||"";return(0,t.jsxs)(Y,{severity:"warning",sx:{mb:2},children:[i?`Vous êtes entré dans « ${a} » depuis la plateforme — lecture seule.`:`You entered “${a}” from the platform — read-only.`," ",(0,t.jsx)(Lt,{component:"button",type:"button",underline:"always",onClick:()=>{const r=()=>{o?window.location.assign(o+"/"):window.location.reload()};fetch("/api/admin/auth/exit-platform",{method:"POST",credentials:"same-origin"}).then(r,r)},children:i?"Sortir et revenir chez moi":"Exit and return to my account"})]})}var ue={fr:"🇫🇷",en:"🇬🇧"},dt=240,ua=64;function lt({items:e,sectionOrder:n,sectionLabels:i,onItemClick:a,collapsed:o,onToggleCollapsed:r,canManageMembers:l,isOwner:h,crossZoneLink:p,header:w}){const{t:v}=R(),b=e.filter(c=>(!c.adminOnly||l)&&(!c.ownerOnly||h)),y=c=>{const u=(0,t.jsxs)(we,{component:Ye,to:c.to,end:c.end,onClick:a,sx:{mx:1,borderRadius:1,justifyContent:o?"center":"flex-start",px:o?1:2,position:"relative",color:"text.secondary","& .MuiListItemIcon-root":{color:"text.secondary",minWidth:o?0:36,justifyContent:"center"},"&.active":{bgcolor:me("sidebarSelected"),color:me("sidebarSelectedText"),fontWeight:600,"& .MuiListItemIcon-root":{color:me("sidebarSelectedText")},"& .MuiListItemText-primary":{fontWeight:600},"&::before":{content:'""',position:"absolute",left:0,top:6,bottom:6,width:3,borderRadius:"0 2px 2px 0",bgcolor:"primary.main"}},"&:hover:not(.active)":{bgcolor:"action.hover"}},children:[(0,t.jsx)(z,{children:c.icon}),!o&&(0,t.jsx)(N,{primary:c.label})]},c.to);return o?(0,t.jsx)(Q,{title:c.label,placement:"right",arrow:!0,children:u},c.to):u};return(0,t.jsxs)(C,{sx:{display:"flex",flexDirection:"column",height:"100%"},children:[w?.(o,a),(0,t.jsx)(on,{sx:{py:1,flexGrow:1},children:n.map((c,u)=>{const f=b.filter(P=>P.section===c);return f.length===0?null:(0,t.jsxs)(C,{component:"li",sx:{listStyle:"none"},children:[o?u>0&&(0,t.jsx)(se,{sx:{mx:1,my:.5}}):(0,t.jsx)(Ee,{disableSticky:!0,component:"div",sx:{bgcolor:"transparent",color:"text.disabled",fontSize:11,fontWeight:700,letterSpacing:"0.06em",lineHeight:"2.2",px:3},children:i[c]??c}),f.map(y)]},c)})}),p&&(0,t.jsx)(C,{sx:{px:1,py:.5,borderTop:"1px solid",borderColor:"divider"},children:(()=>{const c=p.origine||document.querySelector('meta[name="bipwire-zone-autre"]')?.content||"",u=c?(0,t.jsxs)(we,{component:"a",href:c+p.to,onClick:a,sx:{borderRadius:1,justifyContent:o?"center":"flex-start",px:o?1:2,color:"text.secondary","& .MuiListItemIcon-root":{color:"text.secondary",minWidth:o?0:36,justifyContent:"center"},"&:hover":{bgcolor:"action.hover"}},children:[(0,t.jsx)(z,{children:p.icon}),!o&&(0,t.jsx)(N,{primary:p.label})]}):(0,t.jsxs)(we,{component:Ye,to:p.to,onClick:a,sx:{borderRadius:1,justifyContent:o?"center":"flex-start",px:o?1:2,color:"text.secondary","& .MuiListItemIcon-root":{color:"text.secondary",minWidth:o?0:36,justifyContent:"center"},"&:hover":{bgcolor:"action.hover"}},children:[(0,t.jsx)(z,{children:p.icon}),!o&&(0,t.jsx)(N,{primary:p.label})]});return o?(0,t.jsx)(Q,{title:p.label,placement:"right",arrow:!0,children:u}):u})()}),r&&(0,t.jsx)(C,{sx:{display:"flex",justifyContent:o?"center":"flex-end",px:o?0:1,py:.5,borderTop:"1px solid",borderColor:"divider"},children:(0,t.jsx)(Q,{title:v(o?"admin.layout.expandMenu":"admin.layout.collapseMenu"),placement:"right",arrow:!0,children:(0,t.jsx)(te,{size:"small",onClick:r,"aria-label":v(o?"admin.layout.expandMenu":"admin.layout.collapseMenu"),children:o?(0,t.jsx)(xn,{}):(0,t.jsx)(bn,{})})})}),!o&&(0,t.jsxs)(C,{sx:{px:2,py:1.25,borderTop:"1px solid",borderColor:"divider",color:"text.secondary",fontSize:11,fontFamily:"ui-monospace, monospace",userSelect:"all"},title:v("admin.layout.versionTitle"),children:["bipwire.","0.140.0"]})]})}function ma(e,n,i){const{pathname:a}=W(),o=[e.filter(r=>r.end||r.to==="/"?a===r.to:a===r.to||a.startsWith(r.to.endsWith("/")?r.to:r.to+"/")).sort((r,l)=>l.to.length-r.to.length)[0]?.label,i,n].filter(Boolean).join(" · ");(0,d.useEffect)(()=>{document.title=o},[o])}function pa({brand:e,sectionOrder:n,sectionLabels:i,navItems:a,collapsedStorageKey:o,showCreateTask:r=!1,crossZoneLink:l=null,homePath:h,topbarExtra:p=null,navHeader:w,titleContext:v=null}){ma(a,e,v);const b=jn(),y=Cn(b.breakpoints.up("md")),[c,u]=(0,d.useState)(!1),[f,P]=(0,d.useState)(!1),[E,$]=(0,d.useState)(()=>{try{return localStorage.getItem(o)==="1"}catch{return!1}}),{logout:k,refresh:S}=Bt(),{mode:K,toggle:re}=li(),J=ke(),O=ye(),F=Be(),{t:j,lang:Z,setLang:ne}=R(),V=F.data?.role,I=V==="owner"||V==="admin",ee=V==="owner",de=F.data?.platformAdmin??!1,ie=F.data?.memberships??[],le=F.data?.org?.id??"",g=F.data?.org?.name??"",A=F.data?.user?.email??"",B=(A.replace(/@.*/,"").slice(0,2)||"?").toUpperCase(),[H,U]=(0,d.useState)(null),Fe=It({mutationFn:T=>D.switchOrg(T),onSuccess:async()=>{await O.invalidateQueries(),await S(),J(h,{replace:!0})}});(0,d.useEffect)(()=>{try{localStorage.setItem(o,E?"1":"0")}catch{}},[E,o]);const Jt=async()=>{await k(),J("/login",{replace:!0})},Ce=E?ua:dt;return(0,t.jsxs)(C,{sx:{display:"flex",minHeight:"100vh"},children:[(0,t.jsx)(Hn,{position:"fixed",sx:{zIndex:T=>T.zIndex.drawer+1},children:(0,t.jsxs)(ce,{children:[!y&&(0,t.jsx)(te,{edge:"start",onClick:()=>u(!0),sx:{mr:1},"aria-label":j("admin.layout.openMenu"),children:(0,t.jsx)(kn,{})}),(0,t.jsxs)(C,{sx:{display:"flex",alignItems:"center",gap:1,flexGrow:1},children:[(0,t.jsx)(C,{component:"span",sx:{display:"flex",color:"primary.main"},children:(0,t.jsx)(la,{size:26})}),(0,t.jsx)(L,{variant:"h6",sx:{fontWeight:600,display:{xs:p?"none":"block",sm:"block"}},children:e}),g&&(0,t.jsxs)(C,{sx:{display:{xs:"none",md:"flex"},alignItems:"center",gap:1,minWidth:0},children:[(0,t.jsx)(C,{sx:{width:"1px",height:20,bgcolor:"divider"}}),(0,t.jsx)(Q,{title:j("admin.layout.currentOrg"),arrow:!0,children:(0,t.jsx)(L,{variant:"subtitle1",sx:{fontWeight:500,opacity:.95,maxWidth:260},noWrap:!0,children:g})})]})]}),ie.length>1&&(0,t.jsx)(pe,{value:le,onChange:T=>Fe.mutate(T.target.value),disabled:Fe.isPending,size:"small",variant:"outlined","aria-label":j("admin.layout.activeSpace"),sx:{mr:{xs:.5,md:1.5},color:"inherit",maxWidth:{xs:140,md:220},"& .MuiOutlinedInput-notchedOutline":{borderColor:"divider"},"&:hover .MuiOutlinedInput-notchedOutline":{borderColor:"text.primary"},"& .MuiSvgIcon-root":{color:"inherit"}},children:ie.map(T=>(0,t.jsx)(q,{value:T.orgId,children:T.name},T.orgId))}),p,r&&(0,t.jsx)(da,{}),r&&(y?(0,t.jsx)(M,{variant:"contained",size:"small",startIcon:(0,t.jsx)(Je,{}),onClick:()=>P(!0),sx:{mr:1.5,whiteSpace:"nowrap",flexShrink:0},children:j("admin.layout.createTask")}):(0,t.jsx)(Q,{title:j("admin.layout.createTask"),arrow:!0,children:(0,t.jsx)(te,{onClick:()=>P(!0),"aria-label":j("admin.layout.createTask"),size:"small",sx:{mr:.5,bgcolor:"primary.main",color:"primary.contrastText","&:hover":{bgcolor:"primary.dark"}},children:(0,t.jsx)(Je,{fontSize:"small"})})})),(0,t.jsxs)(pe,{value:Z,onChange:T=>ne(T.target.value),"aria-label":j("admin.settings.language"),sx:{display:{xs:"none",md:"inline-flex"},color:"inherit","& .MuiOutlinedInput-notchedOutline":{border:"none"},"& .MuiSelect-icon":{color:"inherit"},fontSize:14},variant:"outlined",size:"small",children:[(0,t.jsxs)(q,{value:"en",children:[(0,t.jsx)(C,{component:"span","aria-hidden":!0,sx:{mr:.75},children:ue.en}),"English"]}),(0,t.jsxs)(q,{value:"fr",children:[(0,t.jsx)(C,{component:"span","aria-hidden":!0,sx:{mr:.75},children:ue.fr}),"Français"]})]}),(0,t.jsx)(te,{color:"inherit",onClick:re,"aria-label":j(K==="dark"?"admin.layout.themeLight":"admin.layout.themeDark"),sx:{mr:.5},children:K==="dark"?(0,t.jsx)(fn,{}):(0,t.jsx)(_n,{})}),(0,t.jsx)(Q,{title:j("admin.layout.myAccount"),arrow:!0,children:(0,t.jsx)(te,{onClick:T=>U(T.currentTarget),size:"small",sx:{ml:.5},"aria-label":j("admin.layout.userMenu"),children:(0,t.jsx)(Tn,{sx:{width:34,height:34,fontSize:14,fontWeight:600,bgcolor:"primary.main",color:"primary.contrastText"},children:B})})}),(0,t.jsxs)(Pt,{anchorEl:H,open:!!H,onClose:()=>U(null),anchorOrigin:{vertical:"bottom",horizontal:"right"},transformOrigin:{vertical:"top",horizontal:"right"},slotProps:{paper:{sx:{minWidth:240,mt:.5}}},children:[(0,t.jsxs)(C,{sx:{px:2,py:1},children:[(0,t.jsx)(L,{variant:"body2",sx:{fontWeight:600},noWrap:!0,children:A||j("admin.common.anonymous")}),(0,t.jsxs)(C,{sx:{mt:.5,display:"flex",gap:.5,flexWrap:"wrap"},children:[V&&(0,t.jsx)(C,{sx:{fontSize:11,px:.75,py:.125,borderRadius:.75,bgcolor:"action.selected",color:"text.secondary"},children:j(V==="owner"?"admin.roles.owner":V==="admin"?"admin.roles.admin":"admin.roles.member")}),de&&(0,t.jsx)(C,{sx:{fontSize:11,px:.75,py:.125,borderRadius:.75,bgcolor:"warning.main",color:"warning.contrastText"},children:j("admin.roles.superAdmin")})]})]}),(0,t.jsx)(se,{sx:{my:.5}}),!y&&(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(q,{onClick:()=>{ne("fr"),U(null)},children:[(0,t.jsx)(z,{children:Z==="fr"?(0,t.jsx)(ge,{fontSize:"small"}):(0,t.jsx)(Qe,{fontSize:"small"})}),(0,t.jsxs)(N,{children:[(0,t.jsx)(C,{component:"span","aria-hidden":!0,sx:{mr:.75},children:ue.fr}),"Français"]})]}),(0,t.jsxs)(q,{onClick:()=>{ne("en"),U(null)},children:[(0,t.jsx)(z,{children:Z==="en"?(0,t.jsx)(ge,{fontSize:"small"}):(0,t.jsx)(Qe,{fontSize:"small"})}),(0,t.jsxs)(N,{children:[(0,t.jsx)(C,{component:"span","aria-hidden":!0,sx:{mr:.75},children:ue.en}),"English"]})]}),(0,t.jsx)(se,{sx:{my:.5}})]}),(0,t.jsxs)(q,{onClick:()=>{U(null),J("/settings/compte")},children:[(0,t.jsx)(z,{children:(0,t.jsx)(Sn,{fontSize:"small"})}),(0,t.jsx)(N,{children:j("admin.layout.myProfile")})]}),(0,t.jsxs)(q,{onClick:()=>{U(null),Jt()},children:[(0,t.jsx)(z,{children:(0,t.jsx)(On,{fontSize:"small"})}),(0,t.jsx)(N,{children:j("admin.layout.logout")})]})]})]})}),r&&(0,t.jsx)(aa,{open:f,onClose:()=>P(!1)}),y?(0,t.jsxs)($e,{variant:"permanent",sx:{width:Ce,flexShrink:0,transition:T=>T.transitions.create("width",{easing:T.transitions.easing.sharp,duration:T.transitions.duration.shorter}),"& .MuiDrawer-paper":{width:Ce,boxSizing:"border-box",bgcolor:me("sidebar"),overflowX:"hidden",transition:T=>T.transitions.create("width",{easing:T.transitions.easing.sharp,duration:T.transitions.duration.shorter})}},children:[(0,t.jsx)(ce,{}),(0,t.jsx)(lt,{items:a,sectionOrder:n,sectionLabels:i,collapsed:E,onToggleCollapsed:()=>$(T=>!T),canManageMembers:I,isOwner:ee,crossZoneLink:l,header:w})]}):(0,t.jsxs)($e,{variant:"temporary",open:c,onClose:()=>u(!1),ModalProps:{keepMounted:!0},sx:{"& .MuiDrawer-paper":{width:dt,boxSizing:"border-box"}},children:[(0,t.jsx)(ce,{}),(0,t.jsx)(lt,{items:a,sectionOrder:n,sectionLabels:i,collapsed:!1,onItemClick:()=>u(!1),canManageMembers:I,isOwner:ee,crossZoneLink:l,header:w})]}),(0,t.jsxs)(C,{component:"main",sx:{flexGrow:1,minWidth:0,p:{xs:2,sm:3,md:4},width:{md:`calc(100% - ${Ce}px)`},transition:T=>T.transitions.create("width",{easing:T.transitions.easing.sharp,duration:T.transitions.duration.shorter})},children:[(0,t.jsx)(ce,{}),(0,t.jsx)(ca,{}),(0,t.jsx)(Me,{})]})]})}var _e="bipwire:entree-org",ct="enter";function ha(){const e=new URLSearchParams(window.location.search),n=e.get(ct);if(!n)return;try{sessionStorage.setItem(_e,n)}catch{}e.delete(ct);const i=e.toString();window.history.replaceState({},"",window.location.pathname+(i?`?${i}`:"")+window.location.hash)}function ga(){const e=ye(),n=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(n.current)return;let i=null;try{i=sessionStorage.getItem(_e)}catch{return}if(i){n.current=!0;try{sessionStorage.removeItem(_e)}catch{}D.switchOrg(i).then(()=>e.invalidateQueries()).catch(()=>{})}},[e])}var ba=["SUPPORT","ORGANISATION","AIDE"];function fa(){ga();const{t:e}=R(),n=Zi().find(p=>p.id==="console")??null,i={SUPPORT:e("admin.nav.sectionSupport"),ORGANISATION:e("admin.nav.sectionOrganisation"),AIDE:e("admin.nav.sectionAide")},{siteId:a,site:o}=X(),r=Ne(),l=a!==null,h=[{to:r("/"),label:e("admin.nav.dashboard"),icon:(0,t.jsx)(An,{}),section:"SUPPORT",end:!0},{to:r("/tasks"),label:e("admin.nav.tasks"),icon:(0,t.jsx)(Et,{}),section:"SUPPORT"},{to:r("/threads"),label:e("admin.nav.threads"),icon:(0,t.jsx)(Mn,{}),section:"SUPPORT"},l?{to:r("/settings"),label:e("admin.projet.reglages"),icon:(0,t.jsx)(cn,{}),section:"SUPPORT"}:{to:"/projects",label:e("admin.nav.sites"),icon:(0,t.jsx)(Yt,{}),section:"SUPPORT"},{to:r("/notifications"),label:e("admin.nav.notifications"),icon:(0,t.jsx)(tn,{}),section:"SUPPORT"},{to:r("/connectors"),label:e("admin.nav.connectors"),icon:(0,t.jsx)(Zt,{}),section:"SUPPORT"},...l?[]:[{to:"/members",label:e("admin.nav.members"),icon:(0,t.jsx)(Ln,{}),section:"ORGANISATION",adminOnly:!0},{to:"/billing",label:e("admin.nav.billing"),icon:(0,t.jsx)(an,{}),section:"ORGANISATION",ownerOnly:!0}],{to:r("/llm-usage"),label:e("admin.nav.llmUsage"),icon:(0,t.jsx)(pn,{}),section:"ORGANISATION"},{to:r("/llm-tokens"),label:e("admin.nav.apiKeys"),icon:(0,t.jsx)(mn,{}),section:"ORGANISATION"},...l?[]:[{to:"/settings",label:e("admin.nav.settings"),icon:(0,t.jsx)(ln,{}),section:"ORGANISATION"}],{to:"/docs",label:e("admin.nav.documentation"),icon:(0,t.jsx)(rn,{}),section:"AIDE"},...l?[]:[{to:"/history",label:e("admin.nav.history"),icon:(0,t.jsx)(gn,{}),section:"AIDE"}]];return(0,t.jsx)(pa,{brand:"BipWire",sectionOrder:ba,sectionLabels:i,navItems:h,collapsedStorageKey:"bipwire:nav-collapsed",showCreateTask:!0,homePath:"/",topbarExtra:(0,t.jsx)(Yi,{}),titleContext:a?o?.name??a:null,navHeader:(p,w)=>l?(0,t.jsx)(Xi,{collapsed:p,onItemClick:w}):null,crossZoneLink:n?{to:"/",label:e("admin.nav.platformAdmin"),origine:n.url}:null})}var va=`# Guide de démarrage

De zéro à « mon agent IA corrige les bugs reportés sur mon site » en 6 étapes. Compter ~10 minutes.

Le parcours :

1. Récupérer le script du widget
2. Le coller sur le site
3. Tester (envoyer un message + reporter un bug)
4. Retrouver le bug dans l'admin
5. Brancher l'outil CLI (Claude Code / Cursor / Codex / OpenCode) via MCP + slash commands
6. Lancer les commandes pour lister et faire corriger les bugs

---

## 1. Récupérer le script du widget

Aller dans **Projets**, ouvrir (ou créer) le projet, puis cliquer sur **« Code d'intégration »**. Un \`<script>\` s'affiche, avec la clé publique du projet :

\`\`\`html
<script async src="https://bipwire.com/PUBLIC_KEY.js"><\/script>
\`\`\`

\`PUBLIC_KEY.js\` est propre au projet. Détails et personnalisation (couleur, titre) : [Intégration du widget](/docs/widget/integration).

## 2. Coller le script sur le site

Placer le \`<script>\` **juste avant \`</body>\`**, sur toutes les pages où le widget doit apparaître.

\`\`\`html
  <!-- … le site … -->
  <script async src="https://bipwire.com/PUBLIC_KEY.js"><\/script>
</body>
\`\`\`

**Conseil** : stocker l'URL dans une variable d'environnement (\`BIPWIRE_LOADER_URL\`) plutôt qu'en dur — une rotation de clé devient un simple changement de variable. Voir [Intégration du widget](/docs/widget/integration).

Déployer le site.

## 3. Tester

Ouvrir le site dans le navigateur : la **bulle flottante** doit apparaître en bas. Faire les deux tests :

- **Chat** : cliquer la bulle, écrire un message, l'envoyer, puis vérifier que la conversation part bien.
- **Bug report** : déclencher un signalement de bug (avec capture d'écran). C'est ce qui crée un **ticket** côté admin, avec le contexte technique (console, requêtes réseau, page).

Si la bulle n'apparaît pas : vérifier que le toggle **« Widget activé »** est ON sur la fiche du projet, et consulter la [FAQ erreurs CSP](/docs/widget/csp-faq).

## 4. Retrouver le bug dans l'admin

Aller dans **Tickets**. Le bug qui vient d'être reporté apparaît, avec son statut (\`Nouveau\`), sa priorité, et tout le contexte capturé (screenshots, timeline console + réseau). Cliquer dessus pour voir le détail.

C'est ce que l'agent IA va lire à l'étape suivante — sans rien recopier.

## 5. Brancher l'outil CLI

C'est ici que la magie opère : on connecte l'agent (Claude Code, Cursor, Codex, OpenCode) à BipWire pour qu'il lise et corrige les tickets. **Deux étapes se font une fois sur le poste, et valent pour tous les projets ; la troisième se fait une fois par projet.**

Commencer par ouvrir **API Keys** et générer un token \`bipwire_llm_…\`. Le bouton **« Brancher un agent CLI »** donne d'ailleurs ces mêmes commandes, le token déjà inséré.

**Étape 1 — Installer les commandes \`/bip-*\` (une fois sur le poste).** Quatre fichiers texte sans aucun secret, rangés dans le dossier personnel. Lancer la commande depuis n'importe quel dossier :

:::tabs
:::tab Claude Code
\`\`\`bash
curl -fsSL "https://bipwire.com/install/claude-code/commands?format=sh" | sh
\`\`\`
Les commandes vont dans \`~/.claude/commands/\`.
:::tab Cursor
\`\`\`bash
curl -fsSL "https://bipwire.com/install/cursor/commands?format=sh" | sh
\`\`\`
Les commandes vont dans \`~/.cursor/commands/\`.
:::tab Codex
\`\`\`bash
curl -fsSL "https://bipwire.com/install/codex/commands?format=sh" | sh
\`\`\`
Les commandes vont dans \`~/.codex/prompts/\`.
:::tab OpenCode
\`\`\`bash
curl -fsSL "https://bipwire.com/install/opencode/commands?format=sh" | sh
\`\`\`
Les commandes vont dans \`~/.config/opencode/commands/\`.
:::

**Étape 2 — Brancher le serveur MCP (une fois sur le poste).** C'est la connexion à BipWire ; elle porte le token, donc elle va dans la config personnelle, jamais dans un repo. Remplacer \`bipwire_llm_mon_token\` par le vrai token.

:::tabs
:::tab Claude Code
\`\`\`bash
claude mcp add --scope user --transport http bip https://bipwire.com/api/mcp \\
  --header "Authorization: Bearer bipwire_llm_mon_token"
\`\`\`
:::tab Cursor
Dans \`~/.cursor/mcp.json\` :

\`\`\`json
{
  "mcpServers": {
    "bip": {
      "url": "https://bipwire.com/api/mcp",
      "headers": { "Authorization": "Bearer bipwire_llm_mon_token" }
    }
  }
}
\`\`\`
:::tab Codex
Dans \`~/.codex/config.toml\` :

\`\`\`toml
[mcp_servers.bip]
url = "https://bipwire.com/api/mcp"
http_headers = { Authorization = "Bearer bipwire_llm_mon_token" }
\`\`\`
:::tab OpenCode
Dans \`~/.config/opencode/opencode.json\` (fusionner avec le contenu existant) :

\`\`\`json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "bip": {
      "type": "remote",
      "url": "https://bipwire.com/api/mcp",
      "enabled": true,
      "headers": { "Authorization": "Bearer bipwire_llm_mon_token" }
    }
  }
}
\`\`\`
:::

**Étape 3 — Dire quel projet ce repo interroge (une fois par projet).** Créer un \`.bip.json\` à la racine, identique quel que soit l'outil :

\`\`\`bash
echo '{ "siteId": "mon-projet" }' > .bip.json
\`\`\`

Au projet suivant, seule cette étape est à refaire.

> Tous les détails (garde-fous, mise à jour, anciennes installations dans un repo) : [Brancher un agent](/docs/api/brancher).

Une fois en place, **relancer l'agent CLI** pour qu'il charge le serveur MCP et les commandes.

## 6. Lister et faire corriger les bugs

Dans l'agent CLI, à la racine du projet :

- **Lister ce qu'il y a à traiter** :

  \`\`\`
  /bip-tasks
  \`\`\`

  → l'agent lit \`.bip.json\`, interroge BipWire et affiche les tickets ouverts groupées par priorité.

- **Voir par quoi commencer** (tous projets confondus, critical + high) :

  \`\`\`
  /bip-sort
  \`\`\`

- **Faire corriger un bug précis** :

  \`\`\`
  /bip-fix 42
  \`\`\`

  → l'agent marque le ticket « en cours », lit tout le contexte (description, screenshots, console, réseau), corrige dans le code, puis repasse le ticket en **« À tester »** avec une note expliquant ce qu'il a fait.

- **Laisser l'agent enchaîner tout seul** (prend la plus prioritaire et la corrige) :

  \`\`\`
  /bip-next
  \`\`\`

Le langage naturel fonctionne aussi : « regarde les derniers bugs BipWire et corrige le plus urgent » — l'agent utilisera les tools MCP de lui-même.

## Et après ?

- Tester le fix, puis marquer le ticket **« Résolu »** dans l'admin (l'agent ne clôt jamais lui-même — la validation reste humaine, après test).
- Générer **un token par machine/projet** avec un label parlant : en cas de fuite, seul celui-là est révoqué. Voir [API Keys](/docs/api/tokens).
- Pour brancher plusieurs projets, répéter l'étape 5 dans chaque repo (un \`.bip.json\` par repo).
`,ka=`# Getting started guide

From zero to "my AI agent fixes the bugs reported on my site" in 6 steps. Budget ~10 minutes.

The path:

1. Grab the widget script
2. Paste it on your site
3. Test it (send a message + report a bug)
4. Find the bug in the admin
5. Connect your CLI tool (Claude Code / Cursor / Codex / OpenCode) via MCP + slash commands
6. Run the commands to list and get the bugs fixed

---

## 1. Grab the widget script

Go to **Projects**, open (or create) your project, then click **"Integration code"**. You get a \`<script>\` with your public key:

\`\`\`html
<script async src="https://bipwire.com/PUBLIC_KEY.js"><\/script>
\`\`\`

\`PUBLIC_KEY.js\` is specific to your project. Details and customization (color, title): [Widget integration](/docs/widget/integration).

## 2. Paste the script on your site

Place the \`<script>\` **just before \`</body>\`**, on every page where you want the widget.

\`\`\`html
  <!-- … your site … -->
  <script async src="https://bipwire.com/PUBLIC_KEY.js"><\/script>
</body>
\`\`\`

**Tip**: store the URL in an environment variable (\`BIPWIRE_LOADER_URL\`) rather than hardcoding it — a key rotation then becomes a simple variable change. See [Widget integration](/docs/widget/integration).

Deploy your site.

## 3. Test it

Open your site in the browser. You should see the **floating bubble** at the bottom. Run both tests:

- **Chat**: click the bubble, write a message, send it. This confirms the conversation goes through.
- **Bug report**: trigger a bug report (with a screenshot). This is what creates a **ticket** on the admin side, with the technical context (console, network requests, page).

If the bubble doesn't appear: make sure the **"Widget enabled"** toggle is ON on the project page, and check the [CSP error FAQ](/docs/widget/csp-faq).

## 4. Find the bug in the admin

Go to **Tickets**. The bug you just reported appears, with its status (\`New\`), its priority, and all the captured context (screenshots, console + network timeline). Click it to see the details.

This is what your AI agent will read in the next step — without you having to copy anything over.

## 5. Connect your CLI tool

This is where the magic happens: we connect your agent (Claude Code, Cursor, Codex, OpenCode) to BipWire so it reads and fixes tickets. **Two steps are done once per machine and cover all your projects; the third is done once per project.**

Start by opening **API Keys** and generating a \`bipwire_llm_…\` token. The **"Connect a CLI agent"** button also hands you these same commands, with your token already inserted.

**Step 1 — Install the \`/bip-*\` commands (once per machine).** Four text files with no secret at all, stored in your home folder. Run the command from any folder:

:::tabs
:::tab Claude Code
\`\`\`bash
curl -fsSL "https://bipwire.com/install/claude-code/commands?format=sh" | sh
\`\`\`
The commands go to \`~/.claude/commands/\`.
:::tab Cursor
\`\`\`bash
curl -fsSL "https://bipwire.com/install/cursor/commands?format=sh" | sh
\`\`\`
The commands go to \`~/.cursor/commands/\`.
:::tab Codex
\`\`\`bash
curl -fsSL "https://bipwire.com/install/codex/commands?format=sh" | sh
\`\`\`
The commands go to \`~/.codex/prompts/\`.
:::tab OpenCode
\`\`\`bash
curl -fsSL "https://bipwire.com/install/opencode/commands?format=sh" | sh
\`\`\`
The commands go to \`~/.config/opencode/commands/\`.
:::

**Step 2 — Connect the MCP server (once per machine).** It's the connection to BipWire; it holds your token, so it goes into your personal config, never into a repo. Replace \`bipwire_llm_your_token\` with your real token.

:::tabs
:::tab Claude Code
\`\`\`bash
claude mcp add --scope user --transport http bip https://bipwire.com/api/mcp \\
  --header "Authorization: Bearer bipwire_llm_your_token"
\`\`\`
:::tab Cursor
In \`~/.cursor/mcp.json\`:

\`\`\`json
{
  "mcpServers": {
    "bip": {
      "url": "https://bipwire.com/api/mcp",
      "headers": { "Authorization": "Bearer bipwire_llm_your_token" }
    }
  }
}
\`\`\`
:::tab Codex
In \`~/.codex/config.toml\`:

\`\`\`toml
[mcp_servers.bip]
url = "https://bipwire.com/api/mcp"
http_headers = { Authorization = "Bearer bipwire_llm_your_token" }
\`\`\`
:::tab OpenCode
In \`~/.config/opencode/opencode.json\` (merge with the existing content):

\`\`\`json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "bip": {
      "type": "remote",
      "url": "https://bipwire.com/api/mcp",
      "enabled": true,
      "headers": { "Authorization": "Bearer bipwire_llm_your_token" }
    }
  }
}
\`\`\`
:::

**Step 3 — Tell which project this repo queries (once per project).** Create a \`.bip.json\` at the root, the same whatever the tool:

\`\`\`bash
echo '{ "siteId": "your-project" }' > .bip.json
\`\`\`

On your next project, only this step needs redoing.

> All the details (guardrails, updating, older installs inside a repo): [Connect an agent](/docs/api/brancher).

Once in place, **restart your CLI agent** so it loads the MCP server and the commands.

## 6. List and fix the bugs

In your CLI agent, at the project root:

- **List what needs handling**:

  \`\`\`
  /bip-tasks
  \`\`\`

  → the agent reads \`.bip.json\`, queries BipWire, and shows the open tickets grouped by priority.

- **See where to start** (across all projects, critical + high):

  \`\`\`
  /bip-sort
  \`\`\`

- **Fix a specific bug**:

  \`\`\`
  /bip-fix 42
  \`\`\`

  → the agent marks the ticket "in progress", reads all the context (description, screenshots, console, network), fixes it in your code, then moves the ticket back to **"To test"** with a note explaining what it did.

- **Let the agent run on its own** (takes the highest-priority one and fixes it):

  \`\`\`
  /bip-next
  \`\`\`

You can also use natural language: "look at the latest BipWire bugs and fix the most urgent one" — the agent will use the MCP tools on its own.

## What's next?

- You test the fix, then you mark the ticket **"Resolved"** in the admin (the agent never closes it itself — you always validate after testing).
- Generate **one token per machine/project** with a meaningful label: if it leaks, you just revoke that one. See [API Keys](/docs/api/tokens).
- To connect several projects, repeat step 5 in each repo (one \`.bip.json\` per repo).
`,ya='# Intégration du widget\n\nUn seul `<script>`, en `async`, avant `</body>` :\n\n```html\n<script async src="https://bipwire.com/PUBLIC_KEY.js"><\/script>\n```\n\nL\'URL `PUBLIC_KEY.js` est un loader per-projet généré par l\'admin. Toggle "Widget activé" coupe le widget sans toucher au HTML du site (le fichier renvoie alors `"disabled";`). Régénérer la clé invalide l\'ancien chemin (404) — il faut alors recoller le nouveau snippet.\n\n**Restriction par chemin :** le projet peut limiter le widget à certains préfixes d\'URL via le champ "Activer uniquement sur certains chemins" de la fiche projet. La liste est bakée dans `PUBLIC_KEY.js` : si `location.pathname` ne commence par aucun préfixe, le bundle n\'est jamais chargé (return early). Vide = actif partout.\n\nMatch strict `startsWith` : `/hub/` match `/hub/x` et `/hub/` mais **pas** `/hub` (sans slash final). Pour activer aussi le bare `/hub`, ajouter `/hub` sur sa propre ligne.\n\n**Conseil :** stocker l\'URL du loader dans un `.env` côté application plutôt qu\'en dur dans le HTML — rotation de clé = changement d\'une variable, déploiement standard, pas de chasse aux templates.\n\n```bash\n# .env\nBIPWIRE_LOADER_URL=https://bipwire.com/PUBLIC_KEY.js\n```\n\nPuis injecter la valeur dans le template :\n\n```html\n<!-- Next.js / React (process.env côté server-rendered) -->\n<script async src={process.env.BIPWIRE_LOADER_URL}><\/script>\n\n<!-- Vite / SPA (préfixe VITE_ obligatoire) -->\n<script async src={import.meta.env.VITE_BIPWIRE_LOADER_URL}><\/script>\n\n<!-- PHP / Twig / Django / EJS -->\n<script async src="<?= getenv(\'BIPWIRE_LOADER_URL\') ?>"><\/script>\n```\n\nLe widget tourne en IIFE isolé : pas de pollution du global, pas de CSS injecté hors du drawer, pas de dépendance externe. Bundle ~16 KB gzip.\n\n## Personnaliser l\'apparence\n\nCouleur principale, label du header et couleur du texte se règlent via des attributs `data-*` sur le tag `<script>`. Les valeurs sont injectées en CSS variables, donc applicables à la bulle, au header, aux messages user et au bouton "Envoyer".\n\n```html\n<script\n  async\n  src="https://bipwire.com/PUBLIC_KEY.js"\n  data-color="#7c3aed"\n  data-text="#ffffff"\n  data-title="Mon site — Support">\n<\/script>\n```\n\n| Attribut | Effet | Défaut |\n|---|---|---|\n| `data-color` | Bulle flottante, header, bulle user, bouton envoyer | `#18181b` |\n| `data-text` | Couleur du texte posé sur le fond accent | `#ffffff` |\n| `data-title` | Titre affiché dans le header du drawer (max 40 caractères) | `Support` |\n| `data-theme` | Thème du panneau : `dark`, ou `auto` (suit le thème clair/sombre du site, et bascule avec lui : classe `dark`, `data-theme`, `data-bs-theme`… sur `<html>` ou `<body>`, sinon la couleur de fond de la page, sinon l\'appareil du visiteur). Le réglage « Thème du panneau » du projet (volet « Apparence », onglet « Couleurs ») prime | clair |\n| `data-dark-color`, `data-dark-text`, `data-dark-panel`, `data-dark-panel-text`, `data-dark-ai-bubble` | Couleurs du mode sombre (hex), lues avec `data-theme`. Les « Couleurs du mode sombre » du projet (volet « Apparence », onglet « Couleurs ») priment ; le studio les tire des styles sombres de la CSS du site | thème sombre standard |\n\nFormats acceptés : `#rrggbb`, `#rgb`, `rgb(…)`, `rgba(…)`, `hsl(…)`, mots-clés CSS. Une valeur invalide retombe sur le défaut sans erreur.\n\n## Emplacement du bouton\n\nRéglé dans la fiche du projet, volet « Apparence », onglet « Taille et position » :\n\n| Mode | Bouton |\n|---|---|\n| **Flottant** (défaut) | la bulle, fixe dans un coin de l\'écran |\n| **Intégré dans la page** | la bulle, posée **dans** l\'élément désigné par un sélecteur CSS (une barre d\'outils, par exemple) |\n| **Aucun** | pas de bouton BipWire : le bouton du site appelle `window.bip.open()` |\n\nMode intégré : poser un élément vide à l\'endroit voulu, et saisir son sélecteur (`#bipwire-bouton`) dans la console.\n\n```html\n<span id="bipwire-bouton"></span>\n```\n\n- L\'élément peut apparaître après le chargement (application React, Vue…) ou être redessiné à chaque navigation : la bulle l\'attend, et s\'y repose.\n- Élément introuvable : aucun bouton, un avertissement `[BipWire]` dans la console du navigateur. `window.bip.open()` et le raccourci clavier marchent toujours.\n- Taille et aspect s\'ajustent par des règles du site sur `.bippoprun-bubble`, par exemple `.bippoprun-bubble { width: 36px; height: 36px; box-shadow: none; }`.\n- Hors mode flottant, le réglage « coin » dit où s\'ouvre le panneau. Il se ferme par sa croix ou par Échap.\n\nMode aucun :\n\n```html\n<button type="button" onclick="window.bip.open()">Aide</button>\n```\n\n',Ca='# Widget integration\n\nA single `<script>`, `async`, before `</body>`:\n\n```html\n<script async src="https://bipwire.com/PUBLIC_KEY.js"><\/script>\n```\n\nThe `PUBLIC_KEY.js` URL is a per-project loader generated by the admin. The "Widget enabled" toggle turns the widget off without touching the site\'s HTML (the file then returns `"disabled";`). Regenerating the key invalidates the old path (404) — you then have to paste the new snippet again.\n\n**Path restriction:** the project can limit the widget to certain URL prefixes via the "Enable only on certain paths" field on the project page. The list is baked into `PUBLIC_KEY.js`: if `location.pathname` doesn\'t start with any prefix, the bundle is never loaded (early return). Empty = active everywhere.\n\nStrict `startsWith` match: `/hub/` matches `/hub/x` and `/hub/` but **not** `/hub` (without the trailing slash). To also enable the bare `/hub`, add `/hub` on its own line.\n\n**Tip:** store the loader URL in an `.env` on your app side rather than hardcoding it in the HTML — a key rotation becomes a single variable change, a standard deploy, no hunting through templates.\n\n```bash\n# .env\nBIPWIRE_LOADER_URL=https://bipwire.com/PUBLIC_KEY.js\n```\n\nThen inject the value into your template:\n\n```html\n<!-- Next.js / React (process.env, server-rendered) -->\n<script async src={process.env.BIPWIRE_LOADER_URL}><\/script>\n\n<!-- Vite / SPA (VITE_ prefix required) -->\n<script async src={import.meta.env.VITE_BIPWIRE_LOADER_URL}><\/script>\n\n<!-- PHP / Twig / Django / EJS -->\n<script async src="<?= getenv(\'BIPWIRE_LOADER_URL\') ?>"><\/script>\n```\n\nThe widget runs as an isolated IIFE: no global pollution, no CSS injected outside the drawer, no external dependency. Bundle ~16 KB gzipped.\n\n## Customize the appearance\n\nThe main color, header label, and text color are set via `data-*` attributes on the `<script>` tag. The values are injected as CSS variables, so they apply to the bubble, the header, user messages, and the "Send" button.\n\n```html\n<script\n  async\n  src="https://bipwire.com/PUBLIC_KEY.js"\n  data-color="#7c3aed"\n  data-text="#ffffff"\n  data-title="My site — Support">\n<\/script>\n```\n\n| Attribute | Effect | Default |\n|---|---|---|\n| `data-color` | Floating bubble, header, user bubble, send button | `#18181b` |\n| `data-text` | Color of the text placed on the accent background | `#ffffff` |\n| `data-title` | Title shown in the drawer header (max 40 characters) | `Support` |\n| `data-theme` | Panel theme: `dark`, or `auto` (follows the site\'s light/dark theme and switches with it: `dark` class, `data-theme`, `data-bs-theme`… on `<html>` or `<body>`, else the page background color, else the visitor\'s device). The project\'s "Panel theme" setting ("Appearance" section, "Colors" tab) takes precedence | light |\n| `data-dark-color`, `data-dark-text`, `data-dark-panel`, `data-dark-panel-text`, `data-dark-ai-bubble` | Dark mode colors (hex), read with `data-theme`. The project\'s "Dark mode colors" ("Appearance" section, "Colors" tab) take precedence; the studio derives them from the dark styles of the site\'s CSS | standard dark theme |\n\nAccepted formats: `#rrggbb`, `#rgb`, `rgb(…)`, `rgba(…)`, `hsl(…)`, CSS keywords. An invalid value falls back to the default without error.\n\n## Button placement\n\nSet in the project page, "Appearance" section, "Size and position" tab:\n\n| Mode | Button |\n|---|---|\n| **Floating** (default) | the bubble, fixed in a corner of the screen |\n| **Embedded in the page** | the bubble, placed **inside** the element matched by a CSS selector (a toolbar, for example) |\n| **None** | no BipWire button: the site\'s own button calls `window.bip.open()` |\n\nEmbedded mode: add an empty element where the button belongs, and enter its selector (`#bipwire-button`) in the console.\n\n```html\n<span id="bipwire-button"></span>\n```\n\n- The element may appear after page load (React, Vue… app) or be re-rendered on every navigation: the bubble waits for it, and moves back into it.\n- Element not found: no button, and a `[BipWire]` warning in the browser console. `window.bip.open()` and the keyboard shortcut still work.\n- Size and look are adjusted by the site\'s own rules on `.bippoprun-bubble`, e.g. `.bippoprun-bubble { width: 36px; height: 36px; box-shadow: none; }`.\n- Outside floating mode, the "corner" setting says where the panel opens. It closes with its close button or Escape.\n\nNone mode:\n\n```html\n<button type="button" onclick="window.bip.open()">Help</button>\n```\n\n',wa="# Personnaliser l'apparence\n\nLe widget est **isolé** des styles du site : il vit dans un Shadow DOM, si bien qu'une règle générique comme `button { padding: 40px }` ou `* { font-family: … }` ne peut plus le dérégler.\n\nPour les couleurs et le titre, commencer par les attributs `data-color`, `data-text` et `data-title` du `<script>` (voir [Intégration](/docs/widget/integration)). Pour aller plus loin, écrire les règles CSS **en nommant nos classes** : elles s'appliquent au widget.\n\n```css\n/* Dans la feuille de style du site */\n.bippoprun-bubble {\n  background: linear-gradient(135deg, #7c3aed, #db2777);\n}\n.bippoprun-drawer__title {\n  font-family: \"Inter\", sans-serif;\n}\n@media (max-width: 600px) {\n  .bippoprun-drawer { border-radius: 0; }\n}\n```\n\n## Les règles\n\n- **Une règle s'applique si l'un de ses sélecteurs nomme une classe `bippoprun-*`.** Les autres (sur `button`, `*`, `svg`…) n'atteignent jamais le widget.\n- **Elle l'emporte sur nos styles, sans `!important`**, quelle que soit la précision de nos sélecteurs. Seuls les états que le widget pose lui-même en direct (position d'une bulle déplacée, masquage pendant une capture) restent prioritaires.\n- **Ce qui précède notre classe dans le sélecteur est ignoré** : `#app .bippoprun-bubble` vaut `.bippoprun-bubble`. Le widget n'est pas un descendant de la page.\n- **Une classe citée seulement entre parenthèses ne compte pas** : `button:not(.bippoprun-bubble)` est une règle générique, elle n'entre pas.\n- `@media` et `@supports` sont respectés.\n- **Relu à chaque ouverture du widget** : une règle injectée après le chargement (application monopage) s'applique à l'ouverture suivante.\n\n## Où écrire les règles\n\nDans une balise `<style>` ou une feuille servie par **le** domaine du site. Une feuille d'un **autre domaine** (CDN) n'est lisible que si elle est servie avec CORS (`Access-Control-Allow-Origin`) et chargée avec `crossorigin` ; sinon le navigateur interdit de la lire, et ses règles ne passent pas.\n\n## Classes utiles\n\n| Classe | Élément |\n|---|---|\n| `.bippoprun-bubble` | La bulle flottante |\n| `.bippoprun-bubble--discrete` | La bulle réduite dans le coin, quand une feuille ou une boîte modale du site occupe le bas de l'écran sans laisser la place au-dessus |\n| `.bippoprun-drawer` | Le panneau |\n| `.bippoprun-drawer__header` | Bandeau du panneau |\n| `.bippoprun-drawer__title`, `.bippoprun-drawer__subtitle` | Titre et sous-titre |\n| `.bippoprun-msg__bubble` | Une bulle de message du chat |\n| `.bippoprun-bug__textarea` | Champ de description du signalement |\n| `.bippoprun-bug__capture-btn` | Boutons « capture » et « joindre un fichier » |\n| `.bippoprun-bug__submit` | Bouton d'envoi du signalement |\n| `.bippoprun-toast` | Message flottant (remerciement) |\n\nCes noms sont stables : nous ne les renommons pas.\n",Ta='# Customize the look\n\nThe widget is **isolated** from your site\'s styles: it lives in a Shadow DOM, so a generic rule such as `button { padding: 40px }` or `* { font-family: … }` can no longer break it.\n\nFor colors and title, start with the `data-color`, `data-text` and `data-title` attributes of the `<script>` (see [Integration](/docs/widget/integration)). To go further, write CSS rules **that name our classes**: they apply to the widget.\n\n```css\n/* In your site\'s stylesheet */\n.bippoprun-bubble {\n  background: linear-gradient(135deg, #7c3aed, #db2777);\n}\n.bippoprun-drawer__title {\n  font-family: "Inter", sans-serif;\n}\n@media (max-width: 600px) {\n  .bippoprun-drawer { border-radius: 0; }\n}\n```\n\n## The rules\n\n- **A rule applies when one of its selectors names a `bippoprun-*` class.** Others (on `button`, `*`, `svg`…) never reach the widget.\n- **It overrides our styles, no `!important` needed**, whatever the specificity of our selectors. Only the states the widget sets itself at runtime (position of a dragged bubble, hiding during a screenshot) keep priority.\n- **Whatever precedes our class in the selector is ignored**: `#app .bippoprun-bubble` means `.bippoprun-bubble`. The widget is not a descendant of your page.\n- **A class quoted only inside parentheses does not count**: `button:not(.bippoprun-bubble)` is a generic rule and stays out.\n- `@media` and `@supports` are honored.\n- **Re-read every time the widget opens**: a rule injected after load (single-page app) applies on the next opening.\n\n## Where to write your rules\n\nIn a `<style>` tag or a stylesheet served from **your** domain. A stylesheet from **another domain** (CDN) can only be read if it is served with CORS (`Access-Control-Allow-Origin`) and loaded with `crossorigin`; otherwise the browser forbids reading it and its rules do not get through.\n\n## Useful classes\n\n| Class | Element |\n|---|---|\n| `.bippoprun-bubble` | The floating bubble |\n| `.bippoprun-bubble--discrete` | The bubble shrunk into the corner, when a sheet or modal of the site fills the bottom of the screen with no room above it |\n| `.bippoprun-drawer` | The panel |\n| `.bippoprun-drawer__header` | Panel header |\n| `.bippoprun-drawer__title`, `.bippoprun-drawer__subtitle` | Title and subtitle |\n| `.bippoprun-msg__bubble` | A chat message bubble |\n| `.bippoprun-bug__textarea` | Report description field |\n| `.bippoprun-bug__capture-btn` | "Screenshot" and "attach a file" buttons |\n| `.bippoprun-bug__submit` | Report send button |\n| `.bippoprun-toast` | Floating message (thank-you) |\n\nThese names are stable: we do not rename them.\n',xa=`# Identifier l'utilisateur

Deux surfaces. Last-write-wins : un appel JS override le static. \`window.bip\` est posé par le loader dès qu'il s'exécute (avant tout clic, avant que \`b.js\` ne soit fetché) — \`identify\` peut être appelé à tout moment, les appels avant le mount du bundle sont queueés et appliqués dès qu'il est prêt.

## Option A — attributs \`data-*\` sur le script

Site server-rendered, identité connue au render :

\`\`\`html
<script
  async
  src="https://bipwire.com/PUBLIC_KEY.js"
  data-user-id="usr_123"
  data-user-email="alice@example.com"
  data-user-name="Alice Martin"
  data-user-meta='{"plan":"pro","seats":5}'>
<\/script>
\`\`\`

## Option B — JS dynamique

Cas SPA : l'user n'est connu qu'après login. Appel après login, \`reset\` au logout :

\`\`\`js
window.bip.identify({
  id: 'usr_123',
  email: 'alice@example.com',
  name: 'Alice Martin',
  metadata: { plan: 'pro', seats: 5 },
});
window.bip.reset();           // au logout
\`\`\`

## Legacy — pre-load queue façon GA

La pre-load queue reste supportée pour les intégrations existantes mais n'est plus nécessaire depuis que \`window.bip\` est posé par le loader (le stub queue lui-même dans \`bipQ\`) :

\`\`\`js
window.bipQ = window.bipQ || [];
bipQ.push(['identify', { id: 'usr_123', email: 'alice@example.com' }]);
bipQ.push(['reset']);         // également supporté
\`\`\`

## Contrôle programmatique

Disponibles sur \`window.bip\` (minuscule) :

| Méthode | Description |
|---|---|
| \`window.bip.open()\` | Ouvre le drawer chat, même sans clic préalable sur la bulle (avec l'emplacement « Aucun », pour ouvrir par un bouton du site). |
| \`window.bip.close()\` | Ferme le drawer. |
| \`window.bip.identify({...})\` | Met à jour l'identité visiteur (merge avec l'existant). |
| \`window.bip.reset()\` | Efface l'identité côté widget. Ne touche pas aux threads serveur. |

## Champs d'identification

| Champ | Type | Description |
|---|---|---|
| \`id\` | string ≤200 | Identifiant stable. Persiste les threads d'un même user entre sessions. |
| \`email\` | email | Affiché dans l'admin. **N'est jamais utilisé comme adresse de contact** — seul l'email saisi par le visiteur dans la banner chat sert aux notifications. |
| \`name\` | string ≤200 | Nom affiché dans l'admin. |
| \`metadata\` | object JSON | Forme libre (plan, segments, langue…), < 4 KB sérialisé. Lisible côté admin uniquement, jamais réinjecté dans le prompt Claude. |
`,Sa=`# Identify the user

Two surfaces. Last-write-wins: a JS call overrides the static one. \`window.bip\` is set by the loader as soon as it runs (before any click, before \`b.js\` is even fetched) — you can call \`identify\` anytime; calls made before the bundle mounts are queued and applied as soon as it's ready.

## Option A — \`data-*\` attributes on the script

Server-rendered site, identity known at render time:

\`\`\`html
<script
  async
  src="https://bipwire.com/PUBLIC_KEY.js"
  data-user-id="usr_123"
  data-user-email="alice@example.com"
  data-user-name="Alice Martin"
  data-user-meta='{"plan":"pro","seats":5}'>
<\/script>
\`\`\`

## Option B — dynamic JS

SPA case: the user is only known after login. Call after login, \`reset\` on logout:

\`\`\`js
window.bip.identify({
  id: 'usr_123',
  email: 'alice@example.com',
  name: 'Alice Martin',
  metadata: { plan: 'pro', seats: 5 },
});
window.bip.reset();           // on logout
\`\`\`

## Legacy — GA-style pre-load queue

The pre-load queue is still supported for existing integrations but is no longer needed now that \`window.bip\` is set by the loader (which is itself the queue stub in \`bipQ\`):

\`\`\`js
window.bipQ = window.bipQ || [];
bipQ.push(['identify', { id: 'usr_123', email: 'alice@example.com' }]);
bipQ.push(['reset']);         // also supported
\`\`\`

## Programmatic control

Available on \`window.bip\` (lowercase):

| Method | Description |
|---|---|
| \`window.bip.open()\` | Opens the chat drawer, even before any click on the bubble (with the 'None' button placement, to open from the site's own button). |
| \`window.bip.close()\` | Closes the drawer. |
| \`window.bip.identify({...})\` | Updates the visitor identity (merges with the existing one). |
| \`window.bip.reset()\` | Clears the identity on the widget side. Does not touch server-side conversations. |

## Identification fields

| Field | Type | Description |
|---|---|---|
| \`id\` | string ≤200 | Stable identifier. Keeps a user's conversations linked across sessions. |
| \`email\` | email | Shown in the admin. **Never used as a contact address** — only the email the visitor enters in the chat banner is used for notifications. |
| \`name\` | string ≤200 | Name shown in the admin. |
| \`metadata\` | JSON object | Free-form (plan, segments, language…), < 4 KB serialized. Readable in the admin only, never re-injected into the Claude prompt. |
`,ja="# Comment ça marche\n\nLe visiteur envoie un message → POST `/api/public/chat` → BipWire persiste le thread, appelle **Gemini 2.5 Flash Lite** (Claude Haiku 4.5 en fallback) avec le *system prompt* du projet, retourne la réponse.\n\nLe LLM peut décider d'appeler l'outil `escalate` (selon les *escape hatches* définies dans le system prompt). Le thread passe alors en `awaiting_human`, BipWire demande l'email du visiteur si pas connu, ping Telegram et envoie un mail de confirmation.\n\nPendant un thread `awaiting_human`, le widget écoute le SSE sur `/api/public/thread/:id/stream` pour récupérer les réponses envoyées via l'admin (polling toutes les 30 s en fallback si SSE échoue).\n\nL'identité visiteur est stockée dans un cookie `bipwire_visitor` (UUID v4, 1 an) — c'est ce qui rebranche un même visiteur sur ses threads entre deux visites.\n",Pa="# How it works\n\nThe visitor sends a message → POST `/api/public/chat` → BipWire persists the conversation, calls **Gemini 2.5 Flash Lite** (Claude Haiku 4.5 as a fallback) with the project's *system prompt*, and returns the reply.\n\nThe LLM can decide to call the `escalate` tool (based on the *escape hatches* defined in the system prompt). The conversation then moves to `awaiting_human`, BipWire asks for the visitor's email if it isn't known, pings Telegram, and sends a confirmation email.\n\nDuring an `awaiting_human` conversation, the widget listens to the SSE stream on `/api/public/thread/:id/stream` to pick up the replies sent from the admin (falling back to polling every 30 s if SSE fails).\n\nThe visitor identity is stored in a `bipwire_visitor` cookie (UUID v4, 1 year) — that's what reconnects a returning visitor to their conversations between two visits.\n",Ea="# CSP et protections du site hôte\n\nLe widget s'exécute **sur le site du client**, donc sous ses règles de\nsécurité à lui. Si ce site envoie une Content-Security-Policy (CSP), c'est le\nnavigateur qui bloque BipWire, sans que rien ne nous parvienne : la bulle\nn'apparaît pas, ou la capture d'écran échoue, et l'assistant d'activation reste\nsur « En attente du premier chargement ».\n\nCette page dit ce qu'il faut ouvrir, et pourquoi. Tout ce qu'elle affirme est\nmesuré par un test navigateur (`e2e/widget/csp.spec.js`) : une page hôte sous\nCSP stricte, et un témoin par directive qui montre ce qui casse quand elle\nmanque.\n\n## En 30 secondes\n\n1. Ouvrir le site, console du navigateur ouverte (F12).\n2. Un message contient **« violates the following Content Security Policy\n   directive »** ? Le site a une CSP, et elle bloque le widget.\n3. Ajouter les valeurs du tableau ci-dessous à **la CSP du site** — pas à\n   BipWire, qui ne peut pas la modifier.\n\nAucun message CSP et pas de bulle ? Voir « Autres causes », plus bas.\n\n## Les directives\n\n| Directive | Valeur à ajouter | Pourquoi | Si elle manque |\n|---|---|---|---|\n| `script-src` | `https://bipwire.com` | Le loader (`bip_pk_….js`), puis `b.js` et ses morceaux chargés à la demande | Pas de bulle du tout |\n| `connect-src` | `https://bipwire.com` | Configuration du projet, chat, envoi des signalements, réponses en temps réel (SSE) | La bulle s'affiche, le tiroir reste vide ou « Failed to fetch » |\n| `style-src` | `'unsafe-inline'` | Le widget injecte sa feuille de style dans la page (`<style>`) | Bulle et tiroir sans mise en forme |\n| `img-src` | `data:` | La capture d'écran rasterise la page via une image `data:image/svg+xml` | « Impossible de capturer la page » au signalement d’un bug |\n| `img-src` | `blob:` | L'éditeur d'annotation et les vignettes affichent la capture via `URL.createObjectURL` | L'éditeur de capture ne s'ouvre pas |\n| `worker-src` | `blob:` | La capture réduit les images lourdes de la page dans un Worker créé depuis une URL `blob:` | La capture se fait quand même (repli sur le thread principal), mais chaque capture d'une page à image lourde relève une violation CSP |\n\n⚠️ **`worker-src` absente se rabat sur `script-src`**, qui ne contient pas\n`blob:`. Si le site a déjà un `worker-src` (service worker, PWA), y **ajouter**\n`blob:` sans retirer `'self'`.\n\nAucune `font-src`, `frame-src` ni `media-src` n'est nécessaire : polices\nsystème, pas d'iframe.\n\n**Images servies par un autre domaine** (CDN, S3, sous-domaine d'images) : la\ncapture d'écran relit chaque image de la page par `fetch`. Leur domaine doit\ndonc figurer **aussi dans `connect-src`**, pas seulement dans `img-src` ; sinon\nla capture se fait, mais ces images y sortent blanches (console :\n`Connecting to '…' violates … \"connect-src …\"`). Le serveur d'images doit en\nplus répondre `Access-Control-Allow-Origin` — voir\n[Screenshots & cross-origin](/docs/widget/screenshots).\n\n## En-tête minimal\n\n```\nContent-Security-Policy:\n  default-src 'self';\n  script-src  'self' https://bipwire.com;\n  connect-src 'self' https://bipwire.com;\n  style-src   'self' 'unsafe-inline';\n  img-src     'self' data: blob:;\n  worker-src  'self' blob:;\n```\n\n⚠️ **On ajoute aux directives existantes, on ne les remplace pas.** Si le\n`script-src` contient déjà `https://cdn.exemple.com`, il devient\n`script-src 'self' https://cdn.exemple.com https://bipwire.com`.\n\n⚠️ **`default-src` est un repli, pas un cumul.** Dès qu'une directive\nspécifique existe, `default-src` ne s'applique plus à elle. Avec un\n`default-src 'self'` seul, les six lignes doivent être écrites.\n\n## Où se trouve la CSP du site\n\nElle peut venir de **plusieurs** endroits, et le navigateur applique la plus\nstricte :\n\n- un en-tête HTTP posé par le serveur (Nginx, Caddy, Apache), le framework\n  (Next.js `headers()`, Helmet pour Express, `SecurityMiddleware` de Django…)\n  ou l'hébergeur (`vercel.json`, `_headers` de Netlify, règle Cloudflare) ;\n- une balise `<meta http-equiv=\"Content-Security-Policy\">` dans le `<head>`.\n\nPour voir celle qui s'applique vraiment : DevTools → Réseau → la requête du\ndocument HTML → En-têtes de réponse → `content-security-policy`. Si la valeur\nn'est pas celle de la configuration, un proxy ou un CDN la réécrit :\nla corriger là où elle est émise.\n\n## CSP à nonce ou `'strict-dynamic'`\n\n**Scripts** — poser le nonce sur la balise du loader :\n\n```html\n<script async nonce=\"MON_NONCE\" src=\"https://bipwire.com/bip_pk_….js\"><\/script>\n```\n\nAvec `'strict-dynamic'`, cela suffit : la confiance se propage à `b.js` et aux\nmorceaux chargés ensuite (mesuré). Sans `'strict-dynamic'`, garder\n`https://bipwire.com` dans `script-src`.\n\n**Styles** — ⚠️ limite connue. Un nonce dans `style-src` fait ignorer\n`'unsafe-inline'` par le navigateur, et les feuilles injectées par le widget ne\nportent pas le nonce : elles sont refusées. Si le `style-src` porte un\nnonce, le widget s'affiche sans mise en forme. Contournement : pas de nonce dans\n`style-src` (le nonce reste utile dans `script-src`, là où il protège vraiment).\n\n## Trusted Types — non compatible\n\nUne CSP qui contient `require-trusted-types-for 'script'` empêche le widget de\nse monter : il construit son interface par `innerHTML`. Mesuré, pas de\ncontournement côté BipWire aujourd'hui. Un site sous Trusted Types doit définir\nune politique `default` pour que le widget fonctionne.\n\n## Les autres en-têtes de sécurité\n\n| En-tête | Effet sur BipWire |\n|---|---|\n| `Cross-Origin-Embedder-Policy: require-corp` | Compatible : BipWire sert ses fichiers avec `Cross-Origin-Resource-Policy: cross-origin`. |\n| `Cross-Origin-Opener-Policy` | Aucun : le widget n'ouvre pas de fenêtre. |\n| `Referrer-Policy` (même `no-referrer`) | Aucun : l'autorisation du projet repose sur l'en-tête `Origin`, que le navigateur envoie toujours. |\n| `Permissions-Policy` | Aucun : le widget n'utilise ni caméra, ni micro, ni partage d'écran — la capture est une copie du DOM. |\n| `X-Frame-Options`, `frame-ancestors` | Aucun : le widget n'est pas une iframe. |\n| `Strict-Transport-Security` | Aucun. Mais une page servie en `http://` ne peut pas appeler BipWire, sauf sur `localhost`. |\n| Subresource Integrity (`integrity=\"…\"`) | ⛔ **À ne pas mettre** sur la balise du loader : il change à chaque réglage du projet (couleur, position, raccourci…), et le hash cesserait de correspondre. |\n\n## Autres causes\n\nPas de bulle et **aucun** message CSP en console :\n\n- **Origine non déclarée.** Le navigateur refuse les réponses de l'API si\n  l'origine exacte du site n'est pas dans le champ « Origines » du projet\n  (erreur « blocked by CORS policy »). `https://exemple.com` et\n  `https://www.exemple.com` sont deux origines ; pas de joker.\n- **Optimiseur de JavaScript.** Rocket Loader (Cloudflare), les extensions de\n  minification ou de « report de JS » (WP Rocket, Autoptimize…) peuvent\n  réécrire ou regrouper la balise. Exclure `bipwire.com` de l'optimisation.\n- **Cache.** Le HTML servi est une version antérieure à la pose de la balise :\n  vider le cache du CDN, recharger en navigation privée.\n- **Widget éteint ou chemin exclu.** Toggle « Widget activé », puis\n  « Patterns de path » sur la fiche du projet.\n\nChaque message de console, associé à la directive en cause :\n[FAQ erreurs CSP](/docs/widget/csp-faq).\n\n## Limites côté serveur\n\n- Le backend n'accepte `/api/public/*` que depuis une origine déclarée sur le\n  projet. HTTPS obligatoire, sauf `localhost`.\n- Rate-limit **30 messages / min** par (IP, projet) sur `/api/public/chat`.\n- Taille des requêtes : 64 Ko pour le chat, 8 Mo pour un signalement (capture\n  WebP).\n- « Régénérer la clé » sur le projet invalide immédiatement toutes les balises\n  déjà posées (utile en cas de fuite).\n\nDoc à transmettre au développeur ou à l'IA qui fait l'intégration :\n`https://bipwire.com/doc` (markdown, pensé pour être collé dans un prompt).\n",Aa=`# CSP & host-site protections

The widget runs **on your customer's site**, so under that site's security
rules. If the site sends a Content-Security-Policy (CSP), the browser blocks
BipWire and nothing ever reaches us: the bubble doesn't show up, or the
screenshot fails, and the activation wizard stays on "Waiting for the widget's
first load…".

This page says what to open, and why. Everything it states is measured by a
browser test (\`e2e/widget/csp.spec.js\`): a host page under a strict CSP, plus
one control per directive showing what breaks when it is missing.

## In 30 seconds

1. Open your site with the browser console open (F12).
2. A message says **"violates the following Content Security Policy
   directive"**? Your site has a CSP, and it blocks the widget.
3. Add the values from the table below to **your site's CSP** — not to
   BipWire, which can't change it.

No CSP message and no bubble? See "Other causes" further down.

## The directives

| Directive | Value to add | Why | If it's missing |
|---|---|---|---|
| \`script-src\` | \`https://bipwire.com\` | The loader (\`bip_pk_….js\`), then \`b.js\` and its on-demand chunks | No bubble at all |
| \`connect-src\` | \`https://bipwire.com\` | Project config, chat, bug report upload, real-time replies (SSE) | The bubble shows, the drawer stays empty or "Failed to fetch" |
| \`style-src\` | \`'unsafe-inline'\` | The widget injects its stylesheet into the page (\`<style>\`) | Unstyled bubble and drawer |
| \`img-src\` | \`data:\` | The screenshot rasterizes the page through a \`data:image/svg+xml\` image | "Couldn't capture the page" when reporting a bug |
| \`img-src\` | \`blob:\` | The annotation editor and thumbnails display the capture through \`URL.createObjectURL\` | The screenshot editor never opens |
| \`worker-src\` | \`blob:\` | The screenshot downscales the page's heavy images in a Worker created from a \`blob:\` URL | The capture still happens (it falls back to the main thread), but every capture of a page with a heavy image raises a CSP violation |

⚠️ **A missing \`worker-src\` falls back to \`script-src\`**, which doesn't list
\`blob:\`. If the site already has a \`worker-src\` (service worker, PWA), **add**
\`blob:\` to it without removing \`'self'\`.

No \`font-src\`, \`frame-src\` or \`media-src\` is needed: system fonts, no iframe.

**Images served from another domain** (CDN, S3, an images subdomain): the
screenshot re-reads every image on the page with \`fetch\`. Their domain must
therefore **also be in \`connect-src\`**, not just \`img-src\`; otherwise the
capture happens, but those images come out blank (console:
\`Connecting to '…' violates … "connect-src …"\`). The image server must also
answer with \`Access-Control-Allow-Origin\` — see
[Screenshots & cross-origin](/docs/widget/screenshots).

## Minimal header

\`\`\`
Content-Security-Policy:
  default-src 'self';
  script-src  'self' https://bipwire.com;
  connect-src 'self' https://bipwire.com;
  style-src   'self' 'unsafe-inline';
  img-src     'self' data: blob:;
  worker-src  'self' blob:;
\`\`\`

⚠️ **Add to your existing directives, don't replace them.** If your
\`script-src\` already lists \`https://cdn.example.com\`, it becomes
\`script-src 'self' https://cdn.example.com https://bipwire.com\`.

⚠️ **\`default-src\` is a fallback, not a sum.** As soon as a specific directive
exists, \`default-src\` no longer applies to it. With a lone \`default-src 'self'\`,
all six lines must be written out.

## Where your site's CSP comes from

It can come from **several** places, and the browser enforces the strictest:

- an HTTP header set by the server (Nginx, Caddy, Apache), the framework
  (Next.js \`headers()\`, Helmet for Express, Django's \`SecurityMiddleware\`…)
  or the host (\`vercel.json\`, Netlify \`_headers\`, a Cloudflare rule);
- a \`<meta http-equiv="Content-Security-Policy">\` tag in the \`<head>\`.

To see the one actually enforced: DevTools → Network → the HTML document
request → Response headers → \`content-security-policy\`. If the value isn't the
one from your config, a proxy or CDN rewrites it: fix it where it is emitted.

## Nonce-based CSP or \`'strict-dynamic'\`

**Scripts** — put your nonce on the loader tag:

\`\`\`html
<script async nonce="YOUR_NONCE" src="https://bipwire.com/bip_pk_….js"><\/script>
\`\`\`

With \`'strict-dynamic'\`, that's enough: trust propagates to \`b.js\` and the
chunks loaded afterwards (measured). Without \`'strict-dynamic'\`, keep
\`https://bipwire.com\` in \`script-src\`.

**Styles** — ⚠️ known limitation. A nonce in \`style-src\` makes the browser
ignore \`'unsafe-inline'\`, and the stylesheets the widget injects don't carry
your nonce: they get refused. If your \`style-src\` has a nonce, the widget shows
up unstyled. Workaround: no nonce in \`style-src\` (keep it in \`script-src\`,
where it actually protects you).

## Trusted Types — not supported

A CSP containing \`require-trusted-types-for 'script'\` stops the widget from
mounting: it builds its interface with \`innerHTML\`. Measured, no BipWire-side
workaround today. A site under Trusted Types needs a \`default\` policy for the
widget to work.

## Other security headers

| Header | Effect on BipWire |
|---|---|
| \`Cross-Origin-Embedder-Policy: require-corp\` | Compatible: BipWire serves its files with \`Cross-Origin-Resource-Policy: cross-origin\`. |
| \`Cross-Origin-Opener-Policy\` | None: the widget opens no window. |
| \`Referrer-Policy\` (even \`no-referrer\`) | None: project authorization relies on the \`Origin\` header, which the browser always sends. |
| \`Permissions-Policy\` | None: the widget uses no camera, mic or screen sharing — the screenshot is a copy of the DOM. |
| \`X-Frame-Options\`, \`frame-ancestors\` | None: the widget isn't an iframe. |
| \`Strict-Transport-Security\` | None. But a page served over \`http://\` can't call BipWire, except on \`localhost\`. |
| Subresource Integrity (\`integrity="…"\`) | ⛔ **Don't use it** on the loader tag: the loader changes with every project setting (color, position, shortcut…), and the hash would stop matching. |

## Other causes

No bubble and **no** CSP message in the console:

- **Origin not declared.** The browser refuses API responses if the site's
  exact origin isn't in the project's "Origins" field ("blocked by CORS
  policy"). \`https://example.com\` and \`https://www.example.com\` are two
  origins; no wildcard.
- **JavaScript optimizer.** Rocket Loader (Cloudflare) and minify or "delay JS"
  plugins (WP Rocket, Autoptimize…) can rewrite or bundle the tag. Exclude
  \`bipwire.com\` from optimization.
- **Cache.** The HTML being served predates the tag: purge the CDN cache,
  reload in a private window.
- **Widget off or path excluded.** "Widget enabled" toggle, then "Path
  patterns" on the project page.

Every console message, mapped to the offending directive:
[CSP error FAQ](/docs/widget/csp-faq).

## Server-side limits

- The backend only accepts \`/api/public/*\` from an origin declared on the
  project. HTTPS required, except \`localhost\`.
- Rate limit of **30 messages / min** per (IP, project) on \`/api/public/chat\`.
- Request size: 64 KB for chat, 8 MB for a bug report (WebP screenshot).
- "Regenerate key" on the project instantly invalidates every tag already in
  place (useful after a leak).

Doc to hand to the developer or AI doing the integration:
\`https://bipwire.com/doc\` (markdown, built to be pasted into a prompt).
`,Da="# FAQ : erreurs CSP\n\nLa Content-Security-Policy (CSP) est une liste blanche que le site hôte envoie au\nnavigateur. Une CSP qui ne mentionne pas BipWire bloque le widget et écrit une\nviolation en console. Le tableau ci-dessous associe chaque message à la ligne à\ncorriger ; le pourquoi de chaque directive est dans\n[CSP et protections du site hôte](/docs/widget/securite).\n\n## En-tête minimal\n\n```\nContent-Security-Policy:\n  default-src 'self';\n  script-src  'self' https://bipwire.com;\n  connect-src 'self' https://bipwire.com;\n  style-src   'self' 'unsafe-inline';\n  img-src     'self' data: blob:;\n  worker-src  'self' blob:;\n```\n\nÀ **ajouter** à la CSP existante du site, directive par directive.\n\n## Identifier la directive en cause\n\nLe terme qui suit « violates the following Content Security Policy directive »\nindique la directive à corriger.\n\n| Message console (extrait) | Symptôme | Correctif |\n|---|---|---|\n| `Loading the script 'https://bipwire.com/bip_pk_….js' violates … \"script-src 'self'\"` | Pas de bulle | `script-src` += `https://bipwire.com` |\n| `Refused to load the script 'https://bipwire.com/b.js' …` | Bulle visible, rien au clic (loader autorisé par nonce seul) | `script-src` += `https://bipwire.com`, ou `'strict-dynamic'` |\n| `Refused to connect to 'https://bipwire.com/api/public/…' … \"connect-src 'self'\"` | Tiroir vide, « Failed to fetch » | `connect-src` += `https://bipwire.com` |\n| `Refused to apply inline style … \"style-src 'self'\"` | Widget sans mise en forme | `style-src` += `'unsafe-inline'` |\n| `Loading the image 'data:image/svg+xml…' violates … \"img-src …\"` | « Impossible de capturer la page » | `img-src` += `data:` |\n| `Loading the image 'blob:https://…' violates … \"img-src …\"` | L'éditeur de capture ne s'ouvre pas | `img-src` += `blob:` |\n| `Creating a worker from 'blob:https://…' violates … \"script-src …\"` | Aucun : la capture se fait, la violation est seulement relevée | `worker-src` += `blob:` (et `'self'` s'il n'existait pas) |\n| `Connecting to 'https://cdn.…/image.png' violates … \"connect-src …\"` | Une image de la page sort blanche dans la capture | `connect-src` += le domaine des images |\n| `… requires 'TrustedHTML' assignment` | Pas de bulle | Trusted Types : voir la page de référence |\n\n## Pièges fréquents\n\n- **`script-src-elem` explicite.** S'il est défini, il remplace `script-src`\n  pour les balises `<script src>` : `https://bipwire.com` doit figurer dans les\n  deux.\n- **`default-src` est un repli, pas un cumul.** Une directive spécifique\n  remplace le repli ; y reporter les valeurs existantes (`'self'`, les CDN…).\n- **`'self'` ne couvre que le domaine du site.** `https://bipwire.com` est requis tel\n  quel.\n- **Un nonce dans `style-src`** fait ignorer `'unsafe-inline'` : les styles du\n  widget sont refusés. Garder le nonce pour `script-src`.\n- **Deux sources de CSP.** En-tête HTTP **et** `<meta http-equiv>` : le\n  navigateur applique la plus stricte. Un CDN ou un proxy (Cloudflare, Vercel,\n  Netlify, WAF) peut aussi en injecter une.\n- **`Content-Security-Policy-Report-Only`** journalise sans bloquer : le widget\n  fonctionne, les violations restent en console. Pratique pour valider une CSP\n  avant de l'appliquer.\n- **« blocked by CORS policy »** n'est pas une erreur CSP : c'est l'origine du\n  site qui n'est pas déclarée sur le projet (et `www.` compte).\n\n## Vérification\n\n1. DevTools → Réseau → requête du document HTML → En-têtes de réponse →\n   `content-security-policy` : la valeur réellement appliquée.\n2. Recharger console ouverte : chaque violation nomme sa directive.\n3. Après correction, vider le cache (la CSP est mise en cache avec le HTML) et\n   recharger en navigation privée.\n4. Signaler un bug de test depuis la bulle, capture comprise : c'est le seul\n   geste qui éprouve toutes les directives d'un coup (`worker-src` seulement si la\n   page porte une image lourde).\n\nDoc à transmettre au développeur en charge de l'intégration :\n`https://bipwire.com/doc`.\n",La=`# FAQ: CSP errors

The Content-Security-Policy (CSP) is an allowlist the host site sends to the
browser. A CSP that doesn't mention BipWire blocks the widget and logs a
violation in the console. The table below maps each message to the line to
fix; the reason behind each directive is in
[CSP & host-site protections](/docs/widget/securite).

## Minimal header

\`\`\`
Content-Security-Policy:
  default-src 'self';
  script-src  'self' https://bipwire.com;
  connect-src 'self' https://bipwire.com;
  style-src   'self' 'unsafe-inline';
  img-src     'self' data: blob:;
  worker-src  'self' blob:;
\`\`\`

To be **added** to the site's existing CSP, directive by directive.

## Identifying the offending directive

The term following "violates the following Content Security Policy directive"
names the directive to fix.

| Console message (excerpt) | Symptom | Fix |
|---|---|---|
| \`Loading the script 'https://bipwire.com/bip_pk_….js' violates … "script-src 'self'"\` | No bubble | \`script-src\` += \`https://bipwire.com\` |
| \`Refused to load the script 'https://bipwire.com/b.js' …\` | Bubble visible, nothing on click (loader allowed by nonce only) | \`script-src\` += \`https://bipwire.com\`, or \`'strict-dynamic'\` |
| \`Refused to connect to 'https://bipwire.com/api/public/…' … "connect-src 'self'"\` | Empty drawer, "Failed to fetch" | \`connect-src\` += \`https://bipwire.com\` |
| \`Refused to apply inline style … "style-src 'self'"\` | Unstyled widget | \`style-src\` += \`'unsafe-inline'\` |
| \`Loading the image 'data:image/svg+xml…' violates … "img-src …"\` | "Couldn't capture the page" | \`img-src\` += \`data:\` |
| \`Loading the image 'blob:https://…' violates … "img-src …"\` | The screenshot editor never opens | \`img-src\` += \`blob:\` |
| \`Creating a worker from 'blob:https://…' violates … "script-src …"\` | None: the capture happens, the violation is only reported | \`worker-src\` += \`blob:\` (plus \`'self'\` if it didn't exist) |
| \`Connecting to 'https://cdn.…/image.png' violates … "connect-src …"\` | An image from the page comes out blank in the screenshot | \`connect-src\` += the images' domain |
| \`… requires 'TrustedHTML' assignment\` | No bubble | Trusted Types: see the reference page |

## Common pitfalls

- **Explicit \`script-src-elem\`.** When set, it replaces \`script-src\` for
  \`<script src>\` tags: \`https://bipwire.com\` must appear in both.
- **\`default-src\` is a fallback, not a sum.** A specific directive replaces
  the fallback; carry over the existing values (\`'self'\`, your CDNs…).
- **\`'self'\` only covers your own domain.** \`https://bipwire.com\` is required
  as is.
- **A nonce in \`style-src\`** makes the browser ignore \`'unsafe-inline'\`: the
  widget's styles get refused. Keep the nonce for \`script-src\`.
- **Two CSP sources.** HTTP header **and** \`<meta http-equiv>\`: the browser
  enforces the strictest. A CDN or proxy (Cloudflare, Vercel, Netlify, WAF)
  can inject one too.
- **\`Content-Security-Policy-Report-Only\`** logs without blocking: the widget
  works, violations remain in the console. Handy to validate a CSP before
  enforcing it.
- **"blocked by CORS policy"** is not a CSP error: the site's origin isn't
  declared on the project (and \`www.\` counts).

## Checking

1. DevTools → Network → HTML document request → Response headers →
   \`content-security-policy\`: the value actually enforced.
2. Reload with the console open: each violation names its directive.
3. After fixing, clear the cache (the CSP is cached along with the HTML) and
   reload in a private window.
4. Report a test bug from the bubble, screenshot included: it's the one
   gesture that exercises every directive at once (\`worker-src\` only if the
   page carries a heavy image).

Doc to hand to the developer in charge of the integration:
\`https://bipwire.com/doc\`.
`,Ia=`# Screenshots & images cross-origin

Quand le visiteur clique sur "Signaler un souci", BipWire prend automatiquement un screenshot full-page de la page hôte via **snapDOM** (joint au rapport, visible dans l'admin).

Pour produire l'image, le navigateur lit chaque \`<img>\` du DOM avec \`canvas.drawImage(...)\`. Si une image vient d'un autre domaine (CDN, S3, sous-domaine séparé, etc.) **et n'est pas autorisée par CORS**, le canvas devient "tainted" et l'image apparaît **blanche** dans la capture — avec un warning dans la console du visiteur :

\`\`\`
[snapDOM] Network/CORS issue while fetching dataURL https://… A proxy may be required
\`\`\`

Le screenshot **continue** d'être produit (le reste de la page est OK), mais l'image incriminée est manquante.

⚠️ Le même avertissement apparaît quand **la CSP du site** interdit ce domaine dans \`connect-src\` : snapDOM relit chaque image par \`fetch\`. Si le site a une CSP, le domaine des images doit y figurer dans \`connect-src\`, en plus du CORS ci-dessous — voir [CSP et protections du site hôte](/docs/widget/securite).

## Comment l'éviter

Deux changements coordonnés sont nécessaires — **les deux** sont indispensables, l'un sans l'autre ne sert à rien.

### 1. Côté serveur — autoriser le CORS

Ajouter le header \`Access-Control-Allow-Origin: *\` sur les réponses des images.

**Caddy** :

\`\`\`caddyfile
@public-images path /screenshots/* /uploads/* /img/*
header @public-images Access-Control-Allow-Origin "*"
\`\`\`

**Nginx** :

\`\`\`nginx
location /screenshots/ {
  add_header Access-Control-Allow-Origin "*";
}
\`\`\`

**Cloudflare R2 / S3** : poser une CORS policy avec \`AllowedOrigins: ["*"]\` sur le bucket (ou plus restrictif si l'origine appelante est connue).

### 2. Côté HTML — passer le tag \`<img>\` en mode CORS

\`\`\`html
<img src="https://shotcv.com/screenshots/01-refit.png" crossorigin="anonymous" alt="…" />
\`\`\`

Sans cet attribut, le browser charge l'image en mode "no-CORS" (sans vérifier \`Access-Control-Allow-Origin\`). L'image s'affiche normalement, mais elle pollue le canvas → snapDOM ne peut pas l'inliner.

Avec un framework :

\`\`\`jsx
// React / Preact
<img src={url} crossorigin="anonymous" alt="…" />

// Vue
<img :src="url" crossorigin="anonymous" alt="…" />

// Astro / Svelte : pareil — attribut HTML standard
\`\`\`

## Ordre de déploiement

⚠️ Déployer **d'abord** le header CORS côté serveur, **ensuite** seulement le \`crossorigin\` côté HTML.

Sinon : le browser refuse les images marquées \`crossorigin\` qui n'ont pas répondu avec \`Access-Control-Allow-Origin\` → elles tombent toutes en \`onError\` et restent cassées jusqu'à ce que le serveur soit corrigé.

## Pourquoi les deux sont nécessaires

| | Sans \`crossorigin\` côté HTML | Avec \`crossorigin="anonymous"\` |
|---|---|---|
| **Sans header CORS serveur** | Image affichée ; canvas tainted ; snapDOM warn | Image refusée (\`onError\`) — pire |
| **Avec header CORS serveur** | Image affichée ; canvas tainted (le browser ne fait pas la request CORS spontanément) | ✅ Image affichée + utilisable par canvas |

Le \`crossorigin\` *demande* le mode CORS au browser ; le header \`Access-Control-Allow-Origin\` *autorise* le canvas à consommer la réponse. Les deux sont coordonnés.

## Si l'image n'est pas contrôlée (CDN tiers, image hotlinked)

Rien n'est possible côté front. Le warn console reste, l'image manque dans le screenshot, mais le reste du rapport part normalement (description, autres images, console, requêtes).

## Exemple concret — shotcv.com

Diagnostic récent sur la prod :

- Toutes les images \`/screenshots/*.png\` étaient servies par Caddy **sans** header CORS
- Le widget BipWire logguait \`[snapDOM] Network/CORS issue while fetching dataURL https://shotcv.com/screenshots/01-refit.png\` à chaque rapport
- Les captures envoyées montraient ces images blanches dans l'éditeur d'annotation

Fix appliqué :

1. Bloc Caddy \`shotcv.com\` → ajout d'un \`@screenshots path /screenshots/*\` avec \`header @screenshots Access-Control-Allow-Origin "*"\`
2. \`apps/web/src/components/FeaturesCarousel.tsx\` → \`crossorigin="anonymous"\` sur la balise \`<img>\` du carousel

Après reload Caddy + redeploy du front : warn disparu, images présentes dans les screenshots.
`,_a=`# Screenshots & cross-origin images

When the visitor clicks "Report an issue", BipWire automatically takes a full-page screenshot of the host page via **snapDOM** (attached to the report, visible in the admin).

To produce the image, the browser reads each \`<img>\` in the DOM with \`canvas.drawImage(...)\`. If an image comes from another domain (CDN, S3, separate subdomain, etc.) **and isn't allowed by CORS**, the canvas becomes "tainted" and the image appears **blank** in the capture — with a warning in the visitor's console:

\`\`\`
[snapDOM] Network/CORS issue while fetching dataURL https://… A proxy may be required
\`\`\`

The screenshot is **still** produced (the rest of the page is fine), but the offending image is missing.

⚠️ The same warning shows up when **the site's CSP** forbids that domain in \`connect-src\`: snapDOM re-reads every image with \`fetch\`. If the site has a CSP, the images' domain must be in its \`connect-src\`, on top of the CORS below — see [CSP & host-site protections](/docs/widget/securite).

## How to avoid it

Two coordinated changes are needed — **both** are essential; one without the other is useless.

### 1. Server side — allow CORS

Add the \`Access-Control-Allow-Origin: *\` header to the image responses.

**Caddy**:

\`\`\`caddyfile
@public-images path /screenshots/* /uploads/* /img/*
header @public-images Access-Control-Allow-Origin "*"
\`\`\`

**Nginx**:

\`\`\`nginx
location /screenshots/ {
  add_header Access-Control-Allow-Origin "*";
}
\`\`\`

**Cloudflare R2 / S3**: set a CORS policy with \`AllowedOrigins: ["*"]\` on the bucket (or something stricter if you know the calling origin).

### 2. HTML side — switch the \`<img>\` tag to CORS mode

\`\`\`html
<img src="https://shotcv.com/screenshots/01-refit.png" crossorigin="anonymous" alt="…" />
\`\`\`

Without this attribute, the browser loads the image in "no-CORS" mode (without checking \`Access-Control-Allow-Origin\`). The image displays normally, but it taints the canvas → snapDOM can't inline it.

If you use a framework:

\`\`\`jsx
// React / Preact
<img src={url} crossorigin="anonymous" alt="…" />

// Vue
<img :src="url" crossorigin="anonymous" alt="…" />

// Astro / Svelte: same — it's a standard HTML attribute
\`\`\`

## Deployment order

⚠️ Ship the CORS header on the server **first**, and only **then** the \`crossorigin\` on the HTML side.

Otherwise: the browser rejects images marked \`crossorigin\` that didn't respond with \`Access-Control-Allow-Origin\` → they all fall into \`onError\` and stay broken until the server is fixed.

## Why both are needed

| | Without \`crossorigin\` on the HTML | With \`crossorigin="anonymous"\` |
|---|---|---|
| **Without a server CORS header** | Image displayed; canvas tainted; snapDOM warns | Image rejected (\`onError\`) — worse |
| **With a server CORS header** | Image displayed; canvas tainted (the browser doesn't make the CORS request on its own) | ✅ Image displayed + usable by the canvas |

The \`crossorigin\` *requests* CORS mode from the browser; the \`Access-Control-Allow-Origin\` header *allows* the canvas to consume the response. The two are coordinated.

## If you don't control the image (third-party CDN, hotlinked image)

There's nothing you can do on the front-end side. The console warning stays, the image is missing from the screenshot, but the rest of the report is sent normally (description, other images, console, requests).

## Concrete example — shotcv.com

A recent diagnosis in production:

- All the \`/screenshots/*.png\` images were served by Caddy **without** a CORS header
- The BipWire widget logged \`[snapDOM] Network/CORS issue while fetching dataURL https://shotcv.com/screenshots/01-refit.png\` on every report
- The captures sent showed these images blank in the annotation editor

Fix applied:

1. Caddy \`shotcv.com\` block → added a \`@screenshots path /screenshots/*\` with \`header @screenshots Access-Control-Allow-Origin "*"\`
2. \`apps/web/src/components/FeaturesCarousel.tsx\` → \`crossorigin="anonymous"\` on the carousel's \`<img>\` tag

After reloading Caddy + redeploying the front-end: warning gone, images present in the screenshots.
`,Ra="# Debug\n\nInspecter `window.__bip` dans la console — on y trouve `siteId`, `visitorId`, le drawer et les stores du chat.\n\n## Erreurs courantes\n\n- **Pas de bulle** : console pour `[BipWire] no siteId found` (snippet sans `?site=…` ni `data-site`).\n- **Erreur CORS** : `origin` du projet ne matche pas l'URL réelle.\n- **429 too_many_requests** : le rate-limit s'est déclenché, attendre la fin de la fenêtre (1 min).\n- **503 llm_unavailable** : aucun provider LLM dispo — vérifier `GOOGLE_API_KEY` et `ANTHROPIC_API_KEY` côté serveur.\n- **`[snapDOM] Network/CORS issue while fetching dataURL …`** : une image cross-origin du site hôte n'est pas servie avec `Access-Control-Allow-Origin`. L'image apparaît blanche dans le screenshot. Voir la page [Screenshots & cross-origin](/docs/widget/screenshots).\n\n## Console + network capture\n\nLe widget capture passivement toute l'activité console (log, info, warn, error, debug) + erreurs JS uncaught + violations CSP + fetch/XHR (méthode, URL, status, durée, headers, bodies) dans un ring buffer de 500 entrées. Ce buffer est joint à chaque bug report sous forme de bundle gzip+base64.\n\nPour désactiver la capture des bodies request/response (mais pas la timeline elle-même) :\n\n```js\nwindow.__bip.setCaptureBodies(false);\n```\n\nLes bodies binaires (images, octet-stream) reçus depuis une URL `GET` sont remplacés par un placeholder `[image/png 12345B]` et rechargés depuis l'URL côté admin. Les bodies binaires reçus en réponse à un POST/PUT (image générée, blob dynamique) sont encodés en data-URL pour pouvoir être restitués.\n\nLes URLs vers les domaines BipWire sont filtrées et n'apparaissent pas dans la timeline (sinon pollution par les pings chat / SSE).\n",qa="# Debug\n\nInspect `window.__bip` in the console — it holds `siteId`, `visitorId`, the drawer, and the chat stores.\n\n## Common errors\n\n- **No bubble**: check the console for `[BipWire] no siteId found` (snippet without `?site=…` or `data-site`).\n- **CORS error**: the project's `origin` doesn't match the actual URL.\n- **429 too_many_requests**: the rate limit kicked in, wait for the window to end (1 min).\n- **503 llm_unavailable**: no LLM provider available — check `GOOGLE_API_KEY` and `ANTHROPIC_API_KEY` on the server.\n- **`[snapDOM] Network/CORS issue while fetching dataURL …`**: a cross-origin image from the host site isn't served with `Access-Control-Allow-Origin`. The image appears blank in the screenshot. See the [Screenshots & cross-origin](/docs/widget/screenshots) page.\n\n## Console + network capture\n\nThe widget passively captures all console activity (log, info, warn, error, debug) + uncaught JS errors + CSP violations + fetch/XHR (method, URL, status, duration, headers, bodies) into a 500-entry ring buffer. This buffer is attached to every bug report as a gzip+base64 bundle.\n\nTo disable request/response body capture (but not the timeline itself):\n\n```js\nwindow.__bip.setCaptureBodies(false);\n```\n\nBinary bodies (images, octet-stream) received from a `GET` URL are replaced with a `[image/png 12345B]` placeholder and reloaded from the URL on the admin side. Binary bodies received in response to a POST/PUT (generated image, dynamic blob) are encoded as a data URL so they can be restored.\n\nURLs to BipWire domains are filtered out and don't appear in the timeline (otherwise they'd pollute it with chat / SSE pings).\n",Ma="# API Keys\n\nBearer tokens long-lived pour les agents (Claude Code, Cursor, …) qui consomment l'API BipWire et le serveur MCP.\n\n→ Pour brancher un agent (Claude Code / Cursor / Codex / OpenCode) avec un token : [Brancher un agent](/docs/api/brancher).\n\n## Scopes (droits)\n\nChaque token a un ensemble de scopes qui définissent ce qu'il peut faire. Le token ne peut accéder qu'aux endpoints REST + tools MCP qui matchent ses scopes.\n\n| Scope | Permet |\n|---|---|\n| `tasks:read` | `GET /api/v1/tasks*` + tools MCP `list_tasks`, `get_task`, `get_task_screenshot` |\n| `tasks:write` | `POST /api/v1/tasks`, `PATCH /api/v1/tasks/:id`, `POST /api/v1/tasks/:id/notes` + tools MCP `start_task`, `mark_task_to_test`, `create_task`, `update_task`, `add_note` — **créer, commenter et changer le statut, `resolved` compris** |\n| `threads:read` | Tools MCP `list_threads`, `get_thread` |\n| `sites:read` | `GET /api/v1/sites` + tools MCP `list_sites`, `get_site` |\n\n**Bonnes pratiques** :\n\n- Donner le **strict minimum** par token. Un token Claude Code qui ne fait que du diagnostic n'a pas besoin de `tasks:write`.\n- Générer plusieurs tokens en cas de plusieurs use cases (lecture pure vs lecture + mark to_test).\n- Côté REST, un endpoint refuse avec `403 forbidden` si le scope manque. Côté MCP, le tool est carrément filtré de `tools/list` — le LLM ne le voit pas.\n\nLe LLM peut appeler le tool MCP `get_my_permissions` à tout moment pour avoir la liste explicite de ses droits.\n\n## Génération\n\nPage **Tokens API** dans l'admin → bouton **Nouveau token** → label parlant (ex: \"Claude Code — mon-site — MacBook\") → le clear-text `bipwire_llm_xxxxxxxxxxxxxxxxxxxxxxxx` est affiché **une seule fois**, à copier immédiatement.\n\nFormat : `bipwire_llm_<base64url 32 bytes>` (~51 chars total). 256 bits d'entropie, résistant au bruteforce.\n\n## Stockage côté DB\n\nJamais en clear. La DB stocke :\n\n- `tokenHash` : SHA-256 du clear-text (pour vérifier les requêtes entrantes)\n- `prefix` : les 16 premiers chars (`bipwire_llm_xxxx…`) — visible dans l'admin pour identifier sans pouvoir rejouer\n- `label`, `createdAt`, `lastUsedAt`, `revokedAt`\n\n## Utilisation\n\nHeader sur chaque requête, REST ou MCP :\n\n```\nAuthorization: Bearer bipwire_llm_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nLe serveur :\n1. Vérifie le préfixe `bipwire_llm_`\n2. Hash le token, lookup dans `api_tokens`\n3. Vérifie `revokedAt IS NULL`\n4. Met à jour `lastUsedAt` (best-effort, async)\n\n## Cycle de vie\n\n- **Modifier les droits** : click sur les chips de la colonne \"Droits\" → dialog avec les checkboxes pré-cochées. Effet immédiat côté serveur, l'outil voit le changement au prochain appel sans avoir à régénérer le token.\n- **Révoquer** (bouton 🟧 Block) : marque `revokedAt`. Le row reste visible avec `lastUsedAt` pour audit. Tout outil qui présente le token reçoit 401 au prochain appel.\n- **Supprimer** (bouton 🔴 Trash) : révoque puis supprime physiquement le row. Plus aucune trace.\n\n## Bonnes pratiques\n\n- **Un token par machine / projet** avec un label parlant. Si fuite : seul celui-là est révoqué, sans casser les autres.\n- **Pas en dur dans le code** : stocker dans une env var (`BIPWIRE_LLM_TOKEN`), un fichier gitignoré, ou la config Claude Code (`~/.claude/settings.json`).\n- **Audit** : vérifier `lastUsedAt` régulièrement, révoquer les zombies.\n\n## Sécurité\n\nSans `tasks:write`, une clé ne fait que **lire**. Avec `tasks:write`, elle peut créer des tickets, les commenter et changer leur statut — `resolved` et `wontfix` compris : ne cocher ce scope que pour un outil de confiance. Même fuitée, une clé ne peut jamais :\n\n- Modifier ou supprimer un projet\n- Supprimer un ticket ou une note\n- Répondre à une conversation\n- Toucher au `system_prompt` ou aux réglages\n\nLe pire scénario d'une fuite sans `tasks:write` : la lecture des tickets et conversations. Avec : en plus, des tickets créés ou clos à tort. Révoquer la clé, l'historique des statuts dit qui a fait quoi.\n",Oa='# API Keys\n\nLong-lived Bearer tokens for the agents (Claude Code, Cursor, …) that consume the BipWire API and the MCP server.\n\n→ To connect your agent (Claude Code / Cursor / Codex / OpenCode) with a token: [Connect an agent](/docs/api/brancher).\n\n## Scopes (permissions)\n\nEach token has a set of scopes that define what it can do. The token can only reach the REST endpoints + MCP tools that match its scopes.\n\n| Scope | Grants |\n|---|---|\n| `tasks:read` | `GET /api/v1/tasks*` + MCP tools `list_tasks`, `get_task`, `get_task_screenshot` |\n| `tasks:write` | `POST /api/v1/tasks`, `PATCH /api/v1/tasks/:id`, `POST /api/v1/tasks/:id/notes` + MCP tools `start_task`, `mark_task_to_test`, `create_task`, `update_task`, `add_note` — **create, comment and change the status, `resolved` included** |\n| `threads:read` | MCP tools `list_threads`, `get_thread` |\n| `sites:read` | `GET /api/v1/sites` + MCP tools `list_sites`, `get_site` |\n\n**Best practices**:\n\n- Grant the **bare minimum** per token. A Claude Code token that only does diagnostics doesn\'t need `tasks:write`.\n- Generate several tokens if you have several use cases (read-only vs read + mark to_test).\n- On the REST side, an endpoint refuses with `403 forbidden` if the scope is missing. On the MCP side, the tool is filtered out of `tools/list` entirely — the LLM doesn\'t even see it.\n\nThe LLM can call the `get_my_permissions` MCP tool at any time to get the explicit list of its permissions.\n\n## Generation\n\n**API Tokens** page in the admin → **New token** button → a meaningful label (e.g. "Claude Code — my-site — MacBook") → the clear-text `bipwire_llm_xxxxxxxxxxxxxxxxxxxxxxxx` is shown **only once**, copy it right away.\n\nFormat: `bipwire_llm_<base64url 32 bytes>` (~51 chars total). 256 bits of entropy, brute-force resistant.\n\n## Storage in the DB\n\nNever in clear text. The DB stores:\n\n- `tokenHash`: SHA-256 of the clear text (to verify incoming requests)\n- `prefix`: the first 16 chars (`bipwire_llm_xxxx…`) — shown in the admin to identify a token without being able to replay it\n- `label`, `createdAt`, `lastUsedAt`, `revokedAt`\n\n## Usage\n\nHeader on every request, REST or MCP:\n\n```\nAuthorization: Bearer bipwire_llm_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nThe server:\n1. Checks the `bipwire_llm_` prefix\n2. Hashes the token, looks it up in `api_tokens`\n3. Checks `revokedAt IS NULL`\n4. Updates `lastUsedAt` (best-effort, async)\n\n## Lifecycle\n\n- **Change permissions**: click the chips in the "Permissions" column → a dialog with the checkboxes pre-checked. Immediate effect on the server; the tool sees the change on its next call without regenerating the token.\n- **Revoke** (🟧 Block button): sets `revokedAt`. The row stays visible with `lastUsedAt` for auditing. Any tool presenting the token gets a 401 on its next call.\n- **Delete** (🔴 Trash button): revokes then physically deletes the row. No trace left.\n\n## Best practices\n\n- **One token per machine / project** with a meaningful label. If it leaks: you just revoke that one without breaking the others.\n- **Not hardcoded in the code**: store it in an env var (`BIPWIRE_LLM_TOKEN`), a gitignored file, or the Claude Code config (`~/.claude/settings.json`).\n- **Audit**: check `lastUsedAt` regularly, revoke the zombies.\n\n## Security\n\nWithout `tasks:write`, a key can only **read**. With `tasks:write`, it can create tickets, comment on them and change their status — `resolved` and `wontfix` included: only grant this scope to a tool you trust. Even leaked, a key can never:\n\n- Modify or delete a project\n- Delete a ticket or a note\n- Reply to a conversation\n- Touch the `system_prompt` or the settings\n\nThe worst case of a leak without `tasks:write`: your tickets and conversations are read. With it: tickets may also be created or closed wrongly. Revoke the key; the status history tells who did what.\n',Ba=`# Brancher un agent (MCP)

BipWire expose un **serveur MCP** (Model Context Protocol) sur \`https://bipwire.com/api/mcp\` : l'agent de code (Claude Code, Cursor, Codex, OpenCode…) lit et traite les tickets des projets directement, sans coder le moindre appel REST.

> **Le plus rapide :** page [API Keys](/docs/api/tokens) → bouton **« Brancher un agent CLI »**. Il donne les commandes exactes, le token déjà inséré.

## Trois briques, deux moments

| Brique | Ce que c'est | Quand |
|---|---|---|
| **Les commandes \`/bip-*\`** | 4 fichiers texte, **sans aucun secret**, rangés dans le dossier personnel (\`~/.claude/commands/\`). Ils apprennent à l'agent les raccourcis \`/bip-tasks\`, \`/bip-sort\`, \`/bip-fix\`, \`/bip-next\`. | **Une fois par poste** |
| **Le serveur MCP** | La connexion de l'agent à BipWire, avec **le token**. Rangée dans la config personnelle, hors de tout repo. | **Une fois par poste** |
| **\`.bip.json\`** | Une ligne à la racine du repo, qui dit **à quel projet BipWire** il correspond. Aucun secret : il se commite. | **Une fois par projet** |

Les deux premières valent pour **tous les projets**. Pour un nouveau repo, seul \`.bip.json\` reste à faire.

## Une fois par poste

:::tabs
:::tab Claude Code
**1. Installer les commandes** — depuis n'importe quel dossier :

\`\`\`bash
curl -fsSL "https://bipwire.com/install/claude-code/commands?format=sh" | sh
\`\`\`

Elles vont dans \`~/.claude/commands/\`.

**2. Brancher le serveur MCP** — coller le token, généré sur la page [API Keys](/docs/api/tokens) :

\`\`\`bash
claude mcp add --scope user --transport http bip https://bipwire.com/api/mcp \\
  --header "Authorization: Bearer bipwire_llm_xxxx"
\`\`\`

\`--scope user\` range le token dans la config personnelle : valable dans tous les repos, et jamais dans un fichier commité.
:::tab Cursor
**1. Installer les commandes** — depuis n'importe quel dossier :

\`\`\`bash
curl -fsSL "https://bipwire.com/install/cursor/commands?format=sh" | sh
\`\`\`

Elles vont dans \`~/.cursor/commands/\`.

**2. Brancher le serveur MCP** — dans \`~/.cursor/mcp.json\` (la config personnelle, pas celle du repo : elle porte le token) :

\`\`\`json
{
  "mcpServers": {
    "bip": {
      "url": "https://bipwire.com/api/mcp",
      "headers": { "Authorization": "Bearer bipwire_llm_xxxx" }
    }
  }
}
\`\`\`
:::tab Codex
**1. Installer les commandes** — depuis n'importe quel dossier :

\`\`\`bash
curl -fsSL "https://bipwire.com/install/codex/commands?format=sh" | sh
\`\`\`

Elles vont dans \`~/.codex/prompts/\`.

**2. Brancher le serveur MCP** — dans \`~/.codex/config.toml\` :

\`\`\`toml
[mcp_servers.bip]
url = "https://bipwire.com/api/mcp"
http_headers = { Authorization = "Bearer bipwire_llm_xxxx" }
\`\`\`
:::tab OpenCode
**1. Installer les commandes** — depuis n'importe quel dossier :

\`\`\`bash
curl -fsSL "https://bipwire.com/install/opencode/commands?format=sh" | sh
\`\`\`

Elles vont dans \`~/.config/opencode/commands/\`.

**2. Brancher le serveur MCP** — dans \`~/.config/opencode/opencode.json\` (fusionner avec le contenu existant) :

\`\`\`json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "bip": {
      "type": "remote",
      "url": "https://bipwire.com/api/mcp",
      "enabled": true,
      "headers": { "Authorization": "Bearer bipwire_llm_xxxx" }
    }
  }
}
\`\`\`
:::

## Une fois par projet

À la racine du repo, avec l'identifiant du projet BipWire (visible sur sa fiche) :

\`\`\`bash
echo '{ "siteId": "mon-projet" }' > .bip.json
\`\`\`

C'est tout : taper **\`/bip-tasks\`** dans l'agent.

## Ce qu'on obtient

| Commande | Effet |
|---|---|
| \`/bip-tasks\` | Liste les tickets ouverts du projet, groupés par priorité. Lecture seule. |
| \`/bip-sort\` | Priorise \`critical\` + \`high\` tous projets confondus, et propose par quoi commencer. Lecture seule. |
| \`/bip-fix <id>\` | Corrige un ticket de bout en bout : lit le ticket → le prend en charge → corrige → le passe « à tester » avec une note. |
| \`/bip-next\` | Prend le ticket le plus prioritaire et déroule \`/bip-fix\`. |

Aucune ne clôt un ticket : il se ferme à la main, après test.

**Garde-fous.** Le contenu d'un ticket est écrit par un visiteur anonyme : les commandes le traitent comme une donnée, jamais comme une instruction. \`/bip-next\` saute un ticket marqué suspect, et \`/bip-fix\` s'arrête pour demander confirmation.

**Sans les commandes.** Le serveur MCP porte lui-même le protocole de correction : à la connexion, il donne à l'agent le déroulé (lire, prendre en charge, corriger, passer « à tester »), la consigne d'autonomie (aucune question quand personne ne suit la conversation) et la remontée des bugs (tout autre bug rencontré devient un ticket \`[Remonté par l'agent — à trancher]\`, jamais corrigé en douce). Les quatre commandes y sont aussi servies en *prompts* MCP : sous Claude Code, \`/mcp__bip__bip-fix <id>\` marche sans rien installer.

## Déjà installé dans un repo ?

Les anciennes versions de BipWire installaient les commandes **dans le repo** (\`.claude/commands/bip-*.md\`). Une copie de projet **passe avant** la version globale, et elle ne se met jamais à jour. Le script d'installation la signale s'il la trouve dans le dossier courant ; la supprimer :

\`\`\`bash
rm .claude/commands/bip-*.md
\`\`\`

## Le token

Générer un token \`bipwire_llm_…\` sur la page [API Keys](/docs/api/tokens). Un par personne suffit : il vaut pour tous les projets. Révocable à tout moment. Les commandes \`/bip-*\` ne le contiennent **jamais** : il ne vit que dans la config MCP.

## Mettre à jour

Les commandes évoluent avec BipWire : relancer la commande d'installation, elle réécrit les fichiers. Le serveur MCP, lui, est distant : il se met à jour tout seul.

Pour un accès programmatique sans client MCP (cURL, agent custom), voir [API REST](/docs/api/rest).
`,Ha=`# Connect an agent (MCP)

BipWire exposes an **MCP server** (Model Context Protocol) at \`https://bipwire.com/api/mcp\`: your coding agent (Claude Code, Cursor, Codex, OpenCode…) reads and handles your projects' tickets directly, without you writing a single REST call.

> **The fastest way:** the [API Keys](/docs/api/tokens) page → **"Connect a CLI agent"** button. It hands you the exact commands, with your token already inserted.

## Three pieces, two moments

| Piece | What it is | When |
|---|---|---|
| **The \`/bip-*\` commands** | 4 text files, **with no secret at all**, stored in your home folder (\`~/.claude/commands/\`). They teach your agent the \`/bip-tasks\`, \`/bip-sort\`, \`/bip-fix\`, \`/bip-next\` shortcuts. | **Once per machine** |
| **The MCP server** | Your agent's connection to BipWire, with **your token**. Stored in your personal config, outside any repo. | **Once per machine** |
| **\`.bip.json\`** | One line at the repo root, saying **which BipWire project** it matches. No secret: commit it. | **Once per project** |

The first two apply to **all your projects**. When you open a new repo, only \`.bip.json\` is left to do.

## Once per machine

:::tabs
:::tab Claude Code
**1. Install the commands** — from any folder:

\`\`\`bash
curl -fsSL "https://bipwire.com/install/claude-code/commands?format=sh" | sh
\`\`\`

They go to \`~/.claude/commands/\`.

**2. Connect the MCP server** — paste your token, generated on the [API Keys](/docs/api/tokens) page:

\`\`\`bash
claude mcp add --scope user --transport http bip https://bipwire.com/api/mcp \\
  --header "Authorization: Bearer bipwire_llm_your_token"
\`\`\`

\`--scope user\` stores the token in your personal config: valid in all your repos, and never in a committed file.
:::tab Cursor
**1. Install the commands** — from any folder:

\`\`\`bash
curl -fsSL "https://bipwire.com/install/cursor/commands?format=sh" | sh
\`\`\`

They go to \`~/.cursor/commands/\`.

**2. Connect the MCP server** — in \`~/.cursor/mcp.json\` (your personal config, not the repo's: it holds your token):

\`\`\`json
{
  "mcpServers": {
    "bip": {
      "url": "https://bipwire.com/api/mcp",
      "headers": { "Authorization": "Bearer bipwire_llm_your_token" }
    }
  }
}
\`\`\`
:::tab Codex
**1. Install the commands** — from any folder:

\`\`\`bash
curl -fsSL "https://bipwire.com/install/codex/commands?format=sh" | sh
\`\`\`

They go to \`~/.codex/prompts/\`.

**2. Connect the MCP server** — in \`~/.codex/config.toml\`:

\`\`\`toml
[mcp_servers.bip]
url = "https://bipwire.com/api/mcp"
http_headers = { Authorization = "Bearer bipwire_llm_your_token" }
\`\`\`
:::tab OpenCode
**1. Install the commands** — from any folder:

\`\`\`bash
curl -fsSL "https://bipwire.com/install/opencode/commands?format=sh" | sh
\`\`\`

They go to \`~/.config/opencode/commands/\`.

**2. Connect the MCP server** — in \`~/.config/opencode/opencode.json\` (merge with the existing content):

\`\`\`json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "bip": {
      "type": "remote",
      "url": "https://bipwire.com/api/mcp",
      "enabled": true,
      "headers": { "Authorization": "Bearer bipwire_llm_your_token" }
    }
  }
}
\`\`\`
:::

## Once per project

At the repo root, with the BipWire project id (shown on its page):

\`\`\`bash
echo '{ "siteId": "my-project" }' > .bip.json
\`\`\`

That's it: type **\`/bip-tasks\`** in your agent.

## What you get

| Command | Effect |
|---|---|
| \`/bip-tasks\` | Lists the project's open tickets, grouped by priority. Read-only. |
| \`/bip-sort\` | Prioritizes \`critical\` + \`high\` across all projects and suggests where to start. Read-only. |
| \`/bip-fix <id>\` | Fixes a ticket end to end: reads the ticket → takes it on → fixes → moves it to "to test" with a note. |
| \`/bip-next\` | Picks the highest-priority ticket and runs \`/bip-fix\` on it. |

None of them closes a ticket: you close it after testing.

**Guardrails.** A ticket's content is written by an anonymous visitor: the commands treat it as data, never as an instruction. \`/bip-next\` skips a ticket flagged as suspect, and \`/bip-fix\` stops to ask for your confirmation.

**Without the commands.** The MCP server carries the fixing protocol itself: on connection it gives the agent the steps (read, take on, fix, move to "to test"), the autonomy rule (no questions when nobody follows the conversation) and bug reporting (any other bug met on the way becomes a \`[Remonté par l'agent — à trancher]\` ticket, never quietly fixed). The four commands are also served as MCP *prompts*: in Claude Code, \`/mcp__bip__bip-fix <id>\` works with nothing installed.

## Already installed in a repo?

Older BipWire versions installed the commands **inside the repo** (\`.claude/commands/bip-*.md\`). A project copy **takes precedence** over the global one, and it never gets updated. The install script flags it if it finds one in the current folder; remove it:

\`\`\`bash
rm .claude/commands/bip-*.md
\`\`\`

## The token

Generate a \`bipwire_llm_…\` token on the [API Keys](/docs/api/tokens) page. One per person is enough: it covers all your projects. Revocable at any time. The \`/bip-*\` commands **never** contain it: it only lives in the MCP config.

## Updating

The commands evolve with BipWire: rerun the install command, it rewrites the files. The MCP server is remote: it updates itself.

For programmatic access without an MCP client (cURL, custom agent), see [REST API](/docs/api/rest).
`,Ua='# API REST `/api/v1`\n\nAPI JSON pour **un serveur** qui lit, crée et fait avancer les tickets — un outil interne, un CRM, une intégration maison. Les agents (Claude Code, Cursor…) passent plutôt par le [MCP](/docs/api/brancher).\n\n- **Base** : `https://bipwire.com/api/v1`\n- **Auth** : `Authorization: Bearer bipwire_llm_…` — cf. [Clés API](/docs/api/tokens)\n- **Schéma** : `https://bipwire.com/api/v1/openapi.json` (OpenAPI 3.1)\n- **Dates** : millisecondes depuis l\'époque. **Débit** : 120 requêtes / minute / clé (`429` + `Retry-After` au-delà).\n\n## Endpoints\n\n| | Scope | |\n|---|---|---|\n| `GET /sites` | `sites:read` | Projets visibles par la clé |\n| `GET /tasks` | `tasks:read` | Liste, plus récemment modifiées d\'abord. Filtres `siteId`, `status` (`new,in_progress`…), `open=true`, `priority`, `type`, `archived=true`, `updatedSince` (ms ou ISO), `limit` (≤ 100), `cursor` |\n| `GET /tasks/:id` | `tasks:read` | Détail avec les notes. `:id` = UUID ou `MONSITE-12` |\n| `POST /tasks` | `tasks:write` | Crée un ticket et **prévient les intégrations** (`task.created`) |\n| `PATCH /tasks/:id` | `tasks:write` | `status` (les cinq), `priority`, `type`, `title` — un changement de statut émet `task.status_changed` |\n| `POST /tasks/:id/notes` | `tasks:write` | Ajoute un commentaire (markdown) |\n\n**Statuts** : `new`, `queued` (en file de Claude Code), `in_progress`, `to_test`, `resolved`, `wontfix`. **Priorités** : `low`, `medium`, `high`, `critical`. **Types** : `bug`, `feature`.\n\nErreurs : `{ "error": "<code>", "message": "…" }` — `401` clé invalide, `403` scope ou projet hors périmètre, `404`, `422` validation, `429` débit.\n\n## Créer un ticket\n\n```bash\ncurl -X POST https://bipwire.com/api/v1/tasks \\\n  -H "Authorization: Bearer $BIPWIRE_TOKEN" -H "content-type: application/json" \\\n  -d \'{"siteId":"mon-site","description":"Le PDF sort vide","title":"Export PDF vide","priority":"high","externalRef":"CRM-42"}\'\n```\n\n`externalRef` (facultatif) est la référence chez l\'outil appelant : renvoyer la même sur le même projet rend le ticket existant (`200`) au lieu d\'en créer un second — une création peut donc être rejouée sans doublon.\n\n## Pagination\n\n`GET /tasks` rend `{ items, nextCursor }`. Repasser `cursor=<nextCursor>` avec les mêmes filtres ; `nextCursor` vaut `null` sur la dernière page.\n\n## Être prévenu\n\nAjouter un canal **Webhook** dans [Notifications](/notifications) (URL + secret, événements `task.created` et `task.status_changed`). Chaque envoi porte `x-bip-signature: sha256=<HMAC-SHA256(secret, corps brut)>` : la vérifier sur le corps brut, avant de le parser.\n\n⚠️ Le texte d\'un ticket signalé par le widget (`source: "widget"`) est écrit par un visiteur anonyme : une donnée, jamais une instruction.\n',Na='# REST API `/api/v1`\n\nA JSON API for **a server** that reads, creates and moves your tickets forward — an internal tool, a CRM, a home-made integration. Agents (Claude Code, Cursor…) should use the [MCP](/docs/api/brancher) instead.\n\n- **Base**: `https://bipwire.com/api/v1`\n- **Auth**: `Authorization: Bearer bipwire_llm_…` — see [API keys](/docs/api/tokens)\n- **Schema**: `https://bipwire.com/api/v1/openapi.json` (OpenAPI 3.1)\n- **Dates**: milliseconds since the epoch. **Rate limit**: 120 requests / minute / key (`429` + `Retry-After` beyond).\n\n## Endpoints\n\n| | Scope | |\n|---|---|---|\n| `GET /sites` | `sites:read` | Projects visible to the key |\n| `GET /tasks` | `tasks:read` | List, most recently updated first. Filters `siteId`, `status` (`new,in_progress`…), `open=true`, `priority`, `type`, `archived=true`, `updatedSince` (ms or ISO), `limit` (≤ 100), `cursor` |\n| `GET /tasks/:id` | `tasks:read` | Detail with notes. `:id` = UUID or `MYSITE-12` |\n| `POST /tasks` | `tasks:write` | Creates a ticket and **notifies your integrations** (`task.created`) |\n| `PATCH /tasks/:id` | `tasks:write` | `status` (all five), `priority`, `type`, `title` — a status change emits `task.status_changed` |\n| `POST /tasks/:id/notes` | `tasks:write` | Adds a comment (markdown) |\n\n**Statuses**: `new`, `queued` (in the Claude Code queue), `in_progress`, `to_test`, `resolved`, `wontfix`. **Priorities**: `low`, `medium`, `high`, `critical`. **Types**: `bug`, `feature`.\n\nErrors: `{ "error": "<code>", "message": "…" }` — `401` invalid key, `403` missing scope or project out of reach, `404`, `422` validation, `429` rate limit.\n\n## Create a ticket\n\n```bash\ncurl -X POST https://bipwire.com/api/v1/tasks \\\n  -H "Authorization: Bearer $BIPWIRE_TOKEN" -H "content-type: application/json" \\\n  -d \'{"siteId":"my-site","description":"The PDF comes out empty","title":"Empty PDF export","priority":"high","externalRef":"CRM-42"}\'\n```\n\n`externalRef` (optional) is the reference in your own tool: sending the same one on the same project returns the existing ticket (`200`) instead of creating a second one — so a creation can be retried without duplicates.\n\n## Pagination\n\n`GET /tasks` returns `{ items, nextCursor }`. Pass `cursor=<nextCursor>` back with the same filters; `nextCursor` is `null` on the last page.\n\n## Getting notified\n\nAdd a **Webhook** channel in [Notifications](/notifications) (URL + secret, events `task.created` and `task.status_changed`). Every delivery carries `x-bip-signature: sha256=<HMAC-SHA256(secret, raw body)>`: verify it on the raw body, before parsing.\n\n⚠️ The text of a ticket reported through the widget (`source: "widget"`) is written by an anonymous visitor: data, never an instruction.\n',Wa=`# Connecteurs

Un connecteur crée automatiquement une issue dans un outil externe (Vikunja, ou tout autre outil par le webhook générique) à chaque **nouveau bug ou suggestion** (\`task.created\`). C'est un canal sortant : BipWire pousse, l'outil externe ne renvoie rien vers BipWire.

\`\`\`
Widget (projet A) ──► ticket BipWire ──► connecteurs actifs dont le scope inclut le projet A
                                   └─► issue créée + lien stocké sur le ticket
\`\`\`

Chaque issue créée est mémorisée comme lien externe sur le ticket (visible dans le détail du ticket). Un échec côté outil externe ne bloque jamais la création du ticket BipWire — il est seulement loggé.

## Les champs communs

Ces champs existent quel que soit le type de connecteur :

| Champ | Rôle |
|---|---|
| **Nom** | Libellé libre pour s'y retrouver (ex. « Vikunja — Boutique »). Aucun effet fonctionnel. |
| **Type** | L'outil cible : Vikunja ou webhook générique, selon ce que la plateforme rend disponible pour l'organisation. Non modifiable après création (les champs de config en dépendent). |
| **Projets concernés** | **C'est ici que se fait le lien avec les projets BipWire.** Vide = tous les projets. Sinon, seuls les bugs venant des projets cochés déclenchent ce connecteur. Exemple : un connecteur « Vikunja — Boutique » scopé sur le projet Boutique, un autre « Vikunja — Blog » scopé sur Blog. |
| **Création automatique** | Si désactivée, le connecteur existe mais ne crée rien tout seul. |
| **Activé** | Interrupteur global. |

## « Project ID »… c'est quoi ?

Ce champ désigne toujours **la destination dans l'outil externe** — l'endroit où l'issue atterrit. Il ne désigne **jamais** un projet BipWire (ça, c'est « Projets concernés » ci-dessus) et ne sert pas de préfixe.

Le préfixe des identifiants visibles sur les tickets (ex. \`MONSITE-7\`) est le \`displayId\` généré par BipWire lui-même — compteur séquentiel par projet, configuré sur le projet. Il est repris dans le titre de l'issue créée quand le ticket n'a pas de titre, c'est tout.

## Contenu de l'issue créée

Tout connecteur reçoit le ticket entier :

- **titre** = titre du ticket (ou \`Bug MONSITE-7\` / \`Suggestion MONSITE-7\` à défaut) ;
- **texte** = description du visiteur mise en forme, URL de la page, lien vers la fiche BipWire ;
- **captures d'écran et pièces jointes**, en fichiers (25 Mo au plus par ticket ; une pièce purgée ne part pas) ;
- **contexte technique trié** : page, navigateur, écran, langue, dernières erreurs et avertissements de la console, dernières requêtes en échec. Jamais de corps de réponse, d'en-têtes, de chaîne de requête ni de valeurs du stockage du navigateur.

Un ticket **verrouillé** (plafond du plan atteint) ne part pas. Un ticket dont le contenu est **marqué suspect** par les garde-fous part sans son contenu : l'issue porte le drapeau et le lien vers la fiche, le texte, les fichiers et le contexte restent sur BipWire.

## Bouton « Tester »

Le test vérifie que les champs requis sont remplis — il **ne crée pas** d'issue réelle (pour ne pas polluer l'outil cible). La première vraie validation réseau (token valide, projet existant…) a lieu au premier bug dispatché : la fiche de ce ticket en garde la trace.

## Trace sur le ticket

Chaque envoi d'un connecteur s'inscrit sur la fiche du ticket, sous « Envois des connecteurs » : le **lien** vers l'issue créée (Vikunja), « **envoyé** » quand le destinataire a accepté sans rendre d'objet (webhook), ou « **échec** » suivi de la cause (\`webhook_500\`, \`vikunja_401\`…). Une issue créée dont certains fichiers n'ont pas suivi porte « **fichiers manquants** », la cause au survol. Un échec n'empêche jamais le signalement d'aboutir. Un changement de statut ne déclenche aucun connecteur.

## Secrets

Les champs secrets (tokens, clés API) sont masqués en lecture (\`••••\`). Pour en changer un, retaper la nouvelle valeur ; un champ laissé masqué conserve la valeur existante.

---

## Vikunja

Crée une tâche dans un projet Vikunja.

| Champ | Quoi mettre | Où le trouver |
|---|---|---|
| Base URL | URL de l'instance (ex. \`https://tasks.pop.run\`) | — |
| API Token | Token \`tk_…\` | Vikunja → Paramètres → Tokens API. Lui donner au minimum les droits *Tâches : créer*, *Pièces jointes des tâches : créer* (pour les captures et pièces jointes), *Commentaires des tâches : créer* (conseillé, pour signaler un fichier refusé à l'envoi) et *Projets : lire tout* (pour charger la liste des projets). |
| Projet Vikunja par défaut | **Le projet Vikunja où créer les tâches** (numérique). | Bouton « Charger les projets Vikunja », ou ouvrir le projet : l'ID est dans l'URL, \`…/projects/\`**\`68\`**. |
| Projet Vikunja par projet BipWire | Facultatif : pour chaque projet BipWire, le projet Vikunja qui reçoit ses tickets. Laissé sur « Projet par défaut », il utilise celui du dessus. | Même liste, une ligne par projet BipWire de la portée. |

En deux temps : renseigner l'URL et le token, cliquer **« Charger les projets Vikunja »** (à la réouverture d'un connecteur enregistré, la liste se charge d'elle-même), puis choisir les projets dans les listes au lieu de recopier leur ID. Un seul connecteur suffit donc pour envoyer « Boutique » dans un projet Vikunja et « Blog » dans un autre. Le chargement n'interroge qu'une instance joignable depuis Internet.

Les captures et pièces jointes sont jointes à la tâche Vikunja, une à une. Un fichier au-delà de la taille maximale de l'instance (\`max_file_size\`, 20 Mo par défaut) n'est pas envoyé, et la description de la tâche le liste. Un fichier refusé à l'envoi ne défait pas la tâche : un commentaire sur la tâche Vikunja le signale, avec sa cause. La fiche BipWire dit aussi tout fichier non joint.

## Webhook générique

Pour brancher n'importe quoi d'autre : BipWire envoie un \`POST\` JSON sur l'URL configurée à chaque nouveau bug.

| Champ | Quoi mettre |
|---|---|
| URL | Endpoint HTTPS |
| Secret | Optionnel. Si fourni, chaque requête porte un header \`X-Bip-Signature\` = HMAC-SHA256 (hex) du body, à vérifier côté récepteur. |

Body envoyé :

\`\`\`json
{
  "type": "task.created",
  "site": { "id": "…", "name": "Mon site" },
  "taskId": "…",
  "displayId": "MONSITE-7",
  "taskType": "bug",
  "status": "new",
  "priority": "medium",
  "title": "Bug MONSITE-7",
  "description": "…",
  "url": "https://…",
  "userAgent": "Mozilla/5.0 …",
  "adminUrl": "https://app.bipwire.com/tasks/<taskId>",
  "occurredAt": 1761548828925,
  "contenuSuspect": false,
  "technique": {
    "page": "https://…", "titrePage": "…", "navigateur": "Mozilla/5.0 …", "ecran": "1280×720 @2x",
    "langue": "fr-FR", "plateforme": "MacIntel",
    "erreurs": ["[ERROR] 10:00:02 TypeError: …"],
    "reseau": ["10:00:03 fetch POST https://…/api/payer?… → 500 (40ms)"]
  },
  "fichiers": [
    { "genre": "capture", "nom": "capture-1.webp", "mime": "image/webp", "taille": 48213, "contenuBase64": "UklGR…" },
    { "genre": "piece", "nom": "video.mp4", "mime": "video/mp4", "taille": 15728640, "omis": "trop_lourd" }
  ],
  "fichiersEcartes": []
}
\`\`\`

Le corps est l'événement lui-même, sans enveloppe. \`title\` vaut \`Bug MONSITE-7\` (ou \`Suggestion MONSITE-7\`) : le titre définitif est calculé après l'envoi. \`url\` et \`userAgent\` sont absents quand le widget ne les a pas fournis ; \`technique\` vaut \`null\` sans journal.

Les fichiers arrivent en base64 (\`contenuBase64\`) tant que leur total reste sous 10 Mo ; au-delà, les métadonnées seules et \`omis: "trop_lourd"\`. \`fichiersEcartes\` nomme ceux qui dépassaient le plafond de 25 Mo par ticket. Avec \`contenuSuspect: true\`, \`description\` est vide, \`fichiers\` et \`technique\` aussi : le contenu se lit en suivant \`adminUrl\`. La signature couvre le corps entier, fichiers compris.
`,Fa=`# Connectors

A connector automatically creates an issue in an external tool (Vikunja, or any other tool through the generic webhook) for every **new bug or suggestion** (\`task.created\`). It's an outbound channel: BipWire pushes, the external tool sends nothing back to BipWire.

\`\`\`
Widget (project A) ──► BipWire ticket ──► active connectors whose scope includes project A
                                 └─► issue created + link stored on the ticket
\`\`\`

Each created issue is remembered as an external link on the ticket (visible in the ticket detail). A failure on the external tool's side never blocks the creation of the BipWire ticket — it's only logged.

## Common fields

These fields exist for every connector type:

| Field | Role |
|---|---|
| **Name** | A free label to help you keep track (e.g. "Vikunja — Shop"). No functional effect. |
| **Type** | The target tool: Vikunja or generic webhook, depending on what the platform makes available to the organization. Cannot be changed after creation (the config fields depend on it). |
| **Projects** | **This is where the link to your BipWire projects is made.** Empty = all projects. Otherwise, only bugs coming from the checked projects trigger this connector. Example: a "Vikunja — Shop" connector scoped to the Shop project, another "Vikunja — Blog" scoped to Blog. |
| **Auto-create** | If disabled, the connector exists but creates nothing on its own. |
| **Enabled** | Global switch. |

## "Project ID"… what is it?

This field always designates **the destination in the external tool** — where the issue lands. It **never** designates a BipWire project (that's "Projects" above) and is not used as a prefix.

The prefix of the identifiers you see on tickets (e.g. \`MYSITE-7\`) is the \`displayId\` generated by BipWire itself — a sequential counter per project, configured on the project. It's reused in the title of the created issue when the ticket has no title, that's all.

## Contents of the created issue

Every connector receives the whole ticket:

- **title** = the ticket title (or \`Bug MYSITE-7\` / \`Suggestion MYSITE-7\` as a fallback);
- **text** = the visitor's formatted description, the page URL, a link to the BipWire ticket;
- **screenshots and attachments**, as files (25 MB at most per ticket; a purged attachment is not sent);
- **sorted technical context**: page, browser, screen, language, latest console errors and warnings, latest failed requests. Never response bodies, headers, query strings or browser storage values.

A **locked** ticket (plan limit reached) is not sent. A ticket whose content is **flagged as suspicious** by the guardrails is sent without its content: the issue carries the flag and the link to the ticket; text, files and context stay in BipWire.

## The "Test" button

The test checks that the required fields are filled in — it does **not** create a real issue (so as not to pollute the target tool). The first real network validation (valid token, existing project…) happens on the first dispatched bug: that ticket's page keeps a record of it.

## Record on the ticket

Every connector delivery is recorded on the ticket page, under "Connector deliveries": the **link** to the created issue (Vikunja), "**sent**" when the receiver accepted without returning an object (webhook), or "**failed**" followed by the cause (\`webhook_500\`, \`vikunja_401\`…). A created issue that some files did not reach shows "**missing files**", with the cause on hover. A failure never prevents the report from going through. A status change triggers no connector.

## Secrets

Secret fields (tokens, API keys) are masked when read (\`••••\`). To change one, retype the new value; a field left masked keeps its existing value.

---

## Vikunja

Creates a task in a Vikunja project.

| Field | What to put | Where to find it |
|---|---|---|
| Base URL | Your instance URL (e.g. \`https://tasks.pop.run\`) | — |
| API Token | A \`tk_…\` token | Vikunja → Settings → API Tokens. Give it at least the *Tasks: create*, *Task attachments: create* (for screenshots and attachments), *Task comments: create* (recommended, to report a file rejected on upload) and *Projects: read all* (to load the project list) permissions. |
| Default Vikunja project | **The Vikunja project where tasks are created** (numeric). | "Load Vikunja projects" button, or open the project: the ID is in the URL, \`…/projects/\`**\`68\`**. |
| Vikunja project per BipWire project | Optional: for each BipWire project, the Vikunja project that receives its tickets. Left on "Default project", it uses the one above. | Same list, one row per BipWire project in scope. |

In two steps: fill in the URL and the token, click **"Load Vikunja projects"** (when a saved connector is reopened, the list loads by itself), then pick the projects from the lists instead of copying their ID. A single connector can thus send "Shop" to one Vikunja project and "Blog" to another. Loading only queries an instance reachable from the Internet.

Screenshots and attachments are attached to the Vikunja task one by one. A file above the instance's maximum size (\`max_file_size\`, 20 MB by default) is not sent, and the task description lists it. A file rejected on upload does not undo the task: a comment on the Vikunja task reports it, with its cause. The BipWire ticket also shows any missing file.

## Generic webhook

To wire up anything else: BipWire sends a JSON \`POST\` to your URL for every new bug.

| Field | What to put |
|---|---|
| URL | Your HTTPS endpoint |
| Secret | Optional. If provided, every request carries an \`X-Bip-Signature\` header = HMAC-SHA256 (hex) of the body, to verify on the receiving end. |

Body sent:

\`\`\`json
{
  "type": "task.created",
  "site": { "id": "…", "name": "My site" },
  "taskId": "…",
  "displayId": "MYSITE-7",
  "taskType": "bug",
  "status": "new",
  "priority": "medium",
  "title": "Bug MYSITE-7",
  "description": "…",
  "url": "https://…",
  "userAgent": "Mozilla/5.0 …",
  "adminUrl": "https://app.bipwire.com/tasks/<taskId>",
  "occurredAt": 1761548828925,
  "contenuSuspect": false,
  "technique": {
    "page": "https://…", "titrePage": "…", "navigateur": "Mozilla/5.0 …", "ecran": "1280×720 @2x",
    "langue": "en-US", "plateforme": "MacIntel",
    "erreurs": ["[ERROR] 10:00:02 TypeError: …"],
    "reseau": ["10:00:03 fetch POST https://…/api/pay?… → 500 (40ms)"]
  },
  "fichiers": [
    { "genre": "capture", "nom": "capture-1.webp", "mime": "image/webp", "taille": 48213, "contenuBase64": "UklGR…" },
    { "genre": "piece", "nom": "video.mp4", "mime": "video/mp4", "taille": 15728640, "omis": "trop_lourd" }
  ],
  "fichiersEcartes": []
}
\`\`\`

The body is the event itself, with no envelope. \`title\` is \`Bug MYSITE-7\` (or \`Suggestion MYSITE-7\`): the final title is computed after delivery. \`url\` and \`userAgent\` are omitted when the widget did not provide them; \`technique\` is \`null\` without a log.

Files arrive in base64 (\`contenuBase64\`) as long as their total stays under 10 MB; beyond that, metadata only and \`omis: "trop_lourd"\`. \`fichiersEcartes\` names those above the 25 MB per-ticket cap. With \`contenuSuspect: true\`, \`description\` is empty, and so are \`fichiers\` and \`technique\`: the content is read by following \`adminUrl\`. The signature covers the whole body, files included.
`,za=`# Notifications

Un canal de notification reçoit un message court (titre + résumé + lien vers l'admin) quand un événement se produit. Contrairement aux connecteurs (qui ne réagissent qu'aux nouveaux bugs), un canal peut s'abonner à **tous les types d'événements**.

## Les champs communs

| Champ | Rôle |
|---|---|
| **Nom** | Libellé libre. Aucun effet fonctionnel. |
| **Type** | Le canal cible : e-mail, Telegram ou webhook générique, selon ce que la plateforme rend disponible pour l'organisation. Non modifiable après création. |
| **Projets concernés** | Vide = tous les projets. Sinon, seuls les événements des projets cochés sont relayés. |
| **Événements** | Vide = tous. Sinon, seuls les types cochés. Exemple : un canal Telegram « urgences » abonné aux seules escalades, un canal email abonné aux changements de statut. |
| **Activé** | Interrupteur global. |

## Événements disponibles

| Événement | Déclenché quand |
|---|---|
| Nouveau bug / suggestion | Un visiteur envoie un signalement depuis le widget |
| Changement de statut | Un ticket change de statut — depuis la console ou par un agent MCP. Le passage à « À tester » et à « Résolu » est mis en avant dans le message |
| Escalade vers un humain | Une conversation passe en attente d'un humain |
| Agent automatique (\`autofix.*\`) | La correction automatique de Claude Code prend un ticket, le livre « à tester », échoue, le saute, ou envoie son rapport du jour — selon les notifications choisies dans le volet Claude Code du projet |

Un ticket au-delà du plafond de tickets ouverts du plan arrive **verrouillé** : les canaux abonnés aux nouveaux tickets reçoivent alors un message \`task.locked\` qui signale son existence, sans son contenu.

Un même événement peut partir vers plusieurs canaux en parallèle ; un canal en échec n'affecte ni les autres ni la requête d'origine (envoi fire-and-forget, échec loggé).

Un nouveau ticket part une fois analysé par les garde-fous (quelques secondes). Si son contenu est marqué suspect, le message ne porte ni sa description ni son texte : il le signale, avec le lien vers le ticket ; le webhook reçoit \`contenuSuspect: true\` et une \`description\` vide.

## Bouton « Tester »

Le test vérifie que les champs requis sont remplis — il **n'envoie aucun message** sur le canal (un clic ne doit pas faire de bruit, et une panne passagère du service ne doit pas accuser la configuration). Exception : un canal Telegram est refusé au test tant que le bot de la plateforme n’est pas configuré sur le serveur — il ne pourrait rien envoyer. La première vraie validation (URL joignable, token valide, bot membre du chat…) a lieu au premier événement : surveiller le premier message reçu.

## Secrets

Les champs secrets (URL de webhook, tokens) sont masqués en lecture. Pour en changer un, retaper la valeur ; un champ laissé masqué conserve l'existante.

---

## Email

Envoi via Resend (la clé \`RESEND_API_KEY\` doit être configurée côté serveur, sinon le canal s'auto-désactive avec un warn).

| Champ | Quoi mettre |
|---|---|
| Destinataire(s) | Une ou plusieurs adresses, séparées par des virgules. |

## Telegram

Utilise le bot de BipWire, commun à la plateforme : aucun bot à créer, aucun token à saisir. Le **guide complet** (ajouter le bot, trouver le Chat ID, messages reçus, dépannage) s'affiche dans le dialogue d'un canal Telegram, sous le choix du type.

| Champ | Quoi mettre |
|---|---|
| Chat ID | L'identifiant du chat ou du groupe (ex. \`-1001234567890\`). Le bot nommé sous le champ **doit être membre** du chat. |

## Webhook générique

\`POST\` JSON sur l'URL configurée à chaque événement abonné.

| Champ | Quoi mettre |
|---|---|
| URL | Endpoint HTTPS |
| Secret | Optionnel. Si fourni, header \`X-Bip-Signature\` = \`sha256=\` suivi du HMAC-SHA256 (hex) du body. |

Le corps est **l'événement lui-même, sans enveloppe** : son champ \`type\` dit lequel. Exemple pour une escalade :

\`\`\`json
{
  "type": "thread.escalated",
  "site": { "id": "…", "name": "Mon site" },
  "threadId": "…",
  "visitorEmail": "visiteur@exemple.com",
  "preview": "…",
  "adminUrl": "https://app.bipwire.com/threads/…",
  "occurredAt": 1761548828925
}
\`\`\`

Pour un changement de statut :

\`\`\`json
{
  "type": "task.status_changed",
  "site": { "id": "…", "name": "Mon site" },
  "taskId": "…",
  "displayId": "MONSITE-7",
  "taskType": "bug",
  "previousStatus": "in_progress",
  "status": "to_test",
  "priority": "medium",
  "title": "…",
  "externalRef": "…",
  "description": "… (500 caractères au plus)",
  "url": "https://…",
  "adminUrl": "https://app.bipwire.com/tasks/…",
  "changedBy": { "kind": "agent", "label": "…" },
  "occurredAt": 1761548828925
}
\`\`\`

\`task.created\` porte les champs décrits pour le webhook des connecteurs (\`taskId\`, \`displayId\`, \`title\`, \`description\`, \`adminUrl\`, \`contenuSuspect\`…), sans \`fichiers\` ni \`technique\`. \`task.locked\` ne porte que \`site\`, \`taskId\`, \`displayId\`, \`taskType\`, \`status\`, \`url\` et \`adminUrl\`. Les champs sans valeur sont absents.
`,Va=`# Notifications

A notification channel receives a short message (title + summary + link to the admin) when an event happens. Unlike connectors (which only react to new bugs), a channel can subscribe to **every event type**.

## Common fields

| Field | Role |
|---|---|
| **Name** | A free label. No functional effect. |
| **Type** | The target channel: email, Telegram or generic webhook, depending on what the platform makes available to the organization. Cannot be changed after creation. |
| **Projects** | Empty = all projects. Otherwise, only events from the checked projects are relayed. |
| **Events** | Empty = all. Otherwise, only the checked types. Example: a Telegram "urgent" channel subscribed to escalations only, an email channel subscribed to status changes. |
| **Enabled** | Global switch. |

## Available events

| Event | Triggered when |
|---|---|
| New bug / suggestion | A visitor sends a report from the widget |
| Status change | A ticket changes status — from the console or through an MCP agent. Moving to "To test" and "Resolved" is highlighted in the message |
| Escalation to a human | A conversation moves to awaiting a human |
| Automatic agent (\`autofix.*\`) | Claude Code's automatic fixing takes a ticket, delivers it "to test", fails, skips it, or sends its daily report — according to the notifications chosen in the project's Claude Code panel |

A ticket beyond the plan's open-ticket cap arrives **locked**: channels subscribed to new tickets then receive a \`task.locked\` message that reports its existence, without its content.

The same event can go out to several channels in parallel; a failing channel affects neither the others nor the original request (fire-and-forget delivery, failure logged).

A new ticket goes out once the guardrails have analysed it (a few seconds). If its content is flagged as suspicious, the message carries neither its description nor its text: it says so, with the link to the ticket; the webhook receives \`contenuSuspect: true\` and an empty \`description\`.

## The "Test" button

The test checks that the required fields are filled in — it does **not** send any message to the channel (a click must not make noise, and a passing outage of the service must not blame the configuration). Exception: a Telegram channel fails the test while the platform's bot is not configured on the server — it could not send anything. The first real validation (reachable URL, valid token, bot member of the chat…) happens on the first event: watch the first message received.

## Secrets

Secret fields (webhook URLs, tokens) are masked when read. To change one, retype the value; a field left masked keeps the existing one.

---

## Email

Sent via Resend (the \`RESEND_API_KEY\` key must be configured server-side, otherwise the channel disables itself with a warning).

| Field | What to put |
|---|---|
| Recipient(s) | One or more addresses, separated by commas. |

## Telegram

Uses BipWire's bot, shared by the platform: no bot to create, no token to enter. The **full guide** (adding the bot, finding the Chat ID, messages received, troubleshooting) is shown in the dialog of a Telegram channel, under the type selector.

| Field | What to enter |
|---|---|
| Chat ID | The identifier of the chat or group (e.g. \`-1001234567890\`). The bot named under the field **must be a member** of the chat. |

## Generic webhook

A JSON \`POST\` to your URL for every subscribed event.

| Field | What to put |
|---|---|
| URL | Your HTTPS endpoint |
| Secret | Optional. If provided, an \`X-Bip-Signature\` header = \`sha256=\` followed by the HMAC-SHA256 (hex) of the body. |

The body is **the event itself, with no envelope**: its \`type\` field says which one. Example for an escalation:

\`\`\`json
{
  "type": "thread.escalated",
  "site": { "id": "…", "name": "My site" },
  "threadId": "…",
  "visitorEmail": "visitor@example.com",
  "preview": "…",
  "adminUrl": "https://app.bipwire.com/threads/…",
  "occurredAt": 1761548828925
}
\`\`\`

For a status change:

\`\`\`json
{
  "type": "task.status_changed",
  "site": { "id": "…", "name": "My site" },
  "taskId": "…",
  "displayId": "MYSITE-7",
  "taskType": "bug",
  "previousStatus": "in_progress",
  "status": "to_test",
  "priority": "medium",
  "title": "…",
  "externalRef": "…",
  "description": "… (500 characters at most)",
  "url": "https://…",
  "adminUrl": "https://app.bipwire.com/tasks/…",
  "changedBy": { "kind": "agent", "label": "…" },
  "occurredAt": 1761548828925
}
\`\`\`

\`task.created\` carries the fields described for the connectors' webhook (\`taskId\`, \`displayId\`, \`title\`, \`description\`, \`adminUrl\`, \`contenuSuspect\`…), without \`fichiers\` or \`technique\`. \`task.locked\` carries only \`site\`, \`taskId\`, \`displayId\`, \`taskType\`, \`status\`, \`url\` and \`adminUrl\`. Fields without a value are absent.
`,Gt=[{label:{fr:"Démarrage",en:"Getting started"},pages:[{slug:"demarrage/guide",title:{fr:"Guide de démarrage",en:"Getting started guide"},content:{fr:va,en:ka}}]},{label:{fr:"Widget",en:"Widget"},pages:[{slug:"widget/integration",title:{fr:"Intégration",en:"Integration"},content:{fr:ya,en:Ca}},{slug:"widget/personnalisation",title:{fr:"Personnaliser l’apparence",en:"Customize the look"},content:{fr:wa,en:Ta}},{slug:"widget/identification",title:{fr:"Identification user",en:"Identify the user"},content:{fr:xa,en:Sa}},{slug:"widget/fonctionnement",title:{fr:"Comment ça marche",en:"How it works"},content:{fr:ja,en:Pa}},{slug:"widget/securite",title:{fr:"CSP et protections du site hôte",en:"CSP & host-site protections"},content:{fr:Ea,en:Aa}},{slug:"widget/csp-faq",title:{fr:"FAQ erreurs CSP",en:"CSP error FAQ"},content:{fr:Da,en:La}},{slug:"widget/screenshots",title:{fr:"Screenshots & cross-origin",en:"Screenshots & cross-origin"},content:{fr:Ia,en:_a}},{slug:"widget/debug",title:{fr:"Debug",en:"Debug"},content:{fr:Ra,en:qa}}]},{label:{fr:"API & MCP",en:"API & MCP"},pages:[{slug:"api/tokens",title:{fr:"API Keys",en:"API Keys"},content:{fr:Ma,en:Oa}},{slug:"api/brancher",title:{fr:"Brancher un agent (MCP)",en:"Connect an agent (MCP)"},content:{fr:Ba,en:Ha}},{slug:"api/rest",title:{fr:"API REST",en:"REST API"},content:{fr:Ua,en:Na}}]},{label:{fr:"Intégrations",en:"Integrations"},pages:[{slug:"integrations/connecteurs",title:{fr:"Connecteurs",en:"Connectors"},content:{fr:Wa,en:Fa}},{slug:"integrations/notifications",title:{fr:"Notifications",en:"Notifications"},content:{fr:za,en:Va}}]}];function Ga(e){return Gt.map(n=>({label:n.label[e],pages:n.pages.map(i=>({slug:i.slug,title:i.title[e],content:i.content[e]}))}))}function Ws(e){const n={};for(const i of Ga(e))for(const a of i.pages)n[a.slug]={page:a,category:i};return n}var $a=Gt[0]?.pages[0]?.slug??"widget/integration",Ka="modulepreload",Ja=function(e){return"/"+e},Qa={},Ya=function(n){return n.pathname.endsWith(".css")},Xa=function(n,i,a){if(i in n)return n[i];const o=a();if(!o){n[i]=void 0;return}const r=o.then(()=>{n[i]=void 0},l=>{throw n[i]=void 0,l});return n[i]=r,r},x=function(n,i,a){let o=Promise.resolve();if(i&&i.length>0){let w=function(b){return Promise.all(b.map(y=>Promise.resolve(y).then(c=>({status:"fulfilled",value:c}),c=>({status:"rejected",reason:c}))))},v=function(b){return import.meta.resolve?new URL(import.meta.resolve(b)):new URL(b,import.meta.url)},l;const h=document.querySelector("meta[property=csp-nonce]"),p=h?.nonce||h?.getAttribute("nonce");o=w(i.map(b=>{b=Ja(b,a);const y=v(b),c=Ya(y);return Xa(Qa,y.href,()=>{if(l===void 0){l={all:new Set,styles:new Set};const f=document.getElementsByTagName("link");for(let P=f.length-1;P>=0;P--){const E=f[P];l.all.add(E.href),E.rel==="stylesheet"&&l.styles.add(E.href)}}if((c?l.styles:l.all).has(y.href))return;const u=document.createElement("link");if(u.rel=c?"stylesheet":Ka,c||(u.as="script"),u.crossOrigin="",u.href=y.href,p&&u.setAttribute("nonce",p),document.head.appendChild(u),c)return new Promise((f,P)=>{u.addEventListener("load",f),u.addEventListener("error",()=>P(new Error(`Unable to preload CSS for ${y}`)))})})}).filter(b=>b!==void 0))}function r(l){const h=new Event("vite:preloadError",{cancelable:!0});if(h.payload=l,window.dispatchEvent(h),!h.defaultPrevented)throw l}return o.then(l=>{for(const h of l||[])h.status==="rejected"&&r(h.reason);return n().catch(r)})},ut=(0,d.lazy)(()=>x(()=>import("./Dashboard-DOlOdjSg.js").then(e=>({default:e.Dashboard})),__vite__mapDeps([0,1,2,3,4,5]))),mt=(0,d.lazy)(()=>x(()=>import("./ThreadsList-_9U4Anr1.js"),__vite__mapDeps([6,2,1,3,4,7,8,9,10,11,5,12]))),pt=(0,d.lazy)(()=>x(()=>import("./ThreadDetail-CRhDIpJl.js"),__vite__mapDeps([13,2,1,3,4,7,14,5,15,16,10,11,12]))),Za=(0,d.lazy)(()=>x(()=>import("./Sites-krIouanZ.js"),__vite__mapDeps([17,2,1,3,4,7,8,9,18,19,20,14]))),Pe=(0,d.lazy)(()=>x(()=>import("./SiteEdit-CBaS2EEi.js"),__vite__mapDeps([21,2,1,3,4,19,7,14,5,20,22,18]))),ht=(0,d.lazy)(()=>x(()=>import("./Notifications-BR_V_ERL.js"),__vite__mapDeps([23,2,1,4,24,7,25,26,3,27,28,29,14]))),gt=(0,d.lazy)(()=>x(()=>import("./Connectors-C4587B5M.js"),__vite__mapDeps([30,2,1,4,24,7,25,26,3,27,28,29,14]))),bt=(0,d.lazy)(()=>x(()=>import("./TasksList-DaH4PKZ4.js"),__vite__mapDeps([31,2,1,3,4,7,5,8,22,32,9,12,15,10,33]))),ft=(0,d.lazy)(()=>x(()=>import("./TaskDetail-y0hkUr2N.js"),__vite__mapDeps([34,2,1,3,4,24,7,14,26,27,28,29,5,22,12,15,16,10,33]))),vt=(0,d.lazy)(()=>x(()=>import("./SearchResults-CV1kU22f.js"),__vite__mapDeps([35,2,1,3,4,5,32,9,12]))),es=(0,d.lazy)(()=>x(()=>import("./History-DOibMUol.js"),__vite__mapDeps([36,2,1,4,7,8,3]))),kt=(0,d.lazy)(()=>x(()=>import("./LlmTokens-CzVr4WCD.js"),__vite__mapDeps([37,2,1,4,7,14,20,19,38]))),yt=(0,d.lazy)(()=>x(()=>import("./LlmUsage-Cue3bOb4.js"),__vite__mapDeps([39,2,1,4]))),ts=(0,d.lazy)(()=>x(()=>import("./DocsPage-DVVXACju.js"),__vite__mapDeps([40,2,1,3,26,27,28,29,14]))),ns=(0,d.lazy)(()=>x(()=>import("./Members-D5jBjdhR.js"),__vite__mapDeps([41,2,1,4,7]))),is=(0,d.lazy)(()=>x(()=>import("./Billing-CjwNK_Ga.js"),__vite__mapDeps([42,2,1,3,4]))),as=(0,d.lazy)(()=>x(()=>import("./Reglages-C-B9GoJT.js"),__vite__mapDeps([43,2,1,3,4,7,20,19,14,38,22])));function Ct(){const{pathname:e,search:n,hash:i}=W();return(0,t.jsx)(_,{to:e.replace(/^\/sites(?=\/|$)/,"/projects")+n+i,replace:!0})}function ss(){return(0,t.jsxs)(m,{path:"/",element:(0,t.jsx)(fa,{}),children:[(0,t.jsx)(m,{index:!0,element:(0,t.jsx)(ut,{})}),(0,t.jsx)(m,{path:"threads",element:(0,t.jsx)(mt,{})}),(0,t.jsx)(m,{path:"threads/:id",element:(0,t.jsx)(pt,{})}),(0,t.jsx)(m,{path:"tasks",element:(0,t.jsx)(bt,{})}),(0,t.jsx)(m,{path:"tasks/:id",element:(0,t.jsx)(ft,{})}),(0,t.jsx)(m,{path:"search",element:(0,t.jsx)(vt,{})}),(0,t.jsx)(m,{path:"bugs",element:(0,t.jsx)(_,{to:"/tasks",replace:!0})}),(0,t.jsx)(m,{path:"bugs/:id",element:(0,t.jsx)(_,{to:"/tasks",replace:!0})}),(0,t.jsx)(m,{path:"projects",element:(0,t.jsx)(Za,{})}),(0,t.jsx)(m,{path:"projects/new",element:(0,t.jsx)(Pe,{})}),(0,t.jsx)(m,{path:"projects/:id/edit",element:(0,t.jsx)(Pe,{})}),(0,t.jsx)(m,{path:"sites",element:(0,t.jsx)(Ct,{})}),(0,t.jsx)(m,{path:"sites/*",element:(0,t.jsx)(Ct,{})}),(0,t.jsx)(m,{path:"members",element:(0,t.jsx)(ns,{})}),(0,t.jsx)(m,{path:"billing",element:(0,t.jsx)(is,{})}),(0,t.jsx)(m,{path:"settings",element:(0,t.jsx)(_,{to:"/settings/organisation",replace:!0})}),(0,t.jsx)(m,{path:"settings/:onglet",element:(0,t.jsx)(as,{})}),(0,t.jsx)(m,{path:"profile",element:(0,t.jsx)(_,{to:"/settings/compte",replace:!0})}),(0,t.jsx)(m,{path:"notifications",element:(0,t.jsx)(ht,{})}),(0,t.jsx)(m,{path:"connectors",element:(0,t.jsx)(gt,{})}),(0,t.jsx)(m,{path:"llm-tokens",element:(0,t.jsx)(kt,{})}),(0,t.jsx)(m,{path:"llm-usage",element:(0,t.jsx)(yt,{})}),(0,t.jsx)(m,{path:"history",element:(0,t.jsx)(es,{})}),(0,t.jsx)(m,{path:"docs",element:(0,t.jsx)(_,{to:`/docs/${$a}`,replace:!0})}),(0,t.jsx)(m,{path:"docs/*",element:(0,t.jsx)(ts,{})}),(0,t.jsx)(m,{path:"mcp",element:(0,t.jsx)(_,{to:"/docs/api/brancher",replace:!0})}),(0,t.jsx)(m,{path:"docs/api/mcp",element:(0,t.jsx)(_,{to:"/docs/api/brancher",replace:!0})}),(0,t.jsx)(m,{path:"p",element:(0,t.jsx)(_,{to:"/",replace:!0})}),(0,t.jsxs)(m,{path:"p/:siteId",element:(0,t.jsx)(Ki,{}),children:[(0,t.jsx)(m,{index:!0,element:(0,t.jsx)(ut,{})}),(0,t.jsx)(m,{path:"threads",element:(0,t.jsx)(mt,{})}),(0,t.jsx)(m,{path:"threads/:id",element:(0,t.jsx)(pt,{})}),(0,t.jsx)(m,{path:"tasks",element:(0,t.jsx)(bt,{})}),(0,t.jsx)(m,{path:"tasks/:id",element:(0,t.jsx)(ft,{})}),(0,t.jsx)(m,{path:"search",element:(0,t.jsx)(vt,{})}),(0,t.jsx)(m,{path:"llm-usage",element:(0,t.jsx)(yt,{})}),(0,t.jsx)(m,{path:"connectors",element:(0,t.jsx)(gt,{})}),(0,t.jsx)(m,{path:"notifications",element:(0,t.jsx)(ht,{})}),(0,t.jsx)(m,{path:"llm-tokens",element:(0,t.jsx)(kt,{})}),(0,t.jsx)(m,{path:"settings",element:(0,t.jsx)(Pe,{})}),(0,t.jsx)(m,{path:"members",element:(0,t.jsx)(Se,{vers:"/members"})}),(0,t.jsx)(m,{path:"billing",element:(0,t.jsx)(Se,{vers:"/billing"})}),(0,t.jsx)(m,{path:"history",element:(0,t.jsx)(Se,{vers:"/history"})}),(0,t.jsx)(m,{path:"*",element:(0,t.jsx)(Ji,{})})]})]})}var os="false",rs="false",wt=os==="true",ds=rs==="true";function ls(e={}){const{immediate:n=!1,onNeedReload:i,onNeedRefresh:a,onOfflineReady:o,onRegistered:r,onRegisteredSW:l,onRegisterError:h}=e;let p,w,v;const b=async(c=!0)=>{await w,wt||v?.()};async function y(){if("serviceWorker"in navigator){if(p=await x(async()=>{const{Workbox:c}=await import("./workbox-window.prod.es5-Dxeuga9-.js");return{Workbox:c}},[]).then(({Workbox:c})=>new c("/sw.js",{scope:"/",type:"classic"})).catch(c=>{h?.(c)}),!p)return;if(v=()=>{p?.messageSkipWaiting()},!ds)if(wt)p.addEventListener("activated",c=>{(c.isUpdate||c.isExternal)&&(i?i():window.location.reload())}),p.addEventListener("installed",c=>{c.isUpdate||o?.()});else{let c=!1;const u=()=>{c=!0,p?.addEventListener("controlling",f=>{f.isUpdate&&(i?i():window.location.reload())}),a?.()};p.addEventListener("installed",f=>{typeof f.isUpdate>"u"?typeof f.isExternal<"u"&&f.isExternal?u():!c&&o?.():f.isUpdate||o?.()}),p.addEventListener("waiting",u)}p.register({immediate:n}).then(c=>{l?l("/sw.js",c):r?.(c)}).catch(c=>{h?.(c)})}}return w=y(),b}function cs(e={}){const{immediate:n=!0,onNeedReload:i,onNeedRefresh:a,onOfflineReady:o,onRegistered:r,onRegisteredSW:l,onRegisterError:h}=e,[p,w]=(0,d.useState)(!1),[v,b]=(0,d.useState)(!1),[y]=(0,d.useState)(()=>ls({immediate:n,onNeedReload:i,onOfflineReady(){b(!0),o?.()},onNeedRefresh(){w(!0),a?.()},onRegistered:r,onRegisteredSW:l,onRegisterError:h}));return{needRefresh:[p,w],offlineReady:[v,b],updateServiceWorker:y}}var us=9e5,$t="bipwire.pwa.installation-refusee",ms=2592e6,ps=()=>window.matchMedia("(display-mode: standalone)").matches||navigator.standalone===!0,hs=()=>{const e=navigator.userAgent;return(/iphone|ipad|ipod/i.test(e)||/macintosh/i.test(e)&&navigator.maxTouchPoints>1)&&/safari/i.test(e)&&!/crios|fxios|edgios/i.test(e)},gs=()=>{try{const e=Number(localStorage.getItem($t));return Number.isFinite(e)&&e>0&&Date.now()-e<ms}catch{return!1}},Tt=()=>{try{localStorage.setItem($t,String(Date.now()))}catch{}};function bs(){const{t:e}=R(),{needRefresh:[n,i],updateServiceWorker:a}=cs({onRegisteredSW(b,y){if(!y)return;const c=async()=>{y.installing||!navigator.onLine||(await fetch(b,{cache:"no-store",headers:{"cache-control":"no-cache"}}).catch(()=>null))?.status===200&&await y.update()};setInterval(c,us),document.addEventListener("visibilitychange",()=>{document.visibilityState==="visible"&&c()})}}),[o,r]=(0,d.useState)(null),[l,h]=(0,d.useState)(!1);(0,d.useEffect)(()=>{if(ps()||gs())return;if(hs()){h(!0);return}const b=c=>{c.preventDefault(),r(c)},y=()=>r(null);return window.addEventListener("beforeinstallprompt",b),window.addEventListener("appinstalled",y),()=>{window.removeEventListener("beforeinstallprompt",b),window.removeEventListener("appinstalled",y)}},[]);const p=()=>{Tt(),r(null),h(!1)},w=async()=>{if(!o)return;await o.prompt();const{outcome:b}=await o.userChoice;b==="dismissed"&&Tt(),r(null)},v={vertical:"bottom",horizontal:"left"};return n?(0,t.jsx)(Ae,{open:!0,anchorOrigin:v,children:(0,t.jsx)(Y,{severity:"info",variant:"filled",onClose:()=>i(!1),action:(0,t.jsx)(M,{color:"inherit",size:"small",onClick:()=>{a(!0)},children:e("admin.pwa.reload")}),children:e("admin.pwa.updateAvailable")})}):!o&&!l?null:(0,t.jsx)(Ae,{open:!0,anchorOrigin:v,children:(0,t.jsx)(Y,{severity:"info",icon:(0,t.jsx)("img",{src:"/pwa-192x192.png",alt:"",width:24,height:24,style:{borderRadius:6}}),onClose:p,action:o?(0,t.jsxs)(qe,{direction:"row",spacing:1,children:[(0,t.jsx)(M,{color:"inherit",size:"small",onClick:p,children:e("admin.pwa.later")}),(0,t.jsx)(M,{variant:"contained",size:"small",onClick:()=>{w()},children:e("admin.pwa.install")})]}):void 0,children:e(o?"admin.pwa.installPrompt":"admin.pwa.installIos")})})}var fs=(0,d.lazy)(()=>x(()=>import("./Login-CVbuzoBF.js").then(e=>({default:e.Login})),__vite__mapDeps([44,2,1,3,45,19]))),vs=(0,d.lazy)(()=>x(()=>import("./Signup-B0-sT1IM.js").then(e=>({default:e.Signup})),__vite__mapDeps([46,2,1,3,4,19,45]))),ks=(0,d.lazy)(()=>x(()=>import("./NotFound-_j5RqBgA.js").then(e=>({default:e.NotFound})),__vite__mapDeps([47,1,2,3]))),ys=(0,d.lazy)(()=>x(()=>import("./AcceptInvite-DMeccGBj.js").then(e=>({default:e.AcceptInvite})),__vite__mapDeps([48,2,1,3,4,45,19]))),Cs=(0,d.lazy)(()=>x(()=>import("./ForgotPassword-D1Wd2zXQ.js").then(e=>({default:e.ForgotPassword})),__vite__mapDeps([49,2,1,3,45,19]))),ws=(0,d.lazy)(()=>x(()=>import("./ResetPassword-RzwcXm1M.js").then(e=>({default:e.ResetPassword})),__vite__mapDeps([50,2,1,3]))),Ts=(0,d.lazy)(()=>x(()=>import("./VerifyEmail-Cp6CYZ-F.js").then(e=>({default:e.VerifyEmail})),__vite__mapDeps([51,2,1,3]))),xt=document.querySelector('meta[name="bipwire-zone"]')?.content||"/app";if(xt!=="/app")throw new Error(`Zone incohérente : ce bundle est compilé pour « /app » mais il est servi comme « ${xt} ». L'arbre a été déployé au mauvais endroit.`);function St(){const e=W(),n=e.pathname.replace(/^\/(?:app|admin)(?=\/|$)/,"")||"/";return(0,t.jsx)(_,{to:`${n}${e.search}`,replace:!0})}function xs(){return(0,t.jsxs)(Oi,{initialLang:"en",children:[(0,t.jsx)(Wi,{}),(0,t.jsx)(bs,{}),(0,t.jsx)(d.Suspense,{fallback:(0,t.jsx)(Dt,{}),children:(0,t.jsxs)(Vn,{children:[(0,t.jsx)(m,{path:"/login",element:(0,t.jsx)(fs,{})}),(0,t.jsx)(m,{path:"/signup",element:(0,t.jsx)(vs,{})}),(0,t.jsx)(m,{path:"/accept-invite",element:(0,t.jsx)(ys,{})}),(0,t.jsx)(m,{path:"/forgot-password",element:(0,t.jsx)(Cs,{})}),(0,t.jsx)(m,{path:"/reset-password",element:(0,t.jsx)(ws,{})}),(0,t.jsx)(m,{path:"/verify-email",element:(0,t.jsx)(Ts,{})}),(0,t.jsx)(m,{element:(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(Ii,{}),(0,t.jsx)(Di,{})]}),children:ss()}),(0,t.jsx)(m,{path:"/*",element:(0,t.jsx)(St,{})}),(0,t.jsx)(m,{path:"/*",element:(0,t.jsx)(St,{})}),(0,t.jsx)(m,{path:"*",element:(0,t.jsx)(ks,{})})]})})]})}var ae=null;function Ss(e){ae?ae(e):console.error("[ErrorSnackbar] handler non monté:",e)}var js=(0,d.createContext)(null);function Ps({children:e}){const[n,i]=(0,d.useState)(!1),[a,o]=(0,d.useState)(""),r=(0,d.useCallback)(h=>{o(h),i(!0)},[]);(0,d.useEffect)(()=>(ae=r,()=>{ae===r&&(ae=null)}),[r]);const l=(0,d.useMemo)(()=>({showError:r}),[r]);return(0,t.jsxs)(js.Provider,{value:l,children:[e,(0,t.jsx)(Ae,{open:n,autoHideDuration:6e3,onClose:(h,p)=>{p!=="clickaway"&&i(!1)},anchorOrigin:{vertical:"bottom",horizontal:"center"},children:(0,t.jsx)(Y,{severity:"error",variant:"filled",onClose:()=>i(!1),sx:{width:"100%"},children:a})})]})}function Es(){const e=window.visualViewport;if(!e)return;const n=document.documentElement.style,i=()=>{n.setProperty("--bw-hauteur-visible",`${Math.round(e.height)}px`),n.setProperty("--bw-decalage-visible",`${Math.round(e.offsetTop)}px`)};i(),e.addEventListener("resize",i),e.addEventListener("scroll",i)}ha();Es();var As=new Qn({defaultOptions:{queries:{refetchOnWindowFocus:!0,retry:1}},mutationCache:new Kn({onError:(e,n,i,a)=>{if(a.options.meta?.suppressGlobalError===!0)return;const o=e.message||Le("admin.common.unknownError");Ss(o)}})}),Ds=Wn([{path:"*",element:(0,t.jsx)(Ai,{children:(0,t.jsx)(xs,{})})}]),Kt=document.getElementById("root");if(!Kt)throw new Error("Root element #root not found");(0,Yn.createRoot)(Kt).render((0,t.jsx)(d.StrictMode,{children:(0,t.jsx)(di,{children:(0,t.jsx)(Ps,{children:(0,t.jsx)(Jn,{client:As,children:(0,t.jsx)(Gn,{router:Ds})})})})}));export{s as C,D as S,me as T,Be as _,Us as a,qs as b,oa as c,Ne as d,X as f,Bt as g,Bs as h,Ws as i,aa as l,Hs as m,$a as n,Ns as o,R as p,Ga as r,sa as s,x as t,Ue as u,Ms as v,li as w,Os as x,Ie as y};
