Browse Source

feat: deploy site config to remote server

0xJacky 1 year ago
parent
commit
4135146b67

+ 1 - 0
frontend/components.d.ts

@@ -63,6 +63,7 @@ declare module '@vue/runtime-core' {
     ATabs: typeof import('ant-design-vue/es')['Tabs']
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
+    ATooltip: typeof import('ant-design-vue/es')['Tooltip']
     BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
     ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']

+ 57 - 72
frontend/src/components/ChatGPT/ChatGPT.vue

@@ -185,93 +185,74 @@ async function regenerate(index: number) {
 
 const editing_idx = ref(-1)
 
-const show = computed(() => messages?.value?.length > 1)
+const show = computed(() => messages?.value?.length === 0)
 
 </script>
 
 <template>
-    <a-card class="chatgpt" title="ChatGPT" v-if="show">
-        <div class="chatgpt-container">
-            <a-list
-                class="chatgpt-log"
-                item-layout="horizontal"
-                :data-source="messages"
-            >
-                <template #renderItem="{ item, index }">
-                    <a-list-item>
-                        <a-comment :author="item.role" :avatar="item.avatar">
-                            <template #content>
-                                <div class="content" v-if="item.role==='assistant'||editing_idx!=index"
-                                     v-html="marked.parse(item.content)"></div>
-                                <a-input style="padding: 0" v-else v-model:value="item.content"
-                                         :bordered="false"/>
-                            </template>
-                            <template #actions>
+    <div class="chat-start" v-if="show">
+        <a-button @click="send" :loading="loading">
+            <Icon v-if="!loading" :component="ChatGPT_logo"/>
+            {{ $gettext('Ask ChatGPT for Help') }}
+        </a-button>
+    </div>
+    <div class="chatgpt-container" v-else>
+        <a-list
+            class="chatgpt-log"
+            item-layout="horizontal"
+            :data-source="messages"
+        >
+            <template #renderItem="{ item, index }">
+                <a-list-item>
+                    <a-comment :author="item.role==='assistant'?$gettext('Assistant'):$gettext('User')">
+                        <template #content>
+                            <div class="content" v-if="item.role==='assistant'||editing_idx!=index"
+                                 v-html="marked.parse(item.content)"></div>
+                            <a-input style="padding: 0" v-else v-model:value="item.content"
+                                     :bordered="false"/>
+                        </template>
+                        <template #actions>
                                     <span v-if="item.role==='user'&&editing_idx!==index" @click="editing_idx=index">
                                         {{ $gettext('Modify') }}
                                     </span>
-                                <template v-else-if="editing_idx==index">
-                                    <span @click="regenerate(index+1)">{{ $gettext('Save') }}</span>
-                                    <span @click="editing_idx=-1">{{ $gettext('Cancel') }}</span>
-                                </template>
-                                <span v-else-if="!loading" @click="regenerate(index)" :disabled="loading">
+                            <template v-else-if="editing_idx==index">
+                                <span @click="regenerate(index+1)">{{ $gettext('Save') }}</span>
+                                <span @click="editing_idx=-1">{{ $gettext('Cancel') }}</span>
+                            </template>
+                            <span v-else-if="!loading" @click="regenerate(index)" :disabled="loading">
                                         {{ $gettext('Reload') }}
                                     </span>
-                            </template>
-                        </a-comment>
-                    </a-list-item>
-                </template>
-            </a-list>
-            <div class="input-msg">
-                <div class="control-btn">
-                    <a-space v-show="!loading">
-                        <a-popconfirm
-                            :cancelText="$gettext('No')"
-                            :okText="$gettext('OK')"
-                            :title="$gettext('Are you sure you want to clear the record of chat?')"
-                            @confirm="clear_record">
-                            <a-button type="text">{{ $gettext('Clear') }}</a-button>
-                        </a-popconfirm>
-                        <a-button type="text" @click="regenerate(messages?.length-1)">
-                            {{ $gettext('Regenerate response') }}
-                        </a-button>
-                    </a-space>
-                </div>
-                <a-textarea auto-size v-model:value="ask_buffer"/>
-                <div class="sned-btn">
-                    <a-button size="small" type="text" :loading="loading" @click="send">
-                        <send-outlined/>
+                        </template>
+                    </a-comment>
+                </a-list-item>
+            </template>
+        </a-list>
+        <div class="input-msg">
+            <div class="control-btn">
+                <a-space v-show="!loading">
+                    <a-popconfirm
+                        :cancelText="$gettext('No')"
+                        :okText="$gettext('OK')"
+                        :title="$gettext('Are you sure you want to clear the record of chat?')"
+                        @confirm="clear_record">
+                        <a-button type="text">{{ $gettext('Clear') }}</a-button>
+                    </a-popconfirm>
+                    <a-button type="text" @click="regenerate(messages?.length-1)">
+                        {{ $gettext('Regenerate response') }}
                     </a-button>
-                </div>
+                </a-space>
+            </div>
+            <a-textarea auto-size v-model:value="ask_buffer"/>
+            <div class="sned-btn">
+                <a-button size="small" type="text" :loading="loading" @click="send">
+                    <send-outlined/>
+                </a-button>
             </div>
         </div>
-    </a-card>
-    <template v-else>
-        <div class="chat-start">
-            <a-button size="large" shape="circle" @click="send" :loading="loading">
-                <Icon v-if="!loading" :component="ChatGPT_logo"/>
-            </a-button>
-        </div>
-    </template>
+    </div>
 </template>
 
 <style lang="less" scoped>
-.chatgpt {
-    position: sticky;
-    top: 78px;
-
-    :deep(.ant-card-body) {
-        max-height: 100vh;
-        overflow-y: scroll;
-    }
-}
-
-.chat-start {
-    position: fixed !important;
-    right: 36px;
-    bottom: 78px;
-}
-
 .chatgpt-container {
     margin: 0 auto;
     max-width: 800px;
@@ -285,6 +266,10 @@ const show = computed(() => messages?.value?.length > 1)
             }
         }
 
+        :deep(.ant-list-item) {
+            padding: 0;
+        }
+
         :deep(.ant-comment-content) {
             width: 100%;
         }

+ 4 - 5
frontend/src/components/EnvIndicator/EnvIndicator.vue

@@ -12,9 +12,8 @@ const settingsStore = useSettingsStore()
 const {environment} = storeToRefs(settingsStore)
 const router = useRouter()
 
-function clear_env() {
-    router.push('/dashboard')
-    location.reload()
+async function clear_env() {
+    await router.push('/dashboard')
     settingsStore.clear_environment()
 }
 
@@ -24,8 +23,8 @@ const is_local = computed(() => {
 
 const node_id = computed(() => environment.value.id)
 
-watch(node_id, () => {
-    router.push('/dashboard')
+watch(node_id, async () => {
+    await router.push('/dashboard')
     location.reload()
 })
 </script>

+ 6 - 6
frontend/src/components/NodeSelector/NodeSelector.vue

@@ -5,7 +5,7 @@ import {useGettext} from 'vue3-gettext'
 
 const {$gettext} = useGettext()
 
-const props = defineProps(['target', 'map'])
+const props = defineProps(['target', 'map', 'hidden_local'])
 const emit = defineEmits(['update:target'])
 
 const data = ref([])
@@ -35,15 +35,15 @@ const value = computed({
 
 <template>
     <a-checkbox-group v-model:value="value" style="width: 100%">
-        <a-row>
-            <a-col :span="8">
+        <a-row :gutter="[16,16]">
+            <a-col :span="8" v-if="!hidden_local">
                 <a-checkbox :value="0">{{ $gettext('Local') }}</a-checkbox>
-                <a-badge color="green"/>
+                <a-tag color="blue">{{ $gettext('Online') }}</a-tag>
             </a-col>
             <a-col :span="8" v-for="node in data">
                 <a-checkbox :value="node.id">{{ node.name }}</a-checkbox>
-                <a-badge color="green" v-if="node.status"/>
-                <a-badge color="red" v-else/>
+                <a-tag color="blue" v-if="node.status">{{ $gettext('Online') }}</a-tag>
+                <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
             </a-col>
         </a-row>
     </a-checkbox-group>

+ 6 - 7
frontend/src/views/config/ConfigEdit.vue

@@ -65,15 +65,12 @@ function format_code() {
     })
 }
 
-const editor_md = computed(() => history_chatgpt_record?.value?.length > 1 ? 16 : 24)
-const chat_md = computed(() => history_chatgpt_record?.value?.length > 1 ? 8 : 24)
-
 </script>
 
 
 <template>
     <a-row :gutter="16">
-        <a-col :xs="24" :sm="24" :md="editor_md">
+        <a-col :xs="24" :sm="24" :md="18">
             <a-card :title="$gettext('Edit Configuration')">
                 <inspect-config ref="inspect_config"/>
                 <code-editor v-model:content="configText"/>
@@ -93,9 +90,11 @@ const chat_md = computed(() => history_chatgpt_record?.value?.length > 1 ? 8 : 2
             </a-card>
         </a-col>
 
-        <a-col class="col-right" :xs="24" :sm="24" :md="chat_md">
-            <chat-g-p-t :content="configText" :path="file_path"
-                        v-model:history_messages="history_chatgpt_record"/>
+        <a-col class="col-right" :xs="24" :sm="24" :md="6">
+            <a-card>
+                <chat-g-p-t :content="configText" :path="file_path"
+                            v-model:history_messages="history_chatgpt_record"/>
+            </a-card>
         </a-col>
     </a-row>
 </template>

+ 4 - 3
frontend/src/views/dashboard/Environments.vue

@@ -58,8 +58,10 @@ const visible = computed(() => {
                             <a-tag color="blue" v-if="item.status">{{ $gettext('Online') }}</a-tag>
                             <a-tag color="error" v-else>{{ $gettext('Offline') }}</a-tag>
                             <div class="runtime-meta">
-                                <span><Icon :component="pulse"/> {{ formatDateTime(item.response_at) }}</span>
-                                <span><thunderbolt-outlined/>{{ item.version }}</span>
+                                <template v-if="item.status">
+                                    <span><Icon :component="pulse"/> {{ formatDateTime(item.response_at) }}</span>
+                                    <span><thunderbolt-outlined/>{{ item.version }}</span>
+                                </template>
                                 <span><link-outlined/>{{ item.url }}</span>
                             </div>
                         </template>
@@ -85,7 +87,6 @@ const visible = computed(() => {
 
     .runtime-meta {
         display: inline-flex;
-        margin-left: 8px;
 
         span {
             font-weight: 400;

+ 13 - 51
frontend/src/views/domain/DomainEdit.vue

@@ -8,10 +8,9 @@ import {computed, provide, reactive, ref, watch} from 'vue'
 import {useRoute, useRouter} from 'vue-router'
 import domain from '@/api/domain'
 import ngx from '@/api/ngx'
-import Modal from 'ant-design-vue/lib/modal'
 import {message} from 'ant-design-vue'
 import config from '@/api/config'
-import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
+import RightSettings from '@/views/domain/components/RightSettings.vue'
 
 const {$gettext, interpolate} = useGettext()
 
@@ -42,6 +41,7 @@ const saving = ref(false)
 const filename = ref('')
 const parse_error_status = ref(false)
 const parse_error_message = ref('')
+const data = ref({})
 
 init()
 
@@ -74,6 +74,7 @@ function handle_response(r: any) {
     enabled.value = r.enabled
     auto_cert.value = r.auto_cert
     history_chatgpt_record.value = r.chatgpt_messages
+    data.value = r
     Object.assign(ngx_config, r.tokenized)
     Object.assign(cert_info_map, r.cert_info)
 }
@@ -149,49 +150,18 @@ const save = async () => {
     })
 }
 
-function enable() {
-    domain.enable(name.value).then(() => {
-        message.success($gettext('Enabled successfully'))
-        enabled.value = true
-    }).catch(r => {
-        message.error(interpolate($gettext('Failed to enable %{msg}'), {msg: r.message ?? ''}), 10)
-    })
-}
-
-function disable() {
-    domain.disable(name.value).then(() => {
-        message.success($gettext('Disabled successfully'))
-        enabled.value = false
-    }).catch(r => {
-        message.error(interpolate($gettext('Failed to disable %{msg}'), {msg: r.message ?? ''}))
-    })
-}
-
-function on_change_enabled(checked: boolean) {
-    Modal.confirm({
-        title: checked ? $gettext('Do you want to enable this site?') : $gettext('Do you want to disable this site?'),
-        mask: false,
-        centered: true,
-        okText: $gettext('OK'),
-        cancelText: $gettext('Cancel'),
-        async onOk() {
-            if (checked) {
-                enable()
-            } else {
-                disable()
-            }
-        }
-    })
-}
-
-const editor_md = computed(() => history_chatgpt_record?.value?.length > 1 ? 16 : 24)
-const chat_md = computed(() => history_chatgpt_record?.value?.length > 1 ? 8 : 24)
-
 provide('save_site_config', save)
+provide('configText', configText)
+provide('ngx_config', ngx_config)
+provide('history_chatgpt_record', history_chatgpt_record)
+provide('enabled', enabled)
+provide('name', name)
+provide('filename', filename)
+provide('data', data)
 </script>
 <template>
     <a-row :gutter="16">
-        <a-col :xs="24" :sm="24" :md="editor_md">
+        <a-col :xs="24" :sm="24" :md="18">
             <a-card :bordered="false">
                 <template #title>
                     <span style="margin-right: 10px">{{ interpolate($gettext('Edit %{n}'), {n: name}) }}</span>
@@ -217,13 +187,6 @@ provide('save_site_config', save)
                     </div>
                 </template>
 
-                <a-form-item :label="$gettext('Enabled')">
-                    <a-switch :checked="enabled" @change="on_change_enabled"/>
-                </a-form-item>
-                <a-form-item :label="$gettext('Name')">
-                    <a-input v-model:value="filename"/>
-                </a-form-item>
-
                 <transition name="slide-fade">
                     <div v-if="advance_mode" key="advance">
                         <div class="parse-error-alert-wrapper" v-if="parse_error_status">
@@ -252,9 +215,8 @@ provide('save_site_config', save)
             </a-card>
         </a-col>
 
-        <a-col class="col-right" :xs="24" :sm="24" :md="chat_md">
-            <chat-g-p-t :content="configText" :path="ngx_config.file_name"
-                        v-model:history_messages="history_chatgpt_record"/>
+        <a-col class="col-right" :xs="24" :sm="24" :md="6">
+            <right-settings/>
         </a-col>
 
         <footer-tool-bar>

+ 1 - 1
frontend/src/views/domain/DomainList.vue

@@ -7,7 +7,7 @@ import domain from '@/api/domain'
 import {Badge, message} from 'ant-design-vue'
 import {h, ref} from 'vue'
 import {input} from '@/components/StdDataEntry'
-import SiteDuplicate from '@/views/domain/SiteDuplicate.vue'
+import SiteDuplicate from '@/views/domain/components/SiteDuplicate.vue'
 
 const {$gettext, interpolate} = useGettext()
 

+ 108 - 0
frontend/src/views/domain/components/Deploy.vue

@@ -0,0 +1,108 @@
+<script setup lang="ts">
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import {useGettext} from 'vue3-gettext'
+import {inject, reactive, ref} from 'vue'
+import {InfoCircleOutlined} from '@ant-design/icons-vue'
+import Modal from 'ant-design-vue/lib/modal'
+import domain from '@/api/domain'
+import {notification} from 'ant-design-vue'
+import template from '@/api/template'
+
+const {$gettext, $ngettext} = useGettext()
+
+const node_map = reactive({})
+const target = ref([])
+const overwrite = ref(false)
+const enabled = ref(false)
+const name = inject('name')
+
+function deploy() {
+    Modal.confirm({
+        title: () => $ngettext('Do you want to deploy this file to remote server?',
+            'Do you want to deploy this file to remote servers?', target.value.length),
+        mask: false,
+        centered: true,
+        okText: $gettext('OK'),
+        cancelText: $gettext('Cancel'),
+        onOk() {
+            target.value.forEach(id => {
+                const node_name = node_map[id]
+                // get source content
+                domain.get(name.value).then(r => {
+                    domain.save(name.value, {
+                        name: name.value,
+                        content: r.config,
+                        overwrite: overwrite.value
+                    }, {headers: {'X-Node-ID': id}}).then(async () => {
+                        notification.success({
+                            message: $gettext('Deploy successfully'),
+                            description:
+                                $gettext('Deploy %{conf_name} to %{node_name} successfully',
+                                    {conf_name: name.value, node_name: node_name})
+                        })
+                        if (enabled.value) {
+                            domain.enable(name.value).then(() => {
+                                notification.success({
+                                    message: $gettext('Enable successfully'),
+                                    description:
+                                        $gettext(`Enable %{conf_name} in %{node_name} successfully`,
+                                            {conf_name: name.value, node_name: node_name})
+                                })
+                            }).catch(e => {
+                                notification.error({
+                                    message: $gettext('Enable %{conf_name} in %{node_name} failed', {
+                                        conf_name: name.value,
+                                        node_name: node_name
+                                    }),
+                                    description: $gettext(e?.message ?? 'Server error')
+                                })
+                            })
+                        }
+                    }).catch(e => {
+                        notification.error({
+                            message: $gettext('Deploy %{conf_name} to %{node_name} failed', {
+                                conf_name: name.value,
+                                node_name: node_name
+                            }),
+                            description: $gettext(e?.message ?? 'Server error')
+                        })
+                    })
+                })
+            })
+        }
+    })
+}
+</script>
+
+<template>
+    <node-selector v-model:target="target" :hidden_local="true" :map="node_map"/>
+    <div class="node-deploy-control">
+        <a-checkbox v-model:checked="enabled">{{ $gettext('Enabled') }}</a-checkbox>
+        <div class="overwrite">
+            <a-checkbox v-model:checked="overwrite">{{ $gettext('Overwrite') }}</a-checkbox>
+            <a-tooltip placement="bottom">
+                <template #title>{{ $gettext('Overwrite exist file') }}</template>
+                <info-circle-outlined/>
+            </a-tooltip>
+        </div>
+
+        <a-button :disabled="target.length===0" type="primary" @click="deploy" ghost>{{ $gettext('Deploy') }}</a-button>
+    </div>
+</template>
+
+<style scoped lang="less">
+.overwrite {
+    margin-right: 15px;
+
+    span {
+        color: #9b9b9b;
+    }
+}
+
+.node-deploy-control {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 10px;
+    align-items: center;
+}
+</style>

+ 104 - 0
frontend/src/views/domain/components/RightSettings.vue

@@ -0,0 +1,104 @@
+<script setup lang="ts">
+import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
+import {useGettext} from 'vue3-gettext'
+import {inject, ref} from 'vue'
+import Modal from 'ant-design-vue/lib/modal'
+import domain from '@/api/domain'
+import {message} from 'ant-design-vue'
+import {formatDateTime} from '@/lib/helper'
+import Deploy from '@/views/domain/components/Deploy.vue'
+import {useSettingsStore} from '@/pinia'
+
+const settings = useSettingsStore()
+const {$gettext} = useGettext()
+const configText = inject('configText')
+const ngx_config = inject('ngx_config')
+const enabled = inject('enabled')
+const name = inject('name')
+const history_chatgpt_record = inject('history_chatgpt_record')
+const filename = inject('filename')
+const data: any = inject('data')
+
+const active_key = ref('1')
+
+function enable() {
+    domain.enable(name.value).then(() => {
+        message.success($gettext('Enabled successfully'))
+        enabled.value = true
+    }).catch(r => {
+        message.error($gettext('Failed to enable %{msg}', {msg: r.message ?? ''}), 10)
+    })
+}
+
+function disable() {
+    domain.disable(name.value).then(() => {
+        message.success($gettext('Disabled successfully'))
+        enabled.value = false
+    }).catch(r => {
+        message.error($gettext('Failed to disable %{msg}', {msg: r.message ?? ''}))
+    })
+}
+
+function on_change_enabled(checked: boolean) {
+    Modal.confirm({
+        title: checked ? $gettext('Do you want to enable this site?') : $gettext('Do you want to disable this site?'),
+        mask: false,
+        centered: true,
+        okText: $gettext('OK'),
+        cancelText: $gettext('Cancel'),
+        async onOk() {
+            if (checked) {
+                enable()
+            } else {
+                disable()
+            }
+        }
+    })
+}
+
+</script>
+
+<template>
+    <a-card class="right-settings">
+        <a-collapse v-model:activeKey="active_key" ghost>
+            <a-collapse-panel key="1" :header="$gettext('Basic')">
+                <a-form-item :label="$gettext('Enabled')">
+                    <a-switch :checked="enabled" @change="on_change_enabled"/>
+                </a-form-item>
+                <a-form-item :label="$gettext('Name')">
+                    <a-input v-model:value="filename"/>
+                </a-form-item>
+                <a-form-item :label="$gettext('Updated at')">
+                    {{ formatDateTime(data.modified_at) }}
+                </a-form-item>
+            </a-collapse-panel>
+            <a-collapse-panel key="2" header="Deploy" v-if="!settings.is_remote">
+                <deploy/>
+            </a-collapse-panel>
+            <a-collapse-panel key="3" header="ChatGPT">
+                <chat-g-p-t :content="configText" :path="ngx_config.file_name"
+                            v-model:history_messages="history_chatgpt_record"/>
+            </a-collapse-panel>
+        </a-collapse>
+    </a-card>
+</template>
+
+<style scoped lang="less">
+.right-settings {
+    position: sticky;
+    top: 78px;
+
+    :deep(.ant-card-body) {
+        max-height: 100vh;
+        overflow-y: scroll;
+    }
+}
+
+:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
+    padding: 0;
+}
+
+:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
+    padding: 0 0 10px 0;
+}
+</style>

+ 0 - 0
frontend/src/views/domain/SiteDuplicate.vue → frontend/src/views/domain/components/SiteDuplicate.vue


+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.9.9","build_id":115,"total_build":185}
+{"version":"1.9.9","build_id":116,"total_build":186}

+ 8 - 8
go.mod

@@ -19,9 +19,10 @@ require (
 	github.com/jpillora/overseer v1.1.6
 	github.com/lib/pq v1.10.9
 	github.com/pkg/errors v0.9.1
+	github.com/pretty66/websocketproxy v0.0.0-20220507015215-930b3a686308
 	github.com/sashabaranov/go-openai v1.9.4
 	github.com/shirou/gopsutil/v3 v3.23.4
-	github.com/spf13/cast v1.5.0
+	github.com/spf13/cast v1.5.1
 	github.com/tufanbarisyildirim/gonginx v0.0.0-20230508164033-d7b72d6cd0d5
 	github.com/unknwon/com v1.0.1
 	go.uber.org/zap v1.24.0
@@ -50,10 +51,10 @@ require (
 	github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
 	github.com/StackExchange/wmi v1.2.1 // indirect
 	github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
-	github.com/aliyun/alibaba-cloud-sdk-go v1.62.318 // indirect
+	github.com/aliyun/alibaba-cloud-sdk-go v1.62.320 // indirect
 	github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
 	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
-	github.com/aws/aws-sdk-go v1.44.262 // indirect
+	github.com/aws/aws-sdk-go v1.44.263 // indirect
 	github.com/boombuler/barcode v1.0.1 // indirect
 	github.com/bytedance/sonic v1.8.8 // indirect
 	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
@@ -139,7 +140,6 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
 	github.com/pquerna/otp v1.4.0 // indirect
-	github.com/pretty66/websocketproxy v0.0.0-20220507015215-930b3a686308 // indirect
 	github.com/robfig/cron/v3 v3.0.1 // indirect
 	github.com/sacloud/api-client-go v0.2.7 // indirect
 	github.com/sacloud/go-http v0.1.5 // indirect
@@ -154,8 +154,8 @@ require (
 	github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
 	github.com/stretchr/objx v0.5.0 // indirect
 	github.com/stretchr/testify v1.8.2 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.655 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.655 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.657 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.657 // indirect
 	github.com/tklauser/go-sysconf v0.3.11 // indirect
 	github.com/tklauser/numcpus v0.6.0 // indirect
 	github.com/transip/gotransip/v6 v6.20.0 // indirect
@@ -164,8 +164,8 @@ require (
 	github.com/ultradns/ultradns-go-sdk v1.5.0-20230427130837-23c9b0c // indirect
 	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
 	github.com/vultr/govultr/v2 v2.17.2 // indirect
-	github.com/yandex-cloud/go-genproto v0.0.0-20230511103421-ecb0cd1514ab // indirect
-	github.com/yandex-cloud/go-sdk v0.0.0-20230511104317-0ccfef4d3a91 // indirect
+	github.com/yandex-cloud/go-genproto v0.0.0-20230515103554-c6064682c41e // indirect
+	github.com/yandex-cloud/go-sdk v0.0.0-20230515104003-a4cf880c2959 // indirect
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
 	go.opencensus.io v0.24.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect

+ 14 - 0
go.sum

@@ -70,6 +70,8 @@ github.com/aliyun/alibaba-cloud-sdk-go v1.62.281 h1:sN94THxWQA+nPMDZD0esg1PGy6pm
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.281/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.318 h1:1ntKWopst53IVWKlEVrgutJpEgQN+FyNZXO+h6ePgXw=
 github.com/aliyun/alibaba-cloud-sdk-go v1.62.318/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.320 h1:KiT5EgbU/rxcx4wiH1sFKaR3KyzGqB89D7JSN1vH40A=
+github.com/aliyun/alibaba-cloud-sdk-go v1.62.320/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI=
 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -84,6 +86,8 @@ github.com/aws/aws-sdk-go v1.44.242 h1:bb6Rqd7dxh1gTUoVXLJTNC2c+zNaHpLRlNKk0kGN3
 github.com/aws/aws-sdk-go v1.44.242/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/aws/aws-sdk-go v1.44.262 h1:gyXpcJptWoNkK+DiAiaBltlreoWKQXjAIh6FRh60F+I=
 github.com/aws/aws-sdk-go v1.44.262/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
+github.com/aws/aws-sdk-go v1.44.263 h1:Dkt5fcdtL8QtK3cz0bOTQ84m9dGx+YDeTsDl+wY2yW4=
+github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -702,6 +706,8 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
+github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
+github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
 github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
@@ -732,10 +738,14 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.637 h1:qFqi
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.637/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.655 h1:wXBlXLfBbqTBpsiKBBULW63KvMy3wsu3/CD25cR9NEA=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.655/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.657 h1:daDlYUdKRzgi2PxIcXj4vU1enWs6aqrL7K5qD3fKpmo=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.657/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.637 h1:9r85LEYF4CcKDbQQhJ5b3hYh5vj1WNvjsHrWHAV3c60=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.637/go.mod h1:5z3RG36i3UQvMr3aHVjPfrEzLdmk+sTiLgip3aFvKBo=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.655 h1:LgLA3nzvsBggdt1NRDNi6KVk9HRHLwBUltxXupdRMeM=
 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.655/go.mod h1:v8wyOnL22mqDNeBqsasAQzP6eQI0Lpa+cAxFtVThHTk=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.657 h1:9FAIqzmy29PS+CVlec2LaTbwSg0j8Zk55GxPMjrZUqM=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.657/go.mod h1:O2Xg2eAwl+TLAso+0F7Iao9ru2Abf7Mj+Dgv++pvQFw=
 github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
 github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
 github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
@@ -776,10 +786,14 @@ github.com/yandex-cloud/go-genproto v0.0.0-20230410092700-15216dc82345 h1:GpSllt
 github.com/yandex-cloud/go-genproto v0.0.0-20230410092700-15216dc82345/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
 github.com/yandex-cloud/go-genproto v0.0.0-20230511103421-ecb0cd1514ab h1:Y9sWstUXfHwHufw95mI58ZEvZ720KWyR+niLQbd2q1k=
 github.com/yandex-cloud/go-genproto v0.0.0-20230511103421-ecb0cd1514ab/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
+github.com/yandex-cloud/go-genproto v0.0.0-20230515103554-c6064682c41e h1:2jQIfPgSk+/6zl44MEYLehRLRtGRGYvVyfKLXeNFGUM=
+github.com/yandex-cloud/go-genproto v0.0.0-20230515103554-c6064682c41e/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
 github.com/yandex-cloud/go-sdk v0.0.0-20230403093608-cc5174142a48 h1:C3yjOqP3gGxwiW3bXDAGI8tS+eKjxySJ9Ix7lpdtKZw=
 github.com/yandex-cloud/go-sdk v0.0.0-20230403093608-cc5174142a48/go.mod h1:+bvtdW+7bn1Yc7xUCbITnEalQ+hwkAAbUFHpeIY2wUQ=
 github.com/yandex-cloud/go-sdk v0.0.0-20230511104317-0ccfef4d3a91 h1:bYY90Y33XH7xJh8Qa5ZIgmjyWDp2S6sixTRxYbHCQLU=
 github.com/yandex-cloud/go-sdk v0.0.0-20230511104317-0ccfef4d3a91/go.mod h1:QOnqdE3DjwgoKvhw4Scx6HTCfAlYHZMoUzyaC8kcdzk=
+github.com/yandex-cloud/go-sdk v0.0.0-20230515104003-a4cf880c2959 h1:jFoq7f55et+ssQYFgZCWbUV2kUcDx22e1jZx+KhnBcU=
+github.com/yandex-cloud/go-sdk v0.0.0-20230515104003-a4cf880c2959/go.mod h1:JNuOWjkSmssXYwJXPpOxc3IhjWbPKkODDm1gAqPFD9Q=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=

+ 22 - 3
server/api/domain.go

@@ -9,6 +9,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/server/model"
 	"github.com/0xJacky/Nginx-UI/server/query"
 	"github.com/gin-gonic/gin"
+	"github.com/sashabaranov/go-openai"
 	"net/http"
 	"os"
 	"strings"
@@ -88,8 +89,16 @@ func GetDomain(c *gin.Context) {
 	}
 
 	path := nginx.GetConfPath("sites-available", name)
+	file, err := os.Stat(path)
+	if os.IsNotExist(err) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "file not found",
+		})
+		return
+	}
 
 	enabled := true
+
 	if _, err := os.Stat(nginx.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
 		enabled = false
 	}
@@ -102,6 +111,10 @@ func GetDomain(c *gin.Context) {
 		return
 	}
 
+	if chatgpt.Content == nil {
+		chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
+	}
+
 	s := query.Site
 	site, err := s.Where(s.Path.Eq(path)).FirstOrInit()
 
@@ -110,7 +123,11 @@ func GetDomain(c *gin.Context) {
 		return
 	}
 
-	certModel, _ := model.FirstCert(name)
+	certModel, err := model.FirstCert(name)
+
+	if err != nil {
+		logger.Warn("cert", err)
+	}
 
 	if site.Advanced {
 		origContent, err := os.ReadFile(path)
@@ -120,6 +137,7 @@ func GetDomain(c *gin.Context) {
 		}
 
 		c.JSON(http.StatusOK, gin.H{
+			"modified_at":      file.ModTime(),
 			"advanced":         site.Advanced,
 			"enabled":          enabled,
 			"name":             name,
@@ -167,6 +185,7 @@ func GetDomain(c *gin.Context) {
 	c.Set("maybe_error", "nginx_config_syntax_error")
 
 	c.JSON(http.StatusOK, gin.H{
+		"modified_at":      file.ModTime(),
 		"advanced":         site.Advanced,
 		"enabled":          enabled,
 		"name":             name,
@@ -253,7 +272,7 @@ func SaveDomain(c *gin.Context) {
 	if helper.FileExists(enabledConfigFilePath) {
 		// Test nginx configuration
 		output := nginx.TestConf()
-		if nginx.GetLogLevel(output) >= nginx.Warn {
+		if nginx.GetLogLevel(output) > nginx.Warn {
 			c.JSON(http.StatusInternalServerError, gin.H{
 				"message": output,
 				"error":   "nginx_config_syntax_error",
@@ -263,7 +282,7 @@ func SaveDomain(c *gin.Context) {
 
 		output = nginx.Reload()
 
-		if nginx.GetLogLevel(output) >= nginx.Warn {
+		if nginx.GetLogLevel(output) > nginx.Warn {
 			c.JSON(http.StatusInternalServerError, gin.H{
 				"message": output,
 			})

+ 1 - 1
server/model/chatgpt_log.go

@@ -17,7 +17,7 @@ func (j *JSON) Scan(value interface{}) error {
 		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
 	}
 
-	var result []openai.ChatCompletionMessage
+	result := make([]openai.ChatCompletionMessage, 0)
 	err := json.Unmarshal(bytes, &result)
 	*j = result
 	return err