Added guides

This commit is contained in:
arabcoders
2025-05-03 21:44:37 +03:00
parent 37f8d51ac1
commit 6f5509f827
15 changed files with 353 additions and 234 deletions

8
FAQ.md
View File

@@ -137,7 +137,7 @@ re-processed accordingly.
To resolve this conflict and sync the backend with your local state:
* Go to the `WebUI > Backends`.
* Under the relevant backend, find the **Frequently used commands** list.
* Under the relevant backend, find the **Quick operations** list.
* Select **3. Force export local play state to this backend.**
This operation will overwrite the backend's watch state with your current local state to bring them back in sync.
@@ -179,7 +179,7 @@ To synchronize both backends correctly:
* Enable **Full Import** for that backend.
* Go to `Tasks` page, and run the **Import** task via `Run via console` button.
* Once the import is complete, **add the second backend** (the one with incorrect or outdated play state).
* Under the newly added backend, locate the **Frequently used commands** section.
* Under the newly added backend, locate the **Quick operations** section.
* Select **3. Force export local play state to this backend.**
This will push your local watch state to the backend and ensure both are in sync.
@@ -195,7 +195,7 @@ local one, the export may be skipped.
To confirm if this is the issue, follow these steps:
1. Go to the `WebUI > Backends`.
2. Under the relevant backend, locate the **Frequently Used Commands** section.
2. Under the relevant backend, locate the **Quick operations** section.
3. Select **7. Run export and save debug log.**
This will generate a log file at `/config/user@backend_name.export.txt` If the file is too large to view in a regular
@@ -218,7 +218,7 @@ If you see messages such as `Backend date is equal or newer than database date.`
To override the date check and force an update do the following:
* Go to the `WebUI > Backends`.
* Under the relevant backend, find the **Frequently used commands** list.
* Under the relevant backend, find the **Quick operations** list.
* Select **3. Force export local play state to this backend.**
This will sync your local database state to the backend, ignoring date comparisons.

View File

@@ -12,7 +12,9 @@
},
"scripts": {
"test": "vendor/bin/phpunit",
"tests": "vendor/bin/phpunit"
"tests": "vendor/bin/phpunit",
"update_ui": "yarn --cwd ./frontend upgrade --latest",
"generate": "yarn --cwd ./frontend run generate"
},
"require": {
"php": "^8.4",

View File

@@ -269,6 +269,7 @@
</p>
</div>
</div>
<div class="field">
<hr>
<label class="label has-text-danger" for="backup_data">
@@ -282,6 +283,22 @@
</p>
</div>
</div>
<div class="field" v-if="backends.length < 1">
<hr>
<label class="label" for="force_import">
Force one time import from this backend?
</label>
<div class="control">
<input id="force_import" type="checkbox" class="switch is-success" v-model="force_import">
<label for="force_import">Yes</label>
<p class="help">
<span class="icon"><i class="fas fa-info-circle"></i></span>
Run a one time import from this backend after adding it.
</p>
</div>
</div>
<div class="field" v-if="backends.length > 0">
<hr>
<label class="label has-text-danger" for="force_export">
@@ -296,6 +313,7 @@
</p>
</div>
</div>
</template>
</div>
@@ -331,7 +349,7 @@ import request from '~/utils/request'
import {awaitElement, explode, notification, ucFirst} from '~/utils/index'
import {useStorage} from "@vueuse/core";
const emit = defineEmits(['addBackend', 'backupData', 'forceExport'])
const emit = defineEmits(['addBackend', 'backupData', 'forceExport', 'forceImport'])
const props = defineProps({
backends: {
@@ -375,6 +393,7 @@ const exposeToken = ref(false)
const error = ref()
const backup_data = ref(true)
const force_export = ref(false)
const force_import = ref(false)
const isLimited = ref(false)
const accessTokenResponse = ref({})
@@ -678,6 +697,10 @@ const addBackend = async () => {
emit('forceExport', backend)
}
if (true === Boolean(force_import?.value ?? false)) {
emit('forceImport', backend)
}
emit('addBackend')
return true

View File

@@ -1,111 +0,0 @@
<template>
<div>
<div v-if="!selectedRequest">
<div class="columns is-multiline">
<div class="column is-12">
<h2 class="title is-4">What are you trying to do?</h2>
</div>
<div v-for="(choice, index) in choices" :key="index" class="column is-6-tablet is-12-mobile">
<div class="box content">
<h3 class="title is-5">{{ choice.title }}</h3>
<p>{{ choice.text }}</p>
<button class="button is-link is-outlined" @click="pickRequest(index)">
<span class="icon"><i class="fas fa-cogs"/></span>
<span>Start</span>
</button>
</div>
</div>
</div>
</div>
<div v-if="selectedRequest">
<div v-for="stepId in steps" :key="stepId" v-show="stepId === currentStep" class="box">
<slot :name="stepId">
<h3 class="title is-5">Step: {{ stepId }}</h3>
<p>Missing a guide for <code>{{ stepId }}</code> step.</p>
</slot>
</div>
<nav class="level is-mobile">
<div class="level-left">
<div class="field is-grouped">
<div class="control is-fullwidth">
<button class="button is-fullwidth is-info" @click="nextStep" :disabled="isLastStep">
Next
</button>
</div>
<div class="control">
<button class="button ml-2" @click="reset" :class="{'is-danger': !isLastStep, 'is-success': isLastStep}">
{{ !isLastStep ? 'Reset' : 'Completed' }}
</button>
</div>
</div>
</div>
<div class="level-right">
<div class="level-item">
Step {{ currentStepIndex + 1 }} of {{ steps.length }}
</div>
</div>
</nav>
<Message message_class="is-background-warning-90 has-text-dark" icon="fas fa-info-circle" title="Important">
Your progress is saved even if you navigate away. So feel free to do the steps and come back after each one.
</Message>
</div>
</div>
</template>
<script setup>
import {useStorage} from '@vueuse/core'
const choices = [
{
title: 'One-way sync',
text: 'For example, You want to import data from plex, and send it to jellyfin/emby but not the other way around',
steps: ['add_backend', 'import_data', 'force_export'],
},
{
title: 'Two-way sync',
text: 'This will allow all backends to sync with each other. I.e. plex to jellyfin, jellyfin to emby, emby to plex.',
steps: ['add_backend', 'import_data', 'force_export', 'enabled_import'],
},
{
title: 'Enable webhooks',
text: 'Step step on how to get webhooks working.',
steps: ['add_backend', 'import_data', 'force_export', 'enabled_import'],
},
{
title: 'Enable Sub-users',
text: 'Guide on how to enable sub-users.',
steps: ['sub_users_main_user', 'sub_user_create'],
},
]
const selectedRequest = useStorage('guide-request', null)
const currentStepIndex = useStorage('guide-step-index', 0)
const steps = computed(() => {
const c = choices[selectedRequest.value] || null
console.log(c)
return c ? choices[selectedRequest.value].steps : []
})
const currentStep = computed(() => steps.value[currentStepIndex.value])
const isLastStep = computed(() => currentStepIndex.value >= steps.value.length - 1)
const pickRequest = request => {
selectedRequest.value = String(request)
currentStepIndex.value = 0
}
const nextStep = () => {
if (!isLastStep.value) {
currentStepIndex.value++
}
}
const reset = () => {
selectedRequest.value = null
currentStepIndex.value = 0
}
</script>

View File

@@ -1,12 +1,24 @@
<template>
<div class="content" v-html="content"></div>
<div>
<div class="columns is-multiline">
<div class="column is-12">
<Message v-if="isLoading" message_class="has-background-info-90 has-text-dark" title="Loading"
icon="fas fa-spinner fa-spin" message="Loading data. Please wait..."/>
<Message v-if="error" message_class="has-background-warning-90 has-text-dark" title="Error"
icon="fas fa-exclamation" :message="error"/>
<div class="content" v-html="content" v-else/>
</div>
</div>
</div>
</template>
<script setup>
import {useStorage} from '@vueuse/core'
import {marked} from 'marked'
import {baseUrl} from 'marked-base-url'
import {disableOpacity, enableOpacity} from "~/utils/index.js";
import Message from "~/components/Message.vue";
const props = defineProps({
file: {
@@ -17,38 +29,59 @@ const props = defineProps({
const content = ref('')
const api_url = useStorage('api_url', '')
const error = ref('')
const isLoading = ref(true)
onMounted(async () => {
const response = await fetch(`${api_url.value}${props.file}?_=${Date.now()}`)
const text = await response.text()
try {
isLoading.value = true
const response = await fetch(`${api_url.value}${props.file}?_=${Date.now()}`)
if (!response.ok) {
const err = await parse_api_response(response)
console.log(err)
error.value = err.error.message
return
}
marked.use({
gfm: true,
hooks: {
postprocess: (text) => {
// -- replace github [! with icon
text = text.replace(/\[!IMPORTANT\]/g, `
<span class="is-block title is-4">
const text = await response.text()
marked.use({
gfm: true,
hooks: {
postprocess: (text) => {
// -- replace GitHub [! with icon
text = text.replace(/\[!IMPORTANT]/g, `
<span class="is-block title mb-2 is-5">
<span class="icon-text">
<span class="icon"><i class="fas fa-exclamation-triangle has-text-danger fa-fade"></i></span>
<span>Important</span>
<span class="icon"><i class="fas fa-exclamation-triangle has-text-danger"></i></span>
<span>IMPORTANT</span>
</span>
</span>`)
text = text.replace(/\[!NOTE\]/g, `
<span class="is-block title is-4">
text = text.replace(/\[!NOTE]/g, `
<span class="is-block title is-5">
<span class="icon-text">
<span class="icon"><i class="fas fa-info-circle has-text-info-50"></i></span>
<span>Note</span>
<span class="icon"><i class="fas fa-info has-text-info-50"></i></span>
<span>NOTE</span>
</span>
</span>`)
return text
}
},
...baseUrl(api_url.value),
});
content.value = marked.parse(text)
text = text.replace(
/<!--\s*?i:([\w.-]+)\s*?-->/g,
(_, list) => `<span class="icon"><i class="fas ${list.split('.').map(n => n.trim()).join(' ')}"></i></span>`
);
return text
}
},
...baseUrl(api_url.value),
});
content.value = String(marked.parse(text))
} catch (e) {
console.error(e)
error.value = e.message
} finally {
isLoading.value = false
}
});

View File

@@ -152,11 +152,19 @@
</div>
</div>
<div class="navbar-end pr-3">
<div class="navbar-item" v-if="hasAPISettings && !showConnection">
<button class="button is-dark" @click="showUserSelection = !showUserSelection" v-tooltip="'Change User'">
<span class="icon"><i class="fas fa-users"/></span>
</button>
</div>
<template v-if="hasAPISettings && !showConnection">
<div class="navbar-item">
<NuxtLink class="button is-dark" v-tooltip="'Guides'" to="/help">
<span class="icon"><i class="fas fa-circle-question"/></span>
</NuxtLink>
</div>
<div class="navbar-item" v-if="hasAPISettings && !showConnection">
<button class="button is-dark" @click="showUserSelection = !showUserSelection" v-tooltip="'Change User'">
<span class="icon"><i class="fas fa-users"/></span>
</button>
</div>
</template>
<div class="navbar-item">
<button class="button is-dark" @click="showConnection = !showConnection"

View File

@@ -9,9 +9,9 @@
<div class="is-pulled-right">
<div class="field is-grouped">
<p class="control">
<button class="button is-primary" v-tooltip.bottom="'Add New Backend'"
@click="toggleForm = !toggleForm" :disabled="isLoading">
<button class="button is-primary" @click="toggleForm = !toggleForm" :disabled="isLoading">
<span class="icon"><i class="fas fa-add"/></span>
<span>Add Backend</span>
</button>
</p>
<p class="control">
@@ -156,7 +156,7 @@
<div class="control is-fullwidth has-icons-left">
<div class="select is-fullwidth">
<select v-model="selectedCommand" @change="forwardCommand(backend)">
<option value="" disabled>Frequently used commands</option>
<option value="" disabled>Quick operations</option>
<option v-for="(command, index) in usefulCommands" :key="`qc-${index}`" :value="index"
:disabled="!check_state(backend, command)">
{{ command.id }}. {{ command.title }}
@@ -305,7 +305,7 @@ const handleEvents = async (event, backend) => {
switch (event) {
case 'backupData':
try {
const backup_status = await queue_event('run_console', {
await queue_event('run_console', {
command: 'state:backup',
args: [
'-v',
@@ -317,8 +317,6 @@ const handleEvents = async (event, backend) => {
'{user}.{backend}.{date}.initial_backup.json',
]
})
console.log(backup_status);
notification('info', 'Info', `We are going to initiate a backup for '${backend.value.name}' in little bit.`, 5000)
} catch (e) {
notification('error', 'Error', `Failed to queue backup request. ${e.message}`)
@@ -326,26 +324,42 @@ const handleEvents = async (event, backend) => {
break
case 'forceExport':
try {
const export_status = await queue_event('run_console', {
await queue_event('run_console', {
command: 'state:export',
args: [
'-fi',
'-v',
'--user',
api_user.value,
'--dry-run',
'--select-backend',
backend.value.name,
]
}, 180)
console.log(export_status);
notification('info', 'Info', `Soon we are going to force export the local data to '${backend.value.name}'.`, 5000)
} catch (e) {
notification('error', 'Error', `Failed to queue force export request. ${e.message}`)
}
break
case 'forceImport':
try {
await queue_event('run_console', {
command: 'state:import',
args: [
'-f',
'-v',
'--user',
api_user.value,
'--select-backend',
backend.value.name,
]
}, 180)
notification('info', 'Info', `Soon we will import data from '${backend.value.name}'.`, 5000)
} catch (e) {
notification('error', 'Error', `Failed to queue force export request. ${e.message}`)
}
break
case 'addBackend':
toggleForm.value = false
await loadContent()

View File

@@ -44,11 +44,13 @@
<p class="control" v-if="!isLoading">
<button class="button is-primary" type="button" :disabled="hasPrefix" @click="RunCommand">
<span class="icon"><i class="fa fa-paper-plane"></i></span>
<span>Execute</span>
</button>
</p>
<p class="control" v-if="isLoading">
<button class="button is-danger" type="button" @click="finished" v-tooltip="'Close connection.'">
<span class="icon"><i class="fa fa-power-off"></i></span>
<span>Close</span>
</button>
</p>
</div>

View File

@@ -1,62 +0,0 @@
<template>
<div>
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-cogs"/></span>
Getting Started
</span>
<div class="is-pulled-right"></div>
<div class="is-hidden-mobile">
<span class="subtitle">A step by step guide to get you started with specific tasks.</span>
</div>
</div>
</div>
<div class="columns is-multiline">
<div class="column is-12">
<InteractionGuide>
<template #add_backend>
<div id="add_backend">
<h3 class="title is-5">Step: Add Backend</h3>
<p>
1. ...
</p>
</div>
</template>
<template #import_data>
<div id="import_data">
<h3 class="title is-5">Step: Import Data</h3>
<p>
1. ...
</p>
</div>
</template>
<template #force_export>
<div id="force_export">
<h3 class="title is-5">Step: Force Export</h3>
<p>
1. ...
</p>
</div>
</template>
<template #enabled_import>
<div id="enabled_import">
<h3 class="title is-5">Step: Enable Import</h3>
<p>
1. ...
</p>
</div>
</template>
</InteractionGuide>
</div>
</div>
</div>
</template>
<script setup>
import InteractionGuide from "~/components/InteractionGuide.vue"
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div class="columns is-multiline">
<div class="column is-12">
<Markdown :file="`/guides/${slug}.md`"/>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const slug = ref(`${route.params.slug?.length > 0 ? route.params.slug?.join('/') : ''}`)
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div>
<div class="columns is-multiline">
<div class="column is-12 is-clearfix is-unselectable">
<span class="title is-4">
<span class="icon"><i class="fas fa-hands-helping"/></span>
{{ 'Getting started' }}
</span>
<div class="is-hidden-mobile">
<span class="subtitle">
This page contains guides to help you get started with WatchState. This is an early version, we are still
working on the guides.
</span>
</div>
</div>
</div>
<div class="columns is-multiline">
<div v-for="choice in choices" :key="choice.url" class="column is-6-tablet is-12-mobile">
<div class="box content" style="height: 100%">
<h3 class="title is-5">
<NuxtLink :to="choice.url" class="has-text-link" v-text="`${choice.number}. ${choice.title}`"
v-if="choice.url"/>
<span v-else>{{ `${choice.number}. ${choice.title}` }}</span>
</h3>
<hr>
<Message message_class="has-background-warning-90 has-text-dark" v-if="!choice.url" class="p-1">
<p>
<span class="icon"><i class="fas fa-exclamation has-text-danger"/></span>
<span>This guide is not available yet.</span>
</p>
</Message>
<p>{{ choice.text }}</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
const choices = [
{
number: 1,
title: 'One-way sync',
text: 'For example, You want to import data from plex, and send it to jellyfin/emby but not the other way around.',
url: '/help/one-way-sync',
},
{
number: 2,
title: 'Two-way sync',
text: 'This will allow all backends to sync with each other. I.e. plex to jellyfin, jellyfin to emby, emby to plex.',
url: null
},
{
number: 3,
title: 'Enable webhooks',
text: 'How to enable webhooks for your backends and for sub-users.',
url: null
},
{
number: 4,
title: 'Enable Sub-users',
text: 'Guide on how to enable sub-users.',
url: null
},
]
</script>

View File

@@ -105,22 +105,33 @@
<div class="column is-12">
<div class="content">
<Message title="Welcome" message_class="has-background-info-90 has-text-dark" icon="fas fa-heart">
If you have question, or want clarification on something, or just want to chat with other users,
you are
welcome to join our <span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
<span>
<NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank"
v-text="'Discord server'"/>
</span>
</span>. For bug reports, feature requests, or contributions, please visit the
<span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
<p>
If you have question, or want clarification on something, or just want to chat with other users,
you are
welcome to join our <span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-discord"></i></span>
<span>
<NuxtLink to="https://github.com/arabcoders/watchstate/issues/new/choose"
target="_blank" v-text="'GitHub repository'"/>
<NuxtLink to="https://discord.gg/haUXHJyj6Y" target="_blank"
v-text="'Discord server'"/>
</span>
</span>.
</span>. For bug reports, feature requests, or contributions, please visit the
<span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-brands fa-github"></i></span>
<span>
<NuxtLink to="https://github.com/arabcoders/watchstate/issues/new/choose"
target="_blank" v-text="'GitHub repository'"/>
</span>
</span>.
</p>
<p>
We have recently added a guides page to help you get started with WatchState. You can find it
<span class="icon-text is-underlined">
<span class="icon"><i class="fas fa-question-circle"/></span>
<span>
<NuxtLink to="/help" v-text="'here'"/>
</span>
</span>, it still very early version and only contains a few guides, but we are working on it.
</p>
</Message>
</div>
</div>

View File

@@ -6335,9 +6335,9 @@ write-file-atomic@^6.0.0:
signal-exit "^4.0.1"
ws@^8.18.1:
version "8.18.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb"
integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==
version "8.18.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
y18n@^5.0.5:
version "5.0.8"

119
guides/one-way-sync.md Normal file
View File

@@ -0,0 +1,119 @@
# One-way sync
One-way sync in WatchState is the ability to sync data from one backend to one or more backends without
effecting the data in the source backend. This is useful for scenarios where you want to keep a backup of your data or
sync data to a different environment without modifying the original data.
# Use cases
- you want to try jellyfin/plex/emby without altering your original data.
- you want to keep your jellyfin/emby/plex backend in sync with your preferred media backend.
- You made new media backend that doesn't have your play state yet, and you want to get it in sync with your main
backend.
# How to set up one-way sync
First, go to <!--i:fa-server--> Backends and click on <!-- i:fa-plus --> plus button. follow the interactive setup
guide, when you reach the step with `Export data to this backend?`, select `No`, this instruction applies to the main
backend only. as you don't want to alter its data. Keep `Import data from this backend?` to `Yes`, this will allow you
to import data from the backend.
> [!NOTE]
> It's recommended to keep `Create backup for this backend data?` to `Yes`, this will create a snapshot of your
> backend data, so that you may return to it should something happens. via <!--i:fa-tools--> Tools > <!--i:fa-sd-card-->
> Backups.
Enable `Force one time import from this backend?` option to get your current data into WatchState. This will import all
the data from the backend into WatchState.
# Importing the data
It will take a while to import the data, depending on the size of your library.
To see the import status, you can go to <!--i:fa-globe--> Logs page, and check the `task.XXXXXXX.log` file. Or
via <!--i:fa-ellipsis-vertical--> More > <!--i:fa-calendar-alt--> Events page, and look for `run_console` event. We
recommend the tasks log as it's updated more frequently.
To know if it has finished, you can look for the following message:
```text
[DD/MM HH:mm:SS]: NOTICE: SYSTEM: Completed 'XXX' requests in 'XXX.XXX's for 'main' backends.
┌─────────┬───────┬─────────┬────────┐
│ Type │ Added │ Updated │ Failed │
├─────────┼───────┼─────────┼────────┤
│ Movie │ XXX │ XXX │ XXX │
├─────────┼───────┼─────────┼────────┤
│ Episode │ XXX │ XXX │ XXX │
└─────────┴───────┴─────────┴────────┘
```
Or go to the <!--i:fa-server--> Backends page, and check the backend last import date, if it shows the current time it
means the import is done.
After you see the message, you can go to <!--i:fa-history--> History page and see the import history. Navigate around to
check the data, once you are satisfied with the data, proceed to the next step.
# Adding the other backends
For each backend do these steps
## Step 1
Do exactly as you did for the main backend, But change the follow options:
- `Import data from this backend?`: No
- `Import metadata only from this backend?`: Yes
- `Export data to this backend?`: Yes
- `Force Export local data to this backend?`: depends
- If you only have 1 extra backend and already have imported your main backend data, then select `Yes`, and skip
step 2.
- Otherwise, keep it disabled and proceed to step 2.
> [!IMPORTANT]
> It's really important that you select those options, otherwise you might inadvertently alter the data in the main
> backend.
>
> The option `Import metadata only from this backend?` only shows up when you select `No` for
`Import data from this backend?`.
## Step 2
You have two options,
### Option 1 (1 Extra backend)
Go to <!--i:fa-server--> Backends page beneath the backend there is `Quick operations` list, select
`2. Force export local play state to this backend.`. Once you select the option, you will be redirected to
the <!--i:fa-ellipsis-vertical--> More > <!--i:fa-terminal--> Console page. Once you are there, the command will be
pre-filled for you, just hit enter to run it. Or click on <!--i:fa-terminal--> Execute button.
### Option 2 (Multiple backends)
If you have multiple backends that you want to sync, then skip step 2 for now, and add all your backends.
Once you are done, go to <!--i:fa-tools--> Tools > <!--i:fa-terminal--> Console page, and run the following command:
```bash
state:export -fi -v -u main
```
This command will force export your locally stored play state to all export enabled backends. Once that is done proceed
to the next step.
## Enable Automation
If you are satisfied with the results, and you want to automate the process from now on, go to <!--i:fa-tasks--> Tasks
page. There are two tasks that you need to enable by clicking on the slider next to the name `Import` and `Export`.
To change how often the tasks runs, you have to go to <!--i:fa-cogs--> Env page, click on <!--i:fa-plus--> add button,
select the relevant environment variable. In this case, `WS_CRON_EXPORT_AT` and `WS_CRON_IMPORT_AT`. These two variables
accept valid CRON timer expressions. if you want to run the export task every 6 hours for example, you can set the
variable `WS_CRON_EXPORT_AT` value to `0 */6 * * *`. For more information about CRON expressions, check
out [crontab.guru](https://crontab.guru/).
## Enabling webhooks
To know how to enable webhooks for faster sync operations, Please check out the webhooks guide.

View File

@@ -43,6 +43,7 @@ final class ServeStatic implements LoggerAwareInterface
private const array MD_IMAGES = [
'/screenshots' => __DIR__ . '/../../',
'/guides' => __DIR__ . '/../../',
];
public function __construct(private string|null $staticPath = null)