@samitouri / QOSami-HFS / commits / 04f02a72

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()