fix: repo volume in dockerfile + remove deprecated ssl admin page + mcp placeholder

scarlett
NGPixel 2 weeks ago
parent d910bae080
commit 59ef5e779d
No known key found for this signature in database

@ -612,6 +612,15 @@
"admin.mail.testRecipientHint": "Email address that should receive the test email.",
"admin.mail.testSend": "Send Email",
"admin.mail.title": "Mail",
"admin.mcp.disableButton": "Disable MCP",
"admin.mcp.disabled": "MCP Disabled",
"admin.mcp.docsButton": "MCP Docs",
"admin.mcp.enableButton": "Enable MCP",
"admin.mcp.enabled": "MCP Enabled",
"admin.mcp.newKeyButton": "New MCP Key",
"admin.mcp.notImplemented": "The MCP server is not implemented yet: this page is a placeholder, and MCP cannot be enabled so far.",
"admin.mcp.subtitle": "Manage the Model Context Protocol server",
"admin.mcp.title": "MCP",
"admin.metrics.auth": "You must provide the {headerName} header with a {tokenType} token. Generate an API key with the {permission} permission and use it as the token.",
"admin.metrics.disabled": "Endpoint Disabled",
"admin.metrics.enabled": "Endpoint Enabled",
@ -811,35 +820,6 @@
"admin.sites.subtitle": "Manage your wiki sites",
"admin.sites.title": "Sites",
"admin.sites.updateSuccess": "Site updated successfully.",
"admin.ssl.currentState": "Current State",
"admin.ssl.domain": "Domain",
"admin.ssl.domainHint": "Enter the fully qualified domain pointing to your wiki. (e.g. wiki.example.com)",
"admin.ssl.expiration": "Certificate Expiration",
"admin.ssl.httpPort": "HTTP Port",
"admin.ssl.httpPortHint": "Non-SSL port the server will listen to for HTTP requests. Usually 80 or 3000.",
"admin.ssl.httpPortRedirect": "Redirect HTTP requests to HTTPS",
"admin.ssl.httpPortRedirectHint": "Will automatically redirect any requests on the HTTP port to HTTPS.",
"admin.ssl.httpPortRedirectSaveSuccess": "HTTP Redirection changed successfully.",
"admin.ssl.httpPortRedirectTurnOff": "Turn Off",
"admin.ssl.httpPortRedirectTurnOn": "Turn On",
"admin.ssl.httpsPort": "HTTPS Port",
"admin.ssl.httpsPortHint": "SSL port the server will listen to for HTTPS requests. Usually 443.",
"admin.ssl.ports": "Ports",
"admin.ssl.provider": "Provider",
"admin.ssl.providerCustomCertificate": "Custom Certificate",
"admin.ssl.providerDisabled": "Disabled",
"admin.ssl.providerHint": "Select Custom Certificate if you have your own certificate already.",
"admin.ssl.providerLetsEncrypt": "Let's Encrypt",
"admin.ssl.providerOptions": "Provider Options",
"admin.ssl.renewCertificate": "Renew Certificate",
"admin.ssl.renewCertificateLoadingSubtitle": "Do not leave this page.",
"admin.ssl.renewCertificateLoadingTitle": "Renewing Certificate...",
"admin.ssl.renewCertificateSuccess": "Certificate renewed successfully.",
"admin.ssl.status": "Certificate Status",
"admin.ssl.subscriberEmail": "Subscriber Email",
"admin.ssl.subtitle": "Manage SSL configuration",
"admin.ssl.title": "SSL",
"admin.ssl.writableConfigFileWarning": "Note that your config file must be writable in order to persist ports configuration.",
"admin.stats.title": "Statistics",
"admin.storage.actionFailed": "Failed to run {action}.",
"admin.storage.actionSuccess": "{action} completed successfully.",

@ -152,6 +152,6 @@ actions:
icon: database-daily-import
purge:
label: Purge Local Repository
hint: Empty the local working copy and clone it again from the remote. A working copy that was replaced re-attaches itself on the next sync, so this is for when git can no longer make sense of at all. The remote is not touched and nothing is committed.
hint: Empty the local working copy and clone it again from the remote. A working copy that was replaced re-attaches itself on the next sync, so this is for one git can no longer make sense of at all. The remote is not touched and nothing is committed.
warn: Any commit that exists only in the local repository and has never been pushed is lost. Run a Force Sync first if you are not sure.
icon: trash

@ -270,7 +270,7 @@ async function ensureRemote(repo: Repo, target: StorageTarget): Promise<{ onRemo
const remoteBranches = branches.all.filter((b) => b.startsWith('remotes/origin/'))
if (remoteBranches.length > 0) {
throw new Error(
`The branch "${branch}" does not exist locally or on the remote, which has ${remoteBranches
`(STORAGE/GIT) The branch "${branch}" does not exist locally or on the remote, which has ${remoteBranches
.map((b) => b.replace('remotes/origin/', ''))
.join(', ')}. Check the branch name, or create it on the remote first.`
)
@ -383,7 +383,7 @@ async function pullRebase(repo: Repo, branch: string): Promise<string[]> {
throw retryErr
}
throw new Error(
`${unsettled.slice(0, 5).join(', ')}${unsettled.length > 5 ? ', ...' : ''} changed here and were deleted on the remote, or the other way round, which nothing can settle on its own. Nothing has been changed either side. Decide which of the two is right - Purge Local Repository takes the remote's answer, Force Sync in Push mode takes the wiki's.`
`(STORAGE/GIT) ${unsettled.slice(0, 5).join(', ')}${unsettled.length > 5 ? ', ...' : ''} changed here and were deleted on the remote, or the other way round, which nothing can settle on its own. Nothing has been changed either side. Decide which of the two is right - Purge Local Repository takes the remote's answer, Force Sync in Push mode takes the wiki's.`
)
}
return conflicted
@ -442,7 +442,7 @@ async function reattach(repo: Repo, branch: string): Promise<string> {
} catch (err: any) {
await abortInterrupted(repo).catch(() => null)
throw new Error(
`This working copy has no history in common with origin/${branch}, and taking the remote's up failed: ${err.message}. Purge Local Repository starts again from the remote's copy.`
`(STORAGE/GIT) This working copy has no history in common with origin/${branch}, and taking the remote's up failed: ${err.message}. Purge Local Repository starts again from the remote's copy.`
)
}
return `The working copy had no history in common with origin/${branch} - which is what a container replaced without a volume for it leaves behind - so the remote's history was taken up and this wiki's own files kept on top of it. Nothing in the wiki itself was changed: run Import Everything if the repository holds content this wiki does not.`
@ -770,7 +770,7 @@ const gitStorage: StorageModule = {
const relPath = assetRelPath(target, ref)
if (!relPath) {
throw new Error(
`${target.title} has no path for ${ref.locale} content, so ${ref.fileName} cannot be stored there.`
`(STORAGE/GIT) ${target.title} has no path for ${ref.locale} content, so ${ref.fileName} cannot be stored there.`
)
}
await withRepo(target, async (repo) => {
@ -909,7 +909,7 @@ const gitStorage: StorageModule = {
const branch = target.config.branch || 'main'
return withRepo(target, async (repo) => {
if (!target.config.repoUrl) {
return 'No repository URI is configured, so there is nothing to sync with. Commits are being made locally.'
return '(STORAGE/GIT) No repository URI is configured, so there is nothing to sync with. Commits are being made locally.'
}
// -> Before anything else touches the repository, since git refuses to move a branch or make a
// commit while an earlier operation is unfinished — `ensureRemote`'s checkout is the first
@ -1119,7 +1119,7 @@ async function applyIncoming(
} catch (err: any) {
// -> One entry the wiki could not let go of must not stop the rest of the commit being applied
WIKI.logger.warn(`(STORAGE/GIT) Could not delete ${segments.join('/')} [ SKIPPED ]`)
WIKI.logger.warn(err.message)
WIKI.logger.warn(`(STORAGE/GIT) ${err.message}`)
}
}

@ -15,6 +15,7 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -qy --no-in
RUN mkdir -p /wiki && \
mkdir -p /logs && \
mkdir -p /wiki/data/content && \
mkdir -p /wiki/data/repo && \
chown -R node:node /wiki /logs
WORKDIR /wiki
@ -54,6 +55,7 @@ RUN PUPPETEER_VERSION="$(sed -n 's/^installVersion: *//p' modules/extensions/pup
WORKDIR /wiki
VOLUME ["/wiki/data/content"]
VOLUME ["/wiki/data/repo"]
EXPOSE 3000
EXPOSE 3443

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.0 KiB

@ -343,6 +343,16 @@
:pulse="!adminStore.info.isMailConfigured" />
</w-item-section>
</w-item>
<w-item to="/_admin/mcp" active-class="bg-primary text-white">
<w-item-section avatar>
<w-icon name="img:/_assets/icons/fluent-ai.svg" />
</w-item-section>
<w-item-section>{{ t('admin.mcp.title') }}</w-item-section>
<w-item-section side>
<status-light
:color="adminStore.info.isMCPEnabled ? `positive` : `negative`" />
</w-item-section>
</w-item>
<w-item to="/_admin/metrics" active-class="bg-primary text-white">
<w-item-section avatar>
<w-icon name="img:/_assets/icons/fluent-graph.svg" />
@ -384,16 +394,6 @@
</w-item-section>
<w-item-section>{{ t('admin.security.title') }}</w-item-section>
</w-item>
<w-item
to="/_admin/ssl"
active-class="bg-primary text-white"
disabled
v-if="flagsStore.experimental">
<w-item-section avatar>
<w-icon name="img:/_assets/icons/fluent-security-ssl.svg" />
</w-item-section>
<w-item-section>{{ t('admin.ssl.title') }}</w-item-section>
</w-item>
<w-item to="/_admin/system" active-class="bg-primary text-white">
<w-item-section avatar>
<w-icon name="img:/_assets/icons/fluent-processor.svg" />

@ -0,0 +1,129 @@
<template>
<w-page class="admin-mcp">
<div class="flex flex-wrap p-4 items-center">
<div class="flex-none">
<img class="admin-icon animated fadeInLeft" src="/_assets/icons/fluent-ai.svg" />
</div>
<div class="min-w-0 flex-1 pl-4">
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.mcp.title') }}</div>
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
{{ t('admin.mcp.subtitle') }}
</div>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center">
<template v-if="state.enabled">
<w-signal class="mr-2" color="green" size="md" />
<div class="text-caption text-green">{{ t('admin.mcp.enabled') }}</div>
</template>
<template v-else>
<w-signal class="mr-2" color="red" size="md" />
<div class="text-caption text-red">{{ t('admin.mcp.disabled') }}</div>
</template>
</div>
</div>
<!-- -> Nothing behind these yet: there is no endpoint to toggle and no keys to create, so
the two actions are laid out here permanently disabled and without a handler -->
<div class="flex-none">
<w-btn
class="mr-2 ml-4 acrylic-btn"
icon="la:question-circle"
flat
color="grey"
:aria-label="t(`common.actions.viewDocs`)"
:href="siteStore.docsBase + `/dev/mcp`"
target="_blank">
<w-tooltip>{{ t(`common.actions.viewDocs`) }}</w-tooltip>
</w-btn>
<w-btn
class="acrylic-btn mr-2"
icon="la:redo-alt"
flat
color="secondary"
:loading="state.loading > 0"
:aria-label="t(`common.actions.refresh`)">
<w-tooltip>{{ t(`common.actions.refresh`) }}</w-tooltip>
</w-btn>
<w-btn
class="mr-2"
unelevated
icon="la:power-off"
disabled
:label="!state.enabled ? t(`admin.mcp.enableButton`) : t(`admin.mcp.disableButton`)"
:color="!state.enabled ? `positive` : `negative`"
:loading="state.isToggleLoading"
:disabled="state.loading > 0" />
<w-btn
unelevated
icon="la:plus"
:label="t(`admin.mcp.newKeyButton`)"
color="primary"
:disabled="state.loading > 0 || true" />
</div>
</div>
<w-separator inset />
<div class="grid grid-cols-12 p-4 gap-4">
<div class="col-span-12">
<w-card
class="rounded"
flat
:class="dark.isActive ? `bg-dark-5 text-white` : `bg-grey-3 text-dark`">
<w-card-section class="items-center" horizontal>
<!-- -> The warning tone swaps with the theme like the card ground it sits on does:
`orange` is 1.9:1 on the light card and unreadable, `deep-orange-10` 3.4:1 on the
dark one. Each is above 4.5:1 on the ground it is paired with -->
<w-card-section class="flex-none pr-0">
<w-icon
name="la:exclamation-triangle"
size="sm"
:color="dark.isActive ? `orange` : `deep-orange-10`" />
</w-card-section>
<w-card-section
class="text-caption"
:class="dark.isActive ? `text-orange` : `text-deep-orange-10`"
>{{ t('admin.mcp.notImplemented') }}</w-card-section
>
</w-card-section>
</w-card>
</div>
</div>
</w-page>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { reactive } from 'vue'
import { useDark } from '@/composables/dark'
import { useMeta } from '@/composables/meta'
import { useSiteStore } from '@/stores/site'
// COMPOSABLES
const dark = useDark()
// STORES
const siteStore = useSiteStore()
// I18N
const { t } = useI18n()
// META
useMeta({
title: t('admin.mcp.title')
})
// DATA
const state = reactive({
enabled: false,
loading: 0,
isToggleLoading: false
})
</script>
<style lang="scss"></style>

@ -1,269 +0,0 @@
<template lang="pug">
v-container(fluid, grid-list-lg)
v-layout(row wrap)
v-flex(xs12)
.admin-header
img.animated.fadeInUp(src='/_assets/svg/icon-validation.svg', alt='SSL', style='width: 80px;')
.admin-header-title
.headline.primary--text.animated.fadeInLeft {{ $t('admin.ssl.title') }}
.subtitle-1.grey--text.animated.fadeInLeft {{ $t('admin.ssl.subtitle') }}
v-spacer
v-btn.animated.fadeInDown(
v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0'
color='black'
dark
depressed
@click='renewCertificate'
large
:loading='loadingRenew'
)
v-icon(left) mdi-cached
span {{$t('admin.ssl.renewCertificate')}}
v-form.pt-3
v-layout(row wrap)
v-flex(lg6 xs12)
v-card.animated.fadeInUp
v-subheader {{ $t('admin.ssl.currentState') }}
v-list(two-line, dense)
v-list-item
v-list-item-avatar
v-icon.indigo.white--text mdi-handshake
v-list-item-content
v-list-item-title {{ $t(`admin.ssl.provider`) }}
v-list-item-subtitle {{ providerTitle }}
template(v-if='info.sslProvider === `letsencrypt` && info.httpsPort > 0')
v-list-item
v-list-item-avatar
v-icon.indigo.white--text mdi-application
v-list-item-content
v-list-item-title {{ $t(`admin.ssl.domain`) }}
v-list-item-subtitle {{ info.sslDomain }}
v-list-item
v-list-item-avatar
v-icon.indigo.white--text mdi-at
v-list-item-content
v-list-item-title {{ $t('admin.ssl.subscriberEmail') }}
v-list-item-subtitle {{ info.sslSubscriberEmail }}
v-list-item
v-list-item-avatar
v-icon.indigo.white--text mdi-calendar-remove-outline
v-list-item-content
v-list-item-title {{ $t('admin.ssl.expiration') }}
v-list-item-subtitle {{ info.sslExpirationDate | moment('calendar') }}
v-list-item
v-list-item-avatar
v-icon.indigo.white--text mdi-traffic-light
v-list-item-content
v-list-item-title {{ $t(`admin.ssl.status`) }}
v-list-item-subtitle {{ info.sslStatus }}
v-flex(lg6 xs12)
v-card.animated.fadeInUp.wait-p2s
v-subheader {{ $t('admin.ssl.ports') }}
v-list(two-line, dense)
v-list-item
v-list-item-avatar
v-icon.blue.white--text mdi-lock-open-variant
v-list-item-content
v-list-item-title {{ $t(`admin.ssl.httpPort`) }}
v-list-item-subtitle {{ info.httpPort }}
template(v-if='info.httpsPort > 0')
v-divider
v-list-item
v-list-item-avatar
v-icon.green.white--text mdi-lock
v-list-item-content
v-list-item-title {{ $t(`admin.ssl.httpsPort`) }}
v-list-item-subtitle {{ info.httpsPort }}
v-divider
v-list-item
v-list-item-avatar
v-icon.indigo.white--text mdi-sign-direction
v-list-item-content
v-list-item-title {{ $t(`admin.ssl.httpPortRedirect`) }}
v-list-item-subtitle {{ info.httpRedirection }}
v-list-item-action
v-btn.red--text(
v-if='info.httpRedirection'
depressed
:color='$vuetify.theme.dark ? `red darken-4` : `red lighten-5`'
:class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'
@click='toggleRedir'
:loading='loadingRedir'
)
v-icon(left) mdi-power
span {{$t('admin.ssl.httpPortRedirectTurnOff')}}
v-btn.green--text(
v-else
depressed
:color='$vuetify.theme.dark ? `green darken-4` : `green lighten-5`'
:class='$vuetify.theme.dark ? `text--lighten-5` : `text--darken-2`'
@click='toggleRedir'
:loading='loadingRedir'
)
v-icon(left) mdi-power
span {{$t('admin.ssl.httpPortRedirectTurnOn')}}
v-dialog(
v-model='loadingRenew'
persistent
max-width='450'
)
v-card(color='black', dark)
v-card-text.pa-10.text-center
semipolar-spinner.animated.fadeIn(
:animation-duration='1500'
:size='65'
color='#FFF'
style='margin: 0 auto;'
)
.mt-5.body-1.white--text {{$t('admin.ssl.renewCertificateLoadingTitle')}}
.caption.mt-4 {{$t('admin.ssl.renewCertificateLoadingSubtitle')}}
</template>
<script>
import _ from 'lodash'
import { SemipolarSpinner } from 'epic-spinners'
export default {
components: {
SemipolarSpinner
},
data() {
return {
loadingRenew: false,
loadingRedir: false,
info: {
sslDomain: '',
sslProvider: '',
sslSubscriberEmail: '',
sslExpirationDate: false,
sslStatus: '',
httpPort: 0,
httpRedirection: false,
httpsPort: 0
}
}
},
computed: {
providerTitle() {
switch (this.info.sslProvider) {
case 'custom':
return this.$t('admin.ssl.providerCustomCertificate')
case 'letsencrypt':
return this.$t('admin.ssl.providerLetsEncrypt')
default:
return this.$t('admin.ssl.providerDisabled')
}
}
},
methods: {
async toggleRedir() {
this.loadingRedir = true
try {
this.info.httpRedirection = !this.info.httpRedirection
await this.$apollo.mutate({
mutation: `
mutation ($enabled: Boolean!) {
system {
setHTTPSRedirection(enabled: $enabled) {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
variables: {
enabled: _.get(this.info, 'httpRedirection', false)
},
watchLoading(isLoading) {
this.$store.commit(
`loading${isLoading ? 'Start' : 'Stop'}`,
'admin-ssl-toggleRedirection'
)
}
})
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('admin.ssl.httpPortRedirectSaveSuccess'),
icon: 'check'
})
} catch (err) {
this.info.httpRedirection = !this.info.httpRedirection
this.$store.commit('pushGraphError', err)
}
this.loadingRedir = false
},
async renewCertificate() {
this.loadingRenew = true
try {
const respRaw = await this.$apollo.mutate({
mutation: `
mutation {
system {
renewHTTPSCertificate {
responseResult {
succeeded
errorCode
slug
message
}
}
}
}
`,
watchLoading(isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-renew')
}
})
const resp = _.get(respRaw, 'data.system.renewHTTPSCertificate.responseResult', {})
if (resp.succeeded) {
this.$store.commit('showNotification', {
style: 'success',
message: this.$t('admin.ssl.renewCertificateSuccess'),
icon: 'check'
})
} else {
throw new Error(resp.message)
}
} catch (err) {
this.$store.commit('pushGraphError', err)
}
this.loadingRenew = false
}
},
apollo: {
info: {
query: `
{
system {
info {
httpPort
httpRedirection
httpsPort
sslDomain
sslExpirationDate
sslProvider
sslStatus
sslSubscriberEmail
}
}
}
`,
fetchPolicy: 'network-only',
update: (data) => _.cloneDeep(data.system.info),
watchLoading(isLoading) {
this.$store.commit(`loading${isLoading ? 'Start' : 'Stop'}`, 'admin-ssl-refresh')
}
}
}
}
</script>
<style lang="scss"></style>

@ -76,6 +76,7 @@ const routes = [
{ path: 'icons', component: () => import('@/pages/AdminIcons.vue') },
{ path: 'instances', component: () => import('@/pages/AdminInstances.vue') },
{ path: 'mail', component: () => import('@/pages/AdminMail.vue') },
{ path: 'mcp', component: () => import('@/pages/AdminMcp.vue') },
{ path: 'metrics', component: () => import('@/pages/AdminMetrics.vue') },
{ path: 'rendering', component: () => import('@/pages/AdminRendering.vue') },
{ path: 'scheduler', component: () => import('@/pages/AdminScheduler.vue') },

@ -19,6 +19,7 @@ export const useAdminStore = defineStore('admin', {
webhooksTotal: 0,
loginsPastDay: 0,
isApiEnabled: false,
isMCPEnabled: false,
isMailConfigured: false,
isMetricsEnabled: false,
isSchedulerHealthy: false

Loading…
Cancel
Save