2026-01-22 17:55:23 +01:00
|
|
|
<!doctype html>
|
|
|
|
|
<html>
|
|
|
|
|
<head>
|
|
|
|
|
<title>Direktil Local Server</title>
|
|
|
|
|
<base href="/ui/" />
|
|
|
|
|
|
|
|
|
|
<link rel="icon" href="favicon.ico" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-25 11:22:11 +01:00
|
|
|
<style>@import url('/ui/style.css');@import url('/ui/app.css');</style>
|
2026-01-22 17:55:23 +01:00
|
|
|
<script src="/ui/jsonpatch.min-942279a1c4209cc2.js" integrity="sha384-GcPrkRS12jtrElEkbJcrZ8asvvb9s3mc+CUq9UW/8bL4L0bpkmh9M+oFnWN6qLq2"></script>
|
2026-06-17 22:40:44 +02:00
|
|
|
<script src="/ui/app-4f0c6935250753e9.js" defer integrity="sha384-SrAfGs5pnGcgL5E78t2MszNYmdkGME9yPgtPsDg61Rljw+dfiP+rabjPnrMkWDUH" type="module"></script>
|
|
|
|
|
<script src="/ui/Downloads-3c8cba0572aebfae.js" integrity="sha384-Pof/sPwhdqKGp7NUmYkDc0pSSyACabwLjkRRylZKHxRmiHBV3SGomPUMAWG9y7Ix"></script>
|
|
|
|
|
<script src="/ui/GetCopy-2e04b7b63750e25a.js" integrity="sha384-sNvTYXhjog3BkYL20RzrUWIG9VGyHNO8oEiP5oxL5f2WKmLj/03fdR79FgHfzlj+"></script>
|
|
|
|
|
<script src="/ui/ClusterAccess-8b4165435ba4fcac.js" integrity="sha384-C1mVnhsaxmn3Dpp1vtcBuctxxXI1Rlh+qmxicCIpNv7ukyTbB524pg0ZwhvAnfH7"></script>
|
|
|
|
|
<script src="/ui/ClusterCAs-4f0a381bfb0c1b88.js" integrity="sha384-QcSHKzEHNSCqyjFELXfYFAdfmIZZwJHDzPqI2prH47jHrkeomJD60rcEMLkVAO3o"></script>
|
|
|
|
|
<script src="/ui/Cluster-34a012ee0910827c.js" integrity="sha384-Q0sXP0WFJu4gHOI/8NMOGwpJvvFDNBR3T/vDCYxCWL2szAu4+sQuwHm81Jcad/ve"></script>
|
2026-01-22 17:55:23 +01:00
|
|
|
<script src="/ui/Host-61916516a854adff.js" integrity="sha384-/wh3KrC0sb4MT7ekO2U84rswxI42WSH/0jB4dbDaaGaGhX60xTEZHFsdQAf7UgTG"></script>
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
<div id="app">
|
|
|
|
|
<header>
|
2026-06-17 22:40:44 +02:00
|
|
|
<span id="logo"><img src="favicon.ico" /> Direktil Local Server</span>
|
|
|
|
|
<span class="utils">
|
2026-01-22 17:55:23 +01:00
|
|
|
<span id="login-hdr" v-if="session.token">
|
|
|
|
|
Logged in
|
|
|
|
|
<button class="link" @click="copyText(session.token)">🗐</button>
|
|
|
|
|
</span>
|
|
|
|
|
<span>server <code>{{ serverVersion || '-----' }}</code></span>
|
|
|
|
|
<span>ui <code>{{ uiHash || '-----' }}</code></span>
|
|
|
|
|
<span :class="publicState ? 'green' : 'red'">🗲</span>
|
2026-06-17 22:40:44 +02:00
|
|
|
</span>
|
2026-01-22 17:55:23 +01:00
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="error" v-if="error">
|
|
|
|
|
<button class="btn-close" @click="error=null">×</button>
|
|
|
|
|
<div class="code" v-if="error.code">{{ error.code }}</div>
|
|
|
|
|
<div class="message">{{ error.message }}</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-17 22:40:44 +02:00
|
|
|
<div class="toasts"><div v-for="t in toasts" :class="'toast '+t.kind" @click="dismissToast(t.id)">{{ t.message }}</div></div>
|
|
|
|
|
|
|
|
|
|
<main class="content" v-if="!publicState">
|
2026-01-22 17:55:23 +01:00
|
|
|
<p>Not connected.</p>
|
2026-06-17 22:40:44 +02:00
|
|
|
</main>
|
2026-01-22 17:55:23 +01:00
|
|
|
|
2026-06-17 22:40:44 +02:00
|
|
|
<main class="content" v-else-if="publicState.Store.New">
|
2026-01-22 17:55:23 +01:00
|
|
|
<p>Store is new.</p>
|
|
|
|
|
<p>Option 1: initialize a new store</p>
|
|
|
|
|
<form @submit="unlockStore">
|
|
|
|
|
<input type="text" v-model="forms.store.name" name="name" placeholder="Name" /><br/>
|
|
|
|
|
<input type="password" v-model="forms.store.pass1" name="passphrase" required placeholder="Passphrase" />
|
|
|
|
|
<input type="password" v-model="forms.store.pass2" required placeholder="Passphrase confirmation" />
|
|
|
|
|
<input type="submit" value="initialize" :disabled="!forms.store.pass1 || forms.store.pass1 != forms.store.pass2" />
|
|
|
|
|
</form>
|
|
|
|
|
<p>Option 2: upload a previously downloaded store</p>
|
|
|
|
|
<form @submit="uploadStore">
|
|
|
|
|
<input type="file" ref="storeUpload" />
|
|
|
|
|
<input type="submit" value="upload" />
|
|
|
|
|
</form>
|
2026-06-17 22:40:44 +02:00
|
|
|
</main>
|
2026-01-22 17:55:23 +01:00
|
|
|
|
2026-06-17 22:40:44 +02:00
|
|
|
<main class="content" v-else-if="!publicState.Store.Open">
|
2026-01-22 17:55:23 +01:00
|
|
|
<p>Store is not open.</p>
|
|
|
|
|
<form @submit="unlockStore">
|
|
|
|
|
<input type="password" name="passphrase" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
|
|
|
|
<input type="submit" value="unlock" :disabled="!forms.store.pass1" />
|
|
|
|
|
</form>
|
2026-06-17 22:40:44 +02:00
|
|
|
</main>
|
2026-01-22 17:55:23 +01:00
|
|
|
|
2026-06-17 22:40:44 +02:00
|
|
|
<main class="content" v-else-if="!state">
|
2026-01-22 17:55:23 +01:00
|
|
|
<p v-if="!session.token">Not logged in.</p>
|
|
|
|
|
<p v-else>Invalid token</p>
|
|
|
|
|
|
|
|
|
|
<form @submit="unlockStore">
|
|
|
|
|
<input type="password" v-model="forms.store.pass1" required placeholder="Passphrase" />
|
|
|
|
|
<input type="submit" value="log in"/>
|
|
|
|
|
</form>
|
2026-06-17 22:40:44 +02:00
|
|
|
</main>
|
2026-01-22 17:55:23 +01:00
|
|
|
|
|
|
|
|
<template v-else>
|
2026-06-17 22:40:44 +02:00
|
|
|
<nav class="sidebar">
|
|
|
|
|
<input type="search" placeholder="Filter" v-model="viewFilter"/>
|
|
|
|
|
<p class="nav-section">Admin</p>
|
|
|
|
|
<p class="view-links"><button type="button" v-for="v in adminViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
|
|
|
|
<p class="nav-section" v-if="clusterViews.length">Clusters</p>
|
|
|
|
|
<p class="view-links" v-if="clusterViews.length"><button type="button" v-for="v in clusterViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
|
|
|
|
<p class="nav-section" v-if="hostViews.length">Hosts</p>
|
|
|
|
|
<p class="view-links" v-if="hostViews.length"><button type="button" v-for="v in hostViews" @click="view = v" :class="{selected: isActive(v)}">{{v.title}}</button></p>
|
|
|
|
|
</nav>
|
|
|
|
|
<main class="content">
|
|
|
|
|
<h2 v-if="view">{{ view.type == 'cluster' ? 'Cluster ' : view.type == 'host' ? 'Host ' : '' }}{{view.title}}</h2>
|
2026-01-22 17:55:23 +01:00
|
|
|
|
|
|
|
|
<div v-if="view.type == 'cluster'" id="clusters">
|
|
|
|
|
<Cluster :cluster="viewObj" :token="session.token" :state="state" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="view.type == 'host'" id="hosts">
|
|
|
|
|
<Host :host="viewObj" :token="session.token" :state="state" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="view.type == 'actions' && view.name == 'admin'">
|
|
|
|
|
<h3>Config</h3>
|
|
|
|
|
<form @submit="uploadConfig">
|
|
|
|
|
<input type="file" ref="configUpload" required />
|
|
|
|
|
<input type="submit" value="upload config" />
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<h3>Store</h3>
|
|
|
|
|
<p><a :href="'/public/store.tar?token='+state.Store.DownloadToken" target="_blank">Download</a></p>
|
|
|
|
|
<form @submit="storeAddKey" action="/store/add-key">
|
|
|
|
|
<p>Add an unlock phrase:</p>
|
|
|
|
|
<input type="text" v-model="forms.store.name" name="name" required placeholder="Name" /><br/>
|
2026-03-16 11:08:16 +01:00
|
|
|
<label><input type="checkbox" v-model="forms.store.byHash"> {{ forms.store.byHash ? "hash (base64)" : "passphrase"}}</label>
|
|
|
|
|
<div v-if="forms.store.byHash">
|
|
|
|
|
<input type="hash" v-model="forms.store.hash" name="passphrase" required placeholder="Hash" />
|
|
|
|
|
{{" "}}generate with <code>dls hash '{{state.Store.Salt}}'</code>
|
|
|
|
|
</div><div v-else>
|
2026-01-22 17:55:23 +01:00
|
|
|
<input type="password" v-model="forms.store.pass1" name="passphrase" autocomplete="new-password" required placeholder="Phrase" />
|
|
|
|
|
<input type="password" v-model="forms.store.pass2" autocomplete="new-password" required placeholder="Phrase confirmation" />
|
2026-03-16 11:08:16 +01:00
|
|
|
</div>
|
|
|
|
|
<input type="submit" value="add unlock phrase" :disabled="!((forms.store.pass1 && forms.store.pass1 == forms.store.pass2) || forms.store.hash)" />
|
2026-01-22 17:55:23 +01:00
|
|
|
</form>
|
|
|
|
|
<form @submit="storeDelKey" action="/store/delete-key">
|
|
|
|
|
<p>Remove an unlock phrase:</p>
|
|
|
|
|
<input type="text" v-model="forms.delKey.name" name="name" required placeholder="Name" />
|
|
|
|
|
<input type="submit" value="remove unlock phrase" />
|
|
|
|
|
|
|
|
|
|
<p v-if="state.Store.KeyNames">Available names:
|
|
|
|
|
<template v-for="k,i in state.Store.KeyNames">{{i?", ":""}}<code @click="forms.delKey.name=k">{{k}}</code></template>.</p>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<template v-if="any(state.HostTemplates) || any(hostsFromTemplate)">
|
|
|
|
|
<h3>Hosts from template</h3>
|
|
|
|
|
<form @submit="hostFromTemplateAdd" action="" v-if="any(state.HostTemplates)">
|
|
|
|
|
<p>Add a host from template instance:</p>
|
|
|
|
|
<input type="text" v-model="forms.hostFromTemplate.name" required placeholder="Name" />
|
|
|
|
|
<select v-model="forms.hostFromTemplate.Template" required>
|
|
|
|
|
<option v-for="name in state.HostTemplates" :value="name">{{name}}</option>
|
|
|
|
|
</select>
|
|
|
|
|
<input type="text" v-model="forms.hostFromTemplate.IP" required placeholder="IP" />
|
|
|
|
|
<input type="submit" value="add instance" />
|
|
|
|
|
</form>
|
|
|
|
|
<form @submit="hostFromTemplateDel" action="" v-if="any(hostsFromTemplate)">
|
|
|
|
|
<p>Remove a host from template instance:</p>
|
|
|
|
|
<select v-model="forms.hostFromTemplateDel" required>
|
|
|
|
|
<option v-for="h in hostsFromTemplate" :value="h.Name">{{h.Name}}</option>
|
|
|
|
|
</select>
|
|
|
|
|
<input type="submit" value="delete instance" :disabled="!forms.hostFromTemplateDel" />
|
|
|
|
|
</form>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
2026-06-17 22:40:44 +02:00
|
|
|
</main>
|
2026-01-22 17:55:23 +01:00
|
|
|
</template>
|
|
|
|
|
</div>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|