diff --git a/api/event.go b/api/event.go index e17e2a4f..27e440c3 100644 --- a/api/event.go +++ b/api/event.go @@ -33,6 +33,7 @@ type Status struct { IsModified bool `json:"is_modified"` Revision string `json:"revision"` Status string `json:"status"` + Type string `json:"type"` } func (c *ConnectionSSE) SendEvent(w http.ResponseWriter, m platform.Event) error { @@ -113,6 +114,7 @@ func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) { IsModified: v.IsModified, Revision: v.Revision, Status: "connected", + Type: "status", } body, err := json.Marshal(status) if err != nil { @@ -121,7 +123,7 @@ func streamEvents(w http.ResponseWriter, r *http.Request, u platform.User) { return } - w.Write(body) + fmt.Fprintf(w, "data: %s\n\n", body) w.(http.Flusher).Flush() // Keep the connection open with a ticker sending periodic events diff --git a/ts/AppSync.vue b/ts/AppSync.vue index df26484c..43983aea 100644 --- a/ts/AppSync.vue +++ b/ts/AppSync.vue @@ -8,7 +8,7 @@ import { onMounted } from "vue"; import { apiClient } from "@/client"; import router from "@/route/config"; -import { SSEManager, type SSEMessage } from "@/SSEManager"; +import { SSEManager, type SSEMessageResource } from "@/SSEManager"; async function sentryInit() { const config = await apiClient.JSONGet("/api"); @@ -23,7 +23,7 @@ async function sentryInit() { } onMounted(() => { SSEManager.connect("/api/events"); - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.type != "heartbeat") { console.log("SSE", msg); } diff --git a/ts/SSEManager.ts b/ts/SSEManager.ts index 62945b04..ef503e4e 100644 --- a/ts/SSEManager.ts +++ b/ts/SSEManager.ts @@ -1,17 +1,27 @@ // Define types for the SSE data structure -export interface SSEMessage { +export interface SSEMessageBase { + type: string; +} +export interface SSEMessageResource extends SSEMessageBase { resource: string; time: string; - type: string; uri: string; } -type SSEHandler = (data: SSEMessage) => void; +export interface SSEMessageStatus extends SSEMessageBase { + build_time: Date; + is_modified: boolean; + revision: string; + status: string; +} +type SSEHandlerResource = (data: SSEMessageResource) => void; +type SSEHandlerStatus = (data: SSEMessageStatus) => void; interface SSEManagerType { connect: (url: string) => Promise; disconnect: () => void; - subscribe: (handler: SSEHandler) => string; + subscribe: (handler: SSEHandlerResource) => string; + subscribeStatus: (handler: SSEHandlerStatus) => string; unsubscribe: (uuid: string) => void; ready: (callback: (eventSource: EventSource) => void) => void; } @@ -26,7 +36,8 @@ declare global { export const SSEManager: SSEManagerType = (function (): SSEManagerType { let eventSource: EventSource | null = null; - let subscribers: Map = new Map(); + let subscribersResource: Map = new Map(); + let subscribersStatus: Map = new Map(); let isConnected: boolean = false; let connectionPromise: Promise | null = null; @@ -42,7 +53,7 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType { isConnected = true; eventSource!.addEventListener("message", (message: MessageEvent) => { - const data: SSEMessage = JSON.parse(message.data); + const data: SSEMessageBase = JSON.parse(message.data); handleMessage(data); }); @@ -85,13 +96,18 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType { } } - function handleMessage(msg: SSEMessage) { + function handleMessage(msg: SSEMessageBase) { if (msg.type == "heartbeat") { return; + } else if (msg.type == "status") { + subscribersStatus.forEach((handler: SSEHandlerStatus, _: string) => { + handler(msg as SSEMessageStatus); + }); + } else { + subscribersResource.forEach((handler: SSEHandlerResource, _: string) => { + handler(msg as SSEMessageResource); + }); } - subscribers.forEach((handler: SSEHandler, _: string) => { - handler(msg); - }); } function ready(callback: (eventSource: EventSource) => void): void { @@ -108,15 +124,24 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType { } } - function subscribe(handler: SSEHandler): string { + function subscribe(handler: SSEHandlerResource): string { const uuid = crypto.randomUUID(); - subscribers.set(uuid.toString(), handler); + subscribersResource.set(uuid.toString(), handler); + return uuid; + } + + function subscribeStatus(handler: SSEHandlerStatus): string { + const uuid = crypto.randomUUID(); + subscribersStatus.set(uuid.toString(), handler); return uuid; } function unsubscribe(uuid: string): void { - if (subscribers.has(uuid)) { - subscribers.delete(uuid); + if (subscribersResource.has(uuid)) { + subscribersResource.delete(uuid); + } + if (subscribersStatus.has(uuid)) { + subscribersStatus.delete(uuid); } } @@ -124,6 +149,7 @@ export const SSEManager: SSEManagerType = (function (): SSEManagerType { connect, disconnect, subscribe, + subscribeStatus, unsubscribe, ready, }; diff --git a/ts/components/layout/Sidebar.vue b/ts/components/layout/Sidebar.vue index 55612c0d..574e53e6 100644 --- a/ts/components/layout/Sidebar.vue +++ b/ts/components/layout/Sidebar.vue @@ -211,7 +211,7 @@ import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from "vue"; import { Tooltip, Popover } from "bootstrap"; import NavigationLink from "@/components/common/NavigationLink.vue"; -import { SSEManager, type SSEMessage } from "@/SSEManager"; +import { SSEManager, type SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; import type { Session } from "@/type/api"; @@ -283,7 +283,7 @@ const setTooltipsForSidebar = () => { // Lifecycle hooks onMounted(async () => { - const sub = SSEManager.subscribe((msg: SSEMessage) => { + const sub = SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource != "sync:session") { return; } diff --git a/ts/store/communication.ts b/ts/store/communication.ts index c494324b..feb4b238 100644 --- a/ts/store/communication.ts +++ b/ts/store/communication.ts @@ -2,7 +2,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { apiClient } from "@/client"; -import { SSEManager, SSEMessage } from "@/SSEManager"; +import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; import { Communication, CommunicationDTO } from "@/type/api"; @@ -13,7 +13,7 @@ export const useCommunicationStore = defineStore("communication", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("rmo:")) { fetchAll(); } diff --git a/ts/store/review-task.ts b/ts/store/review-task.ts index d4bb68a2..d221faf4 100644 --- a/ts/store/review-task.ts +++ b/ts/store/review-task.ts @@ -1,6 +1,6 @@ import { defineStore } from "pinia"; import { ref } from "vue"; -import { SSEManager, SSEMessage } from "@/SSEManager"; +import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { ReviewTask, ReviewTaskListResponse } from "@/type/api"; import { useSessionStore } from "@/store/session"; @@ -11,7 +11,7 @@ export const useStoreReviewTask = defineStore("review-task", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:review-task")) { fetchAll(); } diff --git a/ts/store/service_request.ts b/ts/store/service_request.ts index 1b177f37..42ec8282 100644 --- a/ts/store/service_request.ts +++ b/ts/store/service_request.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { ServiceRequest } from "@/type/api"; -import { SSEManager, SSEMessage } from "@/SSEManager"; +import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; export const useStoreServiceRequest = defineStore("service-request", () => { @@ -11,7 +11,7 @@ export const useStoreServiceRequest = defineStore("service-request", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:service-request")) { fetchAll(); } diff --git a/ts/store/session.ts b/ts/store/session.ts index b31bab05..f74524d4 100644 --- a/ts/store/session.ts +++ b/ts/store/session.ts @@ -1,7 +1,7 @@ import * as axios from "axios"; import { defineStore } from "pinia"; import { ref } from "vue"; -import { SSEManager, type SSEMessage } from "@/SSEManager"; +import { SSEManager, type SSEMessageResource } from "@/SSEManager"; import { Organization, Session, @@ -38,7 +38,7 @@ export const useSessionStore = defineStore("session", () => { const urls = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.type == "sync:session") { fetchSession(); } diff --git a/ts/store/signal.ts b/ts/store/signal.ts index 21378e40..8a932998 100644 --- a/ts/store/signal.ts +++ b/ts/store/signal.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { Signal } from "@/type/api"; -import { SSEManager, type SSEMessage } from "@/SSEManager"; +import { SSEManager, type SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; export const useSignalStore = defineStore("signal", () => { @@ -11,7 +11,7 @@ export const useSignalStore = defineStore("signal", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:signal")) { fetchAll(); } diff --git a/ts/store/site.ts b/ts/store/site.ts index 484169ef..38391a91 100644 --- a/ts/store/site.ts +++ b/ts/store/site.ts @@ -1,6 +1,6 @@ import { defineStore } from "pinia"; import { ref } from "vue"; -import { SSEManager, SSEMessage } from "@/SSEManager"; +import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { Site, SiteListResponse } from "@/type/api"; import { useSessionStore } from "@/store/session"; @@ -11,7 +11,7 @@ export const useStoreSite = defineStore("site", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:site")) { fetchAll(); } diff --git a/ts/store/sync.ts b/ts/store/sync.ts index 12c26a81..7b59ce5e 100644 --- a/ts/store/sync.ts +++ b/ts/store/sync.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { Sync } from "@/type/api"; -import { SSEManager, SSEMessage } from "@/SSEManager"; +import { SSEManager, SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; export const useStoreSync = defineStore("sync", () => { @@ -11,7 +11,7 @@ export const useStoreSync = defineStore("sync", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:sync")) { fetchAll(); } diff --git a/ts/store/upload.ts b/ts/store/upload.ts index d0efb888..b2c0c28e 100644 --- a/ts/store/upload.ts +++ b/ts/store/upload.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { Upload } from "@/type/api"; -import { SSEManager, type SSEMessage } from "@/SSEManager"; +import { SSEManager, type SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; export const useUploadStore = defineStore("upload", () => { @@ -12,7 +12,7 @@ export const useUploadStore = defineStore("upload", () => { const error = ref(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:upload")) { fetchAll(); } diff --git a/ts/store/user.ts b/ts/store/user.ts index 7419c11a..893624b9 100644 --- a/ts/store/user.ts +++ b/ts/store/user.ts @@ -1,7 +1,7 @@ import { defineStore } from "pinia"; import { ref } from "vue"; import { User } from "@/type/api"; -import { SSEManager, type SSEMessage } from "@/SSEManager"; +import { SSEManager, type SSEMessageResource } from "@/SSEManager"; import { useSessionStore } from "@/store/session"; export const useUserStore = defineStore("users", () => { @@ -13,7 +13,7 @@ export const useUserStore = defineStore("users", () => { const ongoingFetch = ref | null>(null); // Subscription - SSEManager.subscribe((msg: SSEMessage) => { + SSEManager.subscribe((msg: SSEMessageResource) => { if (msg.resource.startsWith("sync:user")) { fetchAll(); } diff --git a/version/version.go b/version/version.go index e2f1bc4b..e86e1a96 100644 --- a/version/version.go +++ b/version/version.go @@ -1,6 +1,43 @@ -package main +package version -var ( - Version = "dev" - Commit = "none" +import ( + "runtime/debug" + "time" ) + +type VersionInfo struct { + BuildTime time.Time + IsModified bool + Revision string +} + +func Get() VersionInfo { + info, ok := debug.ReadBuildInfo() + if !ok { + return VersionInfo{ + BuildTime: time.Now(), + IsModified: false, + Revision: "unknown", + } + } + + var version VersionInfo + for _, setting := range info.Settings { + switch setting.Key { + case "vcs.modified": + version.IsModified = setting.Value == "true" + case "vcs.revision": + if len(setting.Value) > 7 { + version.Revision = setting.Value[:7] + } else { + version.Revision = setting.Value + } + case "vcs.time": + if t, err := time.Parse(time.RFC3339, setting.Value); err == nil { + version.BuildTime = t + } + } + } + + return version +}