diff --git a/API Server/README.md b/API Server/README.md index 6a6aa7f..05db82c 100644 --- a/API Server/README.md +++ b/API Server/README.md @@ -194,8 +194,9 @@ POST /api/vehicle-providers/{provider}/import # which PATCH /api/me {carOrder} sets) GET /api/cars POST /api/cars GET /api/cars/{id} PATCH /api/cars/{id} DELETE /api/cars/{id} -PUT /api/cars/{id}/view # which tabs + Information rows this car shows, and - # the order of the tabs, the rows and the provider readings +PUT /api/cars/{id}/view # which tabs, Information rows and service-history + # columns this car shows, and the order of the tabs, + # the rows, the columns and the provider readings GET /api/cars/{id}/provider POST /api/cars/{id}/provider POST /api/cars/{id}/provider/sync GET /api/cars/{id}/service-records GET /api/cars/{id}/technical-checks diff --git a/API Server/internal/api/cars.go b/API Server/internal/api/cars.go index 2fc0b77..e7b1a63 100644 --- a/API Server/internal/api/cars.go +++ b/API Server/internal/api/cars.go @@ -271,6 +271,30 @@ var hideableCarFields = map[string]bool{ "vin": true, "fuelType": true, "buildDate": true, "firstRegistration": true, } +// hideableServiceColumns are the columns of the Service history table that can +// be switched off. Date is deliberately not among them: every row of that table +// is a service that happened on a day, and a history with the day taken out +// stops being a history. Mirrors the car.services.col* labels the web app +// renders. +var hideableServiceColumns = map[string]bool{ + "km": true, "nextDate": true, "nextKm": true, "oil": true, + "engineFilter": true, "cabinFilter": true, "notes": true, "file": true, +} + +// arrangeableServiceColumns are the columns that table can be rearranged into: +// the hideable ones plus Date, which cannot be switched off but has no reason to +// be stuck at the left. Derived from hideableServiceColumns so the two sets +// cannot drift as columns are added — the same construction arrangeableCarTabs +// uses for Information. +var arrangeableServiceColumns = func() map[string]bool { + out := make(map[string]bool, len(hideableServiceColumns)+1) + for key := range hideableServiceColumns { + out[key] = true + } + out["date"] = true + return out +}() + // arrangeableCarMetrics are the headline readings on the connected-service tab, // and so the keys a car's arrangement of them may name. Derived from the reading // specs in vehicleproviders.go rather than written out again, so the set cannot @@ -308,21 +332,25 @@ func normalizeKeys(in []string, allowed map[string]bool, what string) ([]string, } // PUT /api/cars/{id}/view — choose what this car's page shows: which tabs, which -// rows of the Information tab, and the order the tabs, the Information rows and -// the connected service's headline readings are laid out in. Body: {hiddenTabs?: -// [...], hiddenFields?: [...], tabOrder?: [...], fieldOrder?: [...], -// metricOrder?: [...]}; only the lists present are written, so a client can -// rearrange one group without resending the others. Its own endpoint rather than fields on the car edit, so an ordinary +// rows of the Information tab, which columns of the Service history table, and +// the order the tabs, the Information rows, those columns and the connected +// service's headline readings are laid out in. Body: {hiddenTabs?: [...], +// hiddenFields?: [...], hiddenServiceColumns?: [...], tabOrder?: [...], +// fieldOrder?: [...], serviceColumnOrder?: [...], metricOrder?: [...]}; only the +// lists present are written, so a client can rearrange one group without +// resending the others. Its own endpoint rather than fields on the car edit, so an ordinary // save of the car form — which sends every other field — can never reveal // something somebody deliberately switched off. Needs write access: the choice // belongs to the car, so it is the same permission as editing it. func (s *Server) updateCarView(w http.ResponseWriter, r *http.Request) { var in struct { - HiddenTabs *[]string `json:"hiddenTabs"` - HiddenFields *[]string `json:"hiddenFields"` - TabOrder *[]string `json:"tabOrder"` - FieldOrder *[]string `json:"fieldOrder"` - MetricOrder *[]string `json:"metricOrder"` + HiddenTabs *[]string `json:"hiddenTabs"` + HiddenFields *[]string `json:"hiddenFields"` + HiddenServiceColumns *[]string `json:"hiddenServiceColumns"` + TabOrder *[]string `json:"tabOrder"` + FieldOrder *[]string `json:"fieldOrder"` + ServiceColumnOrder *[]string `json:"serviceColumnOrder"` + MetricOrder *[]string `json:"metricOrder"` } if err := decodeJSON(r, &in); err != nil { writeError(w, http.StatusBadRequest, err.Error()) @@ -355,6 +383,14 @@ func (s *Server) updateCarView(w http.ResponseWriter, r *http.Request) { } payload["hidden_fields"] = fields } + if in.HiddenServiceColumns != nil { + columns, err := normalizeKeys(*in.HiddenServiceColumns, hideableServiceColumns, "service column") + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + payload["hidden_service_columns"] = columns + } if in.TabOrder != nil { // A wider set than the hidden tabs: Information is arrangeable although it // cannot be switched off. A partial list is accepted, and the tabs it @@ -379,6 +415,19 @@ func (s *Server) updateCarView(w http.ResponseWriter, r *http.Request) { } payload["field_order"] = order } + if in.ServiceColumnOrder != nil { + // A wider set than the hidden columns, for the same reason the tab order + // is: Date is arrangeable although it cannot be switched off. A partial + // list is accepted, and the columns it leaves out follow the arranged + // ones, so a column added in a later release lands at the right-hand end + // rather than in the middle of somebody's table. + order, err := normalizeKeys(*in.ServiceColumnOrder, arrangeableServiceColumns, "service column") + if err != nil { + writeError(w, http.StatusBadRequest, err.Error()) + return + } + payload["service_column_order"] = order + } if in.MetricOrder != nil { // A partial list again, and here it is the normal case: the client can // only arrange the readings the provider actually reported, so one it diff --git a/API Server/internal/api/cartabs_test.go b/API Server/internal/api/cartabs_test.go index ea01faf..50ad0dc 100644 --- a/API Server/internal/api/cartabs_test.go +++ b/API Server/internal/api/cartabs_test.go @@ -2,11 +2,12 @@ package api import "testing" -// What a car's page shows — which tabs, which rows of the Information tab, and -// the order those rows are laid out in — is stored on the car as key lists, so -// the validation has to keep them to keys the page actually renders. -// Information itself stays out of the hideable tabs: a car with no tabs left -// would be a dead end. +// What a car's page shows — which tabs, which rows of the Information tab, which +// columns of the Service history table, and the order each of those is laid out +// in — is stored on the car as key lists, so the validation has to keep them to +// keys the page actually renders. Two keys stay out of their hideable set: +// Information, because a car with no tabs left would be a dead end, and the +// service Date, because a history with the day taken out is not one. func TestNormalizeHiddenTabs(t *testing.T) { got, err := normalizeKeys([]string{" fuel ", "parts", "fuel", ""}, hideableCarTabs, "tab") @@ -160,6 +161,69 @@ func TestNormalizeMetricOrder(t *testing.T) { } } +func TestNormalizeHiddenServiceColumns(t *testing.T) { + got, err := normalizeKeys([]string{" oil ", "notes", "oil", ""}, hideableServiceColumns, "service column") + if err != nil { + t.Fatalf("normalizeKeys: %v", err) + } + assertKeys(t, got, []string{"oil", "notes"}) // trimmed, blanks dropped, deduped + + // The date is what a service record is; a table of them without it would be + // a list of unattributed work. + if _, err := normalizeKeys([]string{"date"}, hideableServiceColumns, "service column"); err == nil { + t.Error("normalizeKeys allowed hiding the date column, want an error") + } + // Neither a field key nor an invented one passes: each set is its own. + if _, err := normalizeKeys([]string{"vin"}, hideableServiceColumns, "service column"); err == nil { + t.Error("normalizeKeys accepted a field key as a service column, want an error") + } + if _, err := normalizeKeys([]string{"oil", "nonsense"}, hideableServiceColumns, "service column"); err == nil { + t.Error("normalizeKeys accepted an unknown service column, want an error") + } + + // The hideable set is the contract the web app's HIDEABLE_SERVICE_COLUMNS + // mirrors: every column that table renders beside the date. + for _, key := range []string{ + "km", "nextDate", "nextKm", "oil", "engineFilter", "cabinFilter", + "notes", "file", + } { + if !hideableServiceColumns[key] { + t.Errorf("service column %q should be hideable", key) + } + } + if len(hideableServiceColumns) != 8 { + t.Errorf("hideableServiceColumns has %d entries, want the 8 columns beside the date", len(hideableServiceColumns)) + } +} + +// The columns arrange against a wider set than they hide against, the way the +// tabs do: the date cannot be switched off, but it can be moved off the left. +func TestNormalizeServiceColumnOrder(t *testing.T) { + got, err := normalizeKeys([]string{"notes", "date", "km"}, arrangeableServiceColumns, "service column") + if err != nil { + t.Fatalf("normalizeKeys: %v", err) + } + assertKeys(t, got, []string{"notes", "date", "km"}) + + for key := range hideableServiceColumns { + if !arrangeableServiceColumns[key] { + t.Errorf("service column %q should be arrangeable", key) + } + } + if !arrangeableServiceColumns["date"] { + t.Error("the date column should be arrangeable even though it cannot be hidden") + } + if len(arrangeableServiceColumns) != len(hideableServiceColumns)+1 { + t.Errorf("arrangeableServiceColumns has %d entries, want the hideable columns plus the date", len(arrangeableServiceColumns)) + } + + // A partial arrangement is fine — the columns it leaves out follow the + // arranged ones — but an invented column is still an error. + if _, err := normalizeKeys([]string{"date", "nonsense"}, arrangeableServiceColumns, "service column"); err == nil { + t.Error("normalizeKeys accepted an unknown column in an arrangement, want an error") + } +} + func assertKeys(t *testing.T, got, want []string) { t.Helper() if len(got) != len(want) { diff --git a/API Server/internal/api/dist/assets/index-D2BCqgpA.js b/API Server/internal/api/dist/assets/index-E4ifC_ff.js similarity index 89% rename from API Server/internal/api/dist/assets/index-D2BCqgpA.js rename to API Server/internal/api/dist/assets/index-E4ifC_ff.js index ab572ff..9b47a6e 100644 --- a/API Server/internal/api/dist/assets/index-D2BCqgpA.js +++ b/API Server/internal/api/dist/assets/index-E4ifC_ff.js @@ -14,4 +14,4 @@ * @vue/runtime-dom v3.5.39 * (c) 2018-present Yuxi (Evan) You and Vue contributors * @license MIT -**/let an;const Xn=typeof window<"u"&&window.trustedTypes;if(Xn)try{an=Xn.createPolicy("vue",{createHTML:e=>e})}catch{}const _o=an?e=>an.createHTML(e):e=>e,ka="http://www.w3.org/2000/svg",Sa="http://www.w3.org/1998/Math/MathML",st=typeof document<"u"?document:null,Qn=st&&st.createElement("template"),Ta={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const i=t==="svg"?st.createElementNS(ka,e):t==="mathml"?st.createElementNS(Sa,e):s?st.createElement(e,{is:s}):st.createElement(e);return e==="select"&&n&&n.multiple!=null&&i.setAttribute("multiple",n.multiple),i},createText:e=>st.createTextNode(e),createComment:e=>st.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>st.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,i,o){const r=s?s.previousSibling:t.lastChild;if(i&&(i===o||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),s),!(i===o||!(i=i.nextSibling)););else{Qn.innerHTML=_o(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const a=Qn.content;if(n==="svg"||n==="mathml"){const l=a.firstChild;for(;l.firstChild;)a.appendChild(l.firstChild);a.removeChild(l)}t.insertBefore(a,s)}return[r?r.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Ea=Symbol("_vtc");function Pa(e,t,s){const n=e[Ea];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const ei=Symbol("_vod"),Ca=Symbol("_vsh"),Aa=Symbol(""),$a=/(?:^|;)\s*display\s*:/;function Oa(e,t,s){const n=e.style,i=oe(s);let o=!1;if(s&&!i){if(t)if(oe(t))for(const r of t.split(";")){const a=r.slice(0,r.indexOf(":")).trim();s[a]==null&&Bt(n,a,"")}else for(const r in t)s[r]==null&&Bt(n,r,"");for(const r in s){r==="display"&&(o=!0);const a=s[r];a!=null?za(e,r,!oe(t)&&t?t[r]:void 0,a)||Bt(n,r,a):Bt(n,r,"")}}else if(i){if(t!==s){const r=n[Aa];r&&(s+=";"+r),n.cssText=s,o=$a.test(s)}}else t&&e.removeAttribute("style");ei in e&&(e[ei]=o?n.display:"",e[Ca]&&(n.display="none"))}const ti=/\s*!important$/;function Bt(e,t,s){if(M(s))s.forEach(n=>Bt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=Ra(e,t);ti.test(s)?e.setProperty(ht(n),s.replace(ti,""),"important"):e[n]=s}}const si=["Webkit","Moz","ms"],qs={};function Ra(e,t){const s=qs[t];if(s)return s;let n=De(t);if(n!=="filter"&&n in e)return qs[t]=n;n=_i(n);for(let i=0;iZs||(Ma.then(()=>Zs=0),Zs=Date.now());function Fa(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;const i=s.value;if(M(i)){const o=n.stopImmediatePropagation;n.stopImmediatePropagation=()=>{o.call(n),n._stopped=!0};const r=i.slice(),a=[n];for(let l=0;le.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Ba=(e,t,s,n,i,o)=>{const r=i==="svg";t==="class"?Pa(e,n,r):t==="style"?Oa(e,s,n):Ts(t)?Es(t)||Ia(e,t,s,n,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ha(e,t,n,r))?(oi(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&ii(e,t,n,r,o,t!=="value")):e._isVueCE&&(Va(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!oe(n)))?oi(e,De(t),n,o,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),ii(e,t,n,r))};function Ha(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&ai(t)&&V(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return ai(t)&&oe(s)?!1:t in e}function Va(e,t){const s=e._def.props;if(!s)return!1;const n=De(t);return Array.isArray(s)?s.some(i=>De(i)===n):Object.keys(s).some(i=>De(i)===n)}const ft=e=>{const t=e.props["onUpdate:modelValue"]||!1;return M(t)?s=>ps(t,s):t};function Ga(e){e.target.composing=!0}function li(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ze=Symbol("_assign");function ci(e,t,s){return t&&(e=e.trim()),s&&(e=Cs(e)),e}const ge={created(e,{modifiers:{lazy:t,trim:s,number:n}},i){e[ze]=ft(i);const o=n||i.props&&i.props.type==="number";rt(e,t?"change":"input",r=>{r.target.composing||e[ze](ci(e.value,s,o))}),(s||o)&&rt(e,"change",()=>{e.value=ci(e.value,s,o)}),t||(rt(e,"compositionstart",Ga),rt(e,"compositionend",li),rt(e,"change",li))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:i,number:o}},r){if(e[ze]=ft(r),e.composing)return;const a=(o||e.type==="number")&&!/^0\d/.test(e.value)?Cs(e.value):e.value,l=t??"";if(a===l)return;const g=e.getRootNode();(g instanceof Document||g instanceof ShadowRoot)&&g.activeElement===e&&e.type!=="range"&&(n&&t===s||i&&e.value.trim()===l)||(e.value=l)}},Ka={deep:!0,created(e,t,s){e[ze]=ft(s),rt(e,"change",()=>{const n=e._modelValue,i=Ot(e),o=e.checked,r=e[ze];if(M(n)){const a=fn(n,i),l=a!==-1;if(o&&!l)r(n.concat(i));else if(!o&&l){const g=[...n];g.splice(a,1),r(g)}}else if(Dt(n)){const a=new Set(n);o?a.add(i):a.delete(i),r(a)}else r(wo(e,o))})},mounted:ui,beforeUpdate(e,t,s){e[ze]=ft(s),ui(e,t,s)}};function ui(e,{value:t,oldValue:s},n){e._modelValue=t;let i;if(M(t))i=fn(t,n.props.value)>-1;else if(Dt(t))i=t.has(n.props.value);else{if(t===s)return;i=dt(t,wo(e,!0))}e.checked!==i&&(e.checked=i)}const Wa={created(e,{value:t},s){e.checked=dt(t,s.props.value),e[ze]=ft(s),rt(e,"change",()=>{e[ze](Ot(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[ze]=ft(n),t!==s&&(e.checked=dt(t,n.props.value))}},ks={deep:!0,created(e,{value:t,modifiers:{number:s}},n){const i=Dt(t);rt(e,"change",()=>{const o=Array.prototype.filter.call(e.options,r=>r.selected).map(r=>s?Cs(Ot(r)):Ot(r));e[ze](e.multiple?i?new Set(o):o:o[0]),e._assigning=!0,Fi(()=>{e._assigning=!1})}),e[ze]=ft(n)},mounted(e,{value:t}){di(e,t)},beforeUpdate(e,t,s){e[ze]=ft(s)},updated(e,{value:t}){e._assigning||di(e,t)}};function di(e,t){const s=e.multiple,n=M(t);if(!(s&&!n&&!Dt(t))){for(let i=0,o=e.options.length;iString(g)===String(a)):r.selected=fn(t,a)>-1}else r.selected=t.has(a);else if(dt(Ot(r),t)){e.selectedIndex!==i&&(e.selectedIndex=i);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ot(e){return"_value"in e?e._value:e.value}function wo(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const qa={created(e,t,s){ds(e,t,s,null,"created")},mounted(e,t,s){ds(e,t,s,null,"mounted")},beforeUpdate(e,t,s,n){ds(e,t,s,n,"beforeUpdate")},updated(e,t,s,n){ds(e,t,s,n,"updated")}};function Za(e,t){switch(e){case"SELECT":return ks;case"TEXTAREA":return ge;default:switch(t){case"checkbox":return Ka;case"radio":return Wa;default:return ge}}}function ds(e,t,s,n,i){const r=Za(e.tagName,s.props&&s.props.type)[i];r&&r(e,t,s,n)}const Ja=["ctrl","shift","alt","meta"],Ya={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Ja.some(s=>e[`${s}Key`]&&!t.includes(s))},Xa=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=((i,...o)=>{for(let r=0;r{const s=e._withKeys||(e._withKeys={}),n=t.join(".");return s[n]||(s[n]=(i=>{if(!("key"in i))return;const o=ht(i.key);if(t.some(r=>r===o||Qa[r]===o))return e(i)}))},el=ve({patchProp:Ba},Ta);let pi;function tl(){return pi||(pi=ia(el))}const sl=((...e)=>{const t=tl().createApp(...e),{mount:s}=t;return t.mount=n=>{const i=il(n);if(!i)return;const o=t._component;!V(o)&&!o.render&&!o.template&&(o.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const r=s(i,!1,nl(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),r},t});function nl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function il(e){return oe(e)?document.querySelector(e):e}const ko="dh-panel-theme";function ol(){var e;try{const t=localStorage.getItem(ko);if(t==="dark"||t==="light")return t}catch{}return(e=window.matchMedia)!=null&&e.call(window,"(prefers-color-scheme: dark)").matches?"dark":"light"}const Ct=H(ol());function So(e){Ct.value=e,document.documentElement.classList.toggle("dark",e==="dark");try{localStorage.setItem(ko,e)}catch{}}function fi(){So(Ct.value==="dark"?"light":"dark")}So(Ct.value);const rl={apiServer:"API server",switchToLight:"Switch to light theme",switchToDark:"Switch to dark theme",theme:"Theme",signOut:"Sign out",loading:"Loading…",standardUserNote:"Signed in as a standard user — management sections need an admin role",footer:"DriverVault — car maintenance & service tracker."},al={overview:"Overview",users:"Users",orgs:"Organizations",server:"API Server",pocketbase:"PocketBase",webapp:"Web App",plugins:"Plugins",api:"API"},ll={title:"Sign in",subtitle:"Superadmin console for the DriverVault API Server.",email:"Email",password:"Password",submit:"Sign in",submitting:"Signing in…",invalid:"Invalid email or password.",failed:"Could not sign in.",authNote:"Authenticates against PocketBase through this server"},cl={title:"Status",unreachable:"unreachable",checking:"Checking…",apiServer:"API Server",pocketBase:"PocketBase",webApp:"Web App",thisProcess:"this process"},ul={create:"Create",save:"Save",cancel:"Cancel",edit:"Edit",delete:"Delete",rename:"Rename",empty:"—",name:"Name",email:"Email",role:"Role",organization:"Organization"},dl={title:"Users",allOrgs:"All organizations",yourOrg:"Your organization",newUser:"New user",password:"Password",passwordUnchanged:"Password (blank = unchanged)",passwordPlaceholder:"min 8 characters",orgNone:"— none —",you:"you",empty:"No users.",confirmDelete:"Delete {email}? This cannot be undone."},pl={title:"Organizations",subtitle:"Tenants users belong to",subtitleOwn:"Your organization",subtitleNone:"Create one to manage your own team",newOrg:"New organization",namePlaceholder:"Acme Fleet",empty:"No organizations yet.",createHint:"You have no organization yet. Create one to become its admin.",colId:"ID",confirmDelete:'Delete the organization "{name}"?',confirmDeleteOwn:'Delete your organization "{name}"? You will be removed from it and become a regular user.'},fl={title:"API Server",subtitle:"How this server identifies itself to the clients that connect to it",name:"Server name",nameHint:"Shown by clients that can be pointed at more than one DriverVault, such as the Web App's server switcher. Reported by /api/health.",saveApply:"Save & apply",persistedEnv:"persisted to .env",savedNotice:"Saved. The new name applies to the next request."},hl={title:"PocketBase",subtitle:"Database connection used by every endpoint",connected:"connected",noSuperuser:"no superuser",unreachable:"unreachable",baseUrl:"Base URL",superuserEmail:"Superuser email",superuserPassword:"Superuser password",passwordUnchanged:"unchanged",passwordNotSet:"not set",saveApply:"Save & apply",testConnection:"Test connection",persistedEnv:"persisted to .env",testOk:"Connection OK — superuser authenticated.",testReachableNoAuth:"PocketBase is reachable, but the service account did not authenticate.",testUnreachable:"PocketBase is not reachable at that address.",savedNotice:"Saved. The server is now using this PocketBase."},gl={title:"Web App",subtitle:"Address probed by the status check, and who may call this API from a browser",reachable:"reachable",unreachable:"unreachable",baseUrl:"Base URL",allowedOrigins:"Allowed origins",originsHint:"Comma separated, or {star} for any. Native mobile apps are not subject to CORS.",saveApply:"Save & apply",testConnection:"Test connection",persistedEnv:"persisted to .env",testOk:"Web App is reachable.",testFailed:"Web App did not answer its health check at that address.",savedNotice:"Saved. New origins apply to the next request."},ml={title:"Plugins",subtitle:"Third-party service integrations",registerExternal:"Register external",cancelRegister:"Cancel",name:"Name",baseUrl:"Base URL",provider:"Provider",register:"Register",empty:"No plugins yet. Register an external one above, or compile a built-in connector.",builtin:"builtin",enabled:"enabled",disabled:"disabled",health:"Health",configure:"Configure",close:"Close",noConfig:"This plugin takes no configuration.",notSet:"Not set — let organizations and users choose",capabilities:"Capabilities",saved:"Saved.",checking:"Checking…",save:"Save",saveEnable:"Save & enable",disable:"Disable",remove:"Remove",builtinNote:"Built-in plugins can be disabled but not removed",confirmRemove:'Remove the external plugin "{name}"? Its saved config is deleted.'},bl={colEndpoint:"Endpoint",colDescription:"Description",authNone:"No auth",authBearer:"Bearer token",authManager:"Admin / superadmin",authSuperadmin:"Superadmin",authCharger:"Charger credentials",groupPublic:"Public",groupIdentity:"Identity",groupCars:"Cars",groupService:"Service records",groupTechnical:"Technical checks",groupParts:"Parts",groupFuel:"Fuel",groupCharging:"Charging",groupMaintenance:"Maintenance",groupDocuments:"Documents",groupReminders:"Reminders",groupAttachments:"Attachments",groupProviders:"Vehicle providers",groupIntegrations:"Integrations",groupOcpp:"OCPP",groupAccount:"Account",groupManagement:"Management",groupSuperadmin:"Superadmin"},vl={app:rl,sections:al,login:ll,status:cl,common:ul,users:dl,orgs:pl,server:fl,pocketbase:hl,webapp:gl,plugins:ml,api:bl},yl={apiServer:"Serwer API",switchToLight:"Przełącz na motyw jasny",switchToDark:"Przełącz na motyw ciemny",theme:"Motyw",signOut:"Wyloguj się",loading:"Ładowanie…",standardUserNote:"Zalogowano jako zwykły użytkownik — sekcje zarządzania wymagają roli administratora",footer:"DriverVault — rejestr serwisu i konserwacji samochodu."},_l={overview:"Przegląd",users:"Użytkownicy",orgs:"Organizacje",server:"Serwer API",pocketbase:"PocketBase",webapp:"Aplikacja webowa",plugins:"Wtyczki",api:"API"},wl={title:"Zaloguj się",subtitle:"Konsola superadministratora serwera API DriverVault.",email:"E-mail",password:"Hasło",submit:"Zaloguj się",submitting:"Logowanie…",invalid:"Nieprawidłowy e-mail lub hasło.",failed:"Nie udało się zalogować.",authNote:"Uwierzytelnia w PocketBase za pośrednictwem tego serwera"},xl={title:"Status",unreachable:"niedostępny",checking:"Sprawdzanie…",apiServer:"Serwer API",pocketBase:"PocketBase",webApp:"Aplikacja webowa",thisProcess:"ten proces"},kl={create:"Utwórz",save:"Zapisz",cancel:"Anuluj",edit:"Edytuj",delete:"Usuń",rename:"Zmień nazwę",empty:"—",name:"Nazwa",email:"E-mail",role:"Rola",organization:"Organizacja"},Sl={title:"Użytkownicy",allOrgs:"Wszystkie organizacje",yourOrg:"Twoja organizacja",newUser:"Nowy użytkownik",password:"Hasło",passwordUnchanged:"Hasło (puste = bez zmian)",passwordPlaceholder:"min. 8 znaków",orgNone:"— brak —",you:"Ty",empty:"Brak użytkowników.",confirmDelete:"Usunąć {email}? Tej operacji nie można cofnąć."},Tl={title:"Organizacje",subtitle:"Podmioty, do których należą użytkownicy",subtitleOwn:"Twoja organizacja",subtitleNone:"Utwórz ją, aby zarządzać własnym zespołem",newOrg:"Nowa organizacja",namePlaceholder:"Acme Fleet",empty:"Brak organizacji.",createHint:"Nie masz jeszcze organizacji. Utwórz ją, aby zostać jej administratorem.",colId:"ID",confirmDelete:"Usunąć organizację „{name}”?",confirmDeleteOwn:"Usunąć Twoją organizację „{name}”? Zostaniesz z niej usunięty i staniesz się zwykłym użytkownikiem."},El={title:"Serwer API",subtitle:"Jak ten serwer przedstawia się klientom, które się z nim łączą",name:"Nazwa serwera",nameHint:"Widoczna w klientach, które można skierować na więcej niż jeden DriverVault, na przykład w przełączniku serwerów aplikacji webowej. Zwracana przez /api/health.",saveApply:"Zapisz i zastosuj",persistedEnv:"zapisano w .env",savedNotice:"Zapisano. Nowa nazwa obowiązuje od następnego żądania."},Pl={title:"PocketBase",subtitle:"Połączenie z bazą danych używane przez każdy punkt końcowy",connected:"połączono",noSuperuser:"brak superużytkownika",unreachable:"niedostępny",baseUrl:"Adres bazowy",superuserEmail:"E-mail superużytkownika",superuserPassword:"Hasło superużytkownika",passwordUnchanged:"bez zmian",passwordNotSet:"nie ustawiono",saveApply:"Zapisz i zastosuj",testConnection:"Testuj połączenie",persistedEnv:"zapisano w .env",testOk:"Połączenie OK — superużytkownik uwierzytelniony.",testReachableNoAuth:"PocketBase jest dostępny, ale konto usługowe nie zostało uwierzytelnione.",testUnreachable:"PocketBase jest niedostępny pod tym adresem.",savedNotice:"Zapisano. Serwer korzysta teraz z tego PocketBase."},Cl={title:"Aplikacja webowa",subtitle:"Adres sprawdzany podczas kontroli statusu oraz kto może wywoływać to API z przeglądarki",reachable:"dostępna",unreachable:"niedostępna",baseUrl:"Adres bazowy",allowedOrigins:"Dozwolone źródła",originsHint:"Oddzielone przecinkami lub {star} dla dowolnego. Natywne aplikacje mobilne nie podlegają CORS.",saveApply:"Zapisz i zastosuj",testConnection:"Testuj połączenie",persistedEnv:"zapisano w .env",testOk:"Aplikacja webowa jest dostępna.",testFailed:"Aplikacja webowa nie odpowiedziała na kontrolę stanu pod tym adresem.",savedNotice:"Zapisano. Nowe źródła obowiązują od następnego żądania."},Al={title:"Wtyczki",subtitle:"Integracje z usługami zewnętrznymi",registerExternal:"Zarejestruj zewnętrzną",cancelRegister:"Anuluj",name:"Nazwa",baseUrl:"Adres bazowy",provider:"Dostawca",register:"Zarejestruj",empty:"Brak wtyczek. Zarejestruj zewnętrzną powyżej lub skompiluj wbudowany łącznik.",builtin:"wbudowana",enabled:"włączona",disabled:"wyłączona",health:"Stan",configure:"Konfiguruj",close:"Zamknij",noConfig:"Ta wtyczka nie wymaga konfiguracji.",notSet:"Nie ustawiono — pozwól organizacjom i użytkownikom wybrać",capabilities:"Możliwości",saved:"Zapisano.",checking:"Sprawdzanie…",save:"Zapisz",saveEnable:"Zapisz i włącz",disable:"Wyłącz",remove:"Usuń",builtinNote:"Wtyczki wbudowane można wyłączyć, ale nie usunąć",confirmRemove:"Usunąć zewnętrzną wtyczkę „{name}”? Jej zapisana konfiguracja zostanie usunięta."},$l={colEndpoint:"Punkt końcowy",colDescription:"Opis",authNone:"Bez uwierzytelniania",authBearer:"Token Bearer",authManager:"Administrator / superadministrator",authSuperadmin:"Superadministrator",authCharger:"Dane logowania ładowarki",groupPublic:"Publiczne",groupIdentity:"Tożsamość",groupCars:"Samochody",groupService:"Wpisy serwisowe",groupTechnical:"Przeglądy techniczne",groupParts:"Części",groupFuel:"Paliwo",groupCharging:"Ładowanie",groupMaintenance:"Naprawy",groupDocuments:"Dokumenty",groupReminders:"Przypomnienia",groupAttachments:"Załączniki",groupProviders:"Dostawcy pojazdów",groupIntegrations:"Integracje",groupOcpp:"OCPP",groupAccount:"Konto",groupManagement:"Zarządzanie",groupSuperadmin:"Superadministrator"},Ol={app:yl,sections:_l,login:wl,status:xl,common:kl,users:Sl,orgs:Tl,server:El,pocketbase:Pl,webapp:Cl,plugins:Al,api:$l},Rl={apiServer:"API-server",switchToLight:"Skift til lyst tema",switchToDark:"Skift til mørkt tema",theme:"Tema",signOut:"Log ud",loading:"Indlæser…",standardUserNote:"Logget ind som almindelig bruger — administrationssektioner kræver en administratorrolle",footer:"DriverVault — bilservice- og vedligeholdelsesregister."},zl={overview:"Oversigt",users:"Brugere",orgs:"Organisationer",server:"API-server",pocketbase:"PocketBase",webapp:"Webapp",plugins:"Plugins",api:"API"},Dl={title:"Log ind",subtitle:"Superadmin-konsol til DriverVault API-serveren.",email:"E-mail",password:"Adgangskode",submit:"Log ind",submitting:"Logger ind…",invalid:"Ugyldig e-mail eller adgangskode.",failed:"Kunne ikke logge ind.",authNote:"Godkender mod PocketBase gennem denne server"},Il={title:"Status",unreachable:"utilgængelig",checking:"Tjekker…",apiServer:"API-server",pocketBase:"PocketBase",webApp:"Webapp",thisProcess:"denne proces"},jl={create:"Opret",save:"Gem",cancel:"Annuller",edit:"Rediger",delete:"Slet",rename:"Omdøb",empty:"—",name:"Navn",email:"E-mail",role:"Rolle",organization:"Organisation"},Ul={title:"Brugere",allOrgs:"Alle organisationer",yourOrg:"Din organisation",newUser:"Ny bruger",password:"Adgangskode",passwordUnchanged:"Adgangskode (tom = uændret)",passwordPlaceholder:"mindst 8 tegn",orgNone:"— ingen —",you:"dig",empty:"Ingen brugere.",confirmDelete:"Slet {email}? Dette kan ikke fortrydes."},Nl={title:"Organisationer",subtitle:"Enheder, som brugere tilhører",subtitleOwn:"Din organisation",subtitleNone:"Opret en for at administrere dit eget team",newOrg:"Ny organisation",namePlaceholder:"Acme Fleet",empty:"Ingen organisationer endnu.",createHint:"Du har endnu ingen organisation. Opret en for at blive dens administrator.",colId:"ID",confirmDelete:'Slet organisationen "{name}"?',confirmDeleteOwn:"Slet din organisation „{name}“? Du fjernes fra den og bliver en almindelig bruger."},Ml={title:"API-server",subtitle:"Hvordan denne server identificerer sig over for de klienter, der forbinder til den",name:"Servernavn",nameHint:"Vises af klienter, der kan pege på mere end én DriverVault, for eksempel webappens serverskifter. Returneres af /api/health.",saveApply:"Gem og anvend",persistedEnv:"gemt i .env",savedNotice:"Gemt. Det nye navn gælder fra næste anmodning."},Ll={title:"PocketBase",subtitle:"Databaseforbindelse brugt af hvert endpoint",connected:"forbundet",noSuperuser:"ingen superbruger",unreachable:"utilgængelig",baseUrl:"Basis-URL",superuserEmail:"Superbrugerens e-mail",superuserPassword:"Superbrugerens adgangskode",passwordUnchanged:"uændret",passwordNotSet:"ikke angivet",saveApply:"Gem og anvend",testConnection:"Test forbindelse",persistedEnv:"gemt i .env",testOk:"Forbindelse OK — superbruger godkendt.",testReachableNoAuth:"PocketBase er tilgængelig, men servicekontoen blev ikke godkendt.",testUnreachable:"PocketBase er ikke tilgængelig på den adresse.",savedNotice:"Gemt. Serveren bruger nu denne PocketBase."},Fl={title:"Webapp",subtitle:"Adressen, der tjekkes ved statuskontrol, og hvem der må kalde dette API fra en browser",reachable:"tilgængelig",unreachable:"utilgængelig",baseUrl:"Basis-URL",allowedOrigins:"Tilladte oprindelser",originsHint:"Kommasepareret, eller {star} for enhver. Native mobilapps er ikke underlagt CORS.",saveApply:"Gem og anvend",testConnection:"Test forbindelse",persistedEnv:"gemt i .env",testOk:"Webappen er tilgængelig.",testFailed:"Webappen svarede ikke på sit helbredstjek på den adresse.",savedNotice:"Gemt. Nye oprindelser gælder fra næste anmodning."},Bl={title:"Plugins",subtitle:"Integrationer med tredjepartstjenester",registerExternal:"Registrér ekstern",cancelRegister:"Annuller",name:"Navn",baseUrl:"Basis-URL",provider:"Udbyder",register:"Registrér",empty:"Ingen plugins endnu. Registrér et eksternt ovenfor, eller kompilér et indbygget stik.",builtin:"indbygget",enabled:"aktiveret",disabled:"deaktiveret",health:"Helbred",configure:"Konfigurer",close:"Luk",noConfig:"Dette plugin kræver ingen konfiguration.",notSet:"Ikke angivet — lad organisationer og brugere vælge",capabilities:"Funktioner",saved:"Gemt.",checking:"Tjekker…",save:"Gem",saveEnable:"Gem og aktivér",disable:"Deaktiver",remove:"Fjern",builtinNote:"Indbyggede plugins kan deaktiveres, men ikke fjernes",confirmRemove:'Fjern det eksterne plugin "{name}"? Dets gemte konfiguration slettes.'},Hl={colEndpoint:"Endpoint",colDescription:"Beskrivelse",authNone:"Ingen godkendelse",authBearer:"Bearer-token",authManager:"Admin / superadmin",authSuperadmin:"Superadmin",authCharger:"Laderens loginoplysninger",groupPublic:"Offentlig",groupIdentity:"Identitet",groupCars:"Biler",groupService:"Serviceposter",groupTechnical:"Syn",groupParts:"Reservedele",groupFuel:"Brændstof",groupCharging:"Opladning",groupMaintenance:"Værksted",groupDocuments:"Dokumenter",groupReminders:"Påmindelser",groupAttachments:"Vedhæftede filer",groupProviders:"Køretøjsudbydere",groupIntegrations:"Integrationer",groupOcpp:"OCPP",groupAccount:"Konto",groupManagement:"Administration",groupSuperadmin:"Superadmin"},Vl={app:Rl,sections:zl,login:Dl,status:Il,common:jl,users:Ul,orgs:Nl,server:Ml,pocketbase:Ll,webapp:Fl,plugins:Bl,api:Hl},To="dh-panel-lang",Eo="en",Rt={en:vl,pl:Ol,da:Vl},Gl=Object.keys(Rt);function Kl(){try{const t=localStorage.getItem(To);if(t&&Rt[t])return t}catch{}const e=(navigator.language||"en").split("-")[0];return Rt[e]?e:Eo}const En=H(Kl());function Wl(e){if(Rt[e]){En.value=e;try{localStorage.setItem(To,e)}catch{}}}function hi(e,t){return t.split(".").reduce((s,n)=>s==null?void 0:s[n],e)}function ql(e,t){return t?e.replace(/\{(\w+)\}/g,(s,n)=>t[n]==null?s:String(t[n])):e}function Zl(e,t,s){let n="other";try{n=new Intl.PluralRules(s).select(t)}catch{n=t===1?"one":"other"}return e[n]??e.other??e.one}function f(e,t){const s=En.value;let n=hi(Rt[s],e);if(n==null&&(n=hi(Rt[Eo],e)),n==null)return e;if(typeof n=="object"){if((t==null?void 0:t.n)==null)return e;n=Zl(n,t.n,s)}return typeof n!="string"?e:ql(n,t)}function gi(e,t,s){const i=f(e,{...s,[t]:"\0"}),o=i.indexOf("\0");return o===-1?{before:i,after:""}:{before:i.slice(0,o),after:i.slice(o+1)}}const ln="dh-panel-token";function Jl(){try{return localStorage.getItem(ln)||""}catch{return""}}const zt=H(Jl()),de=H(null),Ce=je(()=>{var e;return((e=de.value)==null?void 0:e.role)==="superadmin"}),gs=je(()=>{var e,t;return((e=de.value)==null?void 0:e.role)==="admin"||((t=de.value)==null?void 0:t.role)==="superadmin"});function Po(e){zt.value=e;try{e?localStorage.setItem(ln,e):localStorage.removeItem(ln)}catch{}}class Yl extends Error{constructor(t,s,n){super(t),this.status=s,this.body=n}}function Xl(e,t){if(!e||typeof e!="object")return`HTTP ${t}`;if(e.error)return e.error;const s=Object.entries(e.data||{}).map(([n,i])=>`${n}: ${(i==null?void 0:i.message)||i}`).filter(Boolean);return s.length?s.join("; "):e.message||`HTTP ${t}`}async function te(e,{method:t="GET",body:s,auth:n=!0}={}){const i={};s!==void 0&&(i["Content-Type"]="application/json"),n&&zt.value&&(i.Authorization=zt.value);const o=await fetch(e,{method:t,headers:i,body:s===void 0?void 0:JSON.stringify(s)}),r=await o.text();let a=null;try{a=r?JSON.parse(r):null}catch{a=null}if(!o.ok)throw o.status===401&&n&&Pn(),new Yl(Xl(a,o.status),o.status,a);return a}async function Ql(e,t){const s=await te("/api/auth/login",{method:"POST",body:{email:e,password:t},auth:!1});return Po(s.token),await Ss(),de.value}async function Ss(){return de.value=await te("/api/identity"),de.value}function Pn(){Po(""),de.value=null}async function ec(){if(!zt.value)return null;try{return await Ss()}catch{return Pn(),null}}const tc={class:"mx-auto flex w-full max-w-sm flex-col gap-5 pt-24"},sc={class:"dh-card p-6"},nc={class:"text-lg font-bold tracking-[-0.02em] text-strong"},ic={class:"mt-1 mb-5 text-sm text-body"},oc={class:"dh-label",for:"login-email"},rc={class:"dh-label",for:"login-password"},ac={key:0,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},lc=["disabled"],cc={class:"eyebrow text-center"},uc={__name:"LoginView",emits:["authenticated"],setup(e,{emit:t}){const s=t,n=H(""),i=H(""),o=H(""),r=H(!1),a=je(()=>n.value.trim()!==""&&i.value!==""&&!r.value);async function l(){if(a.value){o.value="",r.value=!0;try{const g=await Ql(n.value.trim(),i.value);s("authenticated",g)}catch(g){o.value=g.status===400||g.status===404?f("login.invalid"):g.message||f("login.failed"),i.value=""}finally{r.value=!1}}}return(g,d)=>(y(),T("div",tc,[u("div",sc,[u("h1",nc,m(h(f)("login.title")),1),u("p",ic,m(h(f)("login.subtitle")),1),u("form",{class:"flex flex-col gap-4",onSubmit:Xa(l,["prevent"])},[u("div",null,[u("label",oc,m(h(f)("login.email")),1),le(u("input",{id:"login-email","onUpdate:modelValue":d[0]||(d[0]=b=>n.value=b),class:"dh-input",type:"email",autocomplete:"username",autofocus:"",placeholder:"you@example.com"},null,512),[[ge,n.value]])]),u("div",null,[u("label",rc,m(h(f)("login.password")),1),le(u("input",{id:"login-password","onUpdate:modelValue":d[1]||(d[1]=b=>i.value=b),class:"dh-input",type:"password",autocomplete:"current-password",placeholder:"••••••••"},null,512),[[ge,i.value]])]),o.value?(y(),T("p",ac,m(o.value),1)):B("",!0),u("button",{class:"dh-btn w-full",type:"submit",disabled:!a.value},m(r.value?h(f)("login.submitting"):h(f)("login.submit")),9,lc)],32)]),u("p",cc,m(h(f)("login.authNote")),1)]))}},dc={class:"dh-card overflow-hidden"},pc={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},fc={class:"text-base font-bold tracking-[-0.02em] text-strong"},hc={key:0,class:"dh-pill bg-danger-soft text-danger"},gc={key:0,class:"px-5 py-4 text-sm text-danger"},mc={key:1,class:"w-full text-left text-sm"},bc={class:"px-5 py-3 font-medium text-strong"},vc={class:"data px-5 py-3 text-xs text-muted"},yc={class:"data px-5 py-3 text-right text-xs text-muted"},_c={class:"px-5 py-3 text-right"},wc={key:2,class:"px-5 py-4 text-sm text-muted"},xc={__name:"StatusCard",setup(e){const t=H(null),s=H("");let n=null;async function i(){try{t.value=await te("/api/status",{auth:!1}),s.value=""}catch(a){t.value=null,s.value=a.message||f("status.unreachable")}}const o=je(()=>{var a,l,g;return[{key:"apiServer",label:f("status.apiServer"),h:(a=t.value)==null?void 0:a.apiServer},{key:"pocketBase",label:f("status.pocketBase"),h:(l=t.value)==null?void 0:l.pocketBase},{key:"webApp",label:f("status.webApp"),h:(g=t.value)==null?void 0:g.webApp}]});ct(()=>{i(),n=setInterval(i,1e4)}),kn(()=>clearInterval(n));const r=a=>a==="ok"?"bg-success-soft text-success":"bg-danger-soft text-danger";return(a,l)=>(y(),T("div",dc,[u("div",pc,[u("div",fc,m(h(f)("status.title")),1),s.value?(y(),T("span",hc,[l[0]||(l[0]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(m(h(f)("status.unreachable")),1)])):B("",!0)]),s.value?(y(),T("div",gc,m(s.value),1)):t.value?(y(),T("table",mc,[u("tbody",null,[(y(!0),T(ne,null,$e(o.value,g=>(y(),T("tr",{key:g.key,class:"border-t border-subtle first:border-t-0"},[u("td",bc,m(g.label),1),u("td",vc,m(g.h.url||h(f)("status.thisProcess")),1),u("td",yc,m(g.h.latencyMs!=null?g.h.latencyMs+"ms":h(f)("common.empty")),1),u("td",_c,[u("span",{class:Ue(["dh-pill",r(g.h.status)])},[l[1]||(l[1]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(m(g.h.status),1)],2)])]))),128))])])):(y(),T("div",wc,m(h(f)("status.checking")),1))]))}},kc={class:"dh-card overflow-hidden"},Sc={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},Tc={class:"text-base font-bold tracking-[-0.02em] text-strong"},Ec={class:"mt-0.5 text-xs text-muted"},Pc={class:"flex flex-col gap-4 px-5 py-4"},Cc={class:"dh-label",for:"pb-url"},Ac={class:"grid gap-4 sm:grid-cols-2"},$c={class:"dh-label",for:"pb-email"},Oc={class:"dh-label",for:"pb-password"},Rc=["placeholder"],zc={key:0,class:"data text-xs text-muted"},Dc={key:1,class:"rounded-control bg-info-soft px-3 py-2 text-xs text-info"},Ic={key:2,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},jc={class:"flex items-center gap-2"},Uc=["disabled"],Nc=["disabled"],Mc={class:"eyebrow"},Lc={__name:"PocketBaseCard",setup(e){const t=H(null),s=H({url:"",adminEmail:"",adminPassword:""}),n=H(null),i=H(""),o=H(""),r=H(!1);async function a(){try{t.value=await te("/api/admin/pb-config"),s.value={url:t.value.url,adminEmail:t.value.adminEmail,adminPassword:""},n.value=t.value.probe}catch(d){i.value=d.message}}ct(a);async function l(){i.value="",o.value="",r.value=!0;try{n.value=await te("/api/admin/pb-config/test",{method:"POST",body:s.value}),o.value=n.value.superuser?f("pocketbase.testOk"):n.value.reachable?f("pocketbase.testReachableNoAuth"):f("pocketbase.testUnreachable")}catch(d){i.value=d.message}finally{r.value=!1}}async function g(){i.value="",o.value="",r.value=!0;try{const d=await te("/api/admin/pb-config",{method:"PUT",body:s.value});t.value=d.config,n.value=d.config.probe,s.value.adminPassword="",o.value=d.warning||f("pocketbase.savedNotice")}catch(d){i.value=d.message}finally{r.value=!1}}return(d,b)=>{var P,$;return y(),T("div",kc,[u("div",Sc,[u("div",null,[u("div",Tc,m(h(f)("pocketbase.title")),1),u("p",Ec,m(h(f)("pocketbase.subtitle")),1)]),n.value?(y(),T("span",{key:0,class:Ue(["dh-pill",n.value.superuser?"bg-success-soft text-success":n.value.reachable?"bg-warning-soft text-warning":"bg-danger-soft text-danger"])},[b[3]||(b[3]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(" "+m(n.value.superuser?h(f)("pocketbase.connected"):n.value.reachable?h(f)("pocketbase.noSuperuser"):h(f)("pocketbase.unreachable")),1)],2)):B("",!0)]),u("div",Pc,[u("div",null,[u("label",Cc,m(h(f)("pocketbase.baseUrl")),1),le(u("input",{id:"pb-url","onUpdate:modelValue":b[0]||(b[0]=F=>s.value.url=F),class:"dh-input",placeholder:"http://10.2.1.10:8027"},null,512),[[ge,s.value.url]])]),u("div",Ac,[u("div",null,[u("label",$c,m(h(f)("pocketbase.superuserEmail")),1),le(u("input",{id:"pb-email","onUpdate:modelValue":b[1]||(b[1]=F=>s.value.adminEmail=F),class:"dh-input",autocomplete:"off"},null,512),[[ge,s.value.adminEmail]])]),u("div",null,[u("label",Oc,m(h(f)("pocketbase.superuserPassword")),1),le(u("input",{id:"pb-password","onUpdate:modelValue":b[2]||(b[2]=F=>s.value.adminPassword=F),class:"dh-input",type:"password",autocomplete:"new-password",placeholder:(P=t.value)!=null&&P.adminConfigured?h(f)("pocketbase.passwordUnchanged"):h(f)("pocketbase.passwordNotSet")},null,8,Rc),[[ge,s.value.adminPassword]])])]),($=n.value)!=null&&$.detail?(y(),T("p",zc,m(n.value.detail),1)):B("",!0),o.value?(y(),T("p",Dc,m(o.value),1)):B("",!0),i.value?(y(),T("p",Ic,m(i.value),1)):B("",!0),u("div",jc,[u("button",{class:"dh-btn",disabled:r.value,onClick:g},m(h(f)("pocketbase.saveApply")),9,Uc),u("button",{class:"dh-btn-ghost",disabled:r.value,onClick:l},m(h(f)("pocketbase.testConnection")),9,Nc),b[4]||(b[4]=u("span",{class:"flex-1"},null,-1)),u("span",Mc,m(h(f)("pocketbase.persistedEnv")),1)])])])}}},Fc={class:"dh-card overflow-hidden"},Bc={class:"border-b border-subtle px-5 py-4"},Hc={class:"text-base font-bold tracking-[-0.02em] text-strong"},Vc={class:"mt-0.5 text-xs text-muted"},Gc={class:"flex flex-col gap-4 px-5 py-4"},Kc={class:"dh-label",for:"server-name"},Wc={class:"mt-1 text-xs text-muted"},qc={key:0,class:"rounded-control bg-info-soft px-3 py-2 text-xs text-info"},Zc={key:1,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},Jc={class:"flex items-center gap-2"},Yc=["disabled"],Xc={class:"eyebrow"},Qc={__name:"ServerCard",setup(e){const t=H({name:""}),s=H(""),n=H(""),i=H(!1);async function o(){try{const a=await te("/api/admin/server-config");t.value={name:a.name}}catch(a){s.value=a.message}}ct(o);async function r(){s.value="",n.value="",i.value=!0;try{const a=await te("/api/admin/server-config",{method:"PUT",body:{name:t.value.name}});t.value={name:a.config.name},n.value=a.warning||f("server.savedNotice")}catch(a){s.value=a.message}finally{i.value=!1}}return(a,l)=>(y(),T("div",Fc,[u("div",Bc,[u("div",Hc,m(h(f)("server.title")),1),u("p",Vc,m(h(f)("server.subtitle")),1)]),u("div",Gc,[u("div",null,[u("label",Kc,m(h(f)("server.name")),1),le(u("input",{id:"server-name","onUpdate:modelValue":l[0]||(l[0]=g=>t.value.name=g),class:"dh-input",maxlength:"64",placeholder:"DriverVault API Server",onKeyup:xo(r,["enter"])},null,544),[[ge,t.value.name]]),u("p",Wc,m(h(f)("server.nameHint")),1)]),n.value?(y(),T("p",qc,m(n.value),1)):B("",!0),s.value?(y(),T("p",Zc,m(s.value),1)):B("",!0),u("div",Jc,[u("button",{class:"dh-btn",disabled:i.value,onClick:r},m(h(f)("server.saveApply")),9,Yc),l[1]||(l[1]=u("span",{class:"flex-1"},null,-1)),u("span",Xc,m(h(f)("server.persistedEnv")),1)])])]))}},eu={class:"dh-card overflow-hidden"},tu={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},su={class:"text-base font-bold tracking-[-0.02em] text-strong"},nu={class:"mt-0.5 text-xs text-muted"},iu={class:"flex flex-col gap-4 px-5 py-4"},ou={class:"dh-label",for:"web-url"},ru={class:"dh-label",for:"web-origins"},au={class:"mt-1 text-xs text-muted"},lu={key:0,class:"data text-xs text-muted"},cu={key:1,class:"rounded-control bg-info-soft px-3 py-2 text-xs text-info"},uu={key:2,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},du={class:"flex items-center gap-2"},pu=["disabled"],fu=["disabled"],hu={class:"eyebrow"},gu={__name:"WebAppCard",setup(e){const t=H(null),s=H({url:"",origins:""}),n=H(null),i=H(""),o=H(""),r=H(!1),a=P=>P.split(",").map($=>$.trim()).filter(Boolean);function l(P){t.value=P,s.value={url:P.url,origins:(P.allowOrigins||[]).join(", ")},n.value=P.probe}async function g(){try{l(await te("/api/admin/webapp-config"))}catch(P){i.value=P.message}}ct(g);async function d(){i.value="",o.value="",r.value=!0;try{n.value=await te("/api/admin/webapp-config/test",{method:"POST",body:{url:s.value.url}}),o.value=n.value.status==="ok"?f("webapp.testOk"):f("webapp.testFailed")}catch(P){i.value=P.message}finally{r.value=!1}}async function b(){i.value="",o.value="",r.value=!0;try{const P=await te("/api/admin/webapp-config",{method:"PUT",body:{url:s.value.url,allowOrigins:a(s.value.origins)}});l(P.config),o.value=P.warning||f("webapp.savedNotice")}catch(P){i.value=P.message}finally{r.value=!1}}return(P,$)=>{var F;return y(),T("div",eu,[u("div",tu,[u("div",null,[u("div",su,m(h(f)("webapp.title")),1),u("p",nu,m(h(f)("webapp.subtitle")),1)]),n.value?(y(),T("span",{key:0,class:Ue(["dh-pill",n.value.status==="ok"?"bg-success-soft text-success":"bg-danger-soft text-danger"])},[$[2]||($[2]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(" "+m(n.value.status==="ok"?h(f)("webapp.reachable"):h(f)("webapp.unreachable")),1)],2)):B("",!0)]),u("div",iu,[u("div",null,[u("label",ou,m(h(f)("webapp.baseUrl")),1),le(u("input",{id:"web-url","onUpdate:modelValue":$[0]||($[0]=k=>s.value.url=k),class:"dh-input",placeholder:"http://localhost:8090"},null,512),[[ge,s.value.url]])]),u("div",null,[u("label",ru,m(h(f)("webapp.allowedOrigins")),1),le(u("input",{id:"web-origins","onUpdate:modelValue":$[1]||($[1]=k=>s.value.origins=k),class:"dh-input",placeholder:"http://localhost:8090, https://app.example.com"},null,512),[[ge,s.value.origins]]),u("p",au,[Re(m(h(gi)("webapp.originsHint","star").before),1),$[3]||($[3]=u("span",{class:"data"},"*",-1)),Re(m(h(gi)("webapp.originsHint","star").after),1)])]),(F=n.value)!=null&&F.error?(y(),T("p",lu,m(n.value.error),1)):B("",!0),o.value?(y(),T("p",cu,m(o.value),1)):B("",!0),i.value?(y(),T("p",uu,m(i.value),1)):B("",!0),u("div",du,[u("button",{class:"dh-btn",disabled:r.value,onClick:b},m(h(f)("webapp.saveApply")),9,pu),u("button",{class:"dh-btn-ghost",disabled:r.value,onClick:d},m(h(f)("webapp.testConnection")),9,fu),$[4]||($[4]=u("span",{class:"flex-1"},null,-1)),u("span",hu,m(h(f)("webapp.persistedEnv")),1)])])])}}},mu={class:"dh-card overflow-hidden"},bu={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},vu={class:"text-base font-bold tracking-[-0.02em] text-strong"},yu={class:"mt-0.5 text-xs text-muted"},_u={key:0,class:"border-b border-subtle bg-sunken px-5 py-4"},wu={class:"grid gap-3 sm:grid-cols-3"},xu={class:"dh-label"},ku={class:"dh-label"},Su={class:"dh-label"},Tu=["disabled"],Eu={key:1,class:"border-b border-subtle px-5 py-3 text-xs text-danger"},Pu={key:2,class:"px-5 py-6 text-center text-sm text-muted"},Cu={class:"flex items-center gap-3 px-5 py-3"},Au=["onClick"],$u={class:"font-semibold text-strong"},Ou={class:"dh-pill bg-sunken text-muted"},Ru={key:0,class:"text-xs text-muted"},zu=["disabled","onClick"],Du=["disabled","onClick"],Iu={key:0,class:"bg-sunken px-5 py-4"},ju={key:0,class:"data mb-3 text-xs text-muted"},Uu={key:1,class:"grid gap-3 sm:grid-cols-2"},Nu={class:"dh-label"},Mu={key:0,class:"text-danger"},Lu=["onUpdate:modelValue"],Fu={key:0,value:""},Bu=["value"],Hu=["onUpdate:modelValue","type","placeholder"],Vu={key:2,class:"mt-1 text-xs text-muted"},Gu={key:2,class:"text-xs text-muted"},Ku={key:3,class:"mt-4"},Wu={class:"eyebrow mb-1.5"},qu={class:"data flex flex-col gap-1 text-xs text-muted"},Zu={class:"text-strong"},Ju={key:0},Yu={key:1},Xu={key:4,class:"data mt-3 text-xs text-body"},Qu={class:"mt-4 flex items-center gap-2"},ed=["disabled","onClick"],td=["disabled","onClick"],sd=["disabled","onClick"],nd={key:5,class:"eyebrow mt-2"},id={__name:"PluginsCard",setup(e){const t=H([]),s=H(""),n=H(!1),i=H(null),o=Xt({}),r=Xt({}),a=H(!1),l=H({name:"",baseURL:"",provider:""});async function g(){try{const O=await te("/api/admin/plugins");t.value=O.plugins||[],s.value=""}catch(O){s.value=O.message}}ct(g);function d(O){var C;if(i.value===O.name){i.value=null;return}const E={};for(const I of O.configFields||[])E[I.key]=((C=O.config)==null?void 0:C[I.key])??"";o[O.name]=E,i.value=O.name}async function b(O,E){n.value=!0,r[O.name]="";try{const C=await te(`/api/admin/plugins/${encodeURIComponent(O.name)}`,{method:"PUT",body:{enabled:E,config:o[O.name]??{}}});r[O.name]=C.warning||f("plugins.saved"),await g()}catch(C){r[O.name]=C.message}finally{n.value=!1}}async function P(O){n.value=!0,r[O.name]=f("plugins.checking");try{const E=await te(`/api/admin/plugins/${encodeURIComponent(O.name)}/health`,{method:"POST"});r[O.name]=`${E.health.status}${E.health.detail?" — "+E.health.detail:""}`,await g()}catch(E){r[O.name]=E.message}finally{n.value=!1}}async function $(O){if(confirm(f("plugins.confirmRemove",{name:O.name}))){n.value=!0;try{await te(`/api/admin/plugins/${encodeURIComponent(O.name)}`,{method:"DELETE"}),i.value===O.name&&(i.value=null),await g()}catch(E){r[O.name]=E.message}finally{n.value=!1}}}async function F(){n.value=!0,s.value="";try{await te("/api/admin/plugins",{method:"POST",body:l.value}),l.value={name:"",baseURL:"",provider:""},a.value=!1,await g()}catch(O){s.value=O.message}finally{n.value=!1}}const k=O=>O==="ok"?"bg-success-soft text-success":O==="degraded"?"bg-warning-soft text-warning":"bg-danger-soft text-danger";return(O,E)=>(y(),T("div",mu,[u("div",bu,[u("div",null,[u("div",vu,m(h(f)("plugins.title")),1),u("p",yu,m(h(f)("plugins.subtitle")),1)]),u("button",{class:"dh-btn-ghost",onClick:E[0]||(E[0]=C=>a.value=!a.value)},m(a.value?h(f)("plugins.cancelRegister"):h(f)("plugins.registerExternal")),1)]),a.value?(y(),T("div",_u,[u("div",wu,[u("div",null,[u("label",xu,m(h(f)("plugins.name")),1),le(u("input",{"onUpdate:modelValue":E[1]||(E[1]=C=>l.value.name=C),class:"dh-input",placeholder:"acme-parts"},null,512),[[ge,l.value.name]])]),u("div",null,[u("label",ku,m(h(f)("plugins.baseUrl")),1),le(u("input",{"onUpdate:modelValue":E[2]||(E[2]=C=>l.value.baseURL=C),class:"dh-input",placeholder:"http://127.0.0.1:9100"},null,512),[[ge,l.value.baseURL]])]),u("div",null,[u("label",Su,m(h(f)("plugins.provider")),1),le(u("input",{"onUpdate:modelValue":E[3]||(E[3]=C=>l.value.provider=C),class:"dh-input",placeholder:"ACME Corp"},null,512),[[ge,l.value.provider]])])]),u("button",{class:"dh-btn mt-3",disabled:n.value||!l.value.name||!l.value.baseURL,onClick:F},m(h(f)("plugins.register")),9,Tu)])):B("",!0),s.value?(y(),T("p",Eu,m(s.value),1)):B("",!0),t.value.length?B("",!0):(y(),T("p",Pu,m(h(f)("plugins.empty")),1)),(y(!0),T(ne,null,$e(t.value,C=>(y(),T("div",{key:C.name,class:"border-t border-subtle first:border-t-0"},[u("div",Cu,[u("button",{class:"flex flex-1 items-center gap-3 text-left",onClick:I=>d(C)},[u("span",$u,m(C.name),1),u("span",Ou,m(C.kind||h(f)("plugins.builtin")),1),C.provider?(y(),T("span",Ru,m(C.provider),1)):B("",!0),C.health?(y(),T("span",{key:1,class:Ue(["dh-pill",k(C.health.status)])},[E[4]||(E[4]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(m(C.health.status),1)],2)):B("",!0)],8,Au),u("span",{class:Ue(["dh-pill",C.enabled?"bg-success-soft text-success":"bg-sunken text-muted"])},m(C.enabled?h(f)("plugins.enabled"):h(f)("plugins.disabled")),3),u("button",{class:"dh-btn-ghost",disabled:n.value,onClick:I=>P(C)},m(h(f)("plugins.health")),9,zu),u("button",{class:"dh-btn-ghost",disabled:n.value,onClick:I=>d(C)},m(i.value===C.name?h(f)("plugins.close"):h(f)("plugins.configure")),9,Du)]),i.value===C.name?(y(),T("div",Iu,[C.baseURL?(y(),T("div",ju,m(C.baseURL),1)):B("",!0),(C.configFields||[]).length?(y(),T("div",Uu,[(y(!0),T(ne,null,$e(C.configFields,I=>(y(),T("div",{key:I.key},[u("label",Nu,[Re(m(I.label||I.key),1),I.required?(y(),T("span",Mu," *")):B("",!0)]),I.type==="select"?le((y(),T("select",{key:0,"onUpdate:modelValue":j=>o[C.name][I.key]=j,class:"dh-select"},[I.required?B("",!0):(y(),T("option",Fu,m(h(f)("plugins.notSet")),1)),(y(!0),T(ne,null,$e(I.options||[],j=>(y(),T("option",{key:j.value,value:j.value},m(j.label||j.value),9,Bu))),128))],8,Lu)),[[ks,o[C.name][I.key]]]):le((y(),T("input",{key:1,"onUpdate:modelValue":j=>o[C.name][I.key]=j,class:"dh-input",type:I.type==="password"?"password":I.type==="number"?"number":"text",placeholder:I.default||"",autocomplete:"off"},null,8,Hu)),[[qa,o[C.name][I.key]]]),I.help?(y(),T("p",Vu,m(I.help),1)):B("",!0)]))),128))])):(y(),T("p",Gu,m(h(f)("plugins.noConfig")),1)),(C.capabilities||[]).length?(y(),T("div",Ku,[u("div",Wu,m(h(f)("plugins.capabilities")),1),u("ul",qu,[(y(!0),T(ne,null,$e(C.capabilities,I=>(y(),T("li",{key:I.id},[u("span",Zu,m(I.id),1),I.method||I.endpoint?(y(),T("span",Ju," — "+m(I.method)+" "+m(I.endpoint),1)):B("",!0),I.description?(y(),T("span",Yu," · "+m(I.description),1)):B("",!0)]))),128))])])):B("",!0),r[C.name]?(y(),T("p",Xu,m(r[C.name]),1)):B("",!0),u("div",Qu,[u("button",{class:"dh-btn",disabled:n.value,onClick:I=>b(C,!0)},m(C.enabled?h(f)("plugins.save"):h(f)("plugins.saveEnable")),9,ed),C.enabled?(y(),T("button",{key:0,class:"dh-btn-ghost",disabled:n.value,onClick:I=>b(C,!1)},m(h(f)("plugins.disable")),9,td)):B("",!0),E[5]||(E[5]=u("span",{class:"flex-1"},null,-1)),C.kind==="external"?(y(),T("button",{key:1,class:"dh-btn-danger",disabled:n.value,onClick:I=>$(C)},m(h(f)("plugins.remove")),9,sd)):B("",!0)]),C.kind!=="external"?(y(),T("p",nd,m(h(f)("plugins.builtinNote")),1)):B("",!0)])):B("",!0)]))),128))]))}},od={class:"dh-card overflow-hidden"},rd={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},ad={class:"text-base font-bold tracking-[-0.02em] text-strong"},ld={class:"mt-0.5 text-xs text-muted"},cd={key:0,class:"border-b border-subtle px-5 py-3 text-xs text-danger"},ud={key:1,class:"border-b border-subtle bg-sunken px-5 py-4"},dd={class:"grid gap-3 sm:grid-cols-2"},pd={class:"dh-label"},fd={class:"dh-label"},hd={class:"dh-label"},gd=["placeholder"],md={class:"dh-label"},bd=["value"],vd={key:0},yd={class:"dh-label"},_d={value:""},wd=["value"],xd={class:"mt-3 flex items-center gap-2"},kd=["disabled"],Sd=["disabled"],Td={key:2,class:"px-5 py-6 text-center text-sm text-muted"},Ed={key:3,class:"w-full text-left text-sm"},Pd={class:"[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium"},Cd={class:"data px-5 py-2.5 text-xs text-strong"},Ad={key:0,class:"eyebrow ml-1"},$d={class:"px-5 py-2.5 text-body"},Od={class:"px-5 py-2.5 text-body"},Rd={class:"px-5 py-2.5"},zd={class:"px-5 py-2.5 text-right whitespace-nowrap"},Dd=["onClick"],Id=["disabled","onClick"],jd={__name:"UsersCard",setup(e){const t=H([]),s=H([]),n=H(""),i=H(!1),o=H(null),r=H({}),a=je(()=>Ce.value?["user","admin","superadmin"]:["user","admin"]);async function l(){try{const[k,O]=await Promise.all([te("/api/users"),te("/api/orgs")]);t.value=k.users||[],s.value=O.organizations||[],n.value=""}catch(k){n.value=k.message}}ct(l);function g(){var k;o.value="new",r.value={email:"",name:"",password:"",role:"user",organization:Ce.value?"":((k=de.value)==null?void 0:k.organization)||""}}function d(k){o.value=k.id,r.value={email:k.email,name:k.name||"",password:"",role:k.role,organization:k.organization||""}}function b(){o.value=null,n.value=""}async function P(){i.value=!0,n.value="";try{if(o.value==="new")await te("/api/users",{method:"POST",body:r.value});else{const k={...r.value};k.password||delete k.password,await te(`/api/users/${o.value}`,{method:"PATCH",body:k})}o.value=null,await l()}catch(k){n.value=k.message}finally{i.value=!1}}async function $(k){if(confirm(f("users.confirmDelete",{email:k.email}))){i.value=!0,n.value="";try{await te(`/api/users/${k.id}`,{method:"DELETE"}),await l()}catch(O){n.value=O.message}finally{i.value=!1}}}const F=k=>k==="superadmin"?"bg-info-soft text-info":k==="admin"?"bg-warning-soft text-warning":"bg-sunken text-muted";return(k,O)=>(y(),T("div",od,[u("div",rd,[u("div",null,[u("div",ad,m(h(f)("users.title")),1),u("p",ld,m(h(Ce)?h(f)("users.allOrgs"):h(f)("users.yourOrg")),1)]),u("button",{class:"dh-btn",onClick:g},m(h(f)("users.newUser")),1)]),n.value?(y(),T("p",cd,m(n.value),1)):B("",!0),o.value?(y(),T("div",ud,[u("div",dd,[u("div",null,[u("label",pd,m(h(f)("common.email")),1),le(u("input",{"onUpdate:modelValue":O[0]||(O[0]=E=>r.value.email=E),class:"dh-input",type:"email",autocomplete:"off"},null,512),[[ge,r.value.email]])]),u("div",null,[u("label",fd,m(h(f)("common.name")),1),le(u("input",{"onUpdate:modelValue":O[1]||(O[1]=E=>r.value.name=E),class:"dh-input",autocomplete:"off"},null,512),[[ge,r.value.name]])]),u("div",null,[u("label",hd,m(o.value==="new"?h(f)("users.password"):h(f)("users.passwordUnchanged")),1),le(u("input",{"onUpdate:modelValue":O[2]||(O[2]=E=>r.value.password=E),class:"dh-input",type:"password",autocomplete:"new-password",placeholder:h(f)("users.passwordPlaceholder")},null,8,gd),[[ge,r.value.password]])]),u("div",null,[u("label",md,m(h(f)("common.role")),1),le(u("select",{"onUpdate:modelValue":O[3]||(O[3]=E=>r.value.role=E),class:"dh-select"},[(y(!0),T(ne,null,$e(a.value,E=>(y(),T("option",{key:E,value:E},m(E),9,bd))),128))],512),[[ks,r.value.role]])]),h(Ce)?(y(),T("div",vd,[u("label",yd,m(h(f)("common.organization")),1),le(u("select",{"onUpdate:modelValue":O[4]||(O[4]=E=>r.value.organization=E),class:"dh-select"},[u("option",_d,m(h(f)("users.orgNone")),1),(y(!0),T(ne,null,$e(s.value,E=>(y(),T("option",{key:E.id,value:E.id},m(E.name),9,wd))),128))],512),[[ks,r.value.organization]])])):B("",!0)]),u("div",xd,[u("button",{class:"dh-btn",disabled:i.value,onClick:P},m(o.value==="new"?h(f)("common.create"):h(f)("common.save")),9,kd),u("button",{class:"dh-btn-ghost",disabled:i.value,onClick:b},m(h(f)("common.cancel")),9,Sd)])])):B("",!0),t.value.length?(y(),T("table",Ed,[u("thead",null,[u("tr",Pd,[u("th",null,m(h(f)("common.email")),1),u("th",null,m(h(f)("common.name")),1),u("th",null,m(h(f)("common.organization")),1),u("th",null,m(h(f)("common.role")),1),O[5]||(O[5]=u("th",null,null,-1))])]),u("tbody",null,[(y(!0),T(ne,null,$e(t.value,E=>{var C,I;return y(),T("tr",{key:E.id,class:"border-t border-subtle transition-colors hover:bg-sunken"},[u("td",Cd,[Re(m(E.email)+" ",1),E.id===((C=h(de))==null?void 0:C.id)?(y(),T("span",Ad,m(h(f)("users.you")),1)):B("",!0)]),u("td",$d,m(E.name||h(f)("common.empty")),1),u("td",Od,m(E.organizationName||h(f)("common.empty")),1),u("td",Rd,[u("span",{class:Ue(["dh-pill",F(E.role)])},m(E.role),3)]),u("td",zd,[u("button",{class:"dh-btn-ghost",onClick:j=>d(E)},m(h(f)("common.edit")),9,Dd),E.id!==((I=h(de))==null?void 0:I.id)?(y(),T("button",{key:0,class:"dh-btn-danger ml-1.5",disabled:i.value,onClick:j=>$(E)},m(h(f)("common.delete")),9,Id)):B("",!0)])])}),128))])])):(y(),T("p",Td,m(h(f)("users.empty")),1))]))}},Ud={class:"dh-card overflow-hidden"},Nd={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},Md={class:"text-base font-bold tracking-[-0.02em] text-strong"},Ld={class:"mt-0.5 text-xs text-muted"},Fd={key:0,class:"border-b border-subtle px-5 py-3 text-xs text-danger"},Bd={key:1,class:"border-b border-subtle bg-sunken px-5 py-4"},Hd={class:"dh-label"},Vd=["placeholder"],Gd={class:"mt-3 flex items-center gap-2"},Kd=["disabled"],Wd=["disabled"],qd={key:2,class:"px-5 py-6 text-center text-sm text-muted"},Zd={key:3,class:"w-full text-left text-sm"},Jd={class:"[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium"},Yd={class:"px-5 py-2.5 font-medium text-strong"},Xd={class:"data px-5 py-2.5 text-xs text-muted"},Qd={class:"px-5 py-2.5 text-right whitespace-nowrap"},ep=["onClick"],tp=["disabled","onClick"],sp={__name:"OrgsCard",setup(e){const t=H([]),s=H(""),n=H(!1),i=H(null),o=H(""),r=je(()=>{var k;return((k=de.value)==null?void 0:k.organization)||""}),a=je(()=>Ce.value||!r.value);function l(k){return Ce.value||k.id===r.value}async function g(){if(!gs.value){t.value=[];return}try{const k=await te("/api/orgs");t.value=k.organizations||[],s.value=""}catch(k){s.value=k.message}}ct(g);function d(){i.value="new",o.value=""}function b(k){i.value=k.id,o.value=k.name}function P(){i.value=null,s.value=""}async function $(){n.value=!0,s.value="";try{i.value==="new"?(await te("/api/orgs",{method:"POST",body:{name:o.value}}),Ce.value||await Ss()):await te(`/api/orgs/${i.value}`,{method:"PATCH",body:{name:o.value}}),i.value=null,await g()}catch(k){s.value=k.message}finally{n.value=!1}}async function F(k){const O=!Ce.value&&k.id===r.value;if(confirm(f(O?"orgs.confirmDeleteOwn":"orgs.confirmDelete",{name:k.name}))){n.value=!0,s.value="";try{await te(`/api/orgs/${k.id}`,{method:"DELETE"}),O&&await Ss(),await g()}catch(C){s.value=C.message}finally{n.value=!1}}}return(k,O)=>(y(),T("div",Ud,[u("div",Nd,[u("div",null,[u("div",Md,m(h(Ce)?h(f)("orgs.title"):h(f)("common.organization")),1),u("p",Ld,m(h(Ce)?h(f)("orgs.subtitle"):r.value?h(f)("orgs.subtitleOwn"):h(f)("orgs.subtitleNone")),1)]),a.value?(y(),T("button",{key:0,class:"dh-btn",onClick:d},m(h(f)("orgs.newOrg")),1)):B("",!0)]),s.value?(y(),T("p",Fd,m(s.value),1)):B("",!0),i.value?(y(),T("div",Bd,[u("label",Hd,m(h(f)("common.name")),1),le(u("input",{"onUpdate:modelValue":O[0]||(O[0]=E=>o.value=E),class:"dh-input",placeholder:h(f)("orgs.namePlaceholder"),onKeyup:xo($,["enter"])},null,40,Vd),[[ge,o.value]]),u("div",Gd,[u("button",{class:"dh-btn",disabled:n.value||!o.value.trim(),onClick:$},m(i.value==="new"?h(f)("common.create"):h(f)("common.save")),9,Kd),u("button",{class:"dh-btn-ghost",disabled:n.value,onClick:P},m(h(f)("common.cancel")),9,Wd)])])):B("",!0),t.value.length?(y(),T("table",Zd,[u("thead",null,[u("tr",Jd,[u("th",null,m(h(f)("common.name")),1),u("th",null,m(h(f)("orgs.colId")),1),O[1]||(O[1]=u("th",null,null,-1))])]),u("tbody",null,[(y(!0),T(ne,null,$e(t.value,E=>(y(),T("tr",{key:E.id,class:"border-t border-subtle transition-colors hover:bg-sunken"},[u("td",Yd,m(E.name),1),u("td",Xd,m(E.id),1),u("td",Qd,[l(E)?(y(),T(ne,{key:0},[u("button",{class:"dh-btn-ghost",onClick:C=>b(E)},m(h(f)("common.rename")),9,ep),u("button",{class:"dh-btn-danger ml-1.5",disabled:n.value,onClick:C=>F(E)},m(h(f)("common.delete")),9,tp)],64)):B("",!0)])]))),128))])])):(y(),T("p",qd,m(a.value&&!h(Ce)?h(f)("orgs.createHint"):h(f)("orgs.empty")),1))]))}},np={class:"dh-card overflow-hidden"},ip={class:"border-b border-subtle px-5 py-4"},op={class:"flex items-center justify-between"},rp={class:"text-base font-bold tracking-[-0.02em] text-strong"},ap={class:"eyebrow"},lp={key:0,class:"mt-1.5 text-xs text-muted"},cp={class:"overflow-x-auto"},up={class:"w-full text-left text-sm"},dp={class:"[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium"},pp={class:"data border-t border-subtle px-5 py-2.5 text-xs whitespace-nowrap"},fp={class:"text-strong"},hp={class:"border-t border-subtle px-5 py-2.5 text-body"},ue={__name:"EndpointTable",props:{title:String,auth:String,endpoints:Array,note:String},setup(e){const t={GET:"text-success",POST:"text-brandtext",PUT:"text-info",PATCH:"text-warning",DELETE:"text-danger"};return(s,n)=>(y(),T("div",np,[u("div",ip,[u("div",op,[u("div",rp,m(e.title),1),u("span",ap,m(e.auth),1)]),e.note?(y(),T("p",lp,m(e.note),1)):B("",!0)]),u("div",cp,[u("table",up,[u("thead",null,[u("tr",dp,[u("th",null,m(h(f)("api.colEndpoint")),1),u("th",null,m(h(f)("api.colDescription")),1)])]),u("tbody",null,[(y(!0),T(ne,null,$e(e.endpoints,i=>(y(),T("tr",{key:i.method+i.path,class:"transition-colors hover:bg-sunken"},[u("td",pp,[u("span",{class:Ue(["font-semibold",t[i.method]])},m(i.method),3),u("span",fp,m(i.path),1)]),u("td",hp,m(i.desc),1)]))),128))])])])]))}},gp={class:"mx-auto flex max-w-4xl flex-col gap-6 px-6 pt-12 pb-16"},mp={class:"flex items-center gap-3"},bp={class:"inline-flex items-center gap-2.5 select-none"},vp={class:"h-8 w-8 shrink-0",viewBox:"0 0 48 48",fill:"none","aria-hidden":"true"},yp={transform:"translate(7 0) skewX(-13)"},_p=["fill"],wp=["fill"],xp=["fill"],kp={class:"eyebrow mt-1.5"},Sp={key:0,class:"data hidden text-xs text-muted sm:inline"},Tp={key:0},Ep={key:1,class:"dh-pill bg-info-soft text-info"},Pp=["value","aria-label"],Cp=["value"],Ap=["title"],$p={key:0,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75",class:"h-4 w-4"},Op={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75",class:"h-4 w-4"},Rp={key:0,class:"eyebrow py-16 text-center"},zp={class:"flex flex-wrap gap-1.5"},Dp=["onClick"],Ip={key:8,class:"eyebrow text-center"},jp={class:"eyebrow text-center"},Up="{records} is any of service-records, technical-checks, maintenance, fuel-entries, charging-sessions, car-documents, parts.",Np={__name:"App",setup(e){const t=H(!0),s=H("overview");ct(async()=>{await ec(),t.value=!1});const n=je(()=>{const pe=[{id:"overview",label:f("sections.overview")}];return gs.value&&pe.push({id:"users",label:f("sections.users")}),(gs.value||de.value)&&pe.push({id:"orgs",label:f("sections.orgs")}),Ce.value&&pe.push({id:"server",label:f("sections.server")},{id:"pocketbase",label:f("sections.pocketbase")},{id:"webapp",label:f("sections.webapp")},{id:"plugins",label:f("sections.plugins")}),pe.push({id:"api",label:f("sections.api")}),pe}),i=je(()=>Gl.map(pe=>{let se=pe;try{se=new Intl.DisplayNames([pe],{type:"language"}).of(pe)||pe}catch{}return{code:pe,label:se.charAt(0).toUpperCase()+se.slice(1)}}));function o(){Pn(),s.value="overview"}const r=je(()=>Ct.value==="dark"?["#60a5fa","#93c5fd","#ffffff"]:["var(--brand-700)","var(--brand-500)","var(--brand-400)"]),a=[{method:"GET",path:"/api/health",desc:"Liveness probe (no auth)"},{method:"GET",path:"/healthz",desc:"The same probe under the conventional container path"},{method:"GET",path:"/api/status",desc:"Health of PocketBase + Web App"},{method:"POST",path:"/api/auth/login",desc:"Exchange email + password for a PocketBase token"},{method:"GET",path:"/api/auth/validate",desc:"Check whether a token is still valid"}],l=[{method:"GET",path:"/api/auth/me",desc:"Identity of the bearer token"},{method:"GET",path:"/api/identity",desc:"Identity incl. role + organization"}],g=[{method:"GET",path:"/api/cars",desc:"List owned + shared cars"},{method:"POST",path:"/api/cars",desc:"Create a car"},{method:"GET",path:"/api/cars/{id}",desc:"Fetch one car"},{method:"PATCH",path:"/api/cars/{id}",desc:"Update a car"},{method:"PUT",path:"/api/cars/{id}/view",desc:"Save the car's layout — hidden tabs/fields and their order"},{method:"DELETE",path:"/api/cars/{id}",desc:"Delete a car (owner only)"},{method:"GET",path:"/api/cars/{id}/service-records",desc:"A car's service history"},{method:"GET",path:"/api/cars/{id}/technical-checks",desc:"A car's roadworthiness inspections"},{method:"GET",path:"/api/cars/{id}/parts",desc:"A car's parts catalog"},{method:"GET",path:"/api/cars/{id}/fuel-entries",desc:"A car's refuelings"},{method:"GET",path:"/api/cars/{id}/fuel-stats",desc:"Consumption + cost totals from the fuel log"},{method:"GET",path:"/api/cars/{id}/charging-sessions",desc:"A car's charging sessions"},{method:"GET",path:"/api/cars/{id}/charging-stats",desc:"Energy + cost totals from the charging log"},{method:"GET",path:"/api/cars/{id}/maintenance",desc:"A car's maintenance log"},{method:"GET",path:"/api/cars/{id}/documents",desc:"A car's documents"},{method:"GET",path:"/api/cars/{id}/reminders",desc:"A car's reminders"},{method:"GET",path:"/api/cars/{id}/provider",desc:"Live snapshot from the car's connected service"},{method:"POST",path:"/api/cars/{id}/provider",desc:"Link the car to a provider vehicle (an empty provider unlinks)"},{method:"POST",path:"/api/cars/{id}/provider/sync",desc:"Re-apply the provider's data to the car"},{method:"GET",path:"/api/cars/{id}/shares",desc:"Who a car is shared with (owner)"},{method:"POST",path:"/api/cars/{id}/shares",desc:"Share a car by email (owner)"},{method:"DELETE",path:"/api/cars/{id}/shares/{userId}",desc:"Revoke a share (owner)"}],d=[{method:"GET",path:"/api/service-records",desc:"List one car's service records (?car={id})"},{method:"POST",path:"/api/service-records",desc:"Log a service record"},{method:"GET",path:"/api/service-records/{id}",desc:"Fetch one record"},{method:"PATCH",path:"/api/service-records/{id}",desc:"Update a record"},{method:"DELETE",path:"/api/service-records/{id}",desc:"Delete a record"}],b=[{method:"GET",path:"/api/technical-checks",desc:"List one car's inspections (?car={id})"},{method:"POST",path:"/api/technical-checks",desc:"Log an inspection"},{method:"GET",path:"/api/technical-checks/{id}",desc:"Fetch one inspection"},{method:"PATCH",path:"/api/technical-checks/{id}",desc:"Update an inspection"},{method:"DELETE",path:"/api/technical-checks/{id}",desc:"Delete an inspection"}],P=[{method:"GET",path:"/api/parts",desc:"List one car's parts (?car={id})"},{method:"POST",path:"/api/parts",desc:"Add a part"},{method:"GET",path:"/api/parts/{id}",desc:"Fetch one part"},{method:"PATCH",path:"/api/parts/{id}",desc:"Update a part"},{method:"DELETE",path:"/api/parts/{id}",desc:"Delete a part"}],$=[{method:"GET",path:"/api/fuel-entries",desc:"List one car's refuelings (?car={id})"},{method:"POST",path:"/api/fuel-entries",desc:"Log a refueling"},{method:"GET",path:"/api/fuel-entries/{id}",desc:"Fetch one refueling"},{method:"PATCH",path:"/api/fuel-entries/{id}",desc:"Update a refueling"},{method:"DELETE",path:"/api/fuel-entries/{id}",desc:"Delete a refueling"}],F=[{method:"GET",path:"/api/charging-sessions",desc:"List one car's charging sessions (?car={id})"},{method:"POST",path:"/api/charging-sessions",desc:"Log a charging session"},{method:"GET",path:"/api/charging-sessions/{id}",desc:"Fetch one session"},{method:"PATCH",path:"/api/charging-sessions/{id}",desc:"Update a session"},{method:"DELETE",path:"/api/charging-sessions/{id}",desc:"Delete a session"}],k=[{method:"GET",path:"/api/maintenance",desc:"List one car's maintenance jobs (?car={id})"},{method:"POST",path:"/api/maintenance",desc:"Log a maintenance job"},{method:"GET",path:"/api/maintenance/{id}",desc:"Fetch one job"},{method:"PATCH",path:"/api/maintenance/{id}",desc:"Update a job"},{method:"DELETE",path:"/api/maintenance/{id}",desc:"Delete a job"}],O=[{method:"GET",path:"/api/car-documents",desc:"List one car's documents (?car={id})"},{method:"POST",path:"/api/car-documents",desc:"Add a document"},{method:"GET",path:"/api/car-documents/{id}",desc:"Fetch one document"},{method:"PATCH",path:"/api/car-documents/{id}",desc:"Update a document"},{method:"DELETE",path:"/api/car-documents/{id}",desc:"Delete a document"}],E=[{method:"GET",path:"/api/reminders",desc:"List one car's reminders (?car={id})"},{method:"POST",path:"/api/reminders",desc:"Create a reminder"},{method:"GET",path:"/api/reminders/{id}",desc:"Fetch one reminder"},{method:"PATCH",path:"/api/reminders/{id}",desc:"Update a reminder"},{method:"DELETE",path:"/api/reminders/{id}",desc:"Delete a reminder"},{method:"POST",path:"/api/reminders/{id}/complete",desc:"Mark done — a recurring reminder rolls forward from now"}],C=[{method:"POST",path:"/api/{records}/{id}/file",desc:"Attach a file, replacing any previous one (PDF or image, max 10 MB)"},{method:"GET",path:"/api/{records}/{id}/file",desc:"Download the attachment (car access re-checked on every request)"},{method:"DELETE",path:"/api/{records}/{id}/file",desc:"Detach and delete the file"}],I=[{method:"GET",path:"/api/vehicle-providers",desc:"Registered providers and whether the caller can use each one"},{method:"GET",path:"/api/vehicle-providers/{provider}/vehicles",desc:"Vehicles on the caller's provider account, as importable cars"},{method:"POST",path:"/api/vehicle-providers/{provider}/import",desc:"Create a car from a provider vehicle (409 if already imported)"}],j=[{method:"GET",path:"/api/integrations/toyota",desc:"Resolved Toyota Connected settings (secrets masked)"},{method:"PUT",path:"/api/integrations/toyota",desc:"Save the caller's own layer (user or org scope)"},{method:"POST",path:"/api/integrations/toyota/health",desc:"Live probe with the resolved credentials"},{method:"GET",path:"/api/integrations/toyota/vehicles",desc:"Raw MyToyota vehicle payload"},{method:"GET",path:"/api/integrations/anker-solix",desc:"Resolved Anker Solix settings (secrets masked)"},{method:"PUT",path:"/api/integrations/anker-solix",desc:"Save the caller's own layer (user or org scope)"},{method:"POST",path:"/api/integrations/anker-solix/health",desc:"Live probe with the resolved credentials"},{method:"GET",path:"/api/integrations/anker-solix/chargers",desc:"EV chargers on the linked Anker account"},{method:"GET",path:"/api/integrations/anker-solix/chargers/{sn}/control",desc:"Control mode, token state and live CSMS session for one charger"},{method:"POST",path:"/api/integrations/anker-solix/chargers/{sn}/control/token",desc:"(Re)issue the charger's control token"},{method:"DELETE",path:"/api/integrations/anker-solix/chargers/{sn}/control/token",desc:"Revoke the control token"},{method:"POST",path:"/api/integrations/anker-solix/chargers/{sn}/{action}",desc:"One OCPP command: start, stop, limit, clear-limit, availability, reset, unlock, trigger, config"}],re=[{method:"GET",path:"/ocpp/{serial}",desc:"WebSocket the charger dials out to; OCPP Basic auth with the serial + its control token"}],Ee=[{method:"GET",path:"/api/me",desc:"Current user profile"},{method:"PATCH",path:"/api/me",desc:"Update profile"},{method:"POST",path:"/api/me/password",desc:"Change password"},{method:"POST",path:"/api/me/avatar",desc:"Upload avatar"},{method:"GET",path:"/api/me/avatar",desc:"Fetch avatar"},{method:"DELETE",path:"/api/me/avatar",desc:"Remove avatar"},{method:"POST",path:"/api/me/verify/request",desc:"Request email verification"},{method:"GET",path:"/api/me/export",desc:"Export your data"},{method:"POST",path:"/api/me/import",desc:"Import data"},{method:"POST",path:"/api/me/delete",desc:"Request account deletion"},{method:"POST",path:"/api/me/delete/cancel",desc:"Cancel deletion request"},{method:"DELETE",path:"/api/me",desc:"Finalize account deletion"}],Pe=[{method:"GET",path:"/api/users",desc:"List users (scoped by role)"},{method:"POST",path:"/api/users",desc:"Create a user"},{method:"PATCH",path:"/api/users/{id}",desc:"Update email / name / role / org / password"},{method:"DELETE",path:"/api/users/{id}",desc:"Delete a user"},{method:"GET",path:"/api/orgs",desc:"List organizations"},{method:"POST",path:"/api/orgs",desc:"Create an organization (any user without one; creator becomes admin)"},{method:"PATCH",path:"/api/orgs/{id}",desc:"Rename an organization (own org; any as superadmin)"},{method:"DELETE",path:"/api/orgs/{id}",desc:"Delete an organization (own org; any as superadmin)"}],Le=[{method:"GET",path:"/api/admin/pb-config",desc:"PocketBase connection + live probe"},{method:"POST",path:"/api/admin/pb-config/test",desc:"Probe a candidate connection"},{method:"PUT",path:"/api/admin/pb-config",desc:"Apply + persist a connection"},{method:"GET",path:"/api/admin/server-config",desc:"This server's display name"},{method:"PUT",path:"/api/admin/server-config",desc:"Rename this server (applied + persisted)"},{method:"GET",path:"/api/admin/webapp-config",desc:"Web App URL + CORS origins, with a live probe"},{method:"POST",path:"/api/admin/webapp-config/test",desc:"Probe a candidate Web App address"},{method:"PUT",path:"/api/admin/webapp-config",desc:"Apply + persist Web App settings"},{method:"GET",path:"/api/admin/plugins",desc:"List plugins (secrets masked)"},{method:"POST",path:"/api/admin/plugins",desc:"Register an external plugin"},{method:"GET",path:"/api/admin/plugins/{name}",desc:"Fetch one plugin"},{method:"PUT",path:"/api/admin/plugins/{name}",desc:"Enable/disable + configure"},{method:"DELETE",path:"/api/admin/plugins/{name}",desc:"Remove an external plugin"},{method:"POST",path:"/api/admin/plugins/{name}/health",desc:"Run a health check now"}];return(pe,se)=>(y(),T("div",gp,[u("div",mp,[u("span",bp,[(y(),T("svg",vp,[u("g",yp,[u("rect",{x:"9",y:"16",width:"6",height:"16",rx:"3",fill:r.value[0]},null,8,_p),u("rect",{x:"19",y:"12",width:"6",height:"24",rx:"3",fill:r.value[1]},null,8,wp),u("rect",{x:"29",y:"8",width:"6",height:"32",rx:"3",fill:r.value[2]},null,8,xp)])])),se[2]||(se[2]=u("span",{class:"text-2xl leading-none font-extrabold tracking-[-0.03em] italic"},[u("span",{class:"text-strong"},"Driver"),u("span",{class:"text-brandtext"},"Vault")],-1))]),u("span",kp,m(h(f)("app.apiServer")),1),se[5]||(se[5]=u("div",{class:"flex-1"},null,-1)),h(de)?(y(),T("span",Sp,[Re(m(h(de).email),1),h(de).organizationName?(y(),T("span",Tp," · "+m(h(de).organizationName),1)):B("",!0)])):B("",!0),h(de)?(y(),T("span",Ep,m(h(de).role),1)):B("",!0),u("select",{class:"dh-select !w-auto !py-1 !text-xs",value:h(En),"aria-label":h(f)("sections.overview"),onChange:se[0]||(se[0]=ce=>h(Wl)(ce.target.value))},[(y(!0),T(ne,null,$e(i.value,ce=>(y(),T("option",{key:ce.code,value:ce.code},m(ce.label),9,Cp))),128))],40,Pp),u("button",{class:"dh-btn-ghost",title:h(Ct)==="dark"?h(f)("app.switchToLight"):h(f)("app.switchToDark"),onClick:se[1]||(se[1]=(...ce)=>h(fi)&&h(fi)(...ce))},[h(Ct)==="dark"?(y(),T("svg",$p,[...se[3]||(se[3]=[u("circle",{cx:"12",cy:"12",r:"4"},null,-1),u("path",{"stroke-linecap":"round",d:"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"},null,-1)])])):(y(),T("svg",Op,[...se[4]||(se[4]=[u("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"},null,-1)])])),Re(" "+m(h(f)("app.theme")),1)],8,Ap),h(zt)?(y(),T("button",{key:2,class:"dh-btn-ghost",onClick:o},m(h(f)("app.signOut")),1)):B("",!0)]),t.value?(y(),T("p",Rp,m(h(f)("app.loading")),1)):h(zt)?(y(),T(ne,{key:2},[u("nav",zp,[(y(!0),T(ne,null,$e(n.value,ce=>(y(),T("button",{key:ce.id,class:Ue(["dh-btn-ghost",s.value===ce.id?"border-accent text-brandtext":""]),onClick:It=>s.value=ce.id},m(ce.label),11,Dp))),128))]),s.value==="overview"?(y(),tt(xc,{key:0})):s.value==="users"?(y(),tt(jd,{key:1})):s.value==="orgs"?(y(),tt(sp,{key:2})):s.value==="server"?(y(),tt(Qc,{key:3})):s.value==="pocketbase"?(y(),tt(Lc,{key:4})):s.value==="webapp"?(y(),tt(gu,{key:5})):s.value==="plugins"?(y(),tt(id,{key:6})):s.value==="api"?(y(),T(ne,{key:7},[Y(ue,{title:h(f)("api.groupPublic"),auth:h(f)("api.authNone"),endpoints:a},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupIdentity"),auth:h(f)("api.authBearer"),endpoints:l},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupCars"),auth:h(f)("api.authBearer"),endpoints:g},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupService"),auth:h(f)("api.authBearer"),endpoints:d},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupTechnical"),auth:h(f)("api.authBearer"),endpoints:b},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupParts"),auth:h(f)("api.authBearer"),endpoints:P},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupFuel"),auth:h(f)("api.authBearer"),endpoints:$},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupCharging"),auth:h(f)("api.authBearer"),endpoints:F},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupMaintenance"),auth:h(f)("api.authBearer"),endpoints:k},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupDocuments"),auth:h(f)("api.authBearer"),endpoints:O},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupReminders"),auth:h(f)("api.authBearer"),endpoints:E},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupAttachments"),auth:h(f)("api.authBearer"),endpoints:C,note:Up},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupProviders"),auth:h(f)("api.authBearer"),endpoints:I},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupIntegrations"),auth:h(f)("api.authBearer"),endpoints:j},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupOcpp"),auth:h(f)("api.authCharger"),endpoints:re},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupAccount"),auth:h(f)("api.authBearer"),endpoints:Ee},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupManagement"),auth:h(f)("api.authManager"),endpoints:Pe},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupSuperadmin"),auth:h(f)("api.authSuperadmin"),endpoints:Le},null,8,["title","auth"])],64)):B("",!0),!h(gs)&&s.value==="overview"?(y(),T("p",Ip,m(h(f)("app.standardUserNote")),1)):B("",!0)],64)):(y(),tt(uc,{key:1})),u("p",jp,m(h(f)("app.footer")),1)]))}};sl(Np).mount("#app"); +**/let an;const Xn=typeof window<"u"&&window.trustedTypes;if(Xn)try{an=Xn.createPolicy("vue",{createHTML:e=>e})}catch{}const _o=an?e=>an.createHTML(e):e=>e,ka="http://www.w3.org/2000/svg",Sa="http://www.w3.org/1998/Math/MathML",st=typeof document<"u"?document:null,Qn=st&&st.createElement("template"),Ta={insert:(e,t,s)=>{t.insertBefore(e,s||null)},remove:e=>{const t=e.parentNode;t&&t.removeChild(e)},createElement:(e,t,s,n)=>{const i=t==="svg"?st.createElementNS(ka,e):t==="mathml"?st.createElementNS(Sa,e):s?st.createElement(e,{is:s}):st.createElement(e);return e==="select"&&n&&n.multiple!=null&&i.setAttribute("multiple",n.multiple),i},createText:e=>st.createTextNode(e),createComment:e=>st.createComment(e),setText:(e,t)=>{e.nodeValue=t},setElementText:(e,t)=>{e.textContent=t},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>st.querySelector(e),setScopeId(e,t){e.setAttribute(t,"")},insertStaticContent(e,t,s,n,i,o){const r=s?s.previousSibling:t.lastChild;if(i&&(i===o||i.nextSibling))for(;t.insertBefore(i.cloneNode(!0),s),!(i===o||!(i=i.nextSibling)););else{Qn.innerHTML=_o(n==="svg"?`${e}`:n==="mathml"?`${e}`:e);const a=Qn.content;if(n==="svg"||n==="mathml"){const l=a.firstChild;for(;l.firstChild;)a.appendChild(l.firstChild);a.removeChild(l)}t.insertBefore(a,s)}return[r?r.nextSibling:t.firstChild,s?s.previousSibling:t.lastChild]}},Ea=Symbol("_vtc");function Pa(e,t,s){const n=e[Ea];n&&(t=(t?[t,...n]:[...n]).join(" ")),t==null?e.removeAttribute("class"):s?e.setAttribute("class",t):e.className=t}const ei=Symbol("_vod"),Ca=Symbol("_vsh"),Aa=Symbol(""),$a=/(?:^|;)\s*display\s*:/;function Oa(e,t,s){const n=e.style,i=oe(s);let o=!1;if(s&&!i){if(t)if(oe(t))for(const r of t.split(";")){const a=r.slice(0,r.indexOf(":")).trim();s[a]==null&&Bt(n,a,"")}else for(const r in t)s[r]==null&&Bt(n,r,"");for(const r in s){r==="display"&&(o=!0);const a=s[r];a!=null?za(e,r,!oe(t)&&t?t[r]:void 0,a)||Bt(n,r,a):Bt(n,r,"")}}else if(i){if(t!==s){const r=n[Aa];r&&(s+=";"+r),n.cssText=s,o=$a.test(s)}}else t&&e.removeAttribute("style");ei in e&&(e[ei]=o?n.display:"",e[Ca]&&(n.display="none"))}const ti=/\s*!important$/;function Bt(e,t,s){if(M(s))s.forEach(n=>Bt(e,t,n));else if(s==null&&(s=""),t.startsWith("--"))e.setProperty(t,s);else{const n=Ra(e,t);ti.test(s)?e.setProperty(ht(n),s.replace(ti,""),"important"):e[n]=s}}const si=["Webkit","Moz","ms"],qs={};function Ra(e,t){const s=qs[t];if(s)return s;let n=De(t);if(n!=="filter"&&n in e)return qs[t]=n;n=_i(n);for(let i=0;iZs||(Ma.then(()=>Zs=0),Zs=Date.now());function Fa(e,t){const s=n=>{if(!n._vts)n._vts=Date.now();else if(n._vts<=s.attached)return;const i=s.value;if(M(i)){const o=n.stopImmediatePropagation;n.stopImmediatePropagation=()=>{o.call(n),n._stopped=!0};const r=i.slice(),a=[n];for(let l=0;le.charCodeAt(0)===111&&e.charCodeAt(1)===110&&e.charCodeAt(2)>96&&e.charCodeAt(2)<123,Ba=(e,t,s,n,i,o)=>{const r=i==="svg";t==="class"?Pa(e,n,r):t==="style"?Oa(e,s,n):Ts(t)?Es(t)||Ia(e,t,s,n,o):(t[0]==="."?(t=t.slice(1),!0):t[0]==="^"?(t=t.slice(1),!1):Ha(e,t,n,r))?(oi(e,t,n),!e.tagName.includes("-")&&(t==="value"||t==="checked"||t==="selected")&&ii(e,t,n,r,o,t!=="value")):e._isVueCE&&(Va(e,t)||e._def.__asyncLoader&&(/[A-Z]/.test(t)||!oe(n)))?oi(e,De(t),n,o,t):(t==="true-value"?e._trueValue=n:t==="false-value"&&(e._falseValue=n),ii(e,t,n,r))};function Ha(e,t,s,n){if(n)return!!(t==="innerHTML"||t==="textContent"||t in e&&ai(t)&&V(s));if(t==="spellcheck"||t==="draggable"||t==="translate"||t==="autocorrect"||t==="sandbox"&&e.tagName==="IFRAME"||t==="form"||t==="list"&&e.tagName==="INPUT"||t==="type"&&e.tagName==="TEXTAREA")return!1;if(t==="width"||t==="height"){const i=e.tagName;if(i==="IMG"||i==="VIDEO"||i==="CANVAS"||i==="SOURCE")return!1}return ai(t)&&oe(s)?!1:t in e}function Va(e,t){const s=e._def.props;if(!s)return!1;const n=De(t);return Array.isArray(s)?s.some(i=>De(i)===n):Object.keys(s).some(i=>De(i)===n)}const ft=e=>{const t=e.props["onUpdate:modelValue"]||!1;return M(t)?s=>ps(t,s):t};function Ga(e){e.target.composing=!0}function li(e){const t=e.target;t.composing&&(t.composing=!1,t.dispatchEvent(new Event("input")))}const ze=Symbol("_assign");function ci(e,t,s){return t&&(e=e.trim()),s&&(e=Cs(e)),e}const ge={created(e,{modifiers:{lazy:t,trim:s,number:n}},i){e[ze]=ft(i);const o=n||i.props&&i.props.type==="number";rt(e,t?"change":"input",r=>{r.target.composing||e[ze](ci(e.value,s,o))}),(s||o)&&rt(e,"change",()=>{e.value=ci(e.value,s,o)}),t||(rt(e,"compositionstart",Ga),rt(e,"compositionend",li),rt(e,"change",li))},mounted(e,{value:t}){e.value=t??""},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:n,trim:i,number:o}},r){if(e[ze]=ft(r),e.composing)return;const a=(o||e.type==="number")&&!/^0\d/.test(e.value)?Cs(e.value):e.value,l=t??"";if(a===l)return;const g=e.getRootNode();(g instanceof Document||g instanceof ShadowRoot)&&g.activeElement===e&&e.type!=="range"&&(n&&t===s||i&&e.value.trim()===l)||(e.value=l)}},Ka={deep:!0,created(e,t,s){e[ze]=ft(s),rt(e,"change",()=>{const n=e._modelValue,i=Ot(e),o=e.checked,r=e[ze];if(M(n)){const a=fn(n,i),l=a!==-1;if(o&&!l)r(n.concat(i));else if(!o&&l){const g=[...n];g.splice(a,1),r(g)}}else if(Dt(n)){const a=new Set(n);o?a.add(i):a.delete(i),r(a)}else r(wo(e,o))})},mounted:ui,beforeUpdate(e,t,s){e[ze]=ft(s),ui(e,t,s)}};function ui(e,{value:t,oldValue:s},n){e._modelValue=t;let i;if(M(t))i=fn(t,n.props.value)>-1;else if(Dt(t))i=t.has(n.props.value);else{if(t===s)return;i=dt(t,wo(e,!0))}e.checked!==i&&(e.checked=i)}const Wa={created(e,{value:t},s){e.checked=dt(t,s.props.value),e[ze]=ft(s),rt(e,"change",()=>{e[ze](Ot(e))})},beforeUpdate(e,{value:t,oldValue:s},n){e[ze]=ft(n),t!==s&&(e.checked=dt(t,n.props.value))}},ks={deep:!0,created(e,{value:t,modifiers:{number:s}},n){const i=Dt(t);rt(e,"change",()=>{const o=Array.prototype.filter.call(e.options,r=>r.selected).map(r=>s?Cs(Ot(r)):Ot(r));e[ze](e.multiple?i?new Set(o):o:o[0]),e._assigning=!0,Fi(()=>{e._assigning=!1})}),e[ze]=ft(n)},mounted(e,{value:t}){di(e,t)},beforeUpdate(e,t,s){e[ze]=ft(s)},updated(e,{value:t}){e._assigning||di(e,t)}};function di(e,t){const s=e.multiple,n=M(t);if(!(s&&!n&&!Dt(t))){for(let i=0,o=e.options.length;iString(g)===String(a)):r.selected=fn(t,a)>-1}else r.selected=t.has(a);else if(dt(Ot(r),t)){e.selectedIndex!==i&&(e.selectedIndex=i);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ot(e){return"_value"in e?e._value:e.value}function wo(e,t){const s=t?"_trueValue":"_falseValue";return s in e?e[s]:t}const qa={created(e,t,s){ds(e,t,s,null,"created")},mounted(e,t,s){ds(e,t,s,null,"mounted")},beforeUpdate(e,t,s,n){ds(e,t,s,n,"beforeUpdate")},updated(e,t,s,n){ds(e,t,s,n,"updated")}};function Za(e,t){switch(e){case"SELECT":return ks;case"TEXTAREA":return ge;default:switch(t){case"checkbox":return Ka;case"radio":return Wa;default:return ge}}}function ds(e,t,s,n,i){const r=Za(e.tagName,s.props&&s.props.type)[i];r&&r(e,t,s,n)}const Ja=["ctrl","shift","alt","meta"],Ya={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>Ja.some(s=>e[`${s}Key`]&&!t.includes(s))},Xa=(e,t)=>{if(!e)return e;const s=e._withMods||(e._withMods={}),n=t.join(".");return s[n]||(s[n]=((i,...o)=>{for(let r=0;r{const s=e._withKeys||(e._withKeys={}),n=t.join(".");return s[n]||(s[n]=(i=>{if(!("key"in i))return;const o=ht(i.key);if(t.some(r=>r===o||Qa[r]===o))return e(i)}))},el=ve({patchProp:Ba},Ta);let pi;function tl(){return pi||(pi=ia(el))}const sl=((...e)=>{const t=tl().createApp(...e),{mount:s}=t;return t.mount=n=>{const i=il(n);if(!i)return;const o=t._component;!V(o)&&!o.render&&!o.template&&(o.template=i.innerHTML),i.nodeType===1&&(i.textContent="");const r=s(i,!1,nl(i));return i instanceof Element&&(i.removeAttribute("v-cloak"),i.setAttribute("data-v-app","")),r},t});function nl(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function il(e){return oe(e)?document.querySelector(e):e}const ko="dh-panel-theme";function ol(){var e;try{const t=localStorage.getItem(ko);if(t==="dark"||t==="light")return t}catch{}return(e=window.matchMedia)!=null&&e.call(window,"(prefers-color-scheme: dark)").matches?"dark":"light"}const Ct=H(ol());function So(e){Ct.value=e,document.documentElement.classList.toggle("dark",e==="dark");try{localStorage.setItem(ko,e)}catch{}}function fi(){So(Ct.value==="dark"?"light":"dark")}So(Ct.value);const rl={apiServer:"API server",switchToLight:"Switch to light theme",switchToDark:"Switch to dark theme",theme:"Theme",signOut:"Sign out",loading:"Loading…",standardUserNote:"Signed in as a standard user — management sections need an admin role",footer:"DriverVault — car maintenance & service tracker."},al={overview:"Overview",users:"Users",orgs:"Organizations",server:"API Server",pocketbase:"PocketBase",webapp:"Web App",plugins:"Plugins",api:"API"},ll={title:"Sign in",subtitle:"Superadmin console for the DriverVault API Server.",email:"Email",password:"Password",submit:"Sign in",submitting:"Signing in…",invalid:"Invalid email or password.",failed:"Could not sign in.",authNote:"Authenticates against PocketBase through this server"},cl={title:"Status",unreachable:"unreachable",checking:"Checking…",apiServer:"API Server",pocketBase:"PocketBase",webApp:"Web App",thisProcess:"this process"},ul={create:"Create",save:"Save",cancel:"Cancel",edit:"Edit",delete:"Delete",rename:"Rename",empty:"—",name:"Name",email:"Email",role:"Role",organization:"Organization"},dl={title:"Users",allOrgs:"All organizations",yourOrg:"Your organization",newUser:"New user",password:"Password",passwordUnchanged:"Password (blank = unchanged)",passwordPlaceholder:"min 8 characters",orgNone:"— none —",you:"you",empty:"No users.",confirmDelete:"Delete {email}? This cannot be undone."},pl={title:"Organizations",subtitle:"Tenants users belong to",subtitleOwn:"Your organization",subtitleNone:"Create one to manage your own team",newOrg:"New organization",namePlaceholder:"Acme Fleet",empty:"No organizations yet.",createHint:"You have no organization yet. Create one to become its admin.",colId:"ID",confirmDelete:'Delete the organization "{name}"?',confirmDeleteOwn:'Delete your organization "{name}"? You will be removed from it and become a regular user.'},fl={title:"API Server",subtitle:"How this server identifies itself to the clients that connect to it",name:"Server name",nameHint:"Shown by clients that can be pointed at more than one DriverVault, such as the Web App's server switcher. Reported by /api/health.",saveApply:"Save & apply",persistedEnv:"persisted to .env",savedNotice:"Saved. The new name applies to the next request."},hl={title:"PocketBase",subtitle:"Database connection used by every endpoint",connected:"connected",noSuperuser:"no superuser",unreachable:"unreachable",baseUrl:"Base URL",superuserEmail:"Superuser email",superuserPassword:"Superuser password",passwordUnchanged:"unchanged",passwordNotSet:"not set",saveApply:"Save & apply",testConnection:"Test connection",persistedEnv:"persisted to .env",testOk:"Connection OK — superuser authenticated.",testReachableNoAuth:"PocketBase is reachable, but the service account did not authenticate.",testUnreachable:"PocketBase is not reachable at that address.",savedNotice:"Saved. The server is now using this PocketBase."},gl={title:"Web App",subtitle:"Address probed by the status check, and who may call this API from a browser",reachable:"reachable",unreachable:"unreachable",baseUrl:"Base URL",allowedOrigins:"Allowed origins",originsHint:"Comma separated, or {star} for any. Native mobile apps are not subject to CORS.",saveApply:"Save & apply",testConnection:"Test connection",persistedEnv:"persisted to .env",testOk:"Web App is reachable.",testFailed:"Web App did not answer its health check at that address.",savedNotice:"Saved. New origins apply to the next request."},ml={title:"Plugins",subtitle:"Third-party service integrations",registerExternal:"Register external",cancelRegister:"Cancel",name:"Name",baseUrl:"Base URL",provider:"Provider",register:"Register",empty:"No plugins yet. Register an external one above, or compile a built-in connector.",builtin:"builtin",enabled:"enabled",disabled:"disabled",health:"Health",configure:"Configure",close:"Close",noConfig:"This plugin takes no configuration.",notSet:"Not set — let organizations and users choose",capabilities:"Capabilities",saved:"Saved.",checking:"Checking…",save:"Save",saveEnable:"Save & enable",disable:"Disable",remove:"Remove",builtinNote:"Built-in plugins can be disabled but not removed",confirmRemove:'Remove the external plugin "{name}"? Its saved config is deleted.'},bl={colEndpoint:"Endpoint",colDescription:"Description",authNone:"No auth",authBearer:"Bearer token",authManager:"Admin / superadmin",authSuperadmin:"Superadmin",authCharger:"Charger credentials",groupPublic:"Public",groupIdentity:"Identity",groupCars:"Cars",groupService:"Service records",groupTechnical:"Technical checks",groupParts:"Parts",groupFuel:"Fuel",groupCharging:"Charging",groupMaintenance:"Maintenance",groupDocuments:"Documents",groupReminders:"Reminders",groupAttachments:"Attachments",groupProviders:"Vehicle providers",groupIntegrations:"Integrations",groupOcpp:"OCPP",groupAccount:"Account",groupManagement:"Management",groupSuperadmin:"Superadmin"},vl={app:rl,sections:al,login:ll,status:cl,common:ul,users:dl,orgs:pl,server:fl,pocketbase:hl,webapp:gl,plugins:ml,api:bl},yl={apiServer:"Serwer API",switchToLight:"Przełącz na motyw jasny",switchToDark:"Przełącz na motyw ciemny",theme:"Motyw",signOut:"Wyloguj się",loading:"Ładowanie…",standardUserNote:"Zalogowano jako zwykły użytkownik — sekcje zarządzania wymagają roli administratora",footer:"DriverVault — rejestr serwisu i konserwacji samochodu."},_l={overview:"Przegląd",users:"Użytkownicy",orgs:"Organizacje",server:"Serwer API",pocketbase:"PocketBase",webapp:"Aplikacja webowa",plugins:"Wtyczki",api:"API"},wl={title:"Zaloguj się",subtitle:"Konsola superadministratora serwera API DriverVault.",email:"E-mail",password:"Hasło",submit:"Zaloguj się",submitting:"Logowanie…",invalid:"Nieprawidłowy e-mail lub hasło.",failed:"Nie udało się zalogować.",authNote:"Uwierzytelnia w PocketBase za pośrednictwem tego serwera"},xl={title:"Status",unreachable:"niedostępny",checking:"Sprawdzanie…",apiServer:"Serwer API",pocketBase:"PocketBase",webApp:"Aplikacja webowa",thisProcess:"ten proces"},kl={create:"Utwórz",save:"Zapisz",cancel:"Anuluj",edit:"Edytuj",delete:"Usuń",rename:"Zmień nazwę",empty:"—",name:"Nazwa",email:"E-mail",role:"Rola",organization:"Organizacja"},Sl={title:"Użytkownicy",allOrgs:"Wszystkie organizacje",yourOrg:"Twoja organizacja",newUser:"Nowy użytkownik",password:"Hasło",passwordUnchanged:"Hasło (puste = bez zmian)",passwordPlaceholder:"min. 8 znaków",orgNone:"— brak —",you:"Ty",empty:"Brak użytkowników.",confirmDelete:"Usunąć {email}? Tej operacji nie można cofnąć."},Tl={title:"Organizacje",subtitle:"Podmioty, do których należą użytkownicy",subtitleOwn:"Twoja organizacja",subtitleNone:"Utwórz ją, aby zarządzać własnym zespołem",newOrg:"Nowa organizacja",namePlaceholder:"Acme Fleet",empty:"Brak organizacji.",createHint:"Nie masz jeszcze organizacji. Utwórz ją, aby zostać jej administratorem.",colId:"ID",confirmDelete:"Usunąć organizację „{name}”?",confirmDeleteOwn:"Usunąć Twoją organizację „{name}”? Zostaniesz z niej usunięty i staniesz się zwykłym użytkownikiem."},El={title:"Serwer API",subtitle:"Jak ten serwer przedstawia się klientom, które się z nim łączą",name:"Nazwa serwera",nameHint:"Widoczna w klientach, które można skierować na więcej niż jeden DriverVault, na przykład w przełączniku serwerów aplikacji webowej. Zwracana przez /api/health.",saveApply:"Zapisz i zastosuj",persistedEnv:"zapisano w .env",savedNotice:"Zapisano. Nowa nazwa obowiązuje od następnego żądania."},Pl={title:"PocketBase",subtitle:"Połączenie z bazą danych używane przez każdy punkt końcowy",connected:"połączono",noSuperuser:"brak superużytkownika",unreachable:"niedostępny",baseUrl:"Adres bazowy",superuserEmail:"E-mail superużytkownika",superuserPassword:"Hasło superużytkownika",passwordUnchanged:"bez zmian",passwordNotSet:"nie ustawiono",saveApply:"Zapisz i zastosuj",testConnection:"Testuj połączenie",persistedEnv:"zapisano w .env",testOk:"Połączenie OK — superużytkownik uwierzytelniony.",testReachableNoAuth:"PocketBase jest dostępny, ale konto usługowe nie zostało uwierzytelnione.",testUnreachable:"PocketBase jest niedostępny pod tym adresem.",savedNotice:"Zapisano. Serwer korzysta teraz z tego PocketBase."},Cl={title:"Aplikacja webowa",subtitle:"Adres sprawdzany podczas kontroli statusu oraz kto może wywoływać to API z przeglądarki",reachable:"dostępna",unreachable:"niedostępna",baseUrl:"Adres bazowy",allowedOrigins:"Dozwolone źródła",originsHint:"Oddzielone przecinkami lub {star} dla dowolnego. Natywne aplikacje mobilne nie podlegają CORS.",saveApply:"Zapisz i zastosuj",testConnection:"Testuj połączenie",persistedEnv:"zapisano w .env",testOk:"Aplikacja webowa jest dostępna.",testFailed:"Aplikacja webowa nie odpowiedziała na kontrolę stanu pod tym adresem.",savedNotice:"Zapisano. Nowe źródła obowiązują od następnego żądania."},Al={title:"Wtyczki",subtitle:"Integracje z usługami zewnętrznymi",registerExternal:"Zarejestruj zewnętrzną",cancelRegister:"Anuluj",name:"Nazwa",baseUrl:"Adres bazowy",provider:"Dostawca",register:"Zarejestruj",empty:"Brak wtyczek. Zarejestruj zewnętrzną powyżej lub skompiluj wbudowany łącznik.",builtin:"wbudowana",enabled:"włączona",disabled:"wyłączona",health:"Stan",configure:"Konfiguruj",close:"Zamknij",noConfig:"Ta wtyczka nie wymaga konfiguracji.",notSet:"Nie ustawiono — pozwól organizacjom i użytkownikom wybrać",capabilities:"Możliwości",saved:"Zapisano.",checking:"Sprawdzanie…",save:"Zapisz",saveEnable:"Zapisz i włącz",disable:"Wyłącz",remove:"Usuń",builtinNote:"Wtyczki wbudowane można wyłączyć, ale nie usunąć",confirmRemove:"Usunąć zewnętrzną wtyczkę „{name}”? Jej zapisana konfiguracja zostanie usunięta."},$l={colEndpoint:"Punkt końcowy",colDescription:"Opis",authNone:"Bez uwierzytelniania",authBearer:"Token Bearer",authManager:"Administrator / superadministrator",authSuperadmin:"Superadministrator",authCharger:"Dane logowania ładowarki",groupPublic:"Publiczne",groupIdentity:"Tożsamość",groupCars:"Samochody",groupService:"Wpisy serwisowe",groupTechnical:"Przeglądy techniczne",groupParts:"Części",groupFuel:"Paliwo",groupCharging:"Ładowanie",groupMaintenance:"Naprawy",groupDocuments:"Dokumenty",groupReminders:"Przypomnienia",groupAttachments:"Załączniki",groupProviders:"Dostawcy pojazdów",groupIntegrations:"Integracje",groupOcpp:"OCPP",groupAccount:"Konto",groupManagement:"Zarządzanie",groupSuperadmin:"Superadministrator"},Ol={app:yl,sections:_l,login:wl,status:xl,common:kl,users:Sl,orgs:Tl,server:El,pocketbase:Pl,webapp:Cl,plugins:Al,api:$l},Rl={apiServer:"API-server",switchToLight:"Skift til lyst tema",switchToDark:"Skift til mørkt tema",theme:"Tema",signOut:"Log ud",loading:"Indlæser…",standardUserNote:"Logget ind som almindelig bruger — administrationssektioner kræver en administratorrolle",footer:"DriverVault — bilservice- og vedligeholdelsesregister."},zl={overview:"Oversigt",users:"Brugere",orgs:"Organisationer",server:"API-server",pocketbase:"PocketBase",webapp:"Webapp",plugins:"Plugins",api:"API"},Dl={title:"Log ind",subtitle:"Superadmin-konsol til DriverVault API-serveren.",email:"E-mail",password:"Adgangskode",submit:"Log ind",submitting:"Logger ind…",invalid:"Ugyldig e-mail eller adgangskode.",failed:"Kunne ikke logge ind.",authNote:"Godkender mod PocketBase gennem denne server"},Il={title:"Status",unreachable:"utilgængelig",checking:"Tjekker…",apiServer:"API-server",pocketBase:"PocketBase",webApp:"Webapp",thisProcess:"denne proces"},jl={create:"Opret",save:"Gem",cancel:"Annuller",edit:"Rediger",delete:"Slet",rename:"Omdøb",empty:"—",name:"Navn",email:"E-mail",role:"Rolle",organization:"Organisation"},Ul={title:"Brugere",allOrgs:"Alle organisationer",yourOrg:"Din organisation",newUser:"Ny bruger",password:"Adgangskode",passwordUnchanged:"Adgangskode (tom = uændret)",passwordPlaceholder:"mindst 8 tegn",orgNone:"— ingen —",you:"dig",empty:"Ingen brugere.",confirmDelete:"Slet {email}? Dette kan ikke fortrydes."},Nl={title:"Organisationer",subtitle:"Enheder, som brugere tilhører",subtitleOwn:"Din organisation",subtitleNone:"Opret en for at administrere dit eget team",newOrg:"Ny organisation",namePlaceholder:"Acme Fleet",empty:"Ingen organisationer endnu.",createHint:"Du har endnu ingen organisation. Opret en for at blive dens administrator.",colId:"ID",confirmDelete:'Slet organisationen "{name}"?',confirmDeleteOwn:"Slet din organisation „{name}“? Du fjernes fra den og bliver en almindelig bruger."},Ml={title:"API-server",subtitle:"Hvordan denne server identificerer sig over for de klienter, der forbinder til den",name:"Servernavn",nameHint:"Vises af klienter, der kan pege på mere end én DriverVault, for eksempel webappens serverskifter. Returneres af /api/health.",saveApply:"Gem og anvend",persistedEnv:"gemt i .env",savedNotice:"Gemt. Det nye navn gælder fra næste anmodning."},Ll={title:"PocketBase",subtitle:"Databaseforbindelse brugt af hvert endpoint",connected:"forbundet",noSuperuser:"ingen superbruger",unreachable:"utilgængelig",baseUrl:"Basis-URL",superuserEmail:"Superbrugerens e-mail",superuserPassword:"Superbrugerens adgangskode",passwordUnchanged:"uændret",passwordNotSet:"ikke angivet",saveApply:"Gem og anvend",testConnection:"Test forbindelse",persistedEnv:"gemt i .env",testOk:"Forbindelse OK — superbruger godkendt.",testReachableNoAuth:"PocketBase er tilgængelig, men servicekontoen blev ikke godkendt.",testUnreachable:"PocketBase er ikke tilgængelig på den adresse.",savedNotice:"Gemt. Serveren bruger nu denne PocketBase."},Fl={title:"Webapp",subtitle:"Adressen, der tjekkes ved statuskontrol, og hvem der må kalde dette API fra en browser",reachable:"tilgængelig",unreachable:"utilgængelig",baseUrl:"Basis-URL",allowedOrigins:"Tilladte oprindelser",originsHint:"Kommasepareret, eller {star} for enhver. Native mobilapps er ikke underlagt CORS.",saveApply:"Gem og anvend",testConnection:"Test forbindelse",persistedEnv:"gemt i .env",testOk:"Webappen er tilgængelig.",testFailed:"Webappen svarede ikke på sit helbredstjek på den adresse.",savedNotice:"Gemt. Nye oprindelser gælder fra næste anmodning."},Bl={title:"Plugins",subtitle:"Integrationer med tredjepartstjenester",registerExternal:"Registrér ekstern",cancelRegister:"Annuller",name:"Navn",baseUrl:"Basis-URL",provider:"Udbyder",register:"Registrér",empty:"Ingen plugins endnu. Registrér et eksternt ovenfor, eller kompilér et indbygget stik.",builtin:"indbygget",enabled:"aktiveret",disabled:"deaktiveret",health:"Helbred",configure:"Konfigurer",close:"Luk",noConfig:"Dette plugin kræver ingen konfiguration.",notSet:"Ikke angivet — lad organisationer og brugere vælge",capabilities:"Funktioner",saved:"Gemt.",checking:"Tjekker…",save:"Gem",saveEnable:"Gem og aktivér",disable:"Deaktiver",remove:"Fjern",builtinNote:"Indbyggede plugins kan deaktiveres, men ikke fjernes",confirmRemove:'Fjern det eksterne plugin "{name}"? Dets gemte konfiguration slettes.'},Hl={colEndpoint:"Endpoint",colDescription:"Beskrivelse",authNone:"Ingen godkendelse",authBearer:"Bearer-token",authManager:"Admin / superadmin",authSuperadmin:"Superadmin",authCharger:"Laderens loginoplysninger",groupPublic:"Offentlig",groupIdentity:"Identitet",groupCars:"Biler",groupService:"Serviceposter",groupTechnical:"Syn",groupParts:"Reservedele",groupFuel:"Brændstof",groupCharging:"Opladning",groupMaintenance:"Værksted",groupDocuments:"Dokumenter",groupReminders:"Påmindelser",groupAttachments:"Vedhæftede filer",groupProviders:"Køretøjsudbydere",groupIntegrations:"Integrationer",groupOcpp:"OCPP",groupAccount:"Konto",groupManagement:"Administration",groupSuperadmin:"Superadmin"},Vl={app:Rl,sections:zl,login:Dl,status:Il,common:jl,users:Ul,orgs:Nl,server:Ml,pocketbase:Ll,webapp:Fl,plugins:Bl,api:Hl},To="dh-panel-lang",Eo="en",Rt={en:vl,pl:Ol,da:Vl},Gl=Object.keys(Rt);function Kl(){try{const t=localStorage.getItem(To);if(t&&Rt[t])return t}catch{}const e=(navigator.language||"en").split("-")[0];return Rt[e]?e:Eo}const En=H(Kl());function Wl(e){if(Rt[e]){En.value=e;try{localStorage.setItem(To,e)}catch{}}}function hi(e,t){return t.split(".").reduce((s,n)=>s==null?void 0:s[n],e)}function ql(e,t){return t?e.replace(/\{(\w+)\}/g,(s,n)=>t[n]==null?s:String(t[n])):e}function Zl(e,t,s){let n="other";try{n=new Intl.PluralRules(s).select(t)}catch{n=t===1?"one":"other"}return e[n]??e.other??e.one}function f(e,t){const s=En.value;let n=hi(Rt[s],e);if(n==null&&(n=hi(Rt[Eo],e)),n==null)return e;if(typeof n=="object"){if((t==null?void 0:t.n)==null)return e;n=Zl(n,t.n,s)}return typeof n!="string"?e:ql(n,t)}function gi(e,t,s){const i=f(e,{...s,[t]:"\0"}),o=i.indexOf("\0");return o===-1?{before:i,after:""}:{before:i.slice(0,o),after:i.slice(o+1)}}const ln="dh-panel-token";function Jl(){try{return localStorage.getItem(ln)||""}catch{return""}}const zt=H(Jl()),de=H(null),Ce=je(()=>{var e;return((e=de.value)==null?void 0:e.role)==="superadmin"}),gs=je(()=>{var e,t;return((e=de.value)==null?void 0:e.role)==="admin"||((t=de.value)==null?void 0:t.role)==="superadmin"});function Po(e){zt.value=e;try{e?localStorage.setItem(ln,e):localStorage.removeItem(ln)}catch{}}class Yl extends Error{constructor(t,s,n){super(t),this.status=s,this.body=n}}function Xl(e,t){if(!e||typeof e!="object")return`HTTP ${t}`;if(e.error)return e.error;const s=Object.entries(e.data||{}).map(([n,i])=>`${n}: ${(i==null?void 0:i.message)||i}`).filter(Boolean);return s.length?s.join("; "):e.message||`HTTP ${t}`}async function te(e,{method:t="GET",body:s,auth:n=!0}={}){const i={};s!==void 0&&(i["Content-Type"]="application/json"),n&&zt.value&&(i.Authorization=zt.value);const o=await fetch(e,{method:t,headers:i,body:s===void 0?void 0:JSON.stringify(s)}),r=await o.text();let a=null;try{a=r?JSON.parse(r):null}catch{a=null}if(!o.ok)throw o.status===401&&n&&Pn(),new Yl(Xl(a,o.status),o.status,a);return a}async function Ql(e,t){const s=await te("/api/auth/login",{method:"POST",body:{email:e,password:t},auth:!1});return Po(s.token),await Ss(),de.value}async function Ss(){return de.value=await te("/api/identity"),de.value}function Pn(){Po(""),de.value=null}async function ec(){if(!zt.value)return null;try{return await Ss()}catch{return Pn(),null}}const tc={class:"mx-auto flex w-full max-w-sm flex-col gap-5 pt-24"},sc={class:"dh-card p-6"},nc={class:"text-lg font-bold tracking-[-0.02em] text-strong"},ic={class:"mt-1 mb-5 text-sm text-body"},oc={class:"dh-label",for:"login-email"},rc={class:"dh-label",for:"login-password"},ac={key:0,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},lc=["disabled"],cc={class:"eyebrow text-center"},uc={__name:"LoginView",emits:["authenticated"],setup(e,{emit:t}){const s=t,n=H(""),i=H(""),o=H(""),r=H(!1),a=je(()=>n.value.trim()!==""&&i.value!==""&&!r.value);async function l(){if(a.value){o.value="",r.value=!0;try{const g=await Ql(n.value.trim(),i.value);s("authenticated",g)}catch(g){o.value=g.status===400||g.status===404?f("login.invalid"):g.message||f("login.failed"),i.value=""}finally{r.value=!1}}}return(g,d)=>(y(),T("div",tc,[u("div",sc,[u("h1",nc,m(h(f)("login.title")),1),u("p",ic,m(h(f)("login.subtitle")),1),u("form",{class:"flex flex-col gap-4",onSubmit:Xa(l,["prevent"])},[u("div",null,[u("label",oc,m(h(f)("login.email")),1),le(u("input",{id:"login-email","onUpdate:modelValue":d[0]||(d[0]=b=>n.value=b),class:"dh-input",type:"email",autocomplete:"username",autofocus:"",placeholder:"you@example.com"},null,512),[[ge,n.value]])]),u("div",null,[u("label",rc,m(h(f)("login.password")),1),le(u("input",{id:"login-password","onUpdate:modelValue":d[1]||(d[1]=b=>i.value=b),class:"dh-input",type:"password",autocomplete:"current-password",placeholder:"••••••••"},null,512),[[ge,i.value]])]),o.value?(y(),T("p",ac,m(o.value),1)):B("",!0),u("button",{class:"dh-btn w-full",type:"submit",disabled:!a.value},m(r.value?h(f)("login.submitting"):h(f)("login.submit")),9,lc)],32)]),u("p",cc,m(h(f)("login.authNote")),1)]))}},dc={class:"dh-card overflow-hidden"},pc={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},fc={class:"text-base font-bold tracking-[-0.02em] text-strong"},hc={key:0,class:"dh-pill bg-danger-soft text-danger"},gc={key:0,class:"px-5 py-4 text-sm text-danger"},mc={key:1,class:"w-full text-left text-sm"},bc={class:"px-5 py-3 font-medium text-strong"},vc={class:"data px-5 py-3 text-xs text-muted"},yc={class:"data px-5 py-3 text-right text-xs text-muted"},_c={class:"px-5 py-3 text-right"},wc={key:2,class:"px-5 py-4 text-sm text-muted"},xc={__name:"StatusCard",setup(e){const t=H(null),s=H("");let n=null;async function i(){try{t.value=await te("/api/status",{auth:!1}),s.value=""}catch(a){t.value=null,s.value=a.message||f("status.unreachable")}}const o=je(()=>{var a,l,g;return[{key:"apiServer",label:f("status.apiServer"),h:(a=t.value)==null?void 0:a.apiServer},{key:"pocketBase",label:f("status.pocketBase"),h:(l=t.value)==null?void 0:l.pocketBase},{key:"webApp",label:f("status.webApp"),h:(g=t.value)==null?void 0:g.webApp}]});ct(()=>{i(),n=setInterval(i,1e4)}),kn(()=>clearInterval(n));const r=a=>a==="ok"?"bg-success-soft text-success":"bg-danger-soft text-danger";return(a,l)=>(y(),T("div",dc,[u("div",pc,[u("div",fc,m(h(f)("status.title")),1),s.value?(y(),T("span",hc,[l[0]||(l[0]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(m(h(f)("status.unreachable")),1)])):B("",!0)]),s.value?(y(),T("div",gc,m(s.value),1)):t.value?(y(),T("table",mc,[u("tbody",null,[(y(!0),T(ne,null,$e(o.value,g=>(y(),T("tr",{key:g.key,class:"border-t border-subtle first:border-t-0"},[u("td",bc,m(g.label),1),u("td",vc,m(g.h.url||h(f)("status.thisProcess")),1),u("td",yc,m(g.h.latencyMs!=null?g.h.latencyMs+"ms":h(f)("common.empty")),1),u("td",_c,[u("span",{class:Ue(["dh-pill",r(g.h.status)])},[l[1]||(l[1]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(m(g.h.status),1)],2)])]))),128))])])):(y(),T("div",wc,m(h(f)("status.checking")),1))]))}},kc={class:"dh-card overflow-hidden"},Sc={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},Tc={class:"text-base font-bold tracking-[-0.02em] text-strong"},Ec={class:"mt-0.5 text-xs text-muted"},Pc={class:"flex flex-col gap-4 px-5 py-4"},Cc={class:"dh-label",for:"pb-url"},Ac={class:"grid gap-4 sm:grid-cols-2"},$c={class:"dh-label",for:"pb-email"},Oc={class:"dh-label",for:"pb-password"},Rc=["placeholder"],zc={key:0,class:"data text-xs text-muted"},Dc={key:1,class:"rounded-control bg-info-soft px-3 py-2 text-xs text-info"},Ic={key:2,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},jc={class:"flex items-center gap-2"},Uc=["disabled"],Nc=["disabled"],Mc={class:"eyebrow"},Lc={__name:"PocketBaseCard",setup(e){const t=H(null),s=H({url:"",adminEmail:"",adminPassword:""}),n=H(null),i=H(""),o=H(""),r=H(!1);async function a(){try{t.value=await te("/api/admin/pb-config"),s.value={url:t.value.url,adminEmail:t.value.adminEmail,adminPassword:""},n.value=t.value.probe}catch(d){i.value=d.message}}ct(a);async function l(){i.value="",o.value="",r.value=!0;try{n.value=await te("/api/admin/pb-config/test",{method:"POST",body:s.value}),o.value=n.value.superuser?f("pocketbase.testOk"):n.value.reachable?f("pocketbase.testReachableNoAuth"):f("pocketbase.testUnreachable")}catch(d){i.value=d.message}finally{r.value=!1}}async function g(){i.value="",o.value="",r.value=!0;try{const d=await te("/api/admin/pb-config",{method:"PUT",body:s.value});t.value=d.config,n.value=d.config.probe,s.value.adminPassword="",o.value=d.warning||f("pocketbase.savedNotice")}catch(d){i.value=d.message}finally{r.value=!1}}return(d,b)=>{var P,$;return y(),T("div",kc,[u("div",Sc,[u("div",null,[u("div",Tc,m(h(f)("pocketbase.title")),1),u("p",Ec,m(h(f)("pocketbase.subtitle")),1)]),n.value?(y(),T("span",{key:0,class:Ue(["dh-pill",n.value.superuser?"bg-success-soft text-success":n.value.reachable?"bg-warning-soft text-warning":"bg-danger-soft text-danger"])},[b[3]||(b[3]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(" "+m(n.value.superuser?h(f)("pocketbase.connected"):n.value.reachable?h(f)("pocketbase.noSuperuser"):h(f)("pocketbase.unreachable")),1)],2)):B("",!0)]),u("div",Pc,[u("div",null,[u("label",Cc,m(h(f)("pocketbase.baseUrl")),1),le(u("input",{id:"pb-url","onUpdate:modelValue":b[0]||(b[0]=F=>s.value.url=F),class:"dh-input",placeholder:"http://10.2.1.10:8027"},null,512),[[ge,s.value.url]])]),u("div",Ac,[u("div",null,[u("label",$c,m(h(f)("pocketbase.superuserEmail")),1),le(u("input",{id:"pb-email","onUpdate:modelValue":b[1]||(b[1]=F=>s.value.adminEmail=F),class:"dh-input",autocomplete:"off"},null,512),[[ge,s.value.adminEmail]])]),u("div",null,[u("label",Oc,m(h(f)("pocketbase.superuserPassword")),1),le(u("input",{id:"pb-password","onUpdate:modelValue":b[2]||(b[2]=F=>s.value.adminPassword=F),class:"dh-input",type:"password",autocomplete:"new-password",placeholder:(P=t.value)!=null&&P.adminConfigured?h(f)("pocketbase.passwordUnchanged"):h(f)("pocketbase.passwordNotSet")},null,8,Rc),[[ge,s.value.adminPassword]])])]),($=n.value)!=null&&$.detail?(y(),T("p",zc,m(n.value.detail),1)):B("",!0),o.value?(y(),T("p",Dc,m(o.value),1)):B("",!0),i.value?(y(),T("p",Ic,m(i.value),1)):B("",!0),u("div",jc,[u("button",{class:"dh-btn",disabled:r.value,onClick:g},m(h(f)("pocketbase.saveApply")),9,Uc),u("button",{class:"dh-btn-ghost",disabled:r.value,onClick:l},m(h(f)("pocketbase.testConnection")),9,Nc),b[4]||(b[4]=u("span",{class:"flex-1"},null,-1)),u("span",Mc,m(h(f)("pocketbase.persistedEnv")),1)])])])}}},Fc={class:"dh-card overflow-hidden"},Bc={class:"border-b border-subtle px-5 py-4"},Hc={class:"text-base font-bold tracking-[-0.02em] text-strong"},Vc={class:"mt-0.5 text-xs text-muted"},Gc={class:"flex flex-col gap-4 px-5 py-4"},Kc={class:"dh-label",for:"server-name"},Wc={class:"mt-1 text-xs text-muted"},qc={key:0,class:"rounded-control bg-info-soft px-3 py-2 text-xs text-info"},Zc={key:1,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},Jc={class:"flex items-center gap-2"},Yc=["disabled"],Xc={class:"eyebrow"},Qc={__name:"ServerCard",setup(e){const t=H({name:""}),s=H(""),n=H(""),i=H(!1);async function o(){try{const a=await te("/api/admin/server-config");t.value={name:a.name}}catch(a){s.value=a.message}}ct(o);async function r(){s.value="",n.value="",i.value=!0;try{const a=await te("/api/admin/server-config",{method:"PUT",body:{name:t.value.name}});t.value={name:a.config.name},n.value=a.warning||f("server.savedNotice")}catch(a){s.value=a.message}finally{i.value=!1}}return(a,l)=>(y(),T("div",Fc,[u("div",Bc,[u("div",Hc,m(h(f)("server.title")),1),u("p",Vc,m(h(f)("server.subtitle")),1)]),u("div",Gc,[u("div",null,[u("label",Kc,m(h(f)("server.name")),1),le(u("input",{id:"server-name","onUpdate:modelValue":l[0]||(l[0]=g=>t.value.name=g),class:"dh-input",maxlength:"64",placeholder:"DriverVault API Server",onKeyup:xo(r,["enter"])},null,544),[[ge,t.value.name]]),u("p",Wc,m(h(f)("server.nameHint")),1)]),n.value?(y(),T("p",qc,m(n.value),1)):B("",!0),s.value?(y(),T("p",Zc,m(s.value),1)):B("",!0),u("div",Jc,[u("button",{class:"dh-btn",disabled:i.value,onClick:r},m(h(f)("server.saveApply")),9,Yc),l[1]||(l[1]=u("span",{class:"flex-1"},null,-1)),u("span",Xc,m(h(f)("server.persistedEnv")),1)])])]))}},eu={class:"dh-card overflow-hidden"},tu={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},su={class:"text-base font-bold tracking-[-0.02em] text-strong"},nu={class:"mt-0.5 text-xs text-muted"},iu={class:"flex flex-col gap-4 px-5 py-4"},ou={class:"dh-label",for:"web-url"},ru={class:"dh-label",for:"web-origins"},au={class:"mt-1 text-xs text-muted"},lu={key:0,class:"data text-xs text-muted"},cu={key:1,class:"rounded-control bg-info-soft px-3 py-2 text-xs text-info"},uu={key:2,class:"rounded-control bg-danger-soft px-3 py-2 text-xs text-danger"},du={class:"flex items-center gap-2"},pu=["disabled"],fu=["disabled"],hu={class:"eyebrow"},gu={__name:"WebAppCard",setup(e){const t=H(null),s=H({url:"",origins:""}),n=H(null),i=H(""),o=H(""),r=H(!1),a=P=>P.split(",").map($=>$.trim()).filter(Boolean);function l(P){t.value=P,s.value={url:P.url,origins:(P.allowOrigins||[]).join(", ")},n.value=P.probe}async function g(){try{l(await te("/api/admin/webapp-config"))}catch(P){i.value=P.message}}ct(g);async function d(){i.value="",o.value="",r.value=!0;try{n.value=await te("/api/admin/webapp-config/test",{method:"POST",body:{url:s.value.url}}),o.value=n.value.status==="ok"?f("webapp.testOk"):f("webapp.testFailed")}catch(P){i.value=P.message}finally{r.value=!1}}async function b(){i.value="",o.value="",r.value=!0;try{const P=await te("/api/admin/webapp-config",{method:"PUT",body:{url:s.value.url,allowOrigins:a(s.value.origins)}});l(P.config),o.value=P.warning||f("webapp.savedNotice")}catch(P){i.value=P.message}finally{r.value=!1}}return(P,$)=>{var F;return y(),T("div",eu,[u("div",tu,[u("div",null,[u("div",su,m(h(f)("webapp.title")),1),u("p",nu,m(h(f)("webapp.subtitle")),1)]),n.value?(y(),T("span",{key:0,class:Ue(["dh-pill",n.value.status==="ok"?"bg-success-soft text-success":"bg-danger-soft text-danger"])},[$[2]||($[2]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(" "+m(n.value.status==="ok"?h(f)("webapp.reachable"):h(f)("webapp.unreachable")),1)],2)):B("",!0)]),u("div",iu,[u("div",null,[u("label",ou,m(h(f)("webapp.baseUrl")),1),le(u("input",{id:"web-url","onUpdate:modelValue":$[0]||($[0]=k=>s.value.url=k),class:"dh-input",placeholder:"http://localhost:8090"},null,512),[[ge,s.value.url]])]),u("div",null,[u("label",ru,m(h(f)("webapp.allowedOrigins")),1),le(u("input",{id:"web-origins","onUpdate:modelValue":$[1]||($[1]=k=>s.value.origins=k),class:"dh-input",placeholder:"http://localhost:8090, https://app.example.com"},null,512),[[ge,s.value.origins]]),u("p",au,[Re(m(h(gi)("webapp.originsHint","star").before),1),$[3]||($[3]=u("span",{class:"data"},"*",-1)),Re(m(h(gi)("webapp.originsHint","star").after),1)])]),(F=n.value)!=null&&F.error?(y(),T("p",lu,m(n.value.error),1)):B("",!0),o.value?(y(),T("p",cu,m(o.value),1)):B("",!0),i.value?(y(),T("p",uu,m(i.value),1)):B("",!0),u("div",du,[u("button",{class:"dh-btn",disabled:r.value,onClick:b},m(h(f)("webapp.saveApply")),9,pu),u("button",{class:"dh-btn-ghost",disabled:r.value,onClick:d},m(h(f)("webapp.testConnection")),9,fu),$[4]||($[4]=u("span",{class:"flex-1"},null,-1)),u("span",hu,m(h(f)("webapp.persistedEnv")),1)])])])}}},mu={class:"dh-card overflow-hidden"},bu={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},vu={class:"text-base font-bold tracking-[-0.02em] text-strong"},yu={class:"mt-0.5 text-xs text-muted"},_u={key:0,class:"border-b border-subtle bg-sunken px-5 py-4"},wu={class:"grid gap-3 sm:grid-cols-3"},xu={class:"dh-label"},ku={class:"dh-label"},Su={class:"dh-label"},Tu=["disabled"],Eu={key:1,class:"border-b border-subtle px-5 py-3 text-xs text-danger"},Pu={key:2,class:"px-5 py-6 text-center text-sm text-muted"},Cu={class:"flex items-center gap-3 px-5 py-3"},Au=["onClick"],$u={class:"font-semibold text-strong"},Ou={class:"dh-pill bg-sunken text-muted"},Ru={key:0,class:"text-xs text-muted"},zu=["disabled","onClick"],Du=["disabled","onClick"],Iu={key:0,class:"bg-sunken px-5 py-4"},ju={key:0,class:"data mb-3 text-xs text-muted"},Uu={key:1,class:"grid gap-3 sm:grid-cols-2"},Nu={class:"dh-label"},Mu={key:0,class:"text-danger"},Lu=["onUpdate:modelValue"],Fu={key:0,value:""},Bu=["value"],Hu=["onUpdate:modelValue","type","placeholder"],Vu={key:2,class:"mt-1 text-xs text-muted"},Gu={key:2,class:"text-xs text-muted"},Ku={key:3,class:"mt-4"},Wu={class:"eyebrow mb-1.5"},qu={class:"data flex flex-col gap-1 text-xs text-muted"},Zu={class:"text-strong"},Ju={key:0},Yu={key:1},Xu={key:4,class:"data mt-3 text-xs text-body"},Qu={class:"mt-4 flex items-center gap-2"},ed=["disabled","onClick"],td=["disabled","onClick"],sd=["disabled","onClick"],nd={key:5,class:"eyebrow mt-2"},id={__name:"PluginsCard",setup(e){const t=H([]),s=H(""),n=H(!1),i=H(null),o=Xt({}),r=Xt({}),a=H(!1),l=H({name:"",baseURL:"",provider:""});async function g(){try{const O=await te("/api/admin/plugins");t.value=O.plugins||[],s.value=""}catch(O){s.value=O.message}}ct(g);function d(O){var C;if(i.value===O.name){i.value=null;return}const E={};for(const I of O.configFields||[])E[I.key]=((C=O.config)==null?void 0:C[I.key])??"";o[O.name]=E,i.value=O.name}async function b(O,E){n.value=!0,r[O.name]="";try{const C=await te(`/api/admin/plugins/${encodeURIComponent(O.name)}`,{method:"PUT",body:{enabled:E,config:o[O.name]??{}}});r[O.name]=C.warning||f("plugins.saved"),await g()}catch(C){r[O.name]=C.message}finally{n.value=!1}}async function P(O){n.value=!0,r[O.name]=f("plugins.checking");try{const E=await te(`/api/admin/plugins/${encodeURIComponent(O.name)}/health`,{method:"POST"});r[O.name]=`${E.health.status}${E.health.detail?" — "+E.health.detail:""}`,await g()}catch(E){r[O.name]=E.message}finally{n.value=!1}}async function $(O){if(confirm(f("plugins.confirmRemove",{name:O.name}))){n.value=!0;try{await te(`/api/admin/plugins/${encodeURIComponent(O.name)}`,{method:"DELETE"}),i.value===O.name&&(i.value=null),await g()}catch(E){r[O.name]=E.message}finally{n.value=!1}}}async function F(){n.value=!0,s.value="";try{await te("/api/admin/plugins",{method:"POST",body:l.value}),l.value={name:"",baseURL:"",provider:""},a.value=!1,await g()}catch(O){s.value=O.message}finally{n.value=!1}}const k=O=>O==="ok"?"bg-success-soft text-success":O==="degraded"?"bg-warning-soft text-warning":"bg-danger-soft text-danger";return(O,E)=>(y(),T("div",mu,[u("div",bu,[u("div",null,[u("div",vu,m(h(f)("plugins.title")),1),u("p",yu,m(h(f)("plugins.subtitle")),1)]),u("button",{class:"dh-btn-ghost",onClick:E[0]||(E[0]=C=>a.value=!a.value)},m(a.value?h(f)("plugins.cancelRegister"):h(f)("plugins.registerExternal")),1)]),a.value?(y(),T("div",_u,[u("div",wu,[u("div",null,[u("label",xu,m(h(f)("plugins.name")),1),le(u("input",{"onUpdate:modelValue":E[1]||(E[1]=C=>l.value.name=C),class:"dh-input",placeholder:"acme-parts"},null,512),[[ge,l.value.name]])]),u("div",null,[u("label",ku,m(h(f)("plugins.baseUrl")),1),le(u("input",{"onUpdate:modelValue":E[2]||(E[2]=C=>l.value.baseURL=C),class:"dh-input",placeholder:"http://127.0.0.1:9100"},null,512),[[ge,l.value.baseURL]])]),u("div",null,[u("label",Su,m(h(f)("plugins.provider")),1),le(u("input",{"onUpdate:modelValue":E[3]||(E[3]=C=>l.value.provider=C),class:"dh-input",placeholder:"ACME Corp"},null,512),[[ge,l.value.provider]])])]),u("button",{class:"dh-btn mt-3",disabled:n.value||!l.value.name||!l.value.baseURL,onClick:F},m(h(f)("plugins.register")),9,Tu)])):B("",!0),s.value?(y(),T("p",Eu,m(s.value),1)):B("",!0),t.value.length?B("",!0):(y(),T("p",Pu,m(h(f)("plugins.empty")),1)),(y(!0),T(ne,null,$e(t.value,C=>(y(),T("div",{key:C.name,class:"border-t border-subtle first:border-t-0"},[u("div",Cu,[u("button",{class:"flex flex-1 items-center gap-3 text-left",onClick:I=>d(C)},[u("span",$u,m(C.name),1),u("span",Ou,m(C.kind||h(f)("plugins.builtin")),1),C.provider?(y(),T("span",Ru,m(C.provider),1)):B("",!0),C.health?(y(),T("span",{key:1,class:Ue(["dh-pill",k(C.health.status)])},[E[4]||(E[4]=u("span",{class:"h-1.5 w-1.5 rounded-full bg-current"},null,-1)),Re(m(C.health.status),1)],2)):B("",!0)],8,Au),u("span",{class:Ue(["dh-pill",C.enabled?"bg-success-soft text-success":"bg-sunken text-muted"])},m(C.enabled?h(f)("plugins.enabled"):h(f)("plugins.disabled")),3),u("button",{class:"dh-btn-ghost",disabled:n.value,onClick:I=>P(C)},m(h(f)("plugins.health")),9,zu),u("button",{class:"dh-btn-ghost",disabled:n.value,onClick:I=>d(C)},m(i.value===C.name?h(f)("plugins.close"):h(f)("plugins.configure")),9,Du)]),i.value===C.name?(y(),T("div",Iu,[C.baseURL?(y(),T("div",ju,m(C.baseURL),1)):B("",!0),(C.configFields||[]).length?(y(),T("div",Uu,[(y(!0),T(ne,null,$e(C.configFields,I=>(y(),T("div",{key:I.key},[u("label",Nu,[Re(m(I.label||I.key),1),I.required?(y(),T("span",Mu," *")):B("",!0)]),I.type==="select"?le((y(),T("select",{key:0,"onUpdate:modelValue":j=>o[C.name][I.key]=j,class:"dh-select"},[I.required?B("",!0):(y(),T("option",Fu,m(h(f)("plugins.notSet")),1)),(y(!0),T(ne,null,$e(I.options||[],j=>(y(),T("option",{key:j.value,value:j.value},m(j.label||j.value),9,Bu))),128))],8,Lu)),[[ks,o[C.name][I.key]]]):le((y(),T("input",{key:1,"onUpdate:modelValue":j=>o[C.name][I.key]=j,class:"dh-input",type:I.type==="password"?"password":I.type==="number"?"number":"text",placeholder:I.default||"",autocomplete:"off"},null,8,Hu)),[[qa,o[C.name][I.key]]]),I.help?(y(),T("p",Vu,m(I.help),1)):B("",!0)]))),128))])):(y(),T("p",Gu,m(h(f)("plugins.noConfig")),1)),(C.capabilities||[]).length?(y(),T("div",Ku,[u("div",Wu,m(h(f)("plugins.capabilities")),1),u("ul",qu,[(y(!0),T(ne,null,$e(C.capabilities,I=>(y(),T("li",{key:I.id},[u("span",Zu,m(I.id),1),I.method||I.endpoint?(y(),T("span",Ju," — "+m(I.method)+" "+m(I.endpoint),1)):B("",!0),I.description?(y(),T("span",Yu," · "+m(I.description),1)):B("",!0)]))),128))])])):B("",!0),r[C.name]?(y(),T("p",Xu,m(r[C.name]),1)):B("",!0),u("div",Qu,[u("button",{class:"dh-btn",disabled:n.value,onClick:I=>b(C,!0)},m(C.enabled?h(f)("plugins.save"):h(f)("plugins.saveEnable")),9,ed),C.enabled?(y(),T("button",{key:0,class:"dh-btn-ghost",disabled:n.value,onClick:I=>b(C,!1)},m(h(f)("plugins.disable")),9,td)):B("",!0),E[5]||(E[5]=u("span",{class:"flex-1"},null,-1)),C.kind==="external"?(y(),T("button",{key:1,class:"dh-btn-danger",disabled:n.value,onClick:I=>$(C)},m(h(f)("plugins.remove")),9,sd)):B("",!0)]),C.kind!=="external"?(y(),T("p",nd,m(h(f)("plugins.builtinNote")),1)):B("",!0)])):B("",!0)]))),128))]))}},od={class:"dh-card overflow-hidden"},rd={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},ad={class:"text-base font-bold tracking-[-0.02em] text-strong"},ld={class:"mt-0.5 text-xs text-muted"},cd={key:0,class:"border-b border-subtle px-5 py-3 text-xs text-danger"},ud={key:1,class:"border-b border-subtle bg-sunken px-5 py-4"},dd={class:"grid gap-3 sm:grid-cols-2"},pd={class:"dh-label"},fd={class:"dh-label"},hd={class:"dh-label"},gd=["placeholder"],md={class:"dh-label"},bd=["value"],vd={key:0},yd={class:"dh-label"},_d={value:""},wd=["value"],xd={class:"mt-3 flex items-center gap-2"},kd=["disabled"],Sd=["disabled"],Td={key:2,class:"px-5 py-6 text-center text-sm text-muted"},Ed={key:3,class:"w-full text-left text-sm"},Pd={class:"[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium"},Cd={class:"data px-5 py-2.5 text-xs text-strong"},Ad={key:0,class:"eyebrow ml-1"},$d={class:"px-5 py-2.5 text-body"},Od={class:"px-5 py-2.5 text-body"},Rd={class:"px-5 py-2.5"},zd={class:"px-5 py-2.5 text-right whitespace-nowrap"},Dd=["onClick"],Id=["disabled","onClick"],jd={__name:"UsersCard",setup(e){const t=H([]),s=H([]),n=H(""),i=H(!1),o=H(null),r=H({}),a=je(()=>Ce.value?["user","admin","superadmin"]:["user","admin"]);async function l(){try{const[k,O]=await Promise.all([te("/api/users"),te("/api/orgs")]);t.value=k.users||[],s.value=O.organizations||[],n.value=""}catch(k){n.value=k.message}}ct(l);function g(){var k;o.value="new",r.value={email:"",name:"",password:"",role:"user",organization:Ce.value?"":((k=de.value)==null?void 0:k.organization)||""}}function d(k){o.value=k.id,r.value={email:k.email,name:k.name||"",password:"",role:k.role,organization:k.organization||""}}function b(){o.value=null,n.value=""}async function P(){i.value=!0,n.value="";try{if(o.value==="new")await te("/api/users",{method:"POST",body:r.value});else{const k={...r.value};k.password||delete k.password,await te(`/api/users/${o.value}`,{method:"PATCH",body:k})}o.value=null,await l()}catch(k){n.value=k.message}finally{i.value=!1}}async function $(k){if(confirm(f("users.confirmDelete",{email:k.email}))){i.value=!0,n.value="";try{await te(`/api/users/${k.id}`,{method:"DELETE"}),await l()}catch(O){n.value=O.message}finally{i.value=!1}}}const F=k=>k==="superadmin"?"bg-info-soft text-info":k==="admin"?"bg-warning-soft text-warning":"bg-sunken text-muted";return(k,O)=>(y(),T("div",od,[u("div",rd,[u("div",null,[u("div",ad,m(h(f)("users.title")),1),u("p",ld,m(h(Ce)?h(f)("users.allOrgs"):h(f)("users.yourOrg")),1)]),u("button",{class:"dh-btn",onClick:g},m(h(f)("users.newUser")),1)]),n.value?(y(),T("p",cd,m(n.value),1)):B("",!0),o.value?(y(),T("div",ud,[u("div",dd,[u("div",null,[u("label",pd,m(h(f)("common.email")),1),le(u("input",{"onUpdate:modelValue":O[0]||(O[0]=E=>r.value.email=E),class:"dh-input",type:"email",autocomplete:"off"},null,512),[[ge,r.value.email]])]),u("div",null,[u("label",fd,m(h(f)("common.name")),1),le(u("input",{"onUpdate:modelValue":O[1]||(O[1]=E=>r.value.name=E),class:"dh-input",autocomplete:"off"},null,512),[[ge,r.value.name]])]),u("div",null,[u("label",hd,m(o.value==="new"?h(f)("users.password"):h(f)("users.passwordUnchanged")),1),le(u("input",{"onUpdate:modelValue":O[2]||(O[2]=E=>r.value.password=E),class:"dh-input",type:"password",autocomplete:"new-password",placeholder:h(f)("users.passwordPlaceholder")},null,8,gd),[[ge,r.value.password]])]),u("div",null,[u("label",md,m(h(f)("common.role")),1),le(u("select",{"onUpdate:modelValue":O[3]||(O[3]=E=>r.value.role=E),class:"dh-select"},[(y(!0),T(ne,null,$e(a.value,E=>(y(),T("option",{key:E,value:E},m(E),9,bd))),128))],512),[[ks,r.value.role]])]),h(Ce)?(y(),T("div",vd,[u("label",yd,m(h(f)("common.organization")),1),le(u("select",{"onUpdate:modelValue":O[4]||(O[4]=E=>r.value.organization=E),class:"dh-select"},[u("option",_d,m(h(f)("users.orgNone")),1),(y(!0),T(ne,null,$e(s.value,E=>(y(),T("option",{key:E.id,value:E.id},m(E.name),9,wd))),128))],512),[[ks,r.value.organization]])])):B("",!0)]),u("div",xd,[u("button",{class:"dh-btn",disabled:i.value,onClick:P},m(o.value==="new"?h(f)("common.create"):h(f)("common.save")),9,kd),u("button",{class:"dh-btn-ghost",disabled:i.value,onClick:b},m(h(f)("common.cancel")),9,Sd)])])):B("",!0),t.value.length?(y(),T("table",Ed,[u("thead",null,[u("tr",Pd,[u("th",null,m(h(f)("common.email")),1),u("th",null,m(h(f)("common.name")),1),u("th",null,m(h(f)("common.organization")),1),u("th",null,m(h(f)("common.role")),1),O[5]||(O[5]=u("th",null,null,-1))])]),u("tbody",null,[(y(!0),T(ne,null,$e(t.value,E=>{var C,I;return y(),T("tr",{key:E.id,class:"border-t border-subtle transition-colors hover:bg-sunken"},[u("td",Cd,[Re(m(E.email)+" ",1),E.id===((C=h(de))==null?void 0:C.id)?(y(),T("span",Ad,m(h(f)("users.you")),1)):B("",!0)]),u("td",$d,m(E.name||h(f)("common.empty")),1),u("td",Od,m(E.organizationName||h(f)("common.empty")),1),u("td",Rd,[u("span",{class:Ue(["dh-pill",F(E.role)])},m(E.role),3)]),u("td",zd,[u("button",{class:"dh-btn-ghost",onClick:j=>d(E)},m(h(f)("common.edit")),9,Dd),E.id!==((I=h(de))==null?void 0:I.id)?(y(),T("button",{key:0,class:"dh-btn-danger ml-1.5",disabled:i.value,onClick:j=>$(E)},m(h(f)("common.delete")),9,Id)):B("",!0)])])}),128))])])):(y(),T("p",Td,m(h(f)("users.empty")),1))]))}},Ud={class:"dh-card overflow-hidden"},Nd={class:"flex items-center justify-between border-b border-subtle px-5 py-4"},Md={class:"text-base font-bold tracking-[-0.02em] text-strong"},Ld={class:"mt-0.5 text-xs text-muted"},Fd={key:0,class:"border-b border-subtle px-5 py-3 text-xs text-danger"},Bd={key:1,class:"border-b border-subtle bg-sunken px-5 py-4"},Hd={class:"dh-label"},Vd=["placeholder"],Gd={class:"mt-3 flex items-center gap-2"},Kd=["disabled"],Wd=["disabled"],qd={key:2,class:"px-5 py-6 text-center text-sm text-muted"},Zd={key:3,class:"w-full text-left text-sm"},Jd={class:"[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium"},Yd={class:"px-5 py-2.5 font-medium text-strong"},Xd={class:"data px-5 py-2.5 text-xs text-muted"},Qd={class:"px-5 py-2.5 text-right whitespace-nowrap"},ep=["onClick"],tp=["disabled","onClick"],sp={__name:"OrgsCard",setup(e){const t=H([]),s=H(""),n=H(!1),i=H(null),o=H(""),r=je(()=>{var k;return((k=de.value)==null?void 0:k.organization)||""}),a=je(()=>Ce.value||!r.value);function l(k){return Ce.value||k.id===r.value}async function g(){if(!gs.value){t.value=[];return}try{const k=await te("/api/orgs");t.value=k.organizations||[],s.value=""}catch(k){s.value=k.message}}ct(g);function d(){i.value="new",o.value=""}function b(k){i.value=k.id,o.value=k.name}function P(){i.value=null,s.value=""}async function $(){n.value=!0,s.value="";try{i.value==="new"?(await te("/api/orgs",{method:"POST",body:{name:o.value}}),Ce.value||await Ss()):await te(`/api/orgs/${i.value}`,{method:"PATCH",body:{name:o.value}}),i.value=null,await g()}catch(k){s.value=k.message}finally{n.value=!1}}async function F(k){const O=!Ce.value&&k.id===r.value;if(confirm(f(O?"orgs.confirmDeleteOwn":"orgs.confirmDelete",{name:k.name}))){n.value=!0,s.value="";try{await te(`/api/orgs/${k.id}`,{method:"DELETE"}),O&&await Ss(),await g()}catch(C){s.value=C.message}finally{n.value=!1}}}return(k,O)=>(y(),T("div",Ud,[u("div",Nd,[u("div",null,[u("div",Md,m(h(Ce)?h(f)("orgs.title"):h(f)("common.organization")),1),u("p",Ld,m(h(Ce)?h(f)("orgs.subtitle"):r.value?h(f)("orgs.subtitleOwn"):h(f)("orgs.subtitleNone")),1)]),a.value?(y(),T("button",{key:0,class:"dh-btn",onClick:d},m(h(f)("orgs.newOrg")),1)):B("",!0)]),s.value?(y(),T("p",Fd,m(s.value),1)):B("",!0),i.value?(y(),T("div",Bd,[u("label",Hd,m(h(f)("common.name")),1),le(u("input",{"onUpdate:modelValue":O[0]||(O[0]=E=>o.value=E),class:"dh-input",placeholder:h(f)("orgs.namePlaceholder"),onKeyup:xo($,["enter"])},null,40,Vd),[[ge,o.value]]),u("div",Gd,[u("button",{class:"dh-btn",disabled:n.value||!o.value.trim(),onClick:$},m(i.value==="new"?h(f)("common.create"):h(f)("common.save")),9,Kd),u("button",{class:"dh-btn-ghost",disabled:n.value,onClick:P},m(h(f)("common.cancel")),9,Wd)])])):B("",!0),t.value.length?(y(),T("table",Zd,[u("thead",null,[u("tr",Jd,[u("th",null,m(h(f)("common.name")),1),u("th",null,m(h(f)("orgs.colId")),1),O[1]||(O[1]=u("th",null,null,-1))])]),u("tbody",null,[(y(!0),T(ne,null,$e(t.value,E=>(y(),T("tr",{key:E.id,class:"border-t border-subtle transition-colors hover:bg-sunken"},[u("td",Yd,m(E.name),1),u("td",Xd,m(E.id),1),u("td",Qd,[l(E)?(y(),T(ne,{key:0},[u("button",{class:"dh-btn-ghost",onClick:C=>b(E)},m(h(f)("common.rename")),9,ep),u("button",{class:"dh-btn-danger ml-1.5",disabled:n.value,onClick:C=>F(E)},m(h(f)("common.delete")),9,tp)],64)):B("",!0)])]))),128))])])):(y(),T("p",qd,m(a.value&&!h(Ce)?h(f)("orgs.createHint"):h(f)("orgs.empty")),1))]))}},np={class:"dh-card overflow-hidden"},ip={class:"border-b border-subtle px-5 py-4"},op={class:"flex items-center justify-between"},rp={class:"text-base font-bold tracking-[-0.02em] text-strong"},ap={class:"eyebrow"},lp={key:0,class:"mt-1.5 text-xs text-muted"},cp={class:"overflow-x-auto"},up={class:"w-full text-left text-sm"},dp={class:"[&>th]:eyebrow [&>th]:px-5 [&>th]:py-2.5 [&>th]:font-medium"},pp={class:"data border-t border-subtle px-5 py-2.5 text-xs whitespace-nowrap"},fp={class:"text-strong"},hp={class:"border-t border-subtle px-5 py-2.5 text-body"},ue={__name:"EndpointTable",props:{title:String,auth:String,endpoints:Array,note:String},setup(e){const t={GET:"text-success",POST:"text-brandtext",PUT:"text-info",PATCH:"text-warning",DELETE:"text-danger"};return(s,n)=>(y(),T("div",np,[u("div",ip,[u("div",op,[u("div",rp,m(e.title),1),u("span",ap,m(e.auth),1)]),e.note?(y(),T("p",lp,m(e.note),1)):B("",!0)]),u("div",cp,[u("table",up,[u("thead",null,[u("tr",dp,[u("th",null,m(h(f)("api.colEndpoint")),1),u("th",null,m(h(f)("api.colDescription")),1)])]),u("tbody",null,[(y(!0),T(ne,null,$e(e.endpoints,i=>(y(),T("tr",{key:i.method+i.path,class:"transition-colors hover:bg-sunken"},[u("td",pp,[u("span",{class:Ue(["font-semibold",t[i.method]])},m(i.method),3),u("span",fp,m(i.path),1)]),u("td",hp,m(i.desc),1)]))),128))])])])]))}},gp={class:"mx-auto flex max-w-4xl flex-col gap-6 px-6 pt-12 pb-16"},mp={class:"flex items-center gap-3"},bp={class:"inline-flex items-center gap-2.5 select-none"},vp={class:"h-8 w-8 shrink-0",viewBox:"0 0 48 48",fill:"none","aria-hidden":"true"},yp={transform:"translate(7 0) skewX(-13)"},_p=["fill"],wp=["fill"],xp=["fill"],kp={class:"eyebrow mt-1.5"},Sp={key:0,class:"data hidden text-xs text-muted sm:inline"},Tp={key:0},Ep={key:1,class:"dh-pill bg-info-soft text-info"},Pp=["value","aria-label"],Cp=["value"],Ap=["title"],$p={key:0,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75",class:"h-4 w-4"},Op={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75",class:"h-4 w-4"},Rp={key:0,class:"eyebrow py-16 text-center"},zp={class:"flex flex-wrap gap-1.5"},Dp=["onClick"],Ip={key:8,class:"eyebrow text-center"},jp={class:"eyebrow text-center"},Up="{records} is any of service-records, technical-checks, maintenance, fuel-entries, charging-sessions, car-documents, parts.",Np={__name:"App",setup(e){const t=H(!0),s=H("overview");ct(async()=>{await ec(),t.value=!1});const n=je(()=>{const pe=[{id:"overview",label:f("sections.overview")}];return gs.value&&pe.push({id:"users",label:f("sections.users")}),(gs.value||de.value)&&pe.push({id:"orgs",label:f("sections.orgs")}),Ce.value&&pe.push({id:"server",label:f("sections.server")},{id:"pocketbase",label:f("sections.pocketbase")},{id:"webapp",label:f("sections.webapp")},{id:"plugins",label:f("sections.plugins")}),pe.push({id:"api",label:f("sections.api")}),pe}),i=je(()=>Gl.map(pe=>{let se=pe;try{se=new Intl.DisplayNames([pe],{type:"language"}).of(pe)||pe}catch{}return{code:pe,label:se.charAt(0).toUpperCase()+se.slice(1)}}));function o(){Pn(),s.value="overview"}const r=je(()=>Ct.value==="dark"?["#60a5fa","#93c5fd","#ffffff"]:["var(--brand-700)","var(--brand-500)","var(--brand-400)"]),a=[{method:"GET",path:"/api/health",desc:"Liveness probe (no auth)"},{method:"GET",path:"/healthz",desc:"The same probe under the conventional container path"},{method:"GET",path:"/api/status",desc:"Health of PocketBase + Web App"},{method:"POST",path:"/api/auth/login",desc:"Exchange email + password for a PocketBase token"},{method:"GET",path:"/api/auth/validate",desc:"Check whether a token is still valid"}],l=[{method:"GET",path:"/api/auth/me",desc:"Identity of the bearer token"},{method:"GET",path:"/api/identity",desc:"Identity incl. role + organization"}],g=[{method:"GET",path:"/api/cars",desc:"List owned + shared cars"},{method:"POST",path:"/api/cars",desc:"Create a car"},{method:"GET",path:"/api/cars/{id}",desc:"Fetch one car"},{method:"PATCH",path:"/api/cars/{id}",desc:"Update a car"},{method:"PUT",path:"/api/cars/{id}/view",desc:"Save the car's layout — hidden tabs/fields/columns and their order"},{method:"DELETE",path:"/api/cars/{id}",desc:"Delete a car (owner only)"},{method:"GET",path:"/api/cars/{id}/service-records",desc:"A car's service history"},{method:"GET",path:"/api/cars/{id}/technical-checks",desc:"A car's roadworthiness inspections"},{method:"GET",path:"/api/cars/{id}/parts",desc:"A car's parts catalog"},{method:"GET",path:"/api/cars/{id}/fuel-entries",desc:"A car's refuelings"},{method:"GET",path:"/api/cars/{id}/fuel-stats",desc:"Consumption + cost totals from the fuel log"},{method:"GET",path:"/api/cars/{id}/charging-sessions",desc:"A car's charging sessions"},{method:"GET",path:"/api/cars/{id}/charging-stats",desc:"Energy + cost totals from the charging log"},{method:"GET",path:"/api/cars/{id}/maintenance",desc:"A car's maintenance log"},{method:"GET",path:"/api/cars/{id}/documents",desc:"A car's documents"},{method:"GET",path:"/api/cars/{id}/reminders",desc:"A car's reminders"},{method:"GET",path:"/api/cars/{id}/provider",desc:"Live snapshot from the car's connected service"},{method:"POST",path:"/api/cars/{id}/provider",desc:"Link the car to a provider vehicle (an empty provider unlinks)"},{method:"POST",path:"/api/cars/{id}/provider/sync",desc:"Re-apply the provider's data to the car"},{method:"GET",path:"/api/cars/{id}/shares",desc:"Who a car is shared with (owner)"},{method:"POST",path:"/api/cars/{id}/shares",desc:"Share a car by email (owner)"},{method:"DELETE",path:"/api/cars/{id}/shares/{userId}",desc:"Revoke a share (owner)"}],d=[{method:"GET",path:"/api/service-records",desc:"List one car's service records (?car={id})"},{method:"POST",path:"/api/service-records",desc:"Log a service record"},{method:"GET",path:"/api/service-records/{id}",desc:"Fetch one record"},{method:"PATCH",path:"/api/service-records/{id}",desc:"Update a record"},{method:"DELETE",path:"/api/service-records/{id}",desc:"Delete a record"}],b=[{method:"GET",path:"/api/technical-checks",desc:"List one car's inspections (?car={id})"},{method:"POST",path:"/api/technical-checks",desc:"Log an inspection"},{method:"GET",path:"/api/technical-checks/{id}",desc:"Fetch one inspection"},{method:"PATCH",path:"/api/technical-checks/{id}",desc:"Update an inspection"},{method:"DELETE",path:"/api/technical-checks/{id}",desc:"Delete an inspection"}],P=[{method:"GET",path:"/api/parts",desc:"List one car's parts (?car={id})"},{method:"POST",path:"/api/parts",desc:"Add a part"},{method:"GET",path:"/api/parts/{id}",desc:"Fetch one part"},{method:"PATCH",path:"/api/parts/{id}",desc:"Update a part"},{method:"DELETE",path:"/api/parts/{id}",desc:"Delete a part"}],$=[{method:"GET",path:"/api/fuel-entries",desc:"List one car's refuelings (?car={id})"},{method:"POST",path:"/api/fuel-entries",desc:"Log a refueling"},{method:"GET",path:"/api/fuel-entries/{id}",desc:"Fetch one refueling"},{method:"PATCH",path:"/api/fuel-entries/{id}",desc:"Update a refueling"},{method:"DELETE",path:"/api/fuel-entries/{id}",desc:"Delete a refueling"}],F=[{method:"GET",path:"/api/charging-sessions",desc:"List one car's charging sessions (?car={id})"},{method:"POST",path:"/api/charging-sessions",desc:"Log a charging session"},{method:"GET",path:"/api/charging-sessions/{id}",desc:"Fetch one session"},{method:"PATCH",path:"/api/charging-sessions/{id}",desc:"Update a session"},{method:"DELETE",path:"/api/charging-sessions/{id}",desc:"Delete a session"}],k=[{method:"GET",path:"/api/maintenance",desc:"List one car's maintenance jobs (?car={id})"},{method:"POST",path:"/api/maintenance",desc:"Log a maintenance job"},{method:"GET",path:"/api/maintenance/{id}",desc:"Fetch one job"},{method:"PATCH",path:"/api/maintenance/{id}",desc:"Update a job"},{method:"DELETE",path:"/api/maintenance/{id}",desc:"Delete a job"}],O=[{method:"GET",path:"/api/car-documents",desc:"List one car's documents (?car={id})"},{method:"POST",path:"/api/car-documents",desc:"Add a document"},{method:"GET",path:"/api/car-documents/{id}",desc:"Fetch one document"},{method:"PATCH",path:"/api/car-documents/{id}",desc:"Update a document"},{method:"DELETE",path:"/api/car-documents/{id}",desc:"Delete a document"}],E=[{method:"GET",path:"/api/reminders",desc:"List one car's reminders (?car={id})"},{method:"POST",path:"/api/reminders",desc:"Create a reminder"},{method:"GET",path:"/api/reminders/{id}",desc:"Fetch one reminder"},{method:"PATCH",path:"/api/reminders/{id}",desc:"Update a reminder"},{method:"DELETE",path:"/api/reminders/{id}",desc:"Delete a reminder"},{method:"POST",path:"/api/reminders/{id}/complete",desc:"Mark done — a recurring reminder rolls forward from now"}],C=[{method:"POST",path:"/api/{records}/{id}/file",desc:"Attach a file, replacing any previous one (PDF or image, max 10 MB)"},{method:"GET",path:"/api/{records}/{id}/file",desc:"Download the attachment (car access re-checked on every request)"},{method:"DELETE",path:"/api/{records}/{id}/file",desc:"Detach and delete the file"}],I=[{method:"GET",path:"/api/vehicle-providers",desc:"Registered providers and whether the caller can use each one"},{method:"GET",path:"/api/vehicle-providers/{provider}/vehicles",desc:"Vehicles on the caller's provider account, as importable cars"},{method:"POST",path:"/api/vehicle-providers/{provider}/import",desc:"Create a car from a provider vehicle (409 if already imported)"}],j=[{method:"GET",path:"/api/integrations/toyota",desc:"Resolved Toyota Connected settings (secrets masked)"},{method:"PUT",path:"/api/integrations/toyota",desc:"Save the caller's own layer (user or org scope)"},{method:"POST",path:"/api/integrations/toyota/health",desc:"Live probe with the resolved credentials"},{method:"GET",path:"/api/integrations/toyota/vehicles",desc:"Raw MyToyota vehicle payload"},{method:"GET",path:"/api/integrations/anker-solix",desc:"Resolved Anker Solix settings (secrets masked)"},{method:"PUT",path:"/api/integrations/anker-solix",desc:"Save the caller's own layer (user or org scope)"},{method:"POST",path:"/api/integrations/anker-solix/health",desc:"Live probe with the resolved credentials"},{method:"GET",path:"/api/integrations/anker-solix/chargers",desc:"EV chargers on the linked Anker account"},{method:"GET",path:"/api/integrations/anker-solix/chargers/{sn}/control",desc:"Control mode, token state and live CSMS session for one charger"},{method:"POST",path:"/api/integrations/anker-solix/chargers/{sn}/control/token",desc:"(Re)issue the charger's control token"},{method:"DELETE",path:"/api/integrations/anker-solix/chargers/{sn}/control/token",desc:"Revoke the control token"},{method:"POST",path:"/api/integrations/anker-solix/chargers/{sn}/{action}",desc:"One OCPP command: start, stop, limit, clear-limit, availability, reset, unlock, trigger, config"}],re=[{method:"GET",path:"/ocpp/{serial}",desc:"WebSocket the charger dials out to; OCPP Basic auth with the serial + its control token"}],Ee=[{method:"GET",path:"/api/me",desc:"Current user profile"},{method:"PATCH",path:"/api/me",desc:"Update profile"},{method:"POST",path:"/api/me/password",desc:"Change password"},{method:"POST",path:"/api/me/avatar",desc:"Upload avatar"},{method:"GET",path:"/api/me/avatar",desc:"Fetch avatar"},{method:"DELETE",path:"/api/me/avatar",desc:"Remove avatar"},{method:"POST",path:"/api/me/verify/request",desc:"Request email verification"},{method:"GET",path:"/api/me/export",desc:"Export your data"},{method:"POST",path:"/api/me/import",desc:"Import data"},{method:"POST",path:"/api/me/delete",desc:"Request account deletion"},{method:"POST",path:"/api/me/delete/cancel",desc:"Cancel deletion request"},{method:"DELETE",path:"/api/me",desc:"Finalize account deletion"}],Pe=[{method:"GET",path:"/api/users",desc:"List users (scoped by role)"},{method:"POST",path:"/api/users",desc:"Create a user"},{method:"PATCH",path:"/api/users/{id}",desc:"Update email / name / role / org / password"},{method:"DELETE",path:"/api/users/{id}",desc:"Delete a user"},{method:"GET",path:"/api/orgs",desc:"List organizations"},{method:"POST",path:"/api/orgs",desc:"Create an organization (any user without one; creator becomes admin)"},{method:"PATCH",path:"/api/orgs/{id}",desc:"Rename an organization (own org; any as superadmin)"},{method:"DELETE",path:"/api/orgs/{id}",desc:"Delete an organization (own org; any as superadmin)"}],Le=[{method:"GET",path:"/api/admin/pb-config",desc:"PocketBase connection + live probe"},{method:"POST",path:"/api/admin/pb-config/test",desc:"Probe a candidate connection"},{method:"PUT",path:"/api/admin/pb-config",desc:"Apply + persist a connection"},{method:"GET",path:"/api/admin/server-config",desc:"This server's display name"},{method:"PUT",path:"/api/admin/server-config",desc:"Rename this server (applied + persisted)"},{method:"GET",path:"/api/admin/webapp-config",desc:"Web App URL + CORS origins, with a live probe"},{method:"POST",path:"/api/admin/webapp-config/test",desc:"Probe a candidate Web App address"},{method:"PUT",path:"/api/admin/webapp-config",desc:"Apply + persist Web App settings"},{method:"GET",path:"/api/admin/plugins",desc:"List plugins (secrets masked)"},{method:"POST",path:"/api/admin/plugins",desc:"Register an external plugin"},{method:"GET",path:"/api/admin/plugins/{name}",desc:"Fetch one plugin"},{method:"PUT",path:"/api/admin/plugins/{name}",desc:"Enable/disable + configure"},{method:"DELETE",path:"/api/admin/plugins/{name}",desc:"Remove an external plugin"},{method:"POST",path:"/api/admin/plugins/{name}/health",desc:"Run a health check now"}];return(pe,se)=>(y(),T("div",gp,[u("div",mp,[u("span",bp,[(y(),T("svg",vp,[u("g",yp,[u("rect",{x:"9",y:"16",width:"6",height:"16",rx:"3",fill:r.value[0]},null,8,_p),u("rect",{x:"19",y:"12",width:"6",height:"24",rx:"3",fill:r.value[1]},null,8,wp),u("rect",{x:"29",y:"8",width:"6",height:"32",rx:"3",fill:r.value[2]},null,8,xp)])])),se[2]||(se[2]=u("span",{class:"text-2xl leading-none font-extrabold tracking-[-0.03em] italic"},[u("span",{class:"text-strong"},"Driver"),u("span",{class:"text-brandtext"},"Vault")],-1))]),u("span",kp,m(h(f)("app.apiServer")),1),se[5]||(se[5]=u("div",{class:"flex-1"},null,-1)),h(de)?(y(),T("span",Sp,[Re(m(h(de).email),1),h(de).organizationName?(y(),T("span",Tp," · "+m(h(de).organizationName),1)):B("",!0)])):B("",!0),h(de)?(y(),T("span",Ep,m(h(de).role),1)):B("",!0),u("select",{class:"dh-select !w-auto !py-1 !text-xs",value:h(En),"aria-label":h(f)("sections.overview"),onChange:se[0]||(se[0]=ce=>h(Wl)(ce.target.value))},[(y(!0),T(ne,null,$e(i.value,ce=>(y(),T("option",{key:ce.code,value:ce.code},m(ce.label),9,Cp))),128))],40,Pp),u("button",{class:"dh-btn-ghost",title:h(Ct)==="dark"?h(f)("app.switchToLight"):h(f)("app.switchToDark"),onClick:se[1]||(se[1]=(...ce)=>h(fi)&&h(fi)(...ce))},[h(Ct)==="dark"?(y(),T("svg",$p,[...se[3]||(se[3]=[u("circle",{cx:"12",cy:"12",r:"4"},null,-1),u("path",{"stroke-linecap":"round",d:"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"},null,-1)])])):(y(),T("svg",Op,[...se[4]||(se[4]=[u("path",{"stroke-linecap":"round","stroke-linejoin":"round",d:"M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8Z"},null,-1)])])),Re(" "+m(h(f)("app.theme")),1)],8,Ap),h(zt)?(y(),T("button",{key:2,class:"dh-btn-ghost",onClick:o},m(h(f)("app.signOut")),1)):B("",!0)]),t.value?(y(),T("p",Rp,m(h(f)("app.loading")),1)):h(zt)?(y(),T(ne,{key:2},[u("nav",zp,[(y(!0),T(ne,null,$e(n.value,ce=>(y(),T("button",{key:ce.id,class:Ue(["dh-btn-ghost",s.value===ce.id?"border-accent text-brandtext":""]),onClick:It=>s.value=ce.id},m(ce.label),11,Dp))),128))]),s.value==="overview"?(y(),tt(xc,{key:0})):s.value==="users"?(y(),tt(jd,{key:1})):s.value==="orgs"?(y(),tt(sp,{key:2})):s.value==="server"?(y(),tt(Qc,{key:3})):s.value==="pocketbase"?(y(),tt(Lc,{key:4})):s.value==="webapp"?(y(),tt(gu,{key:5})):s.value==="plugins"?(y(),tt(id,{key:6})):s.value==="api"?(y(),T(ne,{key:7},[Y(ue,{title:h(f)("api.groupPublic"),auth:h(f)("api.authNone"),endpoints:a},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupIdentity"),auth:h(f)("api.authBearer"),endpoints:l},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupCars"),auth:h(f)("api.authBearer"),endpoints:g},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupService"),auth:h(f)("api.authBearer"),endpoints:d},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupTechnical"),auth:h(f)("api.authBearer"),endpoints:b},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupParts"),auth:h(f)("api.authBearer"),endpoints:P},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupFuel"),auth:h(f)("api.authBearer"),endpoints:$},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupCharging"),auth:h(f)("api.authBearer"),endpoints:F},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupMaintenance"),auth:h(f)("api.authBearer"),endpoints:k},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupDocuments"),auth:h(f)("api.authBearer"),endpoints:O},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupReminders"),auth:h(f)("api.authBearer"),endpoints:E},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupAttachments"),auth:h(f)("api.authBearer"),endpoints:C,note:Up},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupProviders"),auth:h(f)("api.authBearer"),endpoints:I},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupIntegrations"),auth:h(f)("api.authBearer"),endpoints:j},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupOcpp"),auth:h(f)("api.authCharger"),endpoints:re},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupAccount"),auth:h(f)("api.authBearer"),endpoints:Ee},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupManagement"),auth:h(f)("api.authManager"),endpoints:Pe},null,8,["title","auth"]),Y(ue,{title:h(f)("api.groupSuperadmin"),auth:h(f)("api.authSuperadmin"),endpoints:Le},null,8,["title","auth"])],64)):B("",!0),!h(gs)&&s.value==="overview"?(y(),T("p",Ip,m(h(f)("app.standardUserNote")),1)):B("",!0)],64)):(y(),tt(uc,{key:1})),u("p",jp,m(h(f)("app.footer")),1)]))}};sl(Np).mount("#app"); diff --git a/API Server/internal/api/dist/index.html b/API Server/internal/api/dist/index.html index d948352..b5347a8 100644 --- a/API Server/internal/api/dist/index.html +++ b/API Server/internal/api/dist/index.html @@ -6,7 +6,7 @@ DriverVault · API Server - + diff --git a/API Server/internal/api/records.go b/API Server/internal/api/records.go index 494e18a..41f3fce 100644 --- a/API Server/internal/api/records.go +++ b/API Server/internal/api/records.go @@ -68,15 +68,18 @@ type carRecord struct { Created string `json:"created"` Updated string `json:"updated"` - // Switched-off tabs and Information fields, plus the arrangements of the - // tabs, the Information rows and the connected service's readings. Raw + // Switched-off tabs, Information fields and Service history columns, plus + // the arrangements of the tabs, the Information rows, those columns and the + // connected service's readings. Raw // because PocketBase hands back whatever a json field holds — null on a car // nobody has configured — which is not a []string. - HiddenTabs json.RawMessage `json:"hidden_tabs"` - HiddenFields json.RawMessage `json:"hidden_fields"` - TabOrder json.RawMessage `json:"tab_order"` - FieldOrder json.RawMessage `json:"field_order"` - MetricOrder json.RawMessage `json:"metric_order"` + HiddenTabs json.RawMessage `json:"hidden_tabs"` + HiddenFields json.RawMessage `json:"hidden_fields"` + HiddenServiceColumns json.RawMessage `json:"hidden_service_columns"` + TabOrder json.RawMessage `json:"tab_order"` + FieldOrder json.RawMessage `json:"field_order"` + ServiceColumnOrder json.RawMessage `json:"service_column_order"` + MetricOrder json.RawMessage `json:"metric_order"` } func (rec carRecord) toModel() models.Car { @@ -105,8 +108,10 @@ func (rec carRecord) toModel() models.Car { ProviderVehicleID: rec.ProviderVehicleID, HiddenTabs: decodeStringList(rec.HiddenTabs), HiddenFields: decodeStringList(rec.HiddenFields), + HiddenServiceColumns: decodeStringList(rec.HiddenServiceColumns), TabOrder: decodeStringList(rec.TabOrder), FieldOrder: decodeStringList(rec.FieldOrder), + ServiceColumnOrder: decodeStringList(rec.ServiceColumnOrder), MetricOrder: decodeStringList(rec.MetricOrder), Owner: rec.Owner, Created: rec.Created, diff --git a/API Server/internal/bootstrap/schema.go b/API Server/internal/bootstrap/schema.go index 0e79741..7fd2e5d 100644 --- a/API Server/internal/bootstrap/schema.go +++ b/API Server/internal/bootstrap/schema.go @@ -46,11 +46,15 @@ var collectionsSchema = map[string][]fieldDef{ // release is on by default. Keys are validated in internal/api/cars.go. fJSON("hidden_tabs", 2000), fJSON("hidden_fields", 2000), + // And the columns of the Service history table (["oil"] on an EV, which + // has no oil to change). Date is not hideable and so never appears here. + fJSON("hidden_service_columns", 2000), // The order the tabs are laid out in, as tab keys, the same for the - // Information rows, and the same for the connected service's headline - // readings. Empty means the page's own default order. + // Information rows, the Service history columns, and the connected + // service's headline readings. Empty means the page's own default order. fJSON("tab_order", 2000), fJSON("field_order", 2000), + fJSON("service_column_order", 2000), fJSON("metric_order", 2000), // Owner of this car. Non-cascading: deleting a user must not wipe their cars. fRelation("owner", "users", false, false), diff --git a/API Server/internal/models/models.go b/API Server/internal/models/models.go index 36f711c..2b8b90e 100644 --- a/API Server/internal/models/models.go +++ b/API Server/internal/models/models.go @@ -83,6 +83,19 @@ type Car struct { // arranged ones rather than appearing in the middle. FieldOrder []string `json:"fieldOrder"` + // HiddenServiceColumns is what the Service history table does not show, as + // column keys (["oil", "engineFilter"] on an EV, whose service is neither). + // The hidden set like the two above, so a column added later is on by + // default, and Date is not among the keys it may name: a service record is + // its date, and a table of them without it reads as a list of nothing. + HiddenServiceColumns []string `json:"hiddenServiceColumns"` + + // ServiceColumnOrder is the arrangement of those columns, covering the + // hidden ones so a column switched back on returns to where it was. It does + // include "date", which cannot be switched off but can be moved off the + // front — the same rule TabOrder applies to Information. + ServiceColumnOrder []string `json:"serviceColumnOrder"` + // MetricOrder is the same thing for the headline readings on the connected // service's tab, as the reading keys ("odometer", "evRange", …). A reading // the provider didn't report at the time it was arranged simply isn't in the diff --git a/API Server/panel/src/App.vue b/API Server/panel/src/App.vue index 1e65731..977ca4a 100644 --- a/API Server/panel/src/App.vue +++ b/API Server/panel/src/App.vue @@ -94,7 +94,7 @@ const carsApi = [ { method: "POST", path: "/api/cars", desc: "Create a car" }, { method: "GET", path: "/api/cars/{id}", desc: "Fetch one car" }, { method: "PATCH", path: "/api/cars/{id}", desc: "Update a car" }, - { method: "PUT", path: "/api/cars/{id}/view", desc: "Save the car's layout — hidden tabs/fields and their order" }, + { method: "PUT", path: "/api/cars/{id}/view", desc: "Save the car's layout — hidden tabs/fields/columns and their order" }, { method: "DELETE", path: "/api/cars/{id}", desc: "Delete a car (owner only)" }, { method: "GET", path: "/api/cars/{id}/service-records", desc: "A car's service history" }, { method: "GET", path: "/api/cars/{id}/technical-checks", desc: "A car's roadworthiness inspections" }, diff --git a/Web App/README.md b/Web App/README.md index 39c8536..275e441 100644 --- a/Web App/README.md +++ b/Web App/README.md @@ -101,10 +101,13 @@ Config (`server/.env`, copy from `.env.example`): sections that car's page shows (connected service, service history, technical checks, maintenance, fuel cost, charging cost, documents, parts, reminders — Fuel cost off on an EV and Charging cost off on a petrol car) and which of the - 14 Information rows it lists (no Differential oil on a car without one). It belongs to the car, so everyone it is shared with sees + 14 Information rows it lists (no Differential oil on a car without one) and + which columns the Service history table shows (no Oil, no Engine air filter on + an EV). It belongs to the car, so everyone it is shared with sees the same page; setting it needs write access. Stored as the *hidden* sets, so - anything added in a later release is on by default, and the Information tab - itself can't be switched off. + anything added in a later release is on by default. Two things can't be + switched off: the Information tab, and the service Date — a history with the + day taken out stops being one. - **Locking the layout** — the padlock in the sidebar, above the theme toggle, holds every arrangement still at once: the garage, a car's tabs, its Information rows, the provider's readings. It is a guard against nudging a @@ -122,6 +125,10 @@ Config (`server/.env`, copy from `.env.example`): into any order, saved on drop. Also a property of the car, and it covers the hidden rows too, so switching one back on returns it to where it was. Same native drag events as the garage, so also pointer-only. +- **Arranging the Service history columns** — the column headings on that tab + drag into any order, saved on drop, and it covers the hidden columns too. Date + is arrangeable although it can't be switched off, the same rule Information + follows in the tab bar. - **Arranging the connected service's readings** — the headline readings on that tab drag the same way. Only what the provider reported can be arranged, so a reading that turns up later (an EV range on a car that was parked unplugged) diff --git a/Web App/web/src/i18n/da.json b/Web App/web/src/i18n/da.json index 417d395..39db230 100644 --- a/Web App/web/src/i18n/da.json +++ b/Web App/web/src/i18n/da.json @@ -380,7 +380,10 @@ "tabsHeading": "Faner", "fieldsHeading": "Oplysninger", "alwaysOn": "{tab} er altid tilgængelig.", - "fieldsOrderHint": "Træk felterne på fanen Oplysninger for at ændre deres rækkefølge." + "fieldsOrderHint": "Træk felterne på fanen Oplysninger for at ændre deres rækkefølge.", + "serviceColumnsHeading": "Kolonner i servicehistorik", + "columnAlwaysOn": "{column} vises altid.", + "serviceColumnsOrderHint": "Træk kolonneoverskrifterne på fanen Servicehistorik for at ændre deres rækkefølge." }, "provider": { @@ -462,6 +465,7 @@ "colCabinFilter": "Kabinefilter", "colNotes": "Noter", "colFile": "Fil", + "dragHint": "Træk en kolonne for at ændre rækkefølgen i bilens servicehistorik.", "confirmDelete": "Slet denne servicepost?" }, diff --git a/Web App/web/src/i18n/en.json b/Web App/web/src/i18n/en.json index d443d38..a779af8 100644 --- a/Web App/web/src/i18n/en.json +++ b/Web App/web/src/i18n/en.json @@ -379,7 +379,10 @@ "tabsHeading": "Tabs", "fieldsHeading": "Information fields", "alwaysOn": "{tab} is always available.", - "fieldsOrderHint": "Drag the fields on the Information tab to change the order they appear in." + "fieldsOrderHint": "Drag the fields on the Information tab to change the order they appear in.", + "serviceColumnsHeading": "Service history columns", + "columnAlwaysOn": "{column} is always shown.", + "serviceColumnsOrderHint": "Drag the column headings on the Service history tab to change the order they appear in." }, "provider": { @@ -461,6 +464,7 @@ "colCabinFilter": "Cabin air filter", "colNotes": "Notes", "colFile": "File", + "dragHint": "Drag a column to rearrange this car's service history.", "confirmDelete": "Delete this service record?" }, diff --git a/Web App/web/src/i18n/pl.json b/Web App/web/src/i18n/pl.json index a8b0bd0..0b7dd13 100644 --- a/Web App/web/src/i18n/pl.json +++ b/Web App/web/src/i18n/pl.json @@ -384,7 +384,10 @@ "tabsHeading": "Zakładki", "fieldsHeading": "Pola informacji", "alwaysOn": "Zakładka {tab} jest zawsze dostępna.", - "fieldsOrderHint": "Przeciągnij pola na zakładce Informacje, aby zmienić ich kolejność." + "fieldsOrderHint": "Przeciągnij pola na zakładce Informacje, aby zmienić ich kolejność.", + "serviceColumnsHeading": "Kolumny historii serwisowej", + "columnAlwaysOn": "Kolumna {column} jest zawsze widoczna.", + "serviceColumnsOrderHint": "Przeciągnij nagłówki kolumn na zakładce Historia serwisowa, aby zmienić ich kolejność." }, "provider": { @@ -466,6 +469,7 @@ "colCabinFilter": "Filtr kabinowy", "colNotes": "Notatki", "colFile": "Plik", + "dragHint": "Przeciągnij kolumnę, aby zmienić układ historii serwisowej tego samochodu.", "confirmDelete": "Usunąć ten wpis serwisowy?" }, diff --git a/Web App/web/src/views/CarDetail.vue b/Web App/web/src/views/CarDetail.vue index cac7f08..350d709 100644 --- a/Web App/web/src/views/CarDetail.vue +++ b/Web App/web/src/views/CarDetail.vue @@ -241,14 +241,28 @@ const INFO_FIELD_KEYS = [ "odometer", "serviceInterval", "nextDue", "registrationPlate", "registrationCountry", "vin", "fuelType", "buildDate", "firstRegistration", ]; +// The Service history columns, in their default order. Keys mirror +// hideableServiceColumns/arrangeableServiceColumns in the API's cars.go — the +// server rejects anything else. Date is in the list because it can be moved, +// but not in HIDEABLE_SERVICE_COLUMNS: a service is the day it happened, and a +// table of them with the day taken out stops being a history. +const ALL_SERVICE_COLUMN_KEYS = [ + "date", "km", "nextDate", "nextKm", "oil", "engineFilter", "cabinFilter", + "notes", "file", +]; +const HIDEABLE_SERVICE_COLUMNS = ALL_SERVICE_COLUMN_KEYS.filter((key) => key !== "date"); const tabDraft = ref([]); // tab keys that stay visible const fieldDraft = ref([]); // Information keys that stay visible +const columnDraft = ref([]); // Service history columns that stay visible const viewSaving = ref(false); const viewError = ref(""); function openViewPicker() { tabDraft.value = HIDEABLE_TABS.filter((key) => !hiddenTabs.value.includes(key)); fieldDraft.value = fieldKeys.value.filter((key) => !hiddenFields.value.includes(key)); + columnDraft.value = serviceColumnKeys.value.filter( + (key) => key !== "date" && !hiddenServiceColumns.value.includes(key) + ); viewError.value = ""; showViewPicker.value = true; } @@ -262,6 +276,9 @@ function toggleTabDraft(key, on) { function toggleFieldDraft(key, on) { fieldDraft.value = on ? [...fieldDraft.value, key] : fieldDraft.value.filter((k) => k !== key); } +function toggleColumnDraft(key, on) { + columnDraft.value = on ? [...columnDraft.value, key] : columnDraft.value.filter((k) => k !== key); +} async function saveView() { viewSaving.value = true; @@ -270,6 +287,7 @@ async function saveView() { const updated = await api.updateCarView(props.id, { hiddenTabs: HIDEABLE_TABS.filter((key) => !tabDraft.value.includes(key)), hiddenFields: INFO_FIELD_KEYS.filter((key) => !fieldDraft.value.includes(key)), + hiddenServiceColumns: HIDEABLE_SERVICE_COLUMNS.filter((key) => !columnDraft.value.includes(key)), }); car.value = { ...updated, access: car.value.access }; showViewPicker.value = false; @@ -291,6 +309,17 @@ function tabPickerLabel(key) { function infoFieldLabel(key) { return t(`car.info.${key}`); } +// The column headings were translated as car.services.col* long before they +// became keys, so the two are mapped rather than derived: renaming a dozen +// strings in three languages to save this table would be the wrong trade. +const SERVICE_COLUMN_LABELS = { + date: "colDate", km: "colKm", nextDate: "colNextDate", nextKm: "colNextKm", + oil: "colOil", engineFilter: "colEngineFilter", cabinFilter: "colCabinFilter", + notes: "colNotes", file: "colFile", +}; +function serviceColumnLabel(key) { + return t(`car.services.${SERVICE_COLUMN_LABELS[key]}`); +} // --- The arrangement of the Information rows --- // @@ -405,6 +434,142 @@ const infoFields = computed(() => { })); }); +// --- The Service history columns --- +// +// The same three pieces as the Information rows, on the same reasoning: a +// hidden set so a column added in a later release is on by default, an +// arrangement that covers the hidden columns so one switched back on returns to +// where it was, and both properties of the car, so everyone it is shared with +// sees the same table. +const hiddenServiceColumns = computed(() => car.value?.hiddenServiceColumns || []); +const serviceColumnKeys = ref([...ALL_SERVICE_COLUMN_KEYS]); +watch( + () => car.value?.serviceColumnOrder, + (order) => { + const arranged = []; + for (const key of order || []) { + if (ALL_SERVICE_COLUMN_KEYS.includes(key) && !arranged.includes(key)) arranged.push(key); + } + // A column the stored arrangement doesn't mention follows the arranged ones, + // so it joins at the right-hand end rather than in the middle of somebody's + // table — the rule the tabs, the Information rows and the garage all use. + serviceColumnKeys.value = [ + ...arranged, + ...ALL_SERVICE_COLUMN_KEYS.filter((k) => !arranged.includes(k)), + ]; + }, + { immediate: true } +); + +// The visible columns in their arranged order, as data, so the head and the body +// are driven by one list and cannot drift apart when a column moves or goes +// away. `center` is for the three yes/no columns, whose heading sits over a +// column of two-letter answers. +const CENTERED_SERVICE_COLUMNS = ["oil", "engineFilter", "cabinFilter"]; +const serviceColumns = computed(() => + serviceColumnKeys.value + .filter((key) => !hiddenServiceColumns.value.includes(key)) + .map((key) => ({ + key, + label: serviceColumnLabel(key), + center: CENTERED_SERVICE_COLUMNS.includes(key), + })) +); + +// One cell of that table. Returns the text and the classes it carries beyond the +// shared padding; the file column is the one whose cell is a button, and says so +// rather than returning text the template would have to special-case by key. +function serviceCell(s, key) { + switch (key) { + case "date": + return { text: formatDate(s.date), classes: "whitespace-nowrap data font-medium text-strong" }; + case "km": + return { text: formatKm(s.km), classes: "whitespace-nowrap data text-body" }; + case "nextDate": + return { text: formatDate(s.nextServiceDate), classes: "whitespace-nowrap data text-muted" }; + case "nextKm": + return { text: formatKm(s.nextServiceKm), classes: "whitespace-nowrap data text-muted" }; + case "oil": + return yesNoCell(s.changedOil); + case "engineFilter": + return yesNoCell(s.changedEngineAirFilter); + case "cabinFilter": + return yesNoCell(s.changedCabinAirFilter); + case "notes": + return { text: s.notes || t("common.empty"), classes: "text-body" }; + default: // file + return { file: true, classes: "whitespace-nowrap" }; + } +} +function yesNoCell(on) { + return { + text: yn(on), + classes: `text-center text-xs font-semibold ${on ? "text-success" : "text-muted"}`, + }; +} + +// One row's cells, already in the arranged order. A row at a time rather than a +// call per cell, so a table of twenty services doesn't rebuild every cell three +// times over to read its classes, its text and whether it is the file column. +function serviceRow(s) { + return serviceColumns.value.map((col) => ({ ...col, ...serviceCell(s, col.key) })); +} + +// Dragging a column header, on the same native drag events as the tabs, the +// Information rows and the garage — so pointer-only, as touch browsers don't +// fire these. Needs write access, since the arrangement belongs to the car, and +// there is nothing to rearrange with one column showing. +const canArrangeServiceColumns = computed( + () => canWrite.value && !prefs.dragLocked && serviceColumns.value.length > 1 +); +const dragColumn = ref(""); // column being dragged +const dropColumn = ref(""); // column it is currently hovering over +const columnOrderError = ref(""); +let columnsMoved = false; // the table changed during this drag and isn't saved yet + +function onColumnDragStart(key, e) { + dragColumn.value = key; + columnsMoved = false; + e.dataTransfer.effectAllowed = "move"; + // Firefox only starts a drag once something is on the transfer. + e.dataTransfer.setData("text/plain", key); +} + +// Reorder live as the pointer crosses headers, so the table shows the +// arrangement you are about to get. The splice works on the full list, hidden +// columns included, which keeps a hidden column anchored between the same two +// visible neighbours. +function onColumnDragEnter(key) { + if (!dragColumn.value || key === dragColumn.value || dropColumn.value === key) return; + dropColumn.value = key; + const list = serviceColumnKeys.value; + const from = list.indexOf(dragColumn.value); + const to = list.indexOf(key); + if (from < 0 || to < 0) return; + list.splice(to, 0, ...list.splice(from, 1)); + columnsMoved = true; +} + +// Save whatever the table now shows. Called from both drop and dragend: a header +// released past the end of the row never produces a drop, and leaving that +// arrangement unsaved would quietly undo itself on the next load. +async function commitServiceColumnOrder() { + dragColumn.value = ""; + dropColumn.value = ""; + if (!columnsMoved) return; + columnsMoved = false; + columnOrderError.value = ""; + try { + const updated = await api.updateCarView(props.id, { serviceColumnOrder: serviceColumnKeys.value }); + car.value = { ...updated, access: car.value.access }; + } catch (e) { + // The arrangement didn't stick; say so and put the stored one back rather + // than leaving the page showing an order the server doesn't have. + columnOrderError.value = e.message; + await load(); + } +} + async function load() { loading.value = true; error.value = ""; @@ -901,8 +1066,11 @@ onMounted(load);

{{ t("car.info.dragHint") }}

- +
+

{{ columnOrderError }}

{{ t("car.services.title") }}

+

{{ t("car.services.dragHint") }}

@@ -1579,6 +1756,27 @@ onMounted(load);

{{ t("car.viewPicker.fieldsOrderHint") }}

+

{{ t("car.viewPicker.serviceColumnsHeading") }}

+
+ +
+

+ {{ t("car.viewPicker.columnAlwaysOn", { column: t("car.services.colDate") }) }} + {{ t("car.viewPicker.serviceColumnsOrderHint") }} +

+

{{ viewError }}