mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/main: automatically reload page if version changes
This commit is contained in:
parent
5ee25b83d4
commit
5638adf6bc
9 changed files with 124 additions and 17 deletions
|
@ -81,6 +81,9 @@ func (c *CommandHandler) Init() {
|
||||||
Data: marshaledPayload,
|
Data: marshaledPayload,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
c.App.EmitEvent("hicli_event", &hicli.JSONCommand{
|
c.App.EmitEvent("hicli_event", &hicli.JSONCommand{
|
||||||
Command: "init_complete",
|
Command: "init_complete",
|
||||||
RequestID: 0,
|
RequestID: 0,
|
||||||
|
|
|
@ -56,6 +56,8 @@ type Gomuks struct {
|
||||||
LogDir string
|
LogDir string
|
||||||
|
|
||||||
FrontendFS embed.FS
|
FrontendFS embed.FS
|
||||||
|
indexWithETag []byte
|
||||||
|
frontendETag string
|
||||||
|
|
||||||
Config Config
|
Config Config
|
||||||
DisableAuth bool
|
DisableAuth bool
|
||||||
|
|
|
@ -17,15 +17,19 @@
|
||||||
package gomuks
|
package gomuks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -73,6 +77,25 @@ func (gmx *Gomuks) StartServer() {
|
||||||
gmx.Log.Warn().Msg("Frontend not found")
|
gmx.Log.Warn().Msg("Frontend not found")
|
||||||
} else {
|
} else {
|
||||||
router.Handle("/", gmx.FrontendCacheMiddleware(http.FileServerFS(frontend)))
|
router.Handle("/", gmx.FrontendCacheMiddleware(http.FileServerFS(frontend)))
|
||||||
|
if gmx.Commit != "unknown" && !gmx.BuildTime.IsZero() {
|
||||||
|
gmx.frontendETag = fmt.Sprintf(`"%s-%s"`, gmx.Commit, gmx.BuildTime.Format(time.RFC3339))
|
||||||
|
|
||||||
|
indexFile, err := frontend.Open("index.html")
|
||||||
|
if err != nil {
|
||||||
|
gmx.Log.Err(err).Msg("Failed to open index.html")
|
||||||
|
} else {
|
||||||
|
data, err := io.ReadAll(indexFile)
|
||||||
|
_ = indexFile.Close()
|
||||||
|
if err == nil {
|
||||||
|
gmx.indexWithETag = bytes.Replace(
|
||||||
|
data,
|
||||||
|
[]byte("<!-- etag placeholder -->"),
|
||||||
|
[]byte(fmt.Sprintf(`<meta name="gomuks-frontend-etag" content="%s">`, html.EscapeString(gmx.frontendETag))),
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
gmx.Server = &http.Server{
|
gmx.Server = &http.Server{
|
||||||
Addr: gmx.Config.Web.ListenAddress,
|
Addr: gmx.Config.Web.ListenAddress,
|
||||||
|
@ -88,20 +111,23 @@ func (gmx *Gomuks) StartServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) FrontendCacheMiddleware(next http.Handler) http.Handler {
|
func (gmx *Gomuks) FrontendCacheMiddleware(next http.Handler) http.Handler {
|
||||||
var frontendCacheETag string
|
|
||||||
if gmx.Commit != "unknown" && !gmx.BuildTime.IsZero() {
|
|
||||||
frontendCacheETag = fmt.Sprintf(`"%s-%s"`, gmx.Commit, gmx.BuildTime.Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Header.Get("If-None-Match") == frontendCacheETag {
|
if gmx.frontendETag != "" && r.Header.Get("If-None-Match") == gmx.frontendETag {
|
||||||
w.WriteHeader(http.StatusNotModified)
|
w.WriteHeader(http.StatusNotModified)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(r.URL.Path, "/assets/") {
|
if strings.HasPrefix(r.URL.Path, "/assets/") {
|
||||||
w.Header().Set("Cache-Control", "max-age=604800, immutable")
|
w.Header().Set("Cache-Control", "max-age=604800, immutable")
|
||||||
}
|
}
|
||||||
if frontendCacheETag != "" {
|
if gmx.frontendETag != "" {
|
||||||
w.Header().Set("ETag", frontendCacheETag)
|
w.Header().Set("ETag", gmx.frontendETag)
|
||||||
|
if r.URL.Path == "/" && gmx.indexWithETag != nil {
|
||||||
|
w.Header().Set("Content-Type", "text/html")
|
||||||
|
w.Header().Set("Content-Length", strconv.Itoa(len(gmx.indexWithETag)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(gmx.indexWithETag)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
|
@ -59,6 +59,11 @@ type PingRequestData struct {
|
||||||
|
|
||||||
var runID = time.Now().UnixNano()
|
var runID = time.Now().UnixNano()
|
||||||
|
|
||||||
|
type RunData struct {
|
||||||
|
RunID string `json:"run_id"`
|
||||||
|
ETag string `json:"etag"`
|
||||||
|
}
|
||||||
|
|
||||||
func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
var conn *websocket.Conn
|
var conn *websocket.Conn
|
||||||
log := zerolog.Ctx(r.Context())
|
log := zerolog.Ctx(r.Context())
|
||||||
|
@ -233,9 +238,12 @@ func (gmx *Gomuks) HandleWebsocket(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
|
log.Trace().Int64("req_id", cmd.RequestID).Msg("Sent response to command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[string]{
|
initErr := writeCmd(ctx, conn, &hicli.JSONCommandCustom[*RunData]{
|
||||||
Command: "run_id",
|
Command: "run_id",
|
||||||
Data: strconv.FormatInt(runID, 10),
|
Data: &RunData{
|
||||||
|
RunID: strconv.FormatInt(runID, 10),
|
||||||
|
ETag: gmx.frontendETag,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if initErr != nil {
|
if initErr != nil {
|
||||||
log.Err(initErr).Msg("Failed to write init client state message")
|
log.Err(initErr).Msg("Failed to write init client state message")
|
||||||
|
@ -315,6 +323,9 @@ func (gmx *Gomuks) sendInitialData(ctx context.Context, conn *websocket.Conn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
err := writeCmd(ctx, conn, &hicli.JSONCommand{
|
err := writeCmd(ctx, conn, &hicli.JSONCommand{
|
||||||
Command: "init_complete",
|
Command: "init_complete",
|
||||||
RequestID: 0,
|
RequestID: 0,
|
||||||
|
|
|
@ -23,6 +23,9 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room)
|
||||||
ad, err := h.DB.AccountData.GetAllRoom(ctx, h.Account.UserID, room.ID)
|
ad, err := h.DB.AccountData.GetAllRoom(ctx, h.Account.UserID, room.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Stringer("room_id", room.ID).Msg("Failed to get room account data")
|
zerolog.Ctx(ctx).Err(err).Stringer("room_id", room.ID).Msg("Failed to get room account data")
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
syncRoom.AccountData = make(map[event.Type]*database.AccountData)
|
syncRoom.AccountData = make(map[event.Type]*database.AccountData)
|
||||||
} else {
|
} else {
|
||||||
syncRoom.AccountData = make(map[event.Type]*database.AccountData, len(ad))
|
syncRoom.AccountData = make(map[event.Type]*database.AccountData, len(ad))
|
||||||
|
@ -34,6 +37,9 @@ func (h *HiClient) getInitialSyncRoom(ctx context.Context, room *database.Room)
|
||||||
previewEvent, err := h.DB.Event.GetByRowID(ctx, room.PreviewEventRowID)
|
previewEvent, err := h.DB.Event.GetByRowID(ctx, room.PreviewEventRowID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Stringer("room_id", room.ID).Msg("Failed to get preview event for room")
|
zerolog.Ctx(ctx).Err(err).Stringer("room_id", room.ID).Msg("Failed to get preview event for room")
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
} else if previewEvent != nil {
|
} else if previewEvent != nil {
|
||||||
h.ReprocessExistingEvent(ctx, previewEvent)
|
h.ReprocessExistingEvent(ctx, previewEvent)
|
||||||
previewMember, err := h.DB.CurrentState.Get(ctx, room.ID, event.StateMember, previewEvent.Sender.String())
|
previewMember, err := h.DB.CurrentState.Get(ctx, room.ID, event.StateMember, previewEvent.Sender.String())
|
||||||
|
@ -85,6 +91,9 @@ func (h *HiClient) GetInitialSync(ctx context.Context, batchSize int) iter.Seq[*
|
||||||
}
|
}
|
||||||
maxTS = room.SortingTimestamp.Time
|
maxTS = room.SortingTimestamp.Time
|
||||||
payload.Rooms[room.ID] = h.getInitialSyncRoom(ctx, room)
|
payload.Rooms[room.ID] = h.getInitialSyncRoom(ctx, room)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !yield(&payload) || len(rooms) < batchSize {
|
if !yield(&payload) || len(rooms) < batchSize {
|
||||||
break
|
break
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<link rel="icon" type="image/png" href="gomuks.png"/>
|
<link rel="icon" type="image/png" href="gomuks.png"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>gomuks web</title>
|
<title>gomuks web</title>
|
||||||
|
<!-- etag placeholder -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
@ -124,7 +124,12 @@ export interface InitCompleteEvent extends BaseRPCCommand<void> {
|
||||||
command: "init_complete"
|
command: "init_complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunIDEvent extends BaseRPCCommand<string> {
|
export interface RunData {
|
||||||
|
run_id: string
|
||||||
|
etag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunIDEvent extends BaseRPCCommand<RunData> {
|
||||||
command: "run_id"
|
command: "run_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,35 @@ import type { RPCCommand } from "./types"
|
||||||
const PING_INTERVAL = 15_000
|
const PING_INTERVAL = 15_000
|
||||||
const RECV_TIMEOUT = 4 * PING_INTERVAL
|
const RECV_TIMEOUT = 4 * PING_INTERVAL
|
||||||
|
|
||||||
|
function checkUpdate(etag: string) {
|
||||||
|
if (!import.meta.env.PROD) {
|
||||||
|
return
|
||||||
|
} else if (!etag) {
|
||||||
|
console.log("Not checking for update, frontend etag not found in websocket init")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentETag = (
|
||||||
|
document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement
|
||||||
|
)?.content
|
||||||
|
if (!currentETag) {
|
||||||
|
console.log("Not checking for update, frontend etag not found in head")
|
||||||
|
} else if (currentETag === etag) {
|
||||||
|
console.log("Frontend is up to date")
|
||||||
|
} else if (localStorage.lastUpdateTo === etag) {
|
||||||
|
console.warn(
|
||||||
|
`Frontend etag mismatch ${currentETag} !== ${etag}, `,
|
||||||
|
"but localstorage says an update was already attempted",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.info(`Frontend etag mismatch ${currentETag} !== ${etag}, reloading`)
|
||||||
|
localStorage.lastUpdateTo = etag
|
||||||
|
location.search = "?" + new URLSearchParams({
|
||||||
|
updateTo: etag,
|
||||||
|
state: JSON.stringify(history.state),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default class WSClient extends RPCClient {
|
export default class WSClient extends RPCClient {
|
||||||
#conn: WebSocket | null = null
|
#conn: WebSocket | null = null
|
||||||
#lastMessage: number = 0
|
#lastMessage: number = 0
|
||||||
|
@ -105,7 +134,9 @@ export default class WSClient extends RPCClient {
|
||||||
if (parsed.request_id < 0) {
|
if (parsed.request_id < 0) {
|
||||||
this.#lastReceivedEvt = parsed.request_id
|
this.#lastReceivedEvt = parsed.request_id
|
||||||
} else if (parsed.command === "run_id") {
|
} else if (parsed.command === "run_id") {
|
||||||
this.#resumeRunID = parsed.data
|
console.log("Received run ID", parsed.data)
|
||||||
|
this.#resumeRunID = parsed.data.run_id
|
||||||
|
checkUpdate(parsed.data.etag)
|
||||||
}
|
}
|
||||||
this.onCommand(parsed)
|
this.onCommand(parsed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ class ContextFields implements MainScreenContextFields {
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentRightPanel(): RightPanelProps | null {
|
get currentRightPanel(): RightPanelProps | null {
|
||||||
return this.rightPanelStack.length ? this.rightPanelStack[this.rightPanelStack.length-1] : null
|
return this.rightPanelStack.length ? this.rightPanelStack[this.rightPanelStack.length - 1] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
setRightPanel = (props: RightPanelProps | null, pushState = true) => {
|
setRightPanel = (props: RightPanelProps | null, pushState = true) => {
|
||||||
|
@ -151,17 +151,35 @@ const SYNC_ERROR_HIDE_DELAY = 30 * 1000
|
||||||
|
|
||||||
const handleURLHash = (client: Client, context: ContextFields) => {
|
const handleURLHash = (client: Client, context: ContextFields) => {
|
||||||
if (!location.hash.startsWith("#/uri/")) {
|
if (!location.hash.startsWith("#/uri/")) {
|
||||||
return
|
if (location.search) {
|
||||||
|
const currentETag = (
|
||||||
|
document.querySelector("meta[name=gomuks-frontend-etag]") as HTMLMetaElement
|
||||||
|
)?.content
|
||||||
|
const newURL = new URL(location.href)
|
||||||
|
const updateTo = newURL.searchParams.get("updateTo")
|
||||||
|
if (updateTo === currentETag) {
|
||||||
|
console.info("Update to etag", updateTo, "successful")
|
||||||
|
} else {
|
||||||
|
console.warn("Update to etag", updateTo, "failed, got", currentETag)
|
||||||
}
|
}
|
||||||
|
const state = JSON.parse(newURL.searchParams.get("state") || "{}")
|
||||||
|
newURL.search = ""
|
||||||
|
history.replaceState(state, "", newURL.toString())
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
return history.state
|
||||||
|
}
|
||||||
|
|
||||||
const decodedURI = decodeURIComponent(location.hash.slice("#/uri/".length))
|
const decodedURI = decodeURIComponent(location.hash.slice("#/uri/".length))
|
||||||
const uri = parseMatrixURI(decodedURI)
|
const uri = parseMatrixURI(decodedURI)
|
||||||
if (!uri) {
|
if (!uri) {
|
||||||
console.error("Invalid matrix URI", decodedURI)
|
console.error("Invalid matrix URI", decodedURI)
|
||||||
return
|
return history.state
|
||||||
}
|
}
|
||||||
console.log("Handling URI", uri)
|
console.log("Handling URI", uri)
|
||||||
const newURL = new URL(location.href)
|
const newURL = new URL(location.href)
|
||||||
newURL.hash = ""
|
newURL.hash = ""
|
||||||
|
newURL.search = ""
|
||||||
if (uri.identifier.startsWith("@")) {
|
if (uri.identifier.startsWith("@")) {
|
||||||
const right_panel = {
|
const right_panel = {
|
||||||
type: "user",
|
type: "user",
|
||||||
|
@ -184,6 +202,7 @@ const handleURLHash = (client: Client, context: ContextFields) => {
|
||||||
} else {
|
} else {
|
||||||
console.error("Invalid matrix URI", uri)
|
console.error("Invalid matrix URI", uri)
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainScreen = () => {
|
const MainScreen = () => {
|
||||||
|
@ -208,8 +227,8 @@ const MainScreen = () => {
|
||||||
}
|
}
|
||||||
window.addEventListener("popstate", listener)
|
window.addEventListener("popstate", listener)
|
||||||
const initHandle = () => {
|
const initHandle = () => {
|
||||||
listener({ state: history.state } as PopStateEvent)
|
const state = handleURLHash(client, context)
|
||||||
handleURLHash(client, context)
|
listener({ state } as PopStateEvent)
|
||||||
}
|
}
|
||||||
let cancel = () => {}
|
let cancel = () => {}
|
||||||
if (client.initComplete.current) {
|
if (client.initComplete.current) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue