Add drone-pilot logbook system (BEK 1649 §5)
Model Denmark's Dronebekendtgørelsen § 5 (on top of EU 2019/947) across all three tiers: schema, API Server, and Web App. Schema (migration 1720300700_add_logbook.js): two collections — `drones` (classification inputs: mtom, is_toy, autologs, c_class, operator no.) and `flights` (§5 minimum content + category/purpose/logging-path + operational maturity + a retention_until computed as operation_date + 5y). Locked API rules; access flows through the service account like users/orgs. API Server (logbook.go, logbook_export.go): /api/drones and /api/flights CRUD with per-role scoping in Go (user→own, admin→org, superadmin→all), plus GET /api/logbook/export (CSV — the "readable electronic format" for Trafikstyrelsen / pending police disclosure). Compliance is computed server-side per flight: exemption (toy / club-area / <250 g hobby), effective logging path, and red flags (autologs-without-FDR, specific-category-without- authorisation, missing §5 fields, past retention). Manual-path saves missing a §5 field are blocked (422). Web App: BFF proxies (export preserves the CSV Content-Type/Disposition), api.js client fns, and a Logbook.vue view (Flights/Drones tabs, inline forms, compliance badges + expandable detail, Export CSV) wired into Dashboard.vue, replacing the placeholder. Includes the rebuilt embedded dist bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
407e34bf0d
commit
e9b27530ec
14 files changed
+1843
-23
No files matched your search
@@ -371,6 +371,100 @@ func (a *App) handleDeleteOrg(w http.ResponseWriter, r *http.Request) {
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
/* ---------- Logbook: drones ---------- */
|
||||
|
||||
// GET /bff/drones → API Server /api/drones
|
||||
func (a *App) handleListDrones(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/drones", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/drones → API Server /api/drones
|
||||
func (a *App) handleCreateDrone(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/drones", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PATCH /bff/drones/{id} → API Server /api/drones/{id}
|
||||
func (a *App) handleUpdateDrone(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPatch, a.apiBaseFor(r)+"/api/drones/"+url.PathEscape(id), bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// DELETE /bff/drones/{id} → API Server /api/drones/{id}
|
||||
func (a *App) handleDeleteDrone(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
req, _ := http.NewRequest(http.MethodDelete, a.apiBaseFor(r)+"/api/drones/"+url.PathEscape(id), nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
/* ---------- Logbook: flights ---------- */
|
||||
|
||||
// GET /bff/flights → API Server /api/flights
|
||||
func (a *App) handleListFlights(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/flights", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// POST /bff/flights → API Server /api/flights
|
||||
func (a *App) handleCreateFlight(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPost, a.apiBaseFor(r)+"/api/flights", bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// PATCH /bff/flights/{id} → API Server /api/flights/{id}
|
||||
func (a *App) handleUpdateFlight(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
req, _ := http.NewRequest(http.MethodPatch, a.apiBaseFor(r)+"/api/flights/"+url.PathEscape(id), bytes.NewReader(body))
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// DELETE /bff/flights/{id} → API Server /api/flights/{id}
|
||||
func (a *App) handleDeleteFlight(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.PathValue("id")
|
||||
req, _ := http.NewRequest(http.MethodDelete, a.apiBaseFor(r)+"/api/flights/"+url.PathEscape(id), nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
a.doRelay(w, req)
|
||||
}
|
||||
|
||||
// GET /bff/logbook/export → API Server /api/logbook/export. Unlike the JSON
|
||||
// endpoints this streams a CSV download, so it preserves the upstream
|
||||
// Content-Type + Content-Disposition instead of forcing application/json.
|
||||
func (a *App) handleExportLogbook(w http.ResponseWriter, r *http.Request) {
|
||||
req, _ := http.NewRequest(http.MethodGet, a.apiBaseFor(r)+"/api/logbook/export", nil)
|
||||
req.Header.Set("Authorization", tokenOf(r))
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": "cannot reach API server"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
if cd := resp.Header.Get("Content-Disposition"); cd != "" {
|
||||
w.Header().Set("Content-Disposition", cd)
|
||||
}
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
// doRelay executes an outbound request and relays the response verbatim.
|
||||
func (a *App) doRelay(w http.ResponseWriter, req *http.Request) {
|
||||
resp, err := client.Do(req)
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const c of l)if(c.type==="childList")for(const h of c.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&a(h)}).observe(document,{childList:!0,subtree:!0});function o(l){const c={};return l.integrity&&(c.integrity=l.integrity),l.referrerPolicy&&(c.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?c.credentials="include":l.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function a(l){if(l.ep)return;l.ep=!0;const c=o(l);fetch(l.href,c)}})();/**
|
||||
* @vue/shared v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/function va(e){const i=Object.create(null);for(const o of e.split(","))i[o]=1;return o=>o in i}const ie={},_s=[],Zn=()=>{},iu=()=>!1,lr=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ur=e=>e.startsWith("onUpdate:"),Ce=Object.assign,ya=(e,i)=>{const o=e.indexOf(i);o>-1&&e.splice(o,1)},Qc=Object.prototype.hasOwnProperty,Xt=(e,i)=>Qc.call(e,i),pt=Array.isArray,vs=e=>po(e)==="[object Map]",Ts=e=>po(e)==="[object Set]",il=e=>po(e)==="[object Date]",Lt=e=>typeof e=="function",pe=e=>typeof e=="string",Ln=e=>typeof e=="symbol",Qt=e=>e!==null&&typeof e=="object",su=e=>(Qt(e)||Lt(e))&&Lt(e.then)&&Lt(e.catch),ou=Object.prototype.toString,po=e=>ou.call(e),td=e=>po(e).slice(8,-1),ru=e=>po(e)==="[object Object]",ba=e=>pe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Xs=va(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),cr=e=>{const i=Object.create(null);return(o=>i[o]||(i[o]=e(o)))},ed=/-\w/g,Pn=cr(e=>e.replace(ed,i=>i.slice(1).toUpperCase())),nd=/\B([A-Z])/g,Li=cr(e=>e.replace(nd,"-$1").toLowerCase()),au=cr(e=>e.charAt(0).toUpperCase()+e.slice(1)),jr=cr(e=>e?`on${au(e)}`:""),Vn=(e,i)=>!Object.is(e,i),Go=(e,...i)=>{for(let o=0;o<e.length;o++)e[o](...i)},lu=(e,i,o,a=!1)=>{Object.defineProperty(e,i,{configurable:!0,enumerable:!1,writable:a,value:o})},dr=e=>{const i=parseFloat(e);return isNaN(i)?e:i},id=e=>{const i=pe(e)?Number(e):NaN;return isNaN(i)?e:i};let sl;const fr=()=>sl||(sl=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function ks(e){if(pt(e)){const i={};for(let o=0;o<e.length;o++){const a=e[o],l=pe(a)?ad(a):ks(a);if(l)for(const c in l)i[c]=l[c]}return i}else if(pe(e)||Qt(e))return e}const sd=/;(?![^(]*\))/g,od=/:([^]+)/,rd=/\/\*[^]*?\*\//g;function ad(e){const i={};return e.replace(rd,"").split(sd).forEach(o=>{if(o){const a=o.split(od);a.length>1&&(i[a[0].trim()]=a[1].trim())}}),i}function Ot(e){let i="";if(pe(e))i=e;else if(pt(e))for(let o=0;o<e.length;o++){const a=Ot(e[o]);a&&(i+=a+" ")}else if(Qt(e))for(const o in e)e[o]&&(i+=o+" ");return i.trim()}const ld="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ud=va(ld);function uu(e){return!!e||e===""}function cd(e,i){if(e.length!==i.length)return!1;let o=!0;for(let a=0;o&&a<e.length;a++)o=Si(e[a],i[a]);return o}function Si(e,i){if(e===i)return!0;let o=il(e),a=il(i);if(o||a)return o&&a?e.getTime()===i.getTime():!1;if(o=Ln(e),a=Ln(i),o||a)return e===i;if(o=pt(e),a=pt(i),o||a)return o&&a?cd(e,i):!1;if(o=Qt(e),a=Qt(i),o||a){if(!o||!a)return!1;const l=Object.keys(e).length,c=Object.keys(i).length;if(l!==c)return!1;for(const h in e){const g=e.hasOwnProperty(h),v=i.hasOwnProperty(h);if(g&&!v||!g&&v||!Si(e[h],i[h]))return!1}}return String(e)===String(i)}function xa(e,i){return e.findIndex(o=>Si(o,i))}const cu=e=>!!(e&&e.__v_isRef===!0),M=e=>pe(e)?e:e==null?"":pt(e)||Qt(e)&&(e.toString===ou||!Lt(e.toString))?cu(e)?M(e.value):JSON.stringify(e,du,2):String(e),du=(e,i)=>cu(i)?du(e,i.value):vs(i)?{[`Map(${i.size})`]:[...i.entries()].reduce((o,[a,l],c)=>(o[Wr(a,c)+" =>"]=l,o),{})}:Ts(i)?{[`Set(${i.size})`]:[...i.values()].map(o=>Wr(o))}:Ln(i)?Wr(i):Qt(i)&&!pt(i)&&!ru(i)?String(i):i,Wr=(e,i="")=>{var o;return Ln(e)?`Symbol(${(o=e.description)!=null?o:i})`:e};/**
|
||||
* @vue/reactivity v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/let ze;class dd{constructor(i=!1){this.detached=i,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!i&&ze&&(ze.active?(this.parent=ze,this.index=(ze.scopes||(ze.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let i,o;if(this.scopes)for(i=0,o=this.scopes.length;i<o;i++)this.scopes[i].pause();for(i=0,o=this.effects.length;i<o;i++)this.effects[i].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let i,o;if(this.scopes)for(i=0,o=this.scopes.length;i<o;i++)this.scopes[i].resume();for(i=0,o=this.effects.length;i<o;i++)this.effects[i].resume()}}run(i){if(this._active){const o=ze;try{return ze=this,i()}finally{ze=o}}}on(){++this._on===1&&(this.prevScope=ze,ze=this)}off(){if(this._on>0&&--this._on===0){if(ze===this)ze=this.prevScope;else{let i=ze;for(;i;){if(i.prevScope===this){i.prevScope=this.prevScope;break}i=i.prevScope}}this.prevScope=void 0}}stop(i){if(this._active){this._active=!1;let o,a;for(o=0,a=this.effects.length;o<a;o++)this.effects[o].stop();for(this.effects.length=0,o=0,a=this.cleanups.length;o<a;o++)this.cleanups[o]();if(this.cleanups.length=0,this.scopes){for(o=0,a=this.scopes.length;o<a;o++)this.scopes[o].stop(!0);this.scopes.length=0}if(!this.detached&&this.parent&&!i){const l=this.parent.scopes.pop();l&&l!==this&&(this.parent.scopes[this.index]=l,l.index=this.index)}this.parent=void 0}}}function fd(){return ze}let re;const Kr=new WeakSet;class fu{constructor(i){this.fn=i,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,ze&&(ze.active?ze.effects.push(this):this.flags&=-2)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,Kr.has(this)&&(Kr.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||pu(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,ol(this),mu(this);const i=re,o=Tn;re=this,Tn=!0;try{return this.fn()}finally{gu(this),re=i,Tn=o,this.flags&=-3}}stop(){if(this.flags&1){for(let i=this.deps;i;i=i.nextDep)Sa(i);this.deps=this.depsTail=void 0,ol(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?Kr.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){ia(this)&&this.run()}get dirty(){return ia(this)}}let hu=0,Qs,to;function pu(e,i=!1){if(e.flags|=8,i){e.next=to,to=e;return}e.next=Qs,Qs=e}function wa(){hu++}function ka(){if(--hu>0)return;if(to){let i=to;for(to=void 0;i;){const o=i.next;i.next=void 0,i.flags&=-9,i=o}}let e;for(;Qs;){let i=Qs;for(Qs=void 0;i;){const o=i.next;if(i.next=void 0,i.flags&=-9,i.flags&1)try{i.trigger()}catch(a){e||(e=a)}i=o}}if(e)throw e}function mu(e){for(let i=e.deps;i;i=i.nextDep)i.version=-1,i.prevActiveLink=i.dep.activeLink,i.dep.activeLink=i}function gu(e){let i,o=e.depsTail,a=o;for(;a;){const l=a.prevDep;a.version===-1?(a===o&&(o=l),Sa(a),hd(a)):i=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=l}e.deps=i,e.depsTail=o}function ia(e){for(let i=e.deps;i;i=i.nextDep)if(i.dep.version!==i.version||i.dep.computed&&(_u(i.dep.computed)||i.dep.version!==i.version))return!0;return!!e._dirty}function _u(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===oo)||(e.globalVersion=oo,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!ia(e))))return;e.flags|=2;const i=e.dep,o=re,a=Tn;re=e,Tn=!0;try{mu(e);const l=e.fn(e._value);(i.version===0||Vn(l,e._value))&&(e.flags|=128,e._value=l,i.version++)}catch(l){throw i.version++,l}finally{re=o,Tn=a,gu(e),e.flags&=-3}}function Sa(e,i=!1){const{dep:o,prevSub:a,nextSub:l}=e;if(a&&(a.nextSub=l,e.prevSub=void 0),l&&(l.prevSub=a,e.nextSub=void 0),o.subs===e&&(o.subs=a,!a&&o.computed)){o.computed.flags&=-5;for(let c=o.computed.deps;c;c=c.nextDep)Sa(c,!0)}!i&&!--o.sc&&o.map&&o.map.delete(o.key)}function hd(e){const{prevDep:i,nextDep:o}=e;i&&(i.nextDep=o,e.prevDep=void 0),o&&(o.prevDep=i,e.nextDep=void 0)}let Tn=!0;const vu=[];function $n(){vu.push(Tn),Tn=!1}function Hn(){const e=vu.pop();Tn=e===void 0?!0:e}function ol(e){const{cleanup:i}=e;if(e.cleanup=void 0,i){const o=re;re=void 0;try{i()}finally{re=o}}}let oo=0;class pd{constructor(i,o){this.sub=i,this.dep=o,this.version=o.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Pa{constructor(i){this.computed=i,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(i){if(!re||!Tn||re===this.computed)return;let o=this.activeLink;if(o===void 0||o.sub!==re)o=this.activeLink=new pd(re,this),re.deps?(o.prevDep=re.depsTail,re.depsTail.nextDep=o,re.depsTail=o):re.deps=re.depsTail=o,yu(o);else if(o.version===-1&&(o.version=this.version,o.nextDep)){const a=o.nextDep;a.prevDep=o.prevDep,o.prevDep&&(o.prevDep.nextDep=a),o.prevDep=re.depsTail,o.nextDep=void 0,re.depsTail.nextDep=o,re.depsTail=o,re.deps===o&&(re.deps=a)}return o}trigger(i){this.versiLine truncated
|
||||
* @vue/runtime-core v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/function mo(e,i,o,a){try{return a?e(...a):e()}catch(l){pr(l,i,o)}}function mn(e,i,o,a){if(Lt(e)){const l=mo(e,i,o,a);return l&&su(l)&&l.catch(c=>{pr(c,i,o)}),l}if(pt(e)){const l=[];for(let c=0;c<e.length;c++)l.push(mn(e[c],i,o,a));return l}}function pr(e,i,o,a=!0){const l=i?i.vnode:null,{errorHandler:c,throwUnhandledErrorInProduction:h}=i&&i.appContext.config||ie;if(i){let g=i.parent;const v=i.proxy,P=`https://vuejs.org/error-reference/#runtime-${o}`;for(;g;){const k=g.ec;if(k){for(let O=0;O<k.length;O++)if(k[O](e,v,P)===!1)return}g=g.parent}if(c){$n(),mo(c,null,10,[e,v,P]),Hn();return}}Fd(e,o,l,a,h)}function Fd(e,i,o,a=!0,l=!1){if(l)throw e;console.error(e)}const je=[];let Dn=-1;const ys=[];let ki=null,gs=0;const Lu=Promise.resolve();let Xo=null;function Mu(e){const i=Xo||Lu;return e?i.then(this?e.bind(this):e):i}function Vd(e){let i=Dn+1,o=je.length;for(;i<o;){const a=i+o>>>1,l=je[a],c=ao(l);c<e||c===e&&l.flags&2?i=a+1:o=a}return i}function Ca(e){if(!(e.flags&1)){const i=ao(e),o=je[je.length-1];!o||!(e.flags&2)&&i>=ao(o)?je.push(e):je.splice(Vd(i),0,e),e.flags|=1,Cu()}}function Cu(){Xo||(Xo=Lu.then(Eu))}function Zd(e){pt(e)?ys.push(...e):ki&&e.id===-1?ki.splice(gs+1,0,e):e.flags&1||(ys.push(e),e.flags|=1),Cu()}function al(e,i,o=Dn+1){for(;o<je.length;o++){const a=je[o];if(a&&a.flags&2){if(e&&a.id!==e.uid)continue;je.splice(o,1),o--,a.flags&4&&(a.flags&=-2),a(),a.flags&4||(a.flags&=-2)}}}function Ou(e){if(ys.length){const i=[...new Set(ys)].sort((o,a)=>ao(o)-ao(a));if(ys.length=0,ki){ki.push(...i);return}for(ki=i,gs=0;gs<ki.length;gs++){const o=ki[gs];o.flags&4&&(o.flags&=-2),o.flags&8||o(),o.flags&=-2}ki=null,gs=0}}const ao=e=>e.id==null?e.flags&2?-1:1/0:e.id;function Eu(e){try{for(Dn=0;Dn<je.length;Dn++){const i=je[Dn];i&&!(i.flags&8)&&(i.flags&4&&(i.flags&=-2),mo(i,i.i,i.i?15:14),i.flags&4||(i.flags&=-2))}}finally{for(;Dn<je.length;Dn++){const i=je[Dn];i&&(i.flags&=-2)}Dn=-1,je.length=0,Ou(),Xo=null,(je.length||ys.length)&&Eu()}}let De=null,zu=null;function Qo(e){const i=De;return De=e,zu=e&&e.type.__scopeId||null,i}function ot(e,i=De,o){if(!i||e._n)return e;const a=(...l)=>{a._d&&nr(-1);const c=Qo(i);let h;try{h=e(...l)}finally{Qo(c),a._d&&nr(1)}return h};return a._n=!0,a._c=!0,a._d=!0,a}function xt(e,i){if(De===null)return e;const o=br(De),a=e.dirs||(e.dirs=[]);for(let l=0;l<i.length;l++){let[c,h,g,v=ie]=i[l];c&&(Lt(c)&&(c={mounted:c,updated:c}),c.deep&&oi(h),a.push({dir:c,instance:o,value:h,oldValue:void 0,arg:g,modifiers:v}))}return e}function Zi(e,i,o,a){const l=e.dirs,c=i&&i.dirs;for(let h=0;h<l.length;h++){const g=l[h];c&&(g.oldValue=c[h].value);let v=g.dir[a];v&&($n(),mn(v,o,8,[e.el,g,e,i]),Hn())}}function Au(e,i){if(We){let o=We.provides;const a=We.parent&&We.parent.provides;a===o&&(o=We.provides=Object.create(a)),o[e]=i}}function eo(e,i,o=!1){const a=dc();if(a||xs){let l=xs?xs._context.provides:a?a.parent==null||a.ce?a.vnode.appContext&&a.vnode.appContext.provides:a.parent.provides:void 0;if(l&&e in l)return l[e];if(arguments.length>1)return o&&Lt(i)?i.call(a&&a.proxy):i}}const $d=Symbol.for("v-scx"),Hd=()=>eo($d);function Je(e,i,o){return Iu(e,i,o)}function Iu(e,i,o=ie){const{immediate:a,deep:l,flush:c,once:h}=o,g=Ce({},o),v=i&&a||!i&&c!=="post";let P;if(fo){if(c==="sync"){const F=Hd();P=F.__watcherHandles||(F.__watcherHandles=[])}else if(!v){const F=()=>{};return F.stop=Zn,F.resume=Zn,F.pause=Zn,F}}const k=We;g.call=(F,nt,q)=>mn(F,k,nt,q);let O=!1;c==="post"?g.scheduler=F=>{Ye(F,k&&k.suspense)}:c!=="sync"&&(O=!0,g.scheduler=(F,nt)=>{nt?F():Ca(F)}),g.augmentJob=F=>{i&&(F.flags|=4),O&&(F.flags|=2,k&&(F.id=k.uid,F.i=k))};const $=Rd(e,i,g);return fo&&(P?P.push($):v&&$()),$}function Ud(e,i,o){const a=this.proxy,l=pe(e)?e.includes(".")?Nu(a,e):()=>a[e]:e.bind(a,a);let c;Lt(i)?c=i:(c=i.handler,o=i);const h=go(this),g=Iu(l,c.bind(a),o);return h(),g}function Nu(e,i){const o=i.split(".");return()=>{let a=e;for(let l=0;l<o.length&&a;l++)a=a[o[l]];return a}}const jd=Symbol("_vte"),Bu=e=>e.__isTeleport,fn=Symbol("_leaveCb"),Ws=Symbol("_enterCb");function Wd(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Ls(()=>{e.isMounted=!0}),_r(()=>{e.isUnmounting=!0}),e}const cn=[Function,Array],Du={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:cn,onEnter:cn,onAfterEnter:cn,onEnterCancelled:cn,onBeforeLeave:cn,onLeave:cn,onAfterLeave:cn,onLeaveCancelled:cn,onBeforeAppear:cn,onAppear:cn,onAfterAppear:cn,onAppearCancelled:cn},Ru=e=>{const i=e.subTree;return i.component?Ru(i.component):i},Kd={name:"BaseTransition",props:Du,setup(e,{slots:i}){const o=dc(),a=Wd();return()=>{const l=i.default&&Zu(i.default(),!0),c=l&&l.length?Fu(l):o.subTree?V():void 0;if(!c)return;const h=Gt(e),{mode:g}=h;if(a.isLeaving)return Yr(c);const v=ll(c);if(!v)return Yr(c);let P=la(v,h,a,o,O=>P=O);v.type!==Be&&lo(v,P);let k=o.subTree&&ll(o.subTree);if(k&&k.type!==Be&&!Wi(k,v)&&Ru(o).type!==Be){let O=la(k,h,a,o);if(lo(k,O),g==="out-in"&&v.type!==Be)return a.isLeaving=!0,O.afterLeave=()=>{a.isLea
Line truncated
|
||||
* @vue/runtime-dom v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/let pa;const xl=typeof window<"u"&&window.trustedTypes;if(xl)try{pa=xl.createPolicy("vue",{createHTML:e=>e})}catch{}const pc=pa?e=>pa.createHTML(e):e=>e,qf="http://www.w3.org/2000/svg",Gf="http://www.w3.org/1998/Math/MathML",ni=typeof document<"u"?document:null,wl=ni&&ni.createElement("template"),Yf={insert:(e,i,o)=>{i.insertBefore(e,o||null)},remove:e=>{const i=e.parentNode;i&&i.removeChild(e)},createElement:(e,i,o,a)=>{const l=i==="svg"?ni.createElementNS(qf,e):i==="mathml"?ni.createElementNS(Gf,e):o?ni.createElement(e,{is:o}):ni.createElement(e);return e==="select"&&a&&a.multiple!=null&&l.setAttribute("multiple",a.multiple),l},createText:e=>ni.createTextNode(e),createComment:e=>ni.createComment(e),setText:(e,i)=>{e.nodeValue=i},setElementText:(e,i)=>{e.textContent=i},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ni.querySelector(e),setScopeId(e,i){e.setAttribute(i,"")},insertStaticContent(e,i,o,a,l,c){const h=o?o.previousSibling:i.lastChild;if(l&&(l===c||l.nextSibling))for(;i.insertBefore(l.cloneNode(!0),o),!(l===c||!(l=l.nextSibling)););else{wl.innerHTML=pc(a==="svg"?`<svg>${e}</svg>`:a==="mathml"?`<math>${e}</math>`:e);const g=wl.content;if(a==="svg"||a==="mathml"){const v=g.firstChild;for(;v.firstChild;)g.appendChild(v.firstChild);g.removeChild(v)}i.insertBefore(g,o)}return[h?h.nextSibling:i.firstChild,o?o.previousSibling:i.lastChild]}},xi="transition",Ks="animation",ho=Symbol("_vtc"),mc={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Jf=Ce({},Du,mc),Xf=e=>(e.displayName="Transition",e.props=Jf,e),Qf=Xf((e,{slots:i})=>Wf(qd,th(e),i)),Hi=(e,i=[])=>{pt(e)?e.forEach(o=>o(...i)):e&&e(...i)},kl=e=>e?pt(e)?e.some(i=>i.length>1):e.length>1:!1;function th(e){const i={};for(const G in e)G in mc||(i[G]=e[G]);if(e.css===!1)return i;const{name:o="v",type:a,duration:l,enterFromClass:c=`${o}-enter-from`,enterActiveClass:h=`${o}-enter-active`,enterToClass:g=`${o}-enter-to`,appearFromClass:v=c,appearActiveClass:P=h,appearToClass:k=g,leaveFromClass:O=`${o}-leave-from`,leaveActiveClass:$=`${o}-leave-active`,leaveToClass:F=`${o}-leave-to`}=e,nt=eh(l),q=nt&&nt[0],At=nt&&nt[1],{onBeforeEnter:Dt,onEnter:bt,onEnterCancelled:ut,onLeave:X,onLeaveCancelled:ft,onBeforeAppear:jt=Dt,onAppear:de=bt,onAppearCancelled:me=ut}=i,vt=(G,st,It,Wt)=>{G._enterCancelled=Wt,Ui(G,st?k:g),Ui(G,st?P:h),It&&It()},Ft=(G,st)=>{G._isLeaving=!1,Ui(G,O),Ui(G,F),Ui(G,$),st&&st()},Tt=G=>(st,It)=>{const Wt=G?de:bt,kt=()=>vt(st,G,It);Hi(Wt,[st,kt]),Sl(()=>{Ui(st,G?v:c),ei(st,G?k:g),kl(Wt)||Pl(st,a,q,kt)})};return Ce(i,{onBeforeEnter(G){Hi(Dt,[G]),ei(G,c),ei(G,h)},onBeforeAppear(G){Hi(jt,[G]),ei(G,v),ei(G,P)},onEnter:Tt(!1),onAppear:Tt(!0),onLeave(G,st){G._isLeaving=!0;const It=()=>Ft(G,st);ei(G,O),G._enterCancelled?(ei(G,$),Ml(G)):(Ml(G),ei(G,$)),Sl(()=>{G._isLeaving&&(Ui(G,O),ei(G,F),kl(X)||Pl(G,a,At,It))}),Hi(X,[G,It])},onEnterCancelled(G){vt(G,!1,void 0,!0),Hi(ut,[G])},onAppearCancelled(G){vt(G,!0,void 0,!0),Hi(me,[G])},onLeaveCancelled(G){Ft(G),Hi(ft,[G])}})}function eh(e){if(e==null)return null;if(Qt(e))return[ta(e.enter),ta(e.leave)];{const i=ta(e);return[i,i]}}function ta(e){return id(e)}function ei(e,i){i.split(/\s+/).forEach(o=>o&&e.classList.add(o)),(e[ho]||(e[ho]=new Set)).add(i)}function Ui(e,i){i.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const o=e[ho];o&&(o.delete(i),o.size||(e[ho]=void 0))}function Sl(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let nh=0;function Pl(e,i,o,a){const l=e._endId=++nh,c=()=>{l===e._endId&&a()};if(o!=null)return setTimeout(c,o);const{type:h,timeout:g,propCount:v}=ih(e,i);if(!h)return a();const P=h+"end";let k=0;const O=()=>{e.removeEventListener(P,$),c()},$=F=>{F.target===e&&++k>=v&&O()};setTimeout(()=>{k<v&&O()},g+1),e.addEventListener(P,$)}function ih(e,i){const o=window.getComputedStyle(e),a=nt=>(o[nt]||"").split(", "),l=a(`${xi}Delay`),c=a(`${xi}Duration`),h=Tl(l,c),g=a(`${Ks}Delay`),v=a(`${Ks}Duration`),P=Tl(g,v);let k=null,O=0,$=0;i===xi?h>0&&(k=xi,O=h,$=c.length):i===Ks?P>0&&(k=Ks,O=P,$=v.length):(O=Math.max(h,P),k=O>0?h>P?xi:Ks:null,$=k?k===xi?c.length:v.length:0);const F=k===xi&&/\b(?:transform|all)(?:,|$)/.test(a(`${xi}Property`).toString());return{type:k,timeout:O,propCount:$,hasTransform:F}}function Tl(e,i){for(;e.length<i.length;)e=e.concat(e);return Math.max(...i.map((o,a)=>Ll(o)+Ll(e[a])))}function Ll(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Ml(e){return(e?e.ownerDocument:document).body.offsetHeight}function sh(e,i,o){const a=e[ho];a&&(i=(i?[i,...a]:[...a]).join(" ")),i==null?e.removeAttribute("class"):o?e.setAttribute("class",i):e.className=i}const or=Symbol("_vod"),gc=Symbol("_vsh"),oh={name:"show",beforeMount(e,{value:i},{transition:o}){e[or]=e.style.display==="none"?"":e.sty
Line truncated
|
||||
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
|
||||
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
|
||||
*/var Lp=Js.exports,Jl;function Mp(){return Jl||(Jl=1,(function(e,i){(function(o,a){a(i)})(Lp,(function(o){var a="1.9.4";function l(t){var n,s,r,u;for(s=1,r=arguments.length;s<r;s++){u=arguments[s];for(n in u)t[n]=u[n]}return t}var c=Object.create||(function(){function t(){}return function(n){return t.prototype=n,new t}})();function h(t,n){var s=Array.prototype.slice;if(t.bind)return t.bind.apply(t,s.call(arguments,1));var r=s.call(arguments,2);return function(){return t.apply(n,r.length?r.concat(s.call(arguments)):arguments)}}var g=0;function v(t){return"_leaflet_id"in t||(t._leaflet_id=++g),t._leaflet_id}function P(t,n,s){var r,u,p,S;return S=function(){r=!1,u&&(p.apply(s,u),u=!1)},p=function(){r?u=arguments:(t.apply(s,arguments),setTimeout(S,n),r=!0)},p}function k(t,n,s){var r=n[1],u=n[0],p=r-u;return t===r&&s?t:((t-u)%p+p)%p+u}function O(){return!1}function $(t,n){if(n===!1)return t;var s=Math.pow(10,n===void 0?6:n);return Math.round(t*s)/s}function F(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function nt(t){return F(t).split(/\s+/)}function q(t,n){Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?c(t.options):{});for(var s in n)t.options[s]=n[s];return t.options}function At(t,n,s){var r=[];for(var u in t)r.push(encodeURIComponent(s?u.toUpperCase():u)+"="+encodeURIComponent(t[u]));return(!n||n.indexOf("?")===-1?"?":"&")+r.join("&")}var Dt=/\{ *([\w_ -]+) *\}/g;function bt(t,n){return t.replace(Dt,function(s,r){var u=n[r];if(u===void 0)throw new Error("No value provided for variable "+s);return typeof u=="function"&&(u=u(n)),u})}var ut=Array.isArray||function(t){return Object.prototype.toString.call(t)==="[object Array]"};function X(t,n){for(var s=0;s<t.length;s++)if(t[s]===n)return s;return-1}var ft="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function jt(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var de=0;function me(t){var n=+new Date,s=Math.max(0,16-(n-de));return de=n+s,window.setTimeout(t,s)}var vt=window.requestAnimationFrame||jt("RequestAnimationFrame")||me,Ft=window.cancelAnimationFrame||jt("CancelAnimationFrame")||jt("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function Tt(t,n,s){if(s&&vt===me)t.call(n);else return vt.call(window,h(t,n))}function G(t){t&&Ft.call(window,t)}var st={__proto__:null,extend:l,create:c,bind:h,get lastId(){return g},stamp:v,throttle:P,wrapNum:k,falseFn:O,formatNum:$,trim:F,splitWords:nt,setOptions:q,getParamString:At,template:bt,isArray:ut,indexOf:X,emptyImageUrl:ft,requestFn:vt,cancelFn:Ft,requestAnimFrame:Tt,cancelAnimFrame:G};function It(){}It.extend=function(t){var n=function(){q(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()},s=n.__super__=this.prototype,r=c(s);r.constructor=n,n.prototype=r;for(var u in this)Object.prototype.hasOwnProperty.call(this,u)&&u!=="prototype"&&u!=="__super__"&&(n[u]=this[u]);return t.statics&&l(n,t.statics),t.includes&&(Wt(t.includes),l.apply(null,[r].concat(t.includes))),l(r,t),delete r.statics,delete r.includes,r.options&&(r.options=s.options?c(s.options):{},l(r.options,t.options)),r._initHooks=[],r.callInitHooks=function(){if(!this._initHooksCalled){s.callInitHooks&&s.callInitHooks.call(this),this._initHooksCalled=!0;for(var p=0,S=r._initHooks.length;p<S;p++)r._initHooks[p].call(this)}},n},It.include=function(t){var n=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=n,this.mergeOptions(t.options)),this},It.mergeOptions=function(t){return l(this.prototype.options,t),this},It.addInitHook=function(t){var n=Array.prototype.slice.call(arguments,1),s=typeof t=="function"?t:function(){this[t].apply(this,n)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(s),this};function Wt(t){if(!(typeof L>"u"||!L||!L.Mixin)){t=ut(t)?t:[t];for(var n=0;n<t.length;n++)t[n]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",new Error().stack)}}var kt={on:function(t,n,s){if(typeof t=="object")for(var r in t)this._on(r,t[r],n);else{t=nt(t);for(var u=0,p=t.length;u<p;u++)this._on(t[u],n,s)}return this},off:function(t,n,s){if(!arguments.length)delete this._events;else if(typeof t=="object")for(var r in t)this._off(r,t[r],n);else{t=nt(t);for(var u=arguments.length===1,p=0,S=t.length;p<S;p++)u?this._off(t[p]):this._off(t[p],n,s)}return this},_on:function(t,n,s,r){if(typeof n!="function"){console.warn("wrong listener type: "+typeof n);return}if(this._listens(t,n,s)===!1){s===this&&(s=void 0);var u={fn:n,ctx:s};r&&(u.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(u)}},_off:function(t,n,s){var r,u,p;if(this._events&&(r=this._events[t],!!r)){if(arguments.length===1){if(this._firingCount)for(u=0,p=r.length;u<p;u++)r[u].fn=O;delete this._events[t];return}if(typeof n!="function"){console.warn("wrong listener typLine truncated
|
||||
@@ -0,0 +1 @@
|
||||
:root,[data-theme=light]{--navy-950: #0B1730;--navy-900: #0F1E3D;--navy-800: #1B2E52;--navy-700: #26406E;--blue-50: #EAF1FE;--blue-100: #D6E3FD;--blue-200: #B4CDFA;--blue-300: #8FB4F6;--blue-400: #5B93F5;--blue-500: #3D7BF0;--blue-600: #2B62CC;--blue-700: #1F4CA0;--slate-0: #FFFFFF;--slate-50: #F6F7F9;--slate-100: #EEF0F3;--slate-150: #E6E9EE;--slate-200: #DCE0E7;--slate-300: #C5CCD7;--slate-400: #97A1B0;--slate-500: #6B7688;--slate-600: #4C5566;--slate-700: #333B4A;--slate-800: #1E2635;--slate-900: #131A28;--slate-950: #0B111C;--steel: #5A6B85;--green-500: #1F8A5B;--green-100: #DCF1E7;--green-600:#177049;--amber-500: #D9852B;--amber-100: #FBEBD5;--amber-600:#B86C1B;--red-500: #D64545;--red-100: #FBE0E0;--red-600: #B83232;--bg-app: var(--slate-100);--bg-subtle: var(--slate-50);--surface: var(--slate-0);--surface-2: var(--slate-50);--surface-inset: var(--slate-100);--border: var(--slate-200);--border-strong: var(--slate-300);--border-subtle: var(--slate-150);--text-primary: var(--navy-900);--text-secondary:var(--steel);--text-tertiary: var(--slate-400);--text-inverse: var(--slate-0);--text-on-accent:#FFFFFF;--accent: var(--blue-500);--accent-hover: var(--blue-600);--accent-active: var(--blue-700);--accent-soft: var(--blue-50);--accent-soft-fg:var(--blue-700);--focus-ring: color-mix(in srgb, var(--blue-500) 45%, transparent);--success: var(--green-500);--success-soft: var(--green-100);--success-fg: var(--green-600);--warning: var(--amber-500);--warning-soft: var(--amber-100);--warning-fg: var(--amber-600);--danger: var(--red-500);--danger-soft: var(--red-100);--danger-fg: var(--red-600);--overlay: color-mix(in srgb, var(--navy-950) 55%, transparent);--font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono: "Space Mono", ui-monospace, "SF Mono", "JetBrains Mono", monospace;--radius-xs: 4px;--radius-sm: 6px;--radius-md: 10px;--radius-lg: 14px;--radius-xl: 20px;--radius-pill: 999px;--shadow-xs: 0 1px 2px rgba(15,30,61,.06);--shadow-sm: 0 1px 2px rgba(15,30,61,.06), 0 1px 3px rgba(15,30,61,.04);--shadow-md: 0 2px 4px rgba(15,30,61,.06), 0 6px 16px rgba(15,30,61,.08);--shadow-lg: 0 8px 24px rgba(15,30,61,.1), 0 2px 6px rgba(15,30,61,.06);--ease-standard: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--dur-fast: .12s;--dur-base: .2s;color-scheme:light}[data-theme=dark]{--bg-app: var(--navy-950);--bg-subtle: var(--slate-950);--surface: #10203F;--surface-2: #142748;--surface-inset: var(--navy-950);--border: color-mix(in srgb, #FFFFFF 10%, transparent);--border-strong: color-mix(in srgb, #FFFFFF 18%, transparent);--border-subtle: color-mix(in srgb, #FFFFFF 6%, transparent);--text-primary: #F4F7FC;--text-secondary:#8FA0BE;--text-tertiary: #5E6E8C;--text-inverse: var(--navy-900);--text-on-accent:#FFFFFF;--accent: var(--blue-400);--accent-hover: var(--blue-300);--accent-active: var(--blue-200);--accent-soft: color-mix(in srgb, var(--blue-500) 18%, transparent);--accent-soft-fg:var(--blue-300);--focus-ring: color-mix(in srgb, var(--blue-400) 55%, transparent);--success:var(--green-500);--success-soft: color-mix(in srgb, var(--green-500) 22%, transparent);--success-fg:#5FD3A0;--warning:var(--amber-500);--warning-soft: color-mix(in srgb, var(--amber-500) 22%, transparent);--warning-fg:#F0B26A;--danger: var(--red-500);--danger-soft: color-mix(in srgb, var(--red-500) 22%, transparent);--danger-fg: #F08A8A;--overlay: color-mix(in srgb, #000000 62%, transparent);--shadow-xs: 0 1px 2px rgba(0,0,0,.35);--shadow-sm: 0 1px 3px rgba(0,0,0,.4);--shadow-md: 0 4px 12px rgba(0,0,0,.45);--shadow-lg: 0 12px 32px rgba(0,0,0,.5);color-scheme:dark}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-Line truncated
|
||||
@@ -1 +0,0 @@
|
||||
:root,[data-theme=light]{--navy-950: #0B1730;--navy-900: #0F1E3D;--navy-800: #1B2E52;--navy-700: #26406E;--blue-50: #EAF1FE;--blue-100: #D6E3FD;--blue-200: #B4CDFA;--blue-300: #8FB4F6;--blue-400: #5B93F5;--blue-500: #3D7BF0;--blue-600: #2B62CC;--blue-700: #1F4CA0;--slate-0: #FFFFFF;--slate-50: #F6F7F9;--slate-100: #EEF0F3;--slate-150: #E6E9EE;--slate-200: #DCE0E7;--slate-300: #C5CCD7;--slate-400: #97A1B0;--slate-500: #6B7688;--slate-600: #4C5566;--slate-700: #333B4A;--slate-800: #1E2635;--slate-900: #131A28;--slate-950: #0B111C;--steel: #5A6B85;--green-500: #1F8A5B;--green-100: #DCF1E7;--green-600:#177049;--amber-500: #D9852B;--amber-100: #FBEBD5;--amber-600:#B86C1B;--red-500: #D64545;--red-100: #FBE0E0;--red-600: #B83232;--bg-app: var(--slate-100);--bg-subtle: var(--slate-50);--surface: var(--slate-0);--surface-2: var(--slate-50);--surface-inset: var(--slate-100);--border: var(--slate-200);--border-strong: var(--slate-300);--border-subtle: var(--slate-150);--text-primary: var(--navy-900);--text-secondary:var(--steel);--text-tertiary: var(--slate-400);--text-inverse: var(--slate-0);--text-on-accent:#FFFFFF;--accent: var(--blue-500);--accent-hover: var(--blue-600);--accent-active: var(--blue-700);--accent-soft: var(--blue-50);--accent-soft-fg:var(--blue-700);--focus-ring: color-mix(in srgb, var(--blue-500) 45%, transparent);--success: var(--green-500);--success-soft: var(--green-100);--success-fg: var(--green-600);--warning: var(--amber-500);--warning-soft: var(--amber-100);--warning-fg: var(--amber-600);--danger: var(--red-500);--danger-soft: var(--red-100);--danger-fg: var(--red-600);--overlay: color-mix(in srgb, var(--navy-950) 55%, transparent);--font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono: "Space Mono", ui-monospace, "SF Mono", "JetBrains Mono", monospace;--radius-xs: 4px;--radius-sm: 6px;--radius-md: 10px;--radius-lg: 14px;--radius-xl: 20px;--radius-pill: 999px;--shadow-xs: 0 1px 2px rgba(15,30,61,.06);--shadow-sm: 0 1px 2px rgba(15,30,61,.06), 0 1px 3px rgba(15,30,61,.04);--shadow-md: 0 2px 4px rgba(15,30,61,.06), 0 6px 16px rgba(15,30,61,.08);--shadow-lg: 0 8px 24px rgba(15,30,61,.1), 0 2px 6px rgba(15,30,61,.06);--ease-standard: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--dur-fast: .12s;--dur-base: .2s;color-scheme:light}[data-theme=dark]{--bg-app: var(--navy-950);--bg-subtle: var(--slate-950);--surface: #10203F;--surface-2: #142748;--surface-inset: var(--navy-950);--border: color-mix(in srgb, #FFFFFF 10%, transparent);--border-strong: color-mix(in srgb, #FFFFFF 18%, transparent);--border-subtle: color-mix(in srgb, #FFFFFF 6%, transparent);--text-primary: #F4F7FC;--text-secondary:#8FA0BE;--text-tertiary: #5E6E8C;--text-inverse: var(--navy-900);--text-on-accent:#FFFFFF;--accent: var(--blue-400);--accent-hover: var(--blue-300);--accent-active: var(--blue-200);--accent-soft: color-mix(in srgb, var(--blue-500) 18%, transparent);--accent-soft-fg:var(--blue-300);--focus-ring: color-mix(in srgb, var(--blue-400) 55%, transparent);--success:var(--green-500);--success-soft: color-mix(in srgb, var(--green-500) 22%, transparent);--success-fg:#5FD3A0;--warning:var(--amber-500);--warning-soft: color-mix(in srgb, var(--amber-500) 22%, transparent);--warning-fg:#F0B26A;--danger: var(--red-500);--danger-soft: color-mix(in srgb, var(--red-500) 22%, transparent);--danger-fg: #F08A8A;--overlay: color-mix(in srgb, #000000 62%, transparent);--shadow-xs: 0 1px 2px rgba(0,0,0,.35);--shadow-sm: 0 1px 3px rgba(0,0,0,.4);--shadow-md: 0 4px 12px rgba(0,0,0,.45);--shadow-lg: 0 12px 32px rgba(0,0,0,.5);color-scheme:dark}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-Line truncated
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))a(l);new MutationObserver(l=>{for(const d of l)if(d.type==="childList")for(const h of d.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&a(h)}).observe(document,{childList:!0,subtree:!0});function o(l){const d={};return l.integrity&&(d.integrity=l.integrity),l.referrerPolicy&&(d.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?d.credentials="include":l.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function a(l){if(l.ep)return;l.ep=!0;const d=o(l);fetch(l.href,d)}})();/**
|
||||
* @vue/shared v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/function ya(e){const i=Object.create(null);for(const o of e.split(","))i[o]=1;return o=>o in i}const ue={},vs=[],Zn=()=>{},su=()=>!1,ur=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),cr=e=>e.startsWith("onUpdate:"),Oe=Object.assign,ba=(e,i)=>{const o=e.indexOf(i);o>-1&&e.splice(o,1)},Qc=Object.prototype.hasOwnProperty,ee=(e,i)=>Qc.call(e,i),wt=Array.isArray,ys=e=>po(e)==="[object Map]",Ls=e=>po(e)==="[object Set]",sl=e=>po(e)==="[object Date]",Dt=e=>typeof e=="function",be=e=>typeof e=="string",Cn=e=>typeof e=="symbol",ne=e=>e!==null&&typeof e=="object",ou=e=>(ne(e)||Dt(e))&&Dt(e.then)&&Dt(e.catch),ru=Object.prototype.toString,po=e=>ru.call(e),td=e=>po(e).slice(8,-1),au=e=>po(e)==="[object Object]",xa=e=>be(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,Xs=ya(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted"),dr=e=>{const i=Object.create(null);return(o=>i[o]||(i[o]=e(o)))},ed=/-\w/g,Tn=dr(e=>e.replace(ed,i=>i.slice(1).toUpperCase())),nd=/\B([A-Z])/g,Li=dr(e=>e.replace(nd,"-$1").toLowerCase()),lu=dr(e=>e.charAt(0).toUpperCase()+e.slice(1)),Wr=dr(e=>e?`on${lu(e)}`:""),Vn=(e,i)=>!Object.is(e,i),qo=(e,...i)=>{for(let o=0;o<e.length;o++)e[o](...i)},uu=(e,i,o,a=!1)=>{Object.defineProperty(e,i,{configurable:!0,enumerable:!1,writable:a,value:o})},fr=e=>{const i=parseFloat(e);return isNaN(i)?e:i},id=e=>{const i=be(e)?Number(e):NaN;return isNaN(i)?e:i};let ol;const hr=()=>ol||(ol=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Ss(e){if(wt(e)){const i={};for(let o=0;o<e.length;o++){const a=e[o],l=be(a)?ad(a):Ss(a);if(l)for(const d in l)i[d]=l[d]}return i}else if(be(e)||ne(e))return e}const sd=/;(?![^(]*\))/g,od=/:([^]+)/,rd=/\/\*[^]*?\*\//g;function ad(e){const i={};return e.replace(rd,"").split(sd).forEach(o=>{if(o){const a=o.split(od);a.length>1&&(i[a[0].trim()]=a[1].trim())}}),i}function Ct(e){let i="";if(be(e))i=e;else if(wt(e))for(let o=0;o<e.length;o++){const a=Ct(e[o]);a&&(i+=a+" ")}else if(ne(e))for(const o in e)e[o]&&(i+=o+" ");return i.trim()}const ld="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ud=ya(ld);function cu(e){return!!e||e===""}function cd(e,i){if(e.length!==i.length)return!1;let o=!0;for(let a=0;o&&a<e.length;a++)o=Si(e[a],i[a]);return o}function Si(e,i){if(e===i)return!0;let o=sl(e),a=sl(i);if(o||a)return o&&a?e.getTime()===i.getTime():!1;if(o=Cn(e),a=Cn(i),o||a)return e===i;if(o=wt(e),a=wt(i),o||a)return o&&a?cd(e,i):!1;if(o=ne(e),a=ne(i),o||a){if(!o||!a)return!1;const l=Object.keys(e).length,d=Object.keys(i).length;if(l!==d)return!1;for(const h in e){const _=e.hasOwnProperty(h),y=i.hasOwnProperty(h);if(_&&!y||!_&&y||!Si(e[h],i[h]))return!1}}return String(e)===String(i)}function wa(e,i){return e.findIndex(o=>Si(o,i))}const du=e=>!!(e&&e.__v_isRef===!0),P=e=>be(e)?e:e==null?"":wt(e)||ne(e)&&(e.toString===ru||!Dt(e.toString))?du(e)?P(e.value):JSON.stringify(e,fu,2):String(e),fu=(e,i)=>du(i)?fu(e,i.value):ys(i)?{[`Map(${i.size})`]:[...i.entries()].reduce((o,[a,l],d)=>(o[Kr(a,d)+" =>"]=l,o),{})}:Ls(i)?{[`Set(${i.size})`]:[...i.values()].map(o=>Kr(o))}:Cn(i)?Kr(i):ne(i)&&!wt(i)&&!au(i)?String(i):i,Kr=(e,i="")=>{var o;return Cn(e)?`Symbol(${(o=e.description)!=null?o:i})`:e};/**
|
||||
* @vue/reactivity v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/let Ae;class dd{constructor(i=!1){this.detached=i,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!i&&Ae&&(Ae.active?(this.parent=Ae,this.index=(Ae.scopes||(Ae.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let i,o;if(this.scopes)for(i=0,o=this.scopes.length;i<o;i++)this.scopes[i].pause();for(i=0,o=this.effects.length;i<o;i++)this.effects[i].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!1;let i,o;if(this.scopes)for(i=0,o=this.scopes.length;i<o;i++)this.scopes[i].resume();for(i=0,o=this.effects.length;i<o;i++)this.effects[i].resume()}}run(i){if(this._active){const o=Ae;try{return Ae=this,i()}finally{Ae=o}}}on(){++this._on===1&&(this.prevScope=Ae,Ae=this)}off(){if(this._on>0&&--this._on===0){if(Ae===this)Ae=this.prevScope;else{let i=Ae;for(;i;){if(i.prevScope===this){i.prevScope=this.prevScope;break}i=i.prevScope}}this.prevScope=void 0}}stop(i){if(this._active){this._active=!1;let o,a;for(o=0,a=this.effects.length;o<a;o++)this.effects[o].stop();for(this.effects.length=0,o=0,a=this.cleanups.length;o<a;o++)this.cleanups[o]();if(this.cleanups.length=0,this.scopes){for(o=0,a=this.scopes.length;o<a;o++)this.scopes[o].stop(!0);this.scopes.length=0}if(!this.detached&&this.parent&&!i){const l=this.parent.scopes.pop();l&&l!==this&&(this.parent.scopes[this.index]=l,l.index=this.index)}this.parent=void 0}}}function fd(){return Ae}let fe;const Gr=new WeakSet;class hu{constructor(i){this.fn=i,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,Ae&&(Ae.active?Ae.effects.push(this):this.flags&=-2)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,Gr.has(this)&&(Gr.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||mu(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,rl(this),gu(this);const i=fe,o=Ln;fe=this,Ln=!0;try{return this.fn()}finally{_u(this),fe=i,Ln=o,this.flags&=-3}}stop(){if(this.flags&1){for(let i=this.deps;i;i=i.nextDep)Pa(i);this.deps=this.depsTail=void 0,rl(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?Gr.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){sa(this)&&this.run()}get dirty(){return sa(this)}}let pu=0,Qs,to;function mu(e,i=!1){if(e.flags|=8,i){e.next=to,to=e;return}e.next=Qs,Qs=e}function ka(){pu++}function Sa(){if(--pu>0)return;if(to){let i=to;for(to=void 0;i;){const o=i.next;i.next=void 0,i.flags&=-9,i=o}}let e;for(;Qs;){let i=Qs;for(Qs=void 0;i;){const o=i.next;if(i.next=void 0,i.flags&=-9,i.flags&1)try{i.trigger()}catch(a){e||(e=a)}i=o}}if(e)throw e}function gu(e){for(let i=e.deps;i;i=i.nextDep)i.version=-1,i.prevActiveLink=i.dep.activeLink,i.dep.activeLink=i}function _u(e){let i,o=e.depsTail,a=o;for(;a;){const l=a.prevDep;a.version===-1?(a===o&&(o=l),Pa(a),hd(a)):i=a,a.dep.activeLink=a.prevActiveLink,a.prevActiveLink=void 0,a=l}e.deps=i,e.depsTail=o}function sa(e){for(let i=e.deps;i;i=i.nextDep)if(i.dep.version!==i.version||i.dep.computed&&(vu(i.dep.computed)||i.dep.version!==i.version))return!0;return!!e._dirty}function vu(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===oo)||(e.globalVersion=oo,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!sa(e))))return;e.flags|=2;const i=e.dep,o=fe,a=Ln;fe=e,Ln=!0;try{gu(e);const l=e.fn(e._value);(i.version===0||Vn(l,e._value))&&(e.flags|=128,e._value=l,i.version++)}catch(l){throw i.version++,l}finally{fe=o,Ln=a,_u(e),e.flags&=-3}}function Pa(e,i=!1){const{dep:o,prevSub:a,nextSub:l}=e;if(a&&(a.nextSub=l,e.prevSub=void 0),l&&(l.prevSub=a,e.nextSub=void 0),o.subs===e&&(o.subs=a,!a&&o.computed)){o.computed.flags&=-5;for(let d=o.computed.deps;d;d=d.nextDep)Pa(d,!0)}!i&&!--o.sc&&o.map&&o.map.delete(o.key)}function hd(e){const{prevDep:i,nextDep:o}=e;i&&(i.nextDep=o,e.prevDep=void 0),o&&(o.prevDep=i,e.nextDep=void 0)}let Ln=!0;const yu=[];function Un(){yu.push(Ln),Ln=!1}function Hn(){const e=yu.pop();Ln=e===void 0?!0:e}function rl(e){const{cleanup:i}=e;if(e.cleanup=void 0,i){const o=fe;fe=void 0;try{i()}finally{fe=o}}}let oo=0;class pd{constructor(i,o){this.sub=i,this.dep=o,this.version=o.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}}class Ta{constructor(i){this.computed=i,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0}track(i){if(!fe||!Ln||fe===this.computed)return;let o=this.activeLink;if(o===void 0||o.sub!==fe)o=this.activeLink=new pd(fe,this),fe.deps?(o.prevDep=fe.depsTail,fe.depsTail.nextDep=o,fe.depsTail=o):fe.deps=fe.depsTail=o,bu(o);else if(o.version===-1&&(o.version=this.version,o.nextDep)){const a=o.nextDep;a.prevDep=o.prevDep,o.prevDep&&(o.prevDep.nextDep=a),o.prevDep=fe.depsTail,o.nextDep=void 0,fe.depsTail.nextDep=o,fe.depsTail=o,fe.deps===o&&(fe.deps=a)}return o}trigger(i){this.versiLine truncated
|
||||
* @vue/runtime-core v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/function mo(e,i,o,a){try{return a?e(...a):e()}catch(l){mr(l,i,o)}}function _n(e,i,o,a){if(Dt(e)){const l=mo(e,i,o,a);return l&&ou(l)&&l.catch(d=>{mr(d,i,o)}),l}if(wt(e)){const l=[];for(let d=0;d<e.length;d++)l.push(_n(e[d],i,o,a));return l}}function mr(e,i,o,a=!0){const l=i?i.vnode:null,{errorHandler:d,throwUnhandledErrorInProduction:h}=i&&i.appContext.config||ue;if(i){let _=i.parent;const y=i.proxy,T=`https://vuejs.org/error-reference/#runtime-${o}`;for(;_;){const w=_.ec;if(w){for(let A=0;A<w.length;A++)if(w[A](e,y,T)===!1)return}_=_.parent}if(d){Un(),mo(d,null,10,[e,y,T]),Hn();return}}Fd(e,o,l,a,h)}function Fd(e,i,o,a=!0,l=!1){if(l)throw e;console.error(e)}const We=[];let Rn=-1;const bs=[];let ki=null,_s=0;const Cu=Promise.resolve();let Xo=null;function Mu(e){const i=Xo||Cu;return e?i.then(this?e.bind(this):e):i}function Bd(e){let i=Rn+1,o=We.length;for(;i<o;){const a=i+o>>>1,l=We[a],d=ao(l);d<e||d===e&&l.flags&2?i=a+1:o=a}return i}function Oa(e){if(!(e.flags&1)){const i=ao(e),o=We[We.length-1];!o||!(e.flags&2)&&i>=ao(o)?We.push(e):We.splice(Bd(i),0,e),e.flags|=1,Ou()}}function Ou(){Xo||(Xo=Cu.then(zu))}function Vd(e){wt(e)?bs.push(...e):ki&&e.id===-1?ki.splice(_s+1,0,e):e.flags&1||(bs.push(e),e.flags|=1),Ou()}function ll(e,i,o=Rn+1){for(;o<We.length;o++){const a=We[o];if(a&&a.flags&2){if(e&&a.id!==e.uid)continue;We.splice(o,1),o--,a.flags&4&&(a.flags&=-2),a(),a.flags&4||(a.flags&=-2)}}}function Eu(e){if(bs.length){const i=[...new Set(bs)].sort((o,a)=>ao(o)-ao(a));if(bs.length=0,ki){ki.push(...i);return}for(ki=i,_s=0;_s<ki.length;_s++){const o=ki[_s];o.flags&4&&(o.flags&=-2),o.flags&8||o(),o.flags&=-2}ki=null,_s=0}}const ao=e=>e.id==null?e.flags&2?-1:1/0:e.id;function zu(e){try{for(Rn=0;Rn<We.length;Rn++){const i=We[Rn];i&&!(i.flags&8)&&(i.flags&4&&(i.flags&=-2),mo(i,i.i,i.i?15:14),i.flags&4||(i.flags&=-2))}}finally{for(;Rn<We.length;Rn++){const i=We[Rn];i&&(i.flags&=-2)}Rn=-1,We.length=0,Eu(),Xo=null,(We.length||bs.length)&&zu()}}let Re=null,Au=null;function Qo(e){const i=Re;return Re=e,Au=e&&e.type.__scopeId||null,i}function mt(e,i=Re,o){if(!i||e._n)return e;const a=(...l)=>{a._d&&nr(-1);const d=Qo(i);let h;try{h=e(...l)}finally{Qo(d),a._d&&nr(1)}return h};return a._n=!0,a._c=!0,a._d=!0,a}function ot(e,i){if(Re===null)return e;const o=xr(Re),a=e.dirs||(e.dirs=[]);for(let l=0;l<i.length;l++){let[d,h,_,y=ue]=i[l];d&&(Dt(d)&&(d={mounted:d,updated:d}),d.deep&&ri(h),a.push({dir:d,instance:o,value:h,oldValue:void 0,arg:_,modifiers:y}))}return e}function Vi(e,i,o,a){const l=e.dirs,d=i&&i.dirs;for(let h=0;h<l.length;h++){const _=l[h];d&&(_.oldValue=d[h].value);let y=_.dir[a];y&&(Un(),_n(y,o,8,[e.el,_,e,i]),Hn())}}function Iu(e,i){if(Ke){let o=Ke.provides;const a=Ke.parent&&Ke.parent.provides;a===o&&(o=Ke.provides=Object.create(a)),o[e]=i}}function eo(e,i,o=!1){const a=fc();if(a||ws){let l=ws?ws._context.provides:a?a.parent==null||a.ce?a.vnode.appContext&&a.vnode.appContext.provides:a.parent.provides:void 0;if(l&&e in l)return l[e];if(arguments.length>1)return o&&Dt(i)?i.call(a&&a.proxy):i}}const Zd=Symbol.for("v-scx"),Ud=()=>eo(Zd);function Qe(e,i,o){return $u(e,i,o)}function $u(e,i,o=ue){const{immediate:a,deep:l,flush:d,once:h}=o,_=Oe({},o),y=i&&a||!i&&d!=="post";let T;if(fo){if(d==="sync"){const V=Ud();T=V.__watcherHandles||(V.__watcherHandles=[])}else if(!y){const V=()=>{};return V.stop=Zn,V.resume=Zn,V.pause=Zn,V}}const w=Ke;_.call=(V,rt,Q)=>_n(V,w,rt,Q);let A=!1;d==="post"?_.scheduler=V=>{Xe(V,w&&w.suspense)}:d!=="sync"&&(A=!0,_.scheduler=(V,rt)=>{rt?V():Oa(V)}),_.augmentJob=V=>{i&&(V.flags|=4),A&&(V.flags|=2,w&&(V.id=w.uid,V.i=w))};const U=Rd(e,i,_);return fo&&(T?T.push(U):y&&U()),U}function Hd(e,i,o){const a=this.proxy,l=be(e)?e.includes(".")?Du(a,e):()=>a[e]:e.bind(a,a);let d;Dt(i)?d=i:(d=i.handler,o=i);const h=go(this),_=$u(l,d.bind(a),o);return h(),_}function Du(e,i){const o=i.split(".");return()=>{let a=e;for(let l=0;l<o.length&&a;l++)a=a[o[l]];return a}}const jd=Symbol("_vte"),Nu=e=>e.__isTeleport,pn=Symbol("_leaveCb"),Ws=Symbol("_enterCb");function Wd(){const e={isMounted:!1,isLeaving:!1,isUnmounting:!1,leavingVNodes:new Map};return Yi(()=>{e.isMounted=!0}),vr(()=>{e.isUnmounting=!0}),e}const fn=[Function,Array],Ru={mode:String,appear:Boolean,persisted:Boolean,onBeforeEnter:fn,onEnter:fn,onAfterEnter:fn,onEnterCancelled:fn,onBeforeLeave:fn,onLeave:fn,onAfterLeave:fn,onLeaveCancelled:fn,onBeforeAppear:fn,onAppear:fn,onAfterAppear:fn,onAppearCancelled:fn},Fu=e=>{const i=e.subTree;return i.component?Fu(i.component):i},Kd={name:"BaseTransition",props:Ru,setup(e,{slots:i}){const o=fc(),a=Wd();return()=>{const l=i.default&&Zu(i.default(),!0),d=l&&l.length?Bu(l):o.subTree?N():void 0;if(!d)return;const h=Xt(e),{mode:_}=h;if(a.isLeaving)return Jr(d);const y=ul(d);if(!y)return Jr(d);let T=ua(y,h,a,o,A=>T=A);y.type!==Ne&&lo(y,T);let w=o.subTree&&ul(o.subTree);if(w&&w.type!==Ne&&!Wi(w,y)&&Fu(o).type!==Ne){let A=ua(w,h,a,o);if(lo(w,A),_==="out-in"&&y.type!==Ne)return a.isLeaving=!0,A.afterLeave=()=>{a.isLea
Line truncated
|
||||
* @vue/runtime-dom v3.5.39
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
* @license MIT
|
||||
**/let ma;const wl=typeof window<"u"&&window.trustedTypes;if(wl)try{ma=wl.createPolicy("vue",{createHTML:e=>e})}catch{}const mc=ma?e=>ma.createHTML(e):e=>e,Gf="http://www.w3.org/2000/svg",qf="http://www.w3.org/1998/Math/MathML",ii=typeof document<"u"?document:null,kl=ii&&ii.createElement("template"),Yf={insert:(e,i,o)=>{i.insertBefore(e,o||null)},remove:e=>{const i=e.parentNode;i&&i.removeChild(e)},createElement:(e,i,o,a)=>{const l=i==="svg"?ii.createElementNS(Gf,e):i==="mathml"?ii.createElementNS(qf,e):o?ii.createElement(e,{is:o}):ii.createElement(e);return e==="select"&&a&&a.multiple!=null&&l.setAttribute("multiple",a.multiple),l},createText:e=>ii.createTextNode(e),createComment:e=>ii.createComment(e),setText:(e,i)=>{e.nodeValue=i},setElementText:(e,i)=>{e.textContent=i},parentNode:e=>e.parentNode,nextSibling:e=>e.nextSibling,querySelector:e=>ii.querySelector(e),setScopeId(e,i){e.setAttribute(i,"")},insertStaticContent(e,i,o,a,l,d){const h=o?o.previousSibling:i.lastChild;if(l&&(l===d||l.nextSibling))for(;i.insertBefore(l.cloneNode(!0),o),!(l===d||!(l=l.nextSibling)););else{kl.innerHTML=mc(a==="svg"?`<svg>${e}</svg>`:a==="mathml"?`<math>${e}</math>`:e);const _=kl.content;if(a==="svg"||a==="mathml"){const y=_.firstChild;for(;y.firstChild;)_.appendChild(y.firstChild);_.removeChild(y)}i.insertBefore(_,o)}return[h?h.nextSibling:i.firstChild,o?o.previousSibling:i.lastChild]}},wi="transition",Ks="animation",ho=Symbol("_vtc"),gc={name:String,type:String,css:{type:Boolean,default:!0},duration:[String,Number,Object],enterFromClass:String,enterActiveClass:String,enterToClass:String,appearFromClass:String,appearActiveClass:String,appearToClass:String,leaveFromClass:String,leaveActiveClass:String,leaveToClass:String},Jf=Oe({},Ru,gc),Xf=e=>(e.displayName="Transition",e.props=Jf,e),Qf=Xf((e,{slots:i})=>Wf(Gd,th(e),i)),Ui=(e,i=[])=>{wt(e)?e.forEach(o=>o(...i)):e&&e(...i)},Sl=e=>e?wt(e)?e.some(i=>i.length>1):e.length>1:!1;function th(e){const i={};for(const nt in e)nt in gc||(i[nt]=e[nt]);if(e.css===!1)return i;const{name:o="v",type:a,duration:l,enterFromClass:d=`${o}-enter-from`,enterActiveClass:h=`${o}-enter-active`,enterToClass:_=`${o}-enter-to`,appearFromClass:y=d,appearActiveClass:T=h,appearToClass:w=_,leaveFromClass:A=`${o}-leave-from`,leaveActiveClass:U=`${o}-leave-active`,leaveToClass:V=`${o}-leave-to`}=e,rt=eh(l),Q=rt&&rt[0],Ot=rt&&rt[1],{onBeforeEnter:Mt,onEnter:q,onEnterCancelled:dt,onLeave:it,onLeaveCancelled:ht,onBeforeAppear:Kt=Mt,onAppear:he=q,onAppearCancelled:pe=dt}=i,St=(nt,ut,zt,Ft)=>{nt._enterCancelled=Ft,Hi(nt,ut?w:_),Hi(nt,ut?T:h),zt&&zt()},Nt=(nt,ut)=>{nt._isLeaving=!1,Hi(nt,A),Hi(nt,V),Hi(nt,U),ut&&ut()},Et=nt=>(ut,zt)=>{const Ft=nt?he:q,lt=()=>St(ut,nt,zt);Ui(Ft,[ut,lt]),Pl(()=>{Hi(ut,nt?y:d),ni(ut,nt?w:_),Sl(Ft)||Tl(ut,a,Q,lt)})};return Oe(i,{onBeforeEnter(nt){Ui(Mt,[nt]),ni(nt,d),ni(nt,h)},onBeforeAppear(nt){Ui(Kt,[nt]),ni(nt,y),ni(nt,T)},onEnter:Et(!1),onAppear:Et(!0),onLeave(nt,ut){nt._isLeaving=!0;const zt=()=>Nt(nt,ut);ni(nt,A),nt._enterCancelled?(ni(nt,U),Ml(nt)):(Ml(nt),ni(nt,U)),Pl(()=>{nt._isLeaving&&(Hi(nt,A),ni(nt,V),Sl(it)||Tl(nt,a,Ot,zt))}),Ui(it,[nt,zt])},onEnterCancelled(nt){St(nt,!1,void 0,!0),Ui(dt,[nt])},onAppearCancelled(nt){St(nt,!0,void 0,!0),Ui(pe,[nt])},onLeaveCancelled(nt){Nt(nt),Ui(ht,[nt])}})}function eh(e){if(e==null)return null;if(ne(e))return[ea(e.enter),ea(e.leave)];{const i=ea(e);return[i,i]}}function ea(e){return id(e)}function ni(e,i){i.split(/\s+/).forEach(o=>o&&e.classList.add(o)),(e[ho]||(e[ho]=new Set)).add(i)}function Hi(e,i){i.split(/\s+/).forEach(a=>a&&e.classList.remove(a));const o=e[ho];o&&(o.delete(i),o.size||(e[ho]=void 0))}function Pl(e){requestAnimationFrame(()=>{requestAnimationFrame(e)})}let nh=0;function Tl(e,i,o,a){const l=e._endId=++nh,d=()=>{l===e._endId&&a()};if(o!=null)return setTimeout(d,o);const{type:h,timeout:_,propCount:y}=ih(e,i);if(!h)return a();const T=h+"end";let w=0;const A=()=>{e.removeEventListener(T,U),d()},U=V=>{V.target===e&&++w>=y&&A()};setTimeout(()=>{w<y&&A()},_+1),e.addEventListener(T,U)}function ih(e,i){const o=window.getComputedStyle(e),a=rt=>(o[rt]||"").split(", "),l=a(`${wi}Delay`),d=a(`${wi}Duration`),h=Ll(l,d),_=a(`${Ks}Delay`),y=a(`${Ks}Duration`),T=Ll(_,y);let w=null,A=0,U=0;i===wi?h>0&&(w=wi,A=h,U=d.length):i===Ks?T>0&&(w=Ks,A=T,U=y.length):(A=Math.max(h,T),w=A>0?h>T?wi:Ks:null,U=w?w===wi?d.length:y.length:0);const V=w===wi&&/\b(?:transform|all)(?:,|$)/.test(a(`${wi}Property`).toString());return{type:w,timeout:A,propCount:U,hasTransform:V}}function Ll(e,i){for(;e.length<i.length;)e=e.concat(e);return Math.max(...i.map((o,a)=>Cl(o)+Cl(e[a])))}function Cl(e){return e==="auto"?0:Number(e.slice(0,-1).replace(",","."))*1e3}function Ml(e){return(e?e.ownerDocument:document).body.offsetHeight}function sh(e,i,o){const a=e[ho];a&&(i=(i?[i,...a]:[...a]).join(" ")),i==null?e.removeAttribute("class"):o?e.setAttribute("class",i):e.className=i}const or=Symbol("_vod"),_c=Symbol("_vsh"),oh={name:"show",beforeMount(e,{value:i},{trans
Line truncated
|
||||
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
|
||||
* (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade
|
||||
*/var Dp=Js.exports,Xl;function Np(){return Xl||(Xl=1,(function(e,i){(function(o,a){a(i)})(Dp,(function(o){var a="1.9.4";function l(t){var n,s,r,c;for(s=1,r=arguments.length;s<r;s++){c=arguments[s];for(n in c)t[n]=c[n]}return t}var d=Object.create||(function(){function t(){}return function(n){return t.prototype=n,new t}})();function h(t,n){var s=Array.prototype.slice;if(t.bind)return t.bind.apply(t,s.call(arguments,1));var r=s.call(arguments,2);return function(){return t.apply(n,r.length?r.concat(s.call(arguments)):arguments)}}var _=0;function y(t){return"_leaflet_id"in t||(t._leaflet_id=++_),t._leaflet_id}function T(t,n,s){var r,c,p,S;return S=function(){r=!1,c&&(p.apply(s,c),c=!1)},p=function(){r?c=arguments:(t.apply(s,arguments),setTimeout(S,n),r=!0)},p}function w(t,n,s){var r=n[1],c=n[0],p=r-c;return t===r&&s?t:((t-c)%p+p)%p+c}function A(){return!1}function U(t,n){if(n===!1)return t;var s=Math.pow(10,n===void 0?6:n);return Math.round(t*s)/s}function V(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function rt(t){return V(t).split(/\s+/)}function Q(t,n){Object.prototype.hasOwnProperty.call(t,"options")||(t.options=t.options?d(t.options):{});for(var s in n)t.options[s]=n[s];return t.options}function Ot(t,n,s){var r=[];for(var c in t)r.push(encodeURIComponent(s?c.toUpperCase():c)+"="+encodeURIComponent(t[c]));return(!n||n.indexOf("?")===-1?"?":"&")+r.join("&")}var Mt=/\{ *([\w_ -]+) *\}/g;function q(t,n){return t.replace(Mt,function(s,r){var c=n[r];if(c===void 0)throw new Error("No value provided for variable "+s);return typeof c=="function"&&(c=c(n)),c})}var dt=Array.isArray||function(t){return Object.prototype.toString.call(t)==="[object Array]"};function it(t,n){for(var s=0;s<t.length;s++)if(t[s]===n)return s;return-1}var ht="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";function Kt(t){return window["webkit"+t]||window["moz"+t]||window["ms"+t]}var he=0;function pe(t){var n=+new Date,s=Math.max(0,16-(n-he));return he=n+s,window.setTimeout(t,s)}var St=window.requestAnimationFrame||Kt("RequestAnimationFrame")||pe,Nt=window.cancelAnimationFrame||Kt("CancelAnimationFrame")||Kt("CancelRequestAnimationFrame")||function(t){window.clearTimeout(t)};function Et(t,n,s){if(s&&St===pe)t.call(n);else return St.call(window,h(t,n))}function nt(t){t&&Nt.call(window,t)}var ut={__proto__:null,extend:l,create:d,bind:h,get lastId(){return _},stamp:y,throttle:T,wrapNum:w,falseFn:A,formatNum:U,trim:V,splitWords:rt,setOptions:Q,getParamString:Ot,template:q,isArray:dt,indexOf:it,emptyImageUrl:ht,requestFn:St,cancelFn:Nt,requestAnimFrame:Et,cancelAnimFrame:nt};function zt(){}zt.extend=function(t){var n=function(){Q(this),this.initialize&&this.initialize.apply(this,arguments),this.callInitHooks()},s=n.__super__=this.prototype,r=d(s);r.constructor=n,n.prototype=r;for(var c in this)Object.prototype.hasOwnProperty.call(this,c)&&c!=="prototype"&&c!=="__super__"&&(n[c]=this[c]);return t.statics&&l(n,t.statics),t.includes&&(Ft(t.includes),l.apply(null,[r].concat(t.includes))),l(r,t),delete r.statics,delete r.includes,r.options&&(r.options=s.options?d(s.options):{},l(r.options,t.options)),r._initHooks=[],r.callInitHooks=function(){if(!this._initHooksCalled){s.callInitHooks&&s.callInitHooks.call(this),this._initHooksCalled=!0;for(var p=0,S=r._initHooks.length;p<S;p++)r._initHooks[p].call(this)}},n},zt.include=function(t){var n=this.prototype.options;return l(this.prototype,t),t.options&&(this.prototype.options=n,this.mergeOptions(t.options)),this},zt.mergeOptions=function(t){return l(this.prototype.options,t),this},zt.addInitHook=function(t){var n=Array.prototype.slice.call(arguments,1),s=typeof t=="function"?t:function(){this[t].apply(this,n)};return this.prototype._initHooks=this.prototype._initHooks||[],this.prototype._initHooks.push(s),this};function Ft(t){if(!(typeof L>"u"||!L||!L.Mixin)){t=dt(t)?t:[t];for(var n=0;n<t.length;n++)t[n]===L.Mixin.Events&&console.warn("Deprecated include of L.Mixin.Events: this property will be removed in future releases, please inherit from L.Evented instead.",new Error().stack)}}var lt={on:function(t,n,s){if(typeof t=="object")for(var r in t)this._on(r,t[r],n);else{t=rt(t);for(var c=0,p=t.length;c<p;c++)this._on(t[c],n,s)}return this},off:function(t,n,s){if(!arguments.length)delete this._events;else if(typeof t=="object")for(var r in t)this._off(r,t[r],n);else{t=rt(t);for(var c=arguments.length===1,p=0,S=t.length;p<S;p++)c?this._off(t[p]):this._off(t[p],n,s)}return this},_on:function(t,n,s,r){if(typeof n!="function"){console.warn("wrong listener type: "+typeof n);return}if(this._listens(t,n,s)===!1){s===this&&(s=void 0);var c={fn:n,ctx:s};r&&(c.once=!0),this._events=this._events||{},this._events[t]=this._events[t]||[],this._events[t].push(c)}},_off:function(t,n,s){var r,c,p;if(this._events&&(r=this._events[t],!!r)){if(arguments.length===1){if(this._firingCount)for(c=0,p=r.length;c<p;c++)r[c].fn=A;delete this._events[t];return}if(typeof n!="function"){console.warn("wrong listener tLine truncated
|
||||
Vendored
+2
-2
@@ -35,8 +35,8 @@
|
||||
})()
|
||||
</script>
|
||||
<title>PilotVault — Control Panel</title>
|
||||
<script type="module" crossorigin src="./assets/index-BldP9Pra.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DBe0h801.css">
|
||||
<script type="module" crossorigin src="./assets/index-DLbqB6QP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-Co-T5CTN.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -72,6 +72,16 @@ func main() {
|
||||
mux.HandleFunc("POST /bff/orgs", app.requireAuth(app.handleCreateOrg))
|
||||
mux.HandleFunc("PATCH /bff/orgs/{id}", app.requireAuth(app.handleUpdateOrg))
|
||||
mux.HandleFunc("DELETE /bff/orgs/{id}", app.requireAuth(app.handleDeleteOrg))
|
||||
// Logbook — drones, flights, and the compliance CSV export (scoping upstream)
|
||||
mux.HandleFunc("GET /bff/drones", app.requireAuth(app.handleListDrones))
|
||||
mux.HandleFunc("POST /bff/drones", app.requireAuth(app.handleCreateDrone))
|
||||
mux.HandleFunc("PATCH /bff/drones/{id}", app.requireAuth(app.handleUpdateDrone))
|
||||
mux.HandleFunc("DELETE /bff/drones/{id}", app.requireAuth(app.handleDeleteDrone))
|
||||
mux.HandleFunc("GET /bff/flights", app.requireAuth(app.handleListFlights))
|
||||
mux.HandleFunc("POST /bff/flights", app.requireAuth(app.handleCreateFlight))
|
||||
mux.HandleFunc("PATCH /bff/flights/{id}", app.requireAuth(app.handleUpdateFlight))
|
||||
mux.HandleFunc("DELETE /bff/flights/{id}", app.requireAuth(app.handleDeleteFlight))
|
||||
mux.HandleFunc("GET /bff/logbook/export", app.requireAuth(app.handleExportLogbook))
|
||||
mux.HandleFunc("GET /bff/ws", app.handleWS)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "api": app.apiBase})
|
||||
|
||||
@@ -253,6 +253,83 @@ export async function testWebDav() {
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
/* ---------- Logbook: drones ---------- */
|
||||
|
||||
export async function getDrones() {
|
||||
try {
|
||||
const r = await fetch('/bff/drones')
|
||||
if (!r.ok) return { ok: false, status: r.status, drones: [] }
|
||||
const d = await r.json()
|
||||
return { ok: true, status: 200, drones: d.drones || [] }
|
||||
} catch {
|
||||
return { ok: false, status: 0, drones: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export async function createDrone(drone) {
|
||||
const r = await fetch('/bff/drones', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(drone),
|
||||
})
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
export async function updateDrone(id, drone) {
|
||||
const r = await fetch(`/bff/drones/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(drone),
|
||||
})
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
export async function deleteDrone(id) {
|
||||
const r = await fetch(`/bff/drones/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
/* ---------- Logbook: flights ---------- */
|
||||
|
||||
export async function getFlights() {
|
||||
try {
|
||||
const r = await fetch('/bff/flights')
|
||||
if (!r.ok) return { ok: false, status: r.status, flights: [] }
|
||||
const d = await r.json()
|
||||
return { ok: true, status: 200, flights: d.flights || [] }
|
||||
} catch {
|
||||
return { ok: false, status: 0, flights: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export async function createFlight(flight) {
|
||||
const r = await fetch('/bff/flights', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(flight),
|
||||
})
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
export async function updateFlight(id, flight) {
|
||||
const r = await fetch(`/bff/flights/${encodeURIComponent(id)}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(flight),
|
||||
})
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
export async function deleteFlight(id) {
|
||||
const r = await fetch(`/bff/flights/${encodeURIComponent(id)}`, { method: 'DELETE' })
|
||||
return { ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) }
|
||||
}
|
||||
|
||||
// Trigger a browser download of the compliance CSV export.
|
||||
export function exportLogbookUrl() {
|
||||
return '/bff/logbook/export'
|
||||
}
|
||||
|
||||
export async function sendCommand(id, command, payload) {
|
||||
const r = await fetch(`/bff/devices/${encodeURIComponent(id)}/command`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -4,6 +4,7 @@ import DeviceMap from './DeviceMap.vue'
|
||||
import BrandMark from './BrandMark.vue'
|
||||
import Icon from './Icon.vue'
|
||||
import Settings from './Settings.vue'
|
||||
import Logbook from './Logbook.vue'
|
||||
import { getDevices, sendCommand } from '../api.js'
|
||||
import { formatTime } from '../prefs.js'
|
||||
|
||||
@@ -627,6 +628,9 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ---------- Logbook ---------- -->
|
||||
<Logbook v-else-if="active === 'Logbook'" :email="email" :role="role" :organization="organization" :organization-name="organizationName" />
|
||||
|
||||
<!-- ---------- Settings ---------- -->
|
||||
<Settings v-else-if="active === 'Settings'" :email="email" :role="role" :organization="organization" :organization-name="organizationName" @logout="emit('logout')" />
|
||||
|
||||
|
||||
@@ -0,0 +1,551 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import Icon from './Icon.vue'
|
||||
import {
|
||||
getDrones, createDrone, updateDrone, deleteDrone,
|
||||
getFlights, createFlight, updateFlight, deleteFlight, exportLogbookUrl,
|
||||
} from '../api.js'
|
||||
|
||||
const props = defineProps({
|
||||
email: { type: String, default: '' },
|
||||
role: { type: String, default: 'user' },
|
||||
organization: { type: String, default: '' },
|
||||
organizationName: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const badgeClass = {
|
||||
success: 'bg-success-soft text-success-fg',
|
||||
warning: 'bg-amber-soft text-amber-fg',
|
||||
danger: 'bg-danger-soft text-danger-fg',
|
||||
accent: 'bg-accent-soft text-accent-soft-fg',
|
||||
neutral: 'bg-surface-2 text-ink-secondary',
|
||||
}
|
||||
|
||||
const tab = ref('flights') // 'flights' | 'drones'
|
||||
|
||||
const drones = ref([])
|
||||
const flights = ref([])
|
||||
const loading = ref(false)
|
||||
const loadErr = ref('')
|
||||
|
||||
const droneById = computed(() => Object.fromEntries(drones.value.map((d) => [d.id, d])))
|
||||
|
||||
async function loadAll() {
|
||||
loading.value = true
|
||||
loadErr.value = ''
|
||||
const [dr, fl] = await Promise.all([getDrones(), getFlights()])
|
||||
if (!dr.ok || !fl.ok) {
|
||||
loadErr.value =
|
||||
dr.status === 503 || fl.status === 503
|
||||
? 'Logbook storage is not configured on the API Server (service account missing).'
|
||||
: 'Could not load the logbook.'
|
||||
}
|
||||
drones.value = dr.drones
|
||||
flights.value = fl.flights
|
||||
loading.value = false
|
||||
}
|
||||
onMounted(loadAll)
|
||||
|
||||
/* ---------------- compliance presentation ---------------- */
|
||||
|
||||
function flightBadge(f) {
|
||||
const c = f.compliance || {}
|
||||
if (c.exempt) return { tone: 'neutral', label: 'Exempt' }
|
||||
if ((c.redFlags || []).length) return { tone: 'danger', label: `${c.redFlags.length} issue${c.redFlags.length > 1 ? 's' : ''}` }
|
||||
return { tone: 'success', label: 'Compliant' }
|
||||
}
|
||||
|
||||
const openRow = ref('') // flight id whose compliance detail is expanded
|
||||
function toggleRow(id) {
|
||||
openRow.value = openRow.value === id ? '' : id
|
||||
}
|
||||
|
||||
/* ---------------- flight form ---------------- */
|
||||
|
||||
const CATEGORIES = [
|
||||
{ value: 'open', label: 'Open' },
|
||||
{ value: 'specific', label: 'Specific' },
|
||||
{ value: 'certified', label: 'Certified' },
|
||||
]
|
||||
const PURPOSES = [
|
||||
{ value: 'commercial', label: 'Commercial' },
|
||||
{ value: 'research', label: 'Research' },
|
||||
{ value: 'public', label: 'Public-benefit' },
|
||||
{ value: 'hobby', label: 'Private hobby' },
|
||||
{ value: 'club_area', label: 'Model-club area' },
|
||||
]
|
||||
const PATHS = [
|
||||
{ value: '', label: 'Auto (from drone)' },
|
||||
{ value: 'manual', label: 'Manual' },
|
||||
{ value: 'automatic', label: 'Automatic (FDR)' },
|
||||
]
|
||||
|
||||
function blankFlight() {
|
||||
return {
|
||||
operationDate: new Date().toISOString().slice(0, 10),
|
||||
startTime: '', endTime: '', drone: drones.value[0]?.id || '',
|
||||
areaRoute: '', maxAltitudeAgl: '', pilotName: props.email, certificateRef: '',
|
||||
category: 'open', purpose: 'commercial', loggingPath: '',
|
||||
rawFdrLogUrl: '', authorisationRef: '',
|
||||
weather: '', airspaceRef: '', observer: '', incidents: '', notes: '',
|
||||
}
|
||||
}
|
||||
const showFlightForm = ref(false)
|
||||
const editingFlightId = ref('')
|
||||
const flightForm = reactive(blankFlight())
|
||||
const flightMsg = ref('')
|
||||
const savingFlight = ref(false)
|
||||
const showDetails = ref(false)
|
||||
|
||||
function newFlight() {
|
||||
Object.assign(flightForm, blankFlight())
|
||||
editingFlightId.value = ''
|
||||
flightMsg.value = ''
|
||||
showDetails.value = false
|
||||
showFlightForm.value = true
|
||||
}
|
||||
function editFlight(f) {
|
||||
Object.assign(flightForm, {
|
||||
operationDate: (f.operationDate || '').slice(0, 10),
|
||||
startTime: f.startTime || '', endTime: f.endTime || '', drone: f.drone || '',
|
||||
areaRoute: f.areaRoute || '', maxAltitudeAgl: f.maxAltitudeAgl || '',
|
||||
pilotName: f.pilotName || '', certificateRef: f.certificateRef || '',
|
||||
category: f.category || 'open', purpose: f.purpose || 'commercial',
|
||||
loggingPath: f.loggingPath || '', rawFdrLogUrl: f.rawFdrLogUrl || '',
|
||||
authorisationRef: f.authorisationRef || '', weather: f.weather || '',
|
||||
airspaceRef: f.airspaceRef || '', observer: f.observer || '',
|
||||
incidents: f.incidents || '', notes: f.notes || '',
|
||||
})
|
||||
editingFlightId.value = f.id
|
||||
flightMsg.value = ''
|
||||
showDetails.value = !!(f.weather || f.airspaceRef || f.observer || f.incidents || f.notes)
|
||||
showFlightForm.value = true
|
||||
}
|
||||
function cancelFlight() {
|
||||
showFlightForm.value = false
|
||||
editingFlightId.value = ''
|
||||
}
|
||||
|
||||
async function saveFlight() {
|
||||
flightMsg.value = ''
|
||||
if (!flightForm.drone) {
|
||||
flightMsg.value = 'Select a drone first (add one on the Drones tab).'
|
||||
return
|
||||
}
|
||||
savingFlight.value = true
|
||||
const payload = { ...flightForm, maxAltitudeAgl: Number(flightForm.maxAltitudeAgl) || 0 }
|
||||
const res = editingFlightId.value
|
||||
? await updateFlight(editingFlightId.value, payload)
|
||||
: await createFlight(payload)
|
||||
savingFlight.value = false
|
||||
if (!res.ok) {
|
||||
flightMsg.value = res.body?.error || 'Could not save the flight.'
|
||||
return
|
||||
}
|
||||
showFlightForm.value = false
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
const confirmFlightId = ref('')
|
||||
async function removeFlight(f) {
|
||||
const res = await deleteFlight(f.id)
|
||||
confirmFlightId.value = ''
|
||||
if (res.ok) await loadAll()
|
||||
}
|
||||
|
||||
/* ---------------- drone form ---------------- */
|
||||
|
||||
const C_CLASSES = ['', 'C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6']
|
||||
function blankDrone() {
|
||||
return {
|
||||
name: '', model: '', serial: '', operatorNumber: '',
|
||||
mtomGrams: '', isToy: false, autologsFlights: false, cClass: '',
|
||||
}
|
||||
}
|
||||
const showDroneForm = ref(false)
|
||||
const editingDroneId = ref('')
|
||||
const droneForm = reactive(blankDrone())
|
||||
const droneMsg = ref('')
|
||||
const savingDrone = ref(false)
|
||||
|
||||
function newDrone() {
|
||||
Object.assign(droneForm, blankDrone())
|
||||
editingDroneId.value = ''
|
||||
droneMsg.value = ''
|
||||
showDroneForm.value = true
|
||||
}
|
||||
function editDrone(d) {
|
||||
Object.assign(droneForm, {
|
||||
name: d.name || '', model: d.model || '', serial: d.serial || '',
|
||||
operatorNumber: d.operatorNumber || '', mtomGrams: d.mtomGrams || '',
|
||||
isToy: !!d.isToy, autologsFlights: !!d.autologsFlights, cClass: d.cClass || '',
|
||||
})
|
||||
editingDroneId.value = d.id
|
||||
droneMsg.value = ''
|
||||
showDroneForm.value = true
|
||||
}
|
||||
function cancelDrone() {
|
||||
showDroneForm.value = false
|
||||
editingDroneId.value = ''
|
||||
}
|
||||
async function saveDrone() {
|
||||
droneMsg.value = ''
|
||||
if (!droneForm.name.trim()) {
|
||||
droneMsg.value = 'Give the drone a name.'
|
||||
return
|
||||
}
|
||||
savingDrone.value = true
|
||||
const payload = { ...droneForm, mtomGrams: Number(droneForm.mtomGrams) || 0 }
|
||||
const res = editingDroneId.value
|
||||
? await updateDrone(editingDroneId.value, payload)
|
||||
: await createDrone(payload)
|
||||
savingDrone.value = false
|
||||
if (!res.ok) {
|
||||
droneMsg.value = res.body?.error || 'Could not save the drone.'
|
||||
return
|
||||
}
|
||||
showDroneForm.value = false
|
||||
await loadAll()
|
||||
}
|
||||
const confirmDroneId = ref('')
|
||||
async function removeDrone(d) {
|
||||
const res = await deleteDrone(d.id)
|
||||
confirmDroneId.value = ''
|
||||
if (res.ok) await loadAll()
|
||||
else droneMsg.value = res.body?.error || 'Could not delete the drone.'
|
||||
}
|
||||
|
||||
/* ---------------- headline stats ---------------- */
|
||||
|
||||
const stats = computed(() => {
|
||||
const total = flights.value.length
|
||||
const flagged = flights.value.filter((f) => (f.compliance?.redFlags || []).length).length
|
||||
const required = flights.value.filter((f) => f.compliance?.required).length
|
||||
return { total, flagged, required, fleet: drones.value.length }
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto flex max-w-[1240px] flex-col gap-5 p-7">
|
||||
<!-- header + actions -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="inline-flex rounded-lg border border-line bg-surface-1 p-0.5">
|
||||
<button
|
||||
v-for="t in [['flights', 'Flights'], ['drones', 'Drones']]"
|
||||
:key="t[0]"
|
||||
class="rounded-md px-3.5 py-1.5 text-sm font-semibold transition"
|
||||
:class="tab === t[0] ? 'bg-accent-soft text-accent-soft-fg' : 'text-ink-secondary hover:text-ink'"
|
||||
@click="tab = t[0]"
|
||||
>
|
||||
{{ t[1] }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<a
|
||||
:href="exportLogbookUrl()"
|
||||
class="btn-ghost inline-flex items-center gap-2"
|
||||
title="Download a compliance CSV (Trafikstyrelsen / police disclosure)"
|
||||
>
|
||||
<Icon name="download" :size="15" /> Export CSV
|
||||
</a>
|
||||
<button v-if="tab === 'flights'" class="btn-accent inline-flex items-center gap-2" @click="newFlight">
|
||||
<Icon name="plus" :size="15" /> Log flight
|
||||
</button>
|
||||
<button v-else class="btn-accent inline-flex items-center gap-2" @click="newDrone">
|
||||
<Icon name="plus" :size="15" /> Add drone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- stat row -->
|
||||
<div class="grid grid-cols-4 gap-4 max-[900px]:grid-cols-2">
|
||||
<div v-for="s in [
|
||||
{ label: 'Flights logged', value: stats.total, tone: 'neutral' },
|
||||
{ label: 'Require logbook', value: stats.required, tone: 'neutral' },
|
||||
{ label: 'Compliance flags', value: stats.flagged, tone: stats.flagged ? 'danger' : 'success' },
|
||||
{ label: 'Registered drones', value: stats.fleet, tone: 'neutral' },
|
||||
]" :key="s.label" class="panel p-5">
|
||||
<div class="eyebrow">{{ s.label }}</div>
|
||||
<div class="mt-2 text-[30px] font-bold leading-none tracking-tightest"
|
||||
:class="s.tone === 'danger' ? 'text-danger-fg' : s.tone === 'success' ? 'text-success-fg' : 'text-ink'">
|
||||
{{ s.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadErr" class="panel border-danger/40 p-4 text-sm text-danger-fg">{{ loadErr }}</div>
|
||||
|
||||
<!-- ============ FLIGHTS ============ -->
|
||||
<template v-if="tab === 'flights'">
|
||||
<!-- add / edit form -->
|
||||
<div v-if="showFlightForm" class="panel p-5">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="eyebrow">{{ editingFlightId ? 'Edit entry' : 'New entry' }}</div>
|
||||
<div class="mt-0.5 text-base font-semibold text-ink">Logbook flight (BEK 1649 §5)</div>
|
||||
</div>
|
||||
<button class="btn-icon" @click="cancelFlight"><Icon name="x" :size="16" /></button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-3 max-[760px]:grid-cols-1">
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Date</span>
|
||||
<input v-model="flightForm.operationDate" type="date" class="field" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Start</span>
|
||||
<input v-model="flightForm.startTime" type="time" class="field" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">End</span>
|
||||
<input v-model="flightForm.endTime" type="time" class="field" />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Drone</span>
|
||||
<select v-model="flightForm.drone" class="field">
|
||||
<option v-if="!drones.length" value="">— add a drone first —</option>
|
||||
<option v-for="d in drones" :key="d.id" :value="d.id">{{ d.name }}{{ d.model ? ` · ${d.model}` : '' }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Max altitude (m AGL)</span>
|
||||
<input v-model="flightForm.maxAltitudeAgl" type="number" min="0" class="field" placeholder="120" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Area / route</span>
|
||||
<input v-model="flightForm.areaRoute" class="field" placeholder="Field N of Roskilde, grid survey" />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Remote pilot name</span>
|
||||
<input v-model="flightForm.pilotName" class="field" placeholder="Full name" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Certificate ref</span>
|
||||
<input v-model="flightForm.certificateRef" class="field" placeholder="A2 / STS cert no." />
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Logging path</span>
|
||||
<select v-model="flightForm.loggingPath" class="field">
|
||||
<option v-for="p in PATHS" :key="p.value" :value="p.value">{{ p.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Category</span>
|
||||
<select v-model="flightForm.category" class="field">
|
||||
<option v-for="c in CATEGORIES" :key="c.value" :value="c.value">{{ c.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Purpose</span>
|
||||
<select v-model="flightForm.purpose" class="field">
|
||||
<option v-for="p in PURPOSES" :key="p.value" :value="p.value">{{ p.label }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="block">
|
||||
<span class="eyebrow mb-1 block">Authorisation ref</span>
|
||||
<input v-model="flightForm.authorisationRef" class="field" placeholder="Specific-category ref" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="mt-3 block">
|
||||
<span class="eyebrow mb-1 block">FDR log URL (automatic path)</span>
|
||||
<input v-model="flightForm.rawFdrLogUrl" class="field" placeholder="Link to the stored flight-data-recorder export" />
|
||||
</label>
|
||||
|
||||
<button class="mt-4 flex items-center gap-1.5 text-sm font-semibold text-accent" @click="showDetails = !showDetails">
|
||||
<Icon :name="showDetails ? 'x' : 'plus'" :size="14" /> Operational details (weather, airspace, incidents)
|
||||
</button>
|
||||
<div v-if="showDetails" class="mt-3 grid grid-cols-2 gap-3 max-[760px]:grid-cols-1">
|
||||
<label class="block"><span class="eyebrow mb-1 block">Weather / wind</span>
|
||||
<input v-model="flightForm.weather" class="field" placeholder="6 m/s NW, CAVOK" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Airspace / NOTAM ref</span>
|
||||
<input v-model="flightForm.airspaceRef" class="field" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Observer</span>
|
||||
<input v-model="flightForm.observer" class="field" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Incidents / anomalies</span>
|
||||
<input v-model="flightForm.incidents" class="field" placeholder="RTH trigger, GPS dropout…" /></label>
|
||||
<label class="col-span-2 block max-[760px]:col-span-1"><span class="eyebrow mb-1 block">Notes</span>
|
||||
<textarea v-model="flightForm.notes" rows="2" class="field"></textarea></label>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<button class="btn-accent" :disabled="savingFlight" @click="saveFlight">
|
||||
{{ savingFlight ? 'Saving…' : editingFlightId ? 'Save changes' : 'Log flight' }}
|
||||
</button>
|
||||
<button class="btn-ghost" @click="cancelFlight">Cancel</button>
|
||||
<span v-if="flightMsg" class="text-sm text-danger-fg">{{ flightMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- flights table -->
|
||||
<div class="panel overflow-hidden p-0">
|
||||
<div v-if="loading" class="px-5 py-12 text-center text-sm text-ink-muted">Loading…</div>
|
||||
<div v-else-if="!flights.length" class="grid place-items-center px-5 py-16 text-center">
|
||||
<Icon name="book" :size="26" class="text-ink-muted" />
|
||||
<div class="mt-3 text-sm font-medium text-ink-secondary">No flights logged yet</div>
|
||||
<div class="mt-1 text-xs text-ink-muted">Log your first operation to start the 5-year retention record.</div>
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="text-left">
|
||||
<th v-for="h in ['Date', 'Drone', 'Area / route', 'Alt', 'Pilot', 'Compliance', '']" :key="h"
|
||||
class="border-y border-line bg-surface-2 px-5 py-2.5 font-mono text-[10.5px] font-normal uppercase tracking-caps text-ink-muted">
|
||||
{{ h }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="f in flights" :key="f.id">
|
||||
<tr class="border-b border-line last:border-0" :class="editingFlightId === f.id ? 'bg-accent-soft' : ''">
|
||||
<td class="whitespace-nowrap px-5 py-3 font-mono text-ink">
|
||||
{{ (f.operationDate || '').slice(0, 10) }}
|
||||
<span v-if="f.startTime" class="text-ink-muted">{{ f.startTime }}</span>
|
||||
</td>
|
||||
<td class="px-5 py-3 text-ink-secondary">{{ f.droneName || '—' }}</td>
|
||||
<td class="max-w-[220px] truncate px-5 py-3 text-ink-secondary" :title="f.areaRoute">{{ f.areaRoute || '—' }}</td>
|
||||
<td class="px-5 py-3 font-mono text-ink-secondary">{{ f.maxAltitudeAgl ? f.maxAltitudeAgl + ' m' : '—' }}</td>
|
||||
<td class="px-5 py-3 text-ink-secondary">{{ f.pilotName || '—' }}</td>
|
||||
<td class="px-5 py-3">
|
||||
<button
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11px] font-semibold"
|
||||
:class="badgeClass[flightBadge(f).tone]"
|
||||
@click="toggleRow(f.id)"
|
||||
>
|
||||
<Icon v-if="flightBadge(f).tone === 'danger'" name="alertTriangle" :size="12" />
|
||||
<Icon v-else-if="flightBadge(f).tone === 'success'" name="check" :size="12" />
|
||||
{{ flightBadge(f).label }}
|
||||
</button>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-5 py-3 text-right">
|
||||
<template v-if="confirmFlightId === f.id">
|
||||
<span class="mr-2 text-xs text-ink-muted">Delete?</span>
|
||||
<button class="btn-ghost mr-1" @click="confirmFlightId = ''">Cancel</button>
|
||||
<button class="rounded bg-danger px-3 py-1.5 text-sm font-semibold text-white transition hover:brightness-110" @click="removeFlight(f)">Delete</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn-ghost mr-1 inline-flex items-center gap-1" @click="editFlight(f)"><Icon name="sliders" :size="13" /> Edit</button>
|
||||
<button class="btn-ghost inline-flex items-center gap-1" @click="confirmFlightId = f.id"><Icon name="trash" :size="13" /></button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="openRow === f.id" class="border-b border-line bg-surface-2">
|
||||
<td colspan="7" class="px-5 py-3">
|
||||
<div class="flex flex-wrap gap-x-8 gap-y-1.5 text-xs">
|
||||
<span class="text-ink-secondary">Logging path: <b class="text-ink">{{ f.compliance?.loggingPath || '—' }}</b></span>
|
||||
<span class="text-ink-secondary">Category: <b class="text-ink">{{ f.category || '—' }}</b></span>
|
||||
<span class="text-ink-secondary">Retain until: <b class="font-mono text-ink">{{ (f.retentionUntil || '').slice(0, 10) || '—' }}</b></span>
|
||||
<span v-if="f.compliance?.exempt" class="text-ink-secondary">Exempt: <b class="text-ink">{{ f.compliance.exemptReason }}</b></span>
|
||||
</div>
|
||||
<ul v-if="(f.compliance?.redFlags || []).length" class="mt-2 space-y-1">
|
||||
<li v-for="(rf, i) in f.compliance.redFlags" :key="i" class="flex items-start gap-2 text-xs text-danger-fg">
|
||||
<Icon name="alertTriangle" :size="13" class="mt-px shrink-0" /> {{ rf }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="!f.compliance?.exempt" class="mt-2 text-xs text-success-fg">No compliance gaps detected.</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ============ DRONES ============ -->
|
||||
<template v-else>
|
||||
<div v-if="showDroneForm" class="panel p-5">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div class="eyebrow">{{ editingDroneId ? 'Edit drone' : 'New drone' }}</div>
|
||||
<div class="mt-0.5 text-base font-semibold text-ink">Aircraft registry</div>
|
||||
</div>
|
||||
<button class="btn-icon" @click="cancelDrone"><Icon name="x" :size="16" /></button>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3 max-[760px]:grid-cols-1">
|
||||
<label class="block"><span class="eyebrow mb-1 block">Name</span>
|
||||
<input v-model="droneForm.name" class="field" placeholder="Mavic-01" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Model</span>
|
||||
<input v-model="droneForm.model" class="field" placeholder="DJI Mavic 3 Enterprise" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Serial</span>
|
||||
<input v-model="droneForm.serial" class="field" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">Operator no.</span>
|
||||
<input v-model="droneForm.operatorNumber" class="field" placeholder="DNK…" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">MTOM (grams)</span>
|
||||
<input v-model="droneForm.mtomGrams" type="number" min="0" class="field" placeholder="920" /></label>
|
||||
<label class="block"><span class="eyebrow mb-1 block">C-class</span>
|
||||
<select v-model="droneForm.cClass" class="field">
|
||||
<option v-for="c in C_CLASSES" :key="c" :value="c">{{ c || '— none —' }}</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-6">
|
||||
<label class="flex items-center gap-2 text-sm text-ink-secondary">
|
||||
<input v-model="droneForm.autologsFlights" type="checkbox" class="h-4 w-4 accent-[var(--accent)]" />
|
||||
Auto-logs flights (onboard FDR)
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-ink-secondary">
|
||||
<input v-model="droneForm.isToy" type="checkbox" class="h-4 w-4 accent-[var(--accent)]" />
|
||||
Toy drone (logbook-exempt)
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<button class="btn-accent" :disabled="savingDrone" @click="saveDrone">
|
||||
{{ savingDrone ? 'Saving…' : editingDroneId ? 'Save changes' : 'Add drone' }}
|
||||
</button>
|
||||
<button class="btn-ghost" @click="cancelDrone">Cancel</button>
|
||||
<span v-if="droneMsg" class="text-sm text-danger-fg">{{ droneMsg }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel overflow-hidden p-0">
|
||||
<div v-if="loading" class="px-5 py-12 text-center text-sm text-ink-muted">Loading…</div>
|
||||
<div v-else-if="!drones.length" class="grid place-items-center px-5 py-16 text-center">
|
||||
<Icon name="drone" :size="26" class="text-ink-muted" />
|
||||
<div class="mt-3 text-sm font-medium text-ink-secondary">No drones registered</div>
|
||||
<div class="mt-1 text-xs text-ink-muted">Register the airframes you fly to log flights against them.</div>
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="text-left">
|
||||
<th v-for="h in ['Name', 'Model', 'MTOM', 'Class', 'FDR', '']" :key="h"
|
||||
class="border-y border-line bg-surface-2 px-5 py-2.5 font-mono text-[10.5px] font-normal uppercase tracking-caps text-ink-muted">
|
||||
{{ h }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="d in drones" :key="d.id" class="border-b border-line last:border-0" :class="editingDroneId === d.id ? 'bg-accent-soft' : ''">
|
||||
<td class="px-5 py-3 font-semibold text-ink">{{ d.name }}</td>
|
||||
<td class="px-5 py-3 text-ink-secondary">{{ d.model || '—' }}</td>
|
||||
<td class="px-5 py-3 font-mono text-ink-secondary">{{ d.mtomGrams ? d.mtomGrams + ' g' : '—' }}</td>
|
||||
<td class="px-5 py-3">
|
||||
<span v-if="d.cClass" class="inline-flex rounded-full px-2 py-0.5 text-[11px] font-semibold" :class="badgeClass.accent">{{ d.cClass }}</span>
|
||||
<span v-else class="text-ink-muted">—</span>
|
||||
<span v-if="d.isToy" class="ml-1 inline-flex rounded-full px-2 py-0.5 text-[11px] font-semibold" :class="badgeClass.neutral">toy</span>
|
||||
</td>
|
||||
<td class="px-5 py-3">
|
||||
<span class="text-xs" :class="d.autologsFlights ? 'text-success-fg' : 'text-ink-muted'">{{ d.autologsFlights ? 'yes' : 'no' }}</span>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-5 py-3 text-right">
|
||||
<template v-if="confirmDroneId === d.id">
|
||||
<span class="mr-2 text-xs text-ink-muted">Delete?</span>
|
||||
<button class="btn-ghost mr-1" @click="confirmDroneId = ''">Cancel</button>
|
||||
<button class="rounded bg-danger px-3 py-1.5 text-sm font-semibold text-white transition hover:brightness-110" @click="removeDrone(d)">Delete</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="btn-ghost mr-1 inline-flex items-center gap-1" @click="editDrone(d)"><Icon name="sliders" :size="13" /> Edit</button>
|
||||
<button class="btn-ghost inline-flex items-center gap-1" @click="confirmDroneId = d.id"><Icon name="trash" :size="13" /></button>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user