mirror of
https://github.com/cupcakearmy/ora.git
synced 2024-12-22 08:06:28 +00:00
progress
This commit is contained in:
parent
ee29c0b25b
commit
1acc0d4244
@ -30,7 +30,8 @@
|
|||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
"js": ["./src/client/index.js"]
|
"js": ["./src/client/index.js"],
|
||||||
|
"css": ["./src/client/index.css"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"web_accessible_resources": ["./icons/watch.png", "./icons/watch-alt.png", "./src/dashboard/index.html"]
|
"web_accessible_resources": ["./icons/watch.png", "./icons/watch-alt.png", "./src/dashboard/index.html"]
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"pretty-bytes": "^5.6.0",
|
||||||
"spectre.css": "^0.5.9",
|
"spectre.css": "^0.5.9",
|
||||||
"svelte-spa-router": "^3.2.0",
|
"svelte-spa-router": "^3.2.0",
|
||||||
"tailwindcss": "^1.9.6",
|
"tailwindcss": "^1.9.6",
|
||||||
|
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@ -14,6 +14,7 @@ specifiers:
|
|||||||
npm-run-all: ^4.1.5
|
npm-run-all: ^4.1.5
|
||||||
parcel: ^2.0.1
|
parcel: ^2.0.1
|
||||||
parcel-transformer-svelte: ^1.2.3
|
parcel-transformer-svelte: ^1.2.3
|
||||||
|
pretty-bytes: ^5.6.0
|
||||||
spectre.css: ^0.5.9
|
spectre.css: ^0.5.9
|
||||||
svelte: ^3.44.2
|
svelte: ^3.44.2
|
||||||
svelte-spa-router: ^3.2.0
|
svelte-spa-router: ^3.2.0
|
||||||
@ -29,6 +30,7 @@ dependencies:
|
|||||||
file-saver: 2.0.5
|
file-saver: 2.0.5
|
||||||
joi: 17.4.2
|
joi: 17.4.2
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
|
pretty-bytes: 5.6.0
|
||||||
spectre.css: 0.5.9
|
spectre.css: 0.5.9
|
||||||
svelte-spa-router: 3.2.0
|
svelte-spa-router: 3.2.0
|
||||||
tailwindcss: 1.9.6
|
tailwindcss: 1.9.6
|
||||||
@ -5707,6 +5709,11 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pretty-bytes/5.6.0:
|
||||||
|
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pretty-hrtime/1.0.3:
|
/pretty-hrtime/1.0.3:
|
||||||
resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=}
|
resolution: {integrity: sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
@ -48,5 +48,11 @@ setInterval(deleteOldLogs, 5 * 60 * 1000) // Delete old logs every 5 minutes
|
|||||||
setInterval(log, frequency)
|
setInterval(log, frequency)
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
return getUsageForHost(message).then((percentages) => percentagesToBool(percentages))
|
switch (message.type) {
|
||||||
|
case 'check':
|
||||||
|
return getUsageForHost(message.host).then((percentages) => percentagesToBool(percentages))
|
||||||
|
case 'report':
|
||||||
|
DB.settings.put({ key: 'lastActivity', value: new Date() })
|
||||||
|
break
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
18
src/client/index.css
Normal file
18
src/client/index.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.ora--wrapper {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
color: #111;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #eee;
|
||||||
|
z-index: 999999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.ora--wrapper {
|
||||||
|
background-color: #000000;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,10 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
|
|
||||||
let wrapper
|
let wrapper
|
||||||
|
let lastReported = 0
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
wrapper = window.document.createElement('div')
|
wrapper = window.document.createElement('div')
|
||||||
Object.assign(wrapper.style, {
|
|
||||||
display: 'none',
|
|
||||||
position: 'fixed',
|
|
||||||
color: '#000',
|
|
||||||
top: '0',
|
|
||||||
left: '0',
|
|
||||||
width: '100vw',
|
|
||||||
height: '100vh',
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
zIndex: '999999999',
|
|
||||||
})
|
|
||||||
wrapper.classList.add('ora--wrapper')
|
wrapper.classList.add('ora--wrapper')
|
||||||
wrapper.classList.add('hidden')
|
wrapper.classList.add('hidden')
|
||||||
|
|
||||||
@ -35,10 +25,27 @@ function init() {
|
|||||||
|
|
||||||
async function check() {
|
async function check() {
|
||||||
if (window.document.hidden) return
|
if (window.document.hidden) return
|
||||||
const isBlocked = await browser.runtime.sendMessage(window.location.host)
|
const isBlocked = await browser.runtime.sendMessage({
|
||||||
|
type: 'check',
|
||||||
|
host: window.location.host,
|
||||||
|
})
|
||||||
wrapper.style.display = isBlocked ? 'initial' : 'none'
|
wrapper.style.display = isBlocked ? 'initial' : 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
init()
|
init()
|
||||||
setInterval(check, 2000)
|
|
||||||
check()
|
check()
|
||||||
|
setInterval(check, 2000)
|
||||||
|
|
||||||
|
function logActivity() {
|
||||||
|
const now = Date.now()
|
||||||
|
// Limit reports to once every second
|
||||||
|
if (now - lastReported < 1000) return
|
||||||
|
lastReported = now
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
type: 'report',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
window.document.addEventListener('mousemove', logActivity, false)
|
||||||
|
window.document.addEventListener('keydown', logActivity, false)
|
||||||
|
window.document.addEventListener('scroll', logActivity, false)
|
||||||
|
14
src/dashboard/components/DurationInput.svelte
Normal file
14
src/dashboard/components/DurationInput.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if Array.isArray(value)}
|
||||||
|
<input type="number" class="form-input" placeholder="1" bind:value={value[0]} />
|
||||||
|
<select class="form-select" bind:value={value[1]}>
|
||||||
|
<option value="m">Minutes</option>
|
||||||
|
<option value="h">Hours</option>
|
||||||
|
<option value="d">Days</option>
|
||||||
|
<option value="w">Weeks</option>
|
||||||
|
<option value="M">Months</option>
|
||||||
|
</select>
|
||||||
|
{/if}
|
@ -1,9 +1,10 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<a href="https://github.com/cupcakearmy/ora" target="_blank" rel="noreferrer">Source Code</a>
|
<small>
|
||||||
- v0.8
|
Made with ❤️ by
|
||||||
<br />
|
<a href="https://nicco.io" target="_blank" rel="noreferrer">🐘</a>
|
||||||
Made with ❤️ by
|
— v0.8 —
|
||||||
<a href="https://nicco.io" target="_blank" rel="noreferrer">🐘</a>
|
<a href="https://github.com/cupcakearmy/ora" target="_blank" rel="noreferrer">Source Code</a>
|
||||||
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -19,17 +19,23 @@
|
|||||||
end = new Date()
|
end = new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const intervals = [
|
||||||
|
{ label: 'Year', set: set('year', 1) },
|
||||||
|
{ label: 'Month', set: set('month', 1) },
|
||||||
|
{ label: 'Week', set: set('week', 1) },
|
||||||
|
{ label: '3 Days', set: set('day', 3) },
|
||||||
|
{ label: 'Today', set: set('day', 0) },
|
||||||
|
]
|
||||||
|
|
||||||
// Init
|
// Init
|
||||||
onMount(() => set('day', 0)())
|
onMount(() => set('day', 0)())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm" on:click={all}>All</button>
|
{#each intervals as interval (interval.label)}
|
||||||
<button class="btn btn-sm" on:click={set('month', 1)}>Month</button>
|
<button class="btn btn-sm" on:click={interval.set}>{interval.label}</button>
|
||||||
<button class="btn btn-sm" on:click={set('week', 1)}>Week</button>
|
{/each}
|
||||||
<button class="btn btn-sm" on:click={set('day', 3)}>3 Days</button>
|
|
||||||
<button class="btn btn-sm" on:click={set('day', 0)}>Today</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
@ -3,11 +3,14 @@
|
|||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
import { DB } from '../../shared/db'
|
import { DB } from '../../shared/db'
|
||||||
|
import { checkForErrors, LimitValidator } from '../../shared/validation'
|
||||||
|
import DurationInput from './DurationInput.svelte'
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const init = { limit: ['1', 'h'], every: [1, 'd'] }
|
const init = { limit: ['1', 'h'], every: [1, 'd'] }
|
||||||
|
|
||||||
export let limit = null
|
export let limit = null
|
||||||
|
export let error = null
|
||||||
$: active = limit !== null
|
$: active = limit !== null
|
||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
@ -23,6 +26,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
|
const errors = checkForErrors(LimitValidator, limit)
|
||||||
|
if (errors) {
|
||||||
|
error = errors
|
||||||
|
return
|
||||||
|
}
|
||||||
await DB.limits.put(limit)
|
await DB.limits.put(limit)
|
||||||
dispatch('update')
|
dispatch('update')
|
||||||
close()
|
close()
|
||||||
@ -45,25 +53,11 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="form-label">Rules</div>
|
<div class="form-label">Rules</div>
|
||||||
{#each limit.rules as { limit, every }, i}
|
{#each limit.rules as rule, i}
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<input type="text" class="form-input" placeholder="1" bind:value={limit[0]} />
|
<DurationInput bind:value={rule.limit} />
|
||||||
<select class="form-select" bind:value={limit[1]}>
|
|
||||||
<option value="m">Minutes</option>
|
|
||||||
<option value="h">Hours</option>
|
|
||||||
<option value="d">Days</option>
|
|
||||||
<option value="w">Weeks</option>
|
|
||||||
<option value="M">Months</option>
|
|
||||||
</select>
|
|
||||||
<span class="input-group-addon">every</span>
|
<span class="input-group-addon">every</span>
|
||||||
<input type="text" class="form-input" bind:value={every[0]} />
|
<DurationInput bind:value={rule.every} />
|
||||||
<select class="form-select" bind:value={every[1]}>
|
|
||||||
<option value="m">Minutes</option>
|
|
||||||
<option value="h">Hours</option>
|
|
||||||
<option value="d">Days</option>
|
|
||||||
<option value="w">Weeks</option>
|
|
||||||
<option value="M">Months</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-error input-group-btn" on:click={del(i)}>X</button>
|
<button class="btn btn-error input-group-btn" on:click={del(i)}>X</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
@ -73,6 +67,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div>
|
<div>
|
||||||
|
{#if error}
|
||||||
|
<span class="text-error">{error}</span>
|
||||||
|
{/if}
|
||||||
<button on:click={close} class="btn">Cancel</button>
|
<button on:click={close} class="btn">Cancel</button>
|
||||||
<button on:click={save} class="btn btn-primary">Save</button>
|
<button on:click={save} class="btn btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
17
src/dashboard/components/StorageQuota.svelte
Normal file
17
src/dashboard/components/StorageQuota.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<script>
|
||||||
|
import pretty from 'pretty-bytes'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
let usage = null
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const estimate = await window.navigator.storage.estimate()
|
||||||
|
usage = pretty(estimate.usage)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if usage === null}
|
||||||
|
<span class="loading" />
|
||||||
|
{:else}
|
||||||
|
<span>Storage used: <span class="font-mono">{usage}</span></span>
|
||||||
|
{/if}
|
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
import { cloneDeep } from 'lodash'
|
||||||
|
|
||||||
import RulesEditor from '../components/RulesEditor.svelte'
|
import RulesEditor from '../components/RulesEditor.svelte'
|
||||||
import Rules from '../components/Rules.svelte'
|
import Rules from '../components/Rules.svelte'
|
||||||
@ -15,7 +16,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edit(id) {
|
function edit(id) {
|
||||||
limit = limits.find((limit) => limit.id === id)
|
limit = cloneDeep(limits.find((limit) => limit.id === id))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
@ -1,93 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import YourData from '../views/YourData.svelte'
|
||||||
import browser from 'webextension-polyfill'
|
import Settings from '../views/Settings.svelte'
|
||||||
import { saveAs } from 'file-saver'
|
|
||||||
import dj from 'dayjs'
|
|
||||||
|
|
||||||
import FileUpload from '../components/FileUpload.svelte'
|
|
||||||
|
|
||||||
import { dump as dumpDB, load as loadDB, clear as clearDB, validate } from '../../shared/db'
|
|
||||||
import { longPress } from '../../shared/lib'
|
|
||||||
|
|
||||||
const DEFAULT = {
|
|
||||||
retention: 90,
|
|
||||||
}
|
|
||||||
|
|
||||||
let settings = DEFAULT
|
|
||||||
let uploaded
|
|
||||||
let disabled = true
|
|
||||||
|
|
||||||
async function read() {
|
|
||||||
settings = {
|
|
||||||
...DEFAULT,
|
|
||||||
...(await browser.storage.local.get()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function write() {
|
|
||||||
return browser.storage.local.set(settings)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
|
||||||
await browser.storage.local.clear()
|
|
||||||
await read()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function dump() {
|
|
||||||
const data = await dumpDB()
|
|
||||||
const blob = new Blob([JSON.stringify(data)], { type: 'application/json;charset=utf-8' })
|
|
||||||
const filename = `Ora [${dj().format('YYYY-MM-DD HH-mm-ss')}].json`
|
|
||||||
saveAs(blob, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function clear() {
|
|
||||||
await clearDB()
|
|
||||||
alert('Done')
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
try {
|
|
||||||
await loadDB(uploaded)
|
|
||||||
alert('Imported')
|
|
||||||
} catch {
|
|
||||||
alert('Error importing')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
disabled = !validate(uploaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(read)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h2 class="mt-8 text-2xl">Settings</h2>
|
<Settings />
|
||||||
<form class="mt-2" on:submit|preventDefault={write}>
|
<YourData />
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">
|
|
||||||
Retention
|
|
||||||
<small>(Days)</small>
|
|
||||||
<input
|
|
||||||
id="retention"
|
|
||||||
class="form-input"
|
|
||||||
type="number"
|
|
||||||
min="3"
|
|
||||||
max="365"
|
|
||||||
step="1"
|
|
||||||
bind:value={settings.retention}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="mt-2">
|
|
||||||
<button type="reset" class="btn" on:click={reset}>Reset</button>
|
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<h2 class="mt-8 text-2xl">Your Data</h2>
|
|
||||||
<div class="mt-2">
|
|
||||||
<FileUpload bind:value={uploaded} />
|
|
||||||
<button class="btn btn-primary" on:click={load} {disabled}>Import</button>
|
|
||||||
<button class="btn btn-primary" on:click={dump}>Export</button>
|
|
||||||
<button class="btn btn-error tooltip" data-tooltip="Hold to delete" use:longPress={clear}>Delete all data</button>
|
|
||||||
</div>
|
|
||||||
|
47
src/dashboard/views/Settings.svelte
Normal file
47
src/dashboard/views/Settings.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
|
import { DB } from '../../shared/db'
|
||||||
|
import { SettingsValidator } from '../../shared/validation'
|
||||||
|
|
||||||
|
let settings = null
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const values = await DB.settings.toArray()
|
||||||
|
const fromDB = Object.fromEntries(values.map((v) => [v.key, v.value]))
|
||||||
|
settings = SettingsValidator.validate(fromDB).value
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
for (const [key, value] of Object.entries(settings)) {
|
||||||
|
await DB.settings.put({ key, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(load)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2 class="mt-8 text-2xl">Settings</h2>
|
||||||
|
{#if settings}
|
||||||
|
<form class="mt-2" on:submit|preventDefault={save}>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Retention
|
||||||
|
<small>(Days)</small>
|
||||||
|
<input class="form-input" type="number" min="3" max="365" step="1" bind:value={settings.retention} />
|
||||||
|
</label>
|
||||||
|
<label class="form-label">
|
||||||
|
Idle Timeout
|
||||||
|
<small>(Minutes)</small>
|
||||||
|
<input class="form-input" type="number" min="0" step="1" bind:value={settings.idleTimeout} />
|
||||||
|
<p>Stop tracking after a certain period of idle behavior. <span class="font-mono">0</span> to disable.</p>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<div class="loading loading-lg" />
|
||||||
|
{/if}
|
49
src/dashboard/views/YourData.svelte
Normal file
49
src/dashboard/views/YourData.svelte
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script>
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import dj from 'dayjs'
|
||||||
|
|
||||||
|
import FileUpload from '../components/FileUpload.svelte'
|
||||||
|
|
||||||
|
import { dump, load, clear } from '../../shared/db'
|
||||||
|
import { checkForErrors, DBValidator } from '../../shared/validation'
|
||||||
|
import { longPress } from '../../shared/lib'
|
||||||
|
|
||||||
|
let uploaded
|
||||||
|
|
||||||
|
async function exportDB() {
|
||||||
|
const data = await dump()
|
||||||
|
const blob = new Blob([JSON.stringify(data)], { type: 'application/json;charset=utf-8' })
|
||||||
|
const filename = `Ora [${dj().format('YYYY-MM-DD HH-mm-ss')}].json`
|
||||||
|
saveAs(blob, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearDB() {
|
||||||
|
await clear()
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importDB() {
|
||||||
|
try {
|
||||||
|
await load(uploaded)
|
||||||
|
alert('Imported')
|
||||||
|
} catch {
|
||||||
|
alert('Error importing')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: disabled = uploaded && !checkForErrors(DBValidator, uploaded)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2 class="mt-8 text-2xl">Your Data</h2>
|
||||||
|
<div class="mt-2">
|
||||||
|
<FileUpload bind:value={uploaded} />
|
||||||
|
<button class="btn btn-primary" on:click={importDB} {disabled}>
|
||||||
|
{#if uploaded && disabled}
|
||||||
|
Invalid data
|
||||||
|
{:else}
|
||||||
|
Import
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" on:click={exportDB}>Export</button>
|
||||||
|
<button class="btn btn-error tooltip" data-tooltip="Hold to delete" use:longPress={clearDB}>Delete all data</button>
|
||||||
|
</div>
|
@ -2,17 +2,23 @@ import dj from 'dayjs'
|
|||||||
import Dexie from 'dexie'
|
import Dexie from 'dexie'
|
||||||
import RelativeTime from 'dayjs/plugin/relativeTime'
|
import RelativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import Duration from 'dayjs/plugin/duration'
|
import Duration from 'dayjs/plugin/duration'
|
||||||
import Joi from 'joi'
|
|
||||||
|
import { checkForErrors, DBValidator } from './validation'
|
||||||
|
|
||||||
dj.extend(Duration)
|
dj.extend(Duration)
|
||||||
dj.extend(RelativeTime)
|
dj.extend(RelativeTime)
|
||||||
|
|
||||||
export const DB = new Dexie('ora')
|
export const DB = new Dexie('ora')
|
||||||
|
|
||||||
DB.version(2).stores({
|
DB.version(2).stores({
|
||||||
logs: `++id, host, timestamp`,
|
logs: `++id, host, timestamp`,
|
||||||
limits: `++id, host`,
|
limits: `++id, host`,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
DB.version(3).stores({
|
||||||
|
settings: `key, value`,
|
||||||
|
})
|
||||||
|
|
||||||
export function normalizeTimestamp(timestamp) {
|
export function normalizeTimestamp(timestamp) {
|
||||||
// Normalize every dato to 15 minutes
|
// Normalize every dato to 15 minutes
|
||||||
const t = dj(timestamp)
|
const t = dj(timestamp)
|
||||||
@ -43,36 +49,8 @@ export async function dump() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validate(data) {
|
|
||||||
const schema = Joi.object({
|
|
||||||
limits: Joi.array().items(
|
|
||||||
Joi.object({
|
|
||||||
host: Joi.string(),
|
|
||||||
id: Joi.number(),
|
|
||||||
rules: Joi.array().items(
|
|
||||||
Joi.object({
|
|
||||||
limit: Joi.array().items(Joi.string(), Joi.number()),
|
|
||||||
every: Joi.array().items(Joi.string(), Joi.number()),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
logs: Joi.array().items(
|
|
||||||
Joi.object({
|
|
||||||
host: Joi.string(),
|
|
||||||
id: Joi.number(),
|
|
||||||
seconds: Joi.number(),
|
|
||||||
timestamp: Joi.string(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
const validated = schema.validate(data, { presence: 'required' })
|
|
||||||
return !validated.error
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function load(data) {
|
export async function load(data) {
|
||||||
if (!validate(data)) throw new Error('Invalid data')
|
if (!checkForErrors(DBValidator, data)) throw new Error('Invalid data')
|
||||||
|
|
||||||
await clear()
|
await clear()
|
||||||
await DB.limits.bulkAdd(data.limits)
|
await DB.limits.bulkAdd(data.limits)
|
||||||
@ -83,3 +61,8 @@ export async function load(data) {
|
|||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateOrSet(table, key, value) {
|
||||||
|
// const updated = await table.update(key, value)
|
||||||
|
// if(updated === 0) await table.
|
||||||
|
}
|
||||||
|
43
src/shared/validation.js
Normal file
43
src/shared/validation.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import Joi from 'joi'
|
||||||
|
|
||||||
|
export const DurationTupleValidator = Joi.array().length(2).ordered(Joi.number(), Joi.string())
|
||||||
|
|
||||||
|
export const LimitValidator = Joi.object({
|
||||||
|
host: Joi.string().domain(),
|
||||||
|
id: Joi.number().optional(),
|
||||||
|
rules: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
limit: DurationTupleValidator,
|
||||||
|
every: DurationTupleValidator,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const LogValidator = Joi.object({
|
||||||
|
host: Joi.string(),
|
||||||
|
id: Joi.number().optional(),
|
||||||
|
seconds: Joi.number(),
|
||||||
|
timestamp: Joi.date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SettingsValidator = Joi.object({
|
||||||
|
lastActivity: Joi.date()
|
||||||
|
.default(() => new Date())
|
||||||
|
.optional(), // Last user activity, to calculate idle time
|
||||||
|
retention: Joi.number().default(90).optional(), // Days to keep logs
|
||||||
|
idleTimeout: DurationTupleValidator.default(5).optional(), // Idle timeout in minutes
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DBValidator = Joi.object({
|
||||||
|
limits: Joi.array().items(LimitValidator),
|
||||||
|
logs: Joi.array().items(LogValidator),
|
||||||
|
settings: SettingsValidator.optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function checkForErrors(validator, data) {
|
||||||
|
const validated = validator.validate(data, { presence: 'required' })
|
||||||
|
if (validated.error) {
|
||||||
|
console.error('Validation error', validated.error)
|
||||||
|
}
|
||||||
|
return validated.error
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user