alerts
Massimo Melina committed
Jun 25, 2024 at 22:51 UTC
04f02a724b739bb5aa5488adb2b3672525de6573
9 files changed
+54
-17
admin/src/HomePage.ts
+7
-2
@@ -51,6 +51,7 @@ export default function HomePage() {
51
]]))
52
return h(Box, { display:'flex', gap: 2, flexDirection:'column', alignItems: 'flex-start', height: '100%' },
53
username && entry('', "Welcome "+username),
54
+ dontBotherWithKeys(status.alerts?.map(x => entry('warning', md(x, { html: false })))),
55
errors.length ? dontBotherWithKeys(errors.map(msg => entry('error', dontBotherWithKeys(msg))))
56
: entry('success', "Server is working"),
57
!vfs ? h(LinearProgress)
@@ -102,7 +103,8 @@ export default function HomePage() {
103
variant: 'outlined',
104
icon: UpdateIcon,
105
onClick() {
105
- setCheckPlugins(true)
106
+ apiCall('wait_project_info').then(reloadStatus)
107
+ setCheckPlugins(true) // this only happens once, actually (until you change page)
108
return apiCall<typeof adminApis.check_update>('check_update').then(x => setUpdates(x.options), alertDialog)
109
},
110
async onContextMenu(ev) {
@@ -143,6 +145,7 @@ function Update({ info, title, bodyCollapsed }: { title?: ReactNode, info: Relea
145
146
function renderChangelog(s: string) {
147
return md(s, {
148
+ html: false,
149
onText: s => replaceStringToReact(s, /(?<=^|\W)#(\d+)\b|(https:.*\S+)/g, m => // link issues and urls
150
m[1] ? h(Link, { href: REPO_URL + 'issues/' + m[1], target: '_blank' }, h(OpenInNew))
151
: h(Link, { href: m[2], target: '_blank' }, m[2] )
@@ -182,7 +185,9 @@ function entry(color: Color, ...content: ReactNode[]) {
185
h(({ success: CheckCircle, info: Info, '': Info, warning: Warning, error: Error })[color], {
186
sx: { mr: 1, color: color ? undefined : 'primary.main' }
187
}),
185
- ...content)
188
+ h('span', { style: ['warning', 'error'].includes(color) ? { animation: '1s blink' } : undefined },
189
+ ...content)
190
+ )
191
}
192
193
function fsLink(text=`File System page`) {
admin/src/index.scss
-4
@@ -67,10 +67,6 @@ h2.MuiDialogTitle-root { /* less padding */
67
}
68
@keyframes animate-dash { to { background-position: 20px 0; } }
69
70
-@keyframes blink {
71
- 0% {opacity: 1}
72
- 50% {opacity: 0.2}
73
-}
70
@keyframes success {
71
50% { transform: scale(1.5); color: var(--success); }
72
100% { transform: inherit; color: inherit; }
frontend/src/index.scss
-4
@@ -213,10 +213,6 @@ kbd {
213
214
.ani-working { animation:1s blink infinite }
215
216
-@keyframes blink {
217
- 0% {opacity: 1}
218
- 50% {opacity: 0.2}
219
-}
216
@keyframes spin {
217
100% { transform: rotate(360deg); }
218
}
shared/_main.scss
+5
@@ -3,3 +3,8 @@
3
clip-path: rect(1px 1px 1px 1px);
4
clip: rect(1px, 1px, 1px, 1px); // legacy browsers
5
}
6
+
7
+@keyframes blink {
8
+ 0% {opacity: 1}
9
+ 50% {opacity: 0.2}
10
+}
shared/md.ts
+2
-2
@@ -10,14 +10,14 @@ export const MD_TAGS = {
10
}
11
type OnText = (s: string) => ReactNode
12
// md-inspired formatting, very simplified
13
-export function md(text: string | TemplateStringsArray, { linkTarget='_blank', onText=(x=>x) as OnText }={}) {
13
+export function md(text: string | TemplateStringsArray, { html=true, linkTarget='_blank', onText=(x=>x) as OnText }={}) {
14
if (typeof text !== 'string')
15
text = text[0]
16
return replaceStringToReact(text, /(`|_|\*\*?)(.+?)\1|(\n)|\[(.+?)\]\((.+?)\)|(<(\w+?)(?:\s+[^>]*?)?>(?:.*?<\/\7>)?)/g, m =>
17
m[4] ? h(MD_TAGS.a, { href: m[5], target: linkTarget }, onText(m[4]))
18
: m[3] ? h('br')
19
: m[1] ? h((MD_TAGS as any)[ m[1] ] || Fragment, {}, onText(m[2]))
20
- : h(Html, {}, m[6]),
20
+ : html ? h(Html, {}, m[6]) : m[6],
21
onText)
22
}
23
src/adminApis.ts
+6
@@ -37,6 +37,7 @@ import { roots } from './roots'
37
import { SendListReadable } from './SendList'
38
import { get_dynamic_dns_error } from './ddns'
39
import { addBlock, BlockingRule } from './block'
40
+import { alerts, getProjectInfo } from './github'
41
42
export const adminApis = {
43
@@ -79,6 +80,10 @@ export const adminApis = {
80
async check_update() {
81
return { options: await getUpdates() }
82
},
83
+ async wait_project_info() { // used by admin/home/check-for-updates
84
+ await getProjectInfo()
85
+ return {}
86
+ },
87
88
async ip_country({ ips }) {
89
const res = await Promise.allSettled(ips.map(ip2country))
@@ -121,6 +126,7 @@ export const adminApis = {
126
roots: roots.get(),
127
updatePossible: !await updateSupported() ? false : (await localUpdateAvailable()) ? 'local' : true,
128
autoCheckUpdateResult: autoCheckUpdateResult.get(), // in this form, we get the same type of the serialized json
129
+ alerts: alerts.get(),
130
proxyDetected: getProxyDetected(),
131
frpDetected: localhostAdmin.get() && !getProxyDetected()
132
&& getConnections().every(isLocalHost)
src/cross.ts
+7
@@ -467,6 +467,13 @@ export function safeDecodeURIComponent(s: string) {
467
catch { return s }
468
}
469
470
+export function popKey(o: any, k: string) {
471
+ if (!o) return
472
+ const x = o[k]
473
+ delete o[k]
474
+ return x
475
+}
476
+
477
export function shortenAgent(agent: string) {
478
return _.findKey(BROWSERS, re => re.test(agent))
479
|| /^[^/(]+ ?/.exec(agent)?.[0]
src/github.ts
+25
-4
@@ -1,18 +1,21 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import events from './events'
4
-import { DAY, httpString, httpStream, unzip, AsapStream, debounceAsync, asyncGeneratorToArray, wait } from './misc'
4
+import { DAY, httpString, httpStream, unzip, AsapStream, debounceAsync, asyncGeneratorToArray, wait, popKey } from './misc'
5
import {
6
DISABLING_SUFFIX, findPluginByRepo, getAvailablePlugins, getPluginInfo, isPluginEnabled, mapPlugins,
7
parsePluginSource, PATH as PLUGINS_PATH, Repo, startPlugin, stopPlugin, STORAGE_FOLDER
8
} from './plugins'
9
import { ApiError } from './apiMiddleware'
10
import _ from 'lodash'
11
-import { DEV, HFS_REPO, HFS_REPO_BRANCH, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_NOT_ACCEPTABLE,
12
- HTTP_SERVER_ERROR } from './const'
11
+import {
12
+ DEV, HFS_REPO, HFS_REPO_BRANCH, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_NOT_ACCEPTABLE,
13
+ HTTP_SERVER_ERROR, VERSION
14
+} from './const'
15
import { rename, rm } from 'fs/promises'
16
import { join } from 'path'
17
import { readFileSync } from 'fs'
18
+import { storedMap } from './persistence'
19
20
const DIST_ROOT = 'dist'
21
@@ -212,11 +215,29 @@ export async function searchPlugins(text='', { skipRepos=[''] }={}) {
215
}))
216
}
217
218
+export const alerts = storedMap.singleSync<string[]>('alerts', [])
219
// centralized hosted information, to be used as little as possible
220
const FN = 'central.json'
221
let builtIn = JSON.parse(readFileSync(join(__dirname, '..', FN), 'utf8'))
222
export const getProjectInfo = debounceAsync(
223
() => readGithubFile(`${HFS_REPO}/${HFS_REPO_BRANCH}/${FN}`)
224
.then(JSON.parse, () => null)
221
- .then(x => Object.assign({ ...builtIn }, DEV ? null : x) ), // fall back to built-in
225
+ .then(o => {
226
+ o = Object.assign({ ...builtIn }, DEV ? null : o) // fall back to built-in
227
+ // merge byVersions info in the main object, but collect alerts separately, to preserve multiple instances
228
+ const allAlerts: string[] = [o.alert]
229
+ for (const [ver, more] of Object.entries(popKey(o, 'byVersion') || {}))
230
+ if (VERSION.match(new RegExp(ver))) {
231
+ allAlerts.push((more as any).alert)
232
+ Object.assign(o, more)
233
+ }
234
+ _.remove(allAlerts, x => !x)
235
+ alerts.set(was => {
236
+ if (!_.isEqual(was, allAlerts))
237
+ for (const a of allAlerts)
238
+ console.log("ALERT:", a)
239
+ return allAlerts
240
+ })
241
+ return o
242
+ }),
243
{ retain: DAY, retainFailure: 60_000 })
\ No newline at end of file
src/update.ts
+2
-1
@@ -1,6 +1,6 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { getRepoInfo } from './github'
3
+import { getProjectInfo, getRepoInfo } from './github'
4
import { argv, HFS_REPO, IS_BINARY, IS_WINDOWS, RUNNING_BETA } from './const'
5
import { dirname, join } from 'path'
6
import { spawn, spawnSync } from 'child_process'
@@ -51,6 +51,7 @@ const ReleaseKeys = ['prerelease', 'tag_name', 'name', 'body', 'assets', 'isNewe
51
const ReleaseAssetKeys = ['name', 'browser_download_url'] satisfies (keyof Release['assets'][0])[]
52
53
export async function getUpdates(strict=false) {
54
+ getProjectInfo() // check for alerts
55
const stable: Release = await getRepoInfo(HFS_REPO + '/releases/latest')
56
const verStable = ver(stable)
57
const ret = await getBetas()