This commit is contained in:
onysd 2026-08-26 02:53:47 +03:00
parent 1c9a192a96
commit f3c4e3c60b
12 changed files with 95 additions and 16 deletions

View file

@ -0,0 +1,45 @@
package main
import "runtime/debug"
// gitCommit/buildTime can be set via -ldflags "-X main.gitCommit=... -X
// main.buildTime=...", mirroring cmd/telesrv/buildinfo.go -- but in
// practice neither procctl's goBuild (used by the admin panel's own
// Restart/Update) nor a plain `go build` sets them, so this normally falls
// back to Go's automatic VCS stamping (debug.ReadBuildInfo's vcs.revision),
// which needs nothing extra to work from a git checkout.
var (
gitCommit = ""
buildTime = ""
)
type buildMetadata struct {
Commit string
Dirty bool
BuildTime string
}
// shortCommit is what the sidebar footer shows next to "Version: O7" -- the
// full hash is one click away in git log, the footer just needs enough to
// tell two builds apart at a glance.
func (m buildMetadata) shortCommit() string {
if len(m.Commit) > 7 {
return m.Commit[:7]
}
return m.Commit
}
func currentBuildMetadata() buildMetadata {
meta := buildMetadata{Commit: gitCommit, BuildTime: buildTime}
if info, ok := debug.ReadBuildInfo(); ok {
settings := map[string]string{}
for _, setting := range info.Settings {
settings[setting.Key] = setting.Value
}
if meta.Commit == "" {
meta.Commit = settings["vcs.revision"]
}
meta.Dirty = settings["vcs.modified"] == "true"
}
return meta
}

View file

@ -292,6 +292,7 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) {
// session carries, so the UI can hide a section the operator may not use rather // session carries, so the UI can hide a section the operator may not use rather
// than letting them walk into a 403. // than letting them walk into a 403.
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) { func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
build := currentBuildMetadata()
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"actor": actorFromContext(r.Context()), "actor": actorFromContext(r.Context()),
"permissions": permissionsFromContext(r.Context()).List(), "permissions": permissionsFromContext(r.Context()).List(),
@ -302,6 +303,15 @@ func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
// tells "the old admin process died and a new one answered" apart // tells "the old admin process died and a new one answered" apart
// from "the old one is just slow to respond". // from "the old one is just slow to respond".
"boot_id": bootID, "boot_id": bootID,
// build is this admin binary's own commit -- shown under "Version"
// in the sidebar footer so an operator can tell at a glance which
// build is actually running, independent of the app version string.
"build": map[string]any{
"commit": build.Commit,
"short_commit": build.shortCommit(),
"dirty": build.Dirty,
"build_time": build.BuildTime,
},
}) })
} }

View file

@ -262,7 +262,7 @@ func (s *server) handleRestartServerAPI(w http.ResponseWriter, r *http.Request)
} }
meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "restart-server") meta := s.commandMetaFromAPI(r, body.CommandID, body.Reason, body.Confirm, "restart-server")
if meta.DryRun { if meta.DryRun {
writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.restart", nil, "restart validated -- rebuilds and relaunches bin/owpengram-server", nil)) writeJSON(w, http.StatusOK, serverCommandResult(meta, "server.restart", nil, "restart validated -- rebuilds both bin/owpengram-server and bin/owpengram-admin-panel, relaunches owpengram-server", nil))
return return
} }
log, err := s.serverCtl.Restart(r.Context()) log, err := s.serverCtl.Restart(r.Context())

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,8 +23,8 @@
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-qfdwgPZE.js"></script> <script type="module" crossorigin src="/assets/index-CgFYD-Jw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dc3900m_.css"> <link rel="stylesheet" crossorigin href="/assets/index-hA2EpjuH.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -42,7 +42,7 @@ export function App() {
return ( return (
<PermissionsProvider permissions={session.permissions ?? []} hideThirdPartyVerification={session.hide_third_party_verification ?? true}> <PermissionsProvider permissions={session.permissions ?? []} hideThirdPartyVerification={session.hide_third_party_verification ?? true}>
<Shell actor={session.actor} route={route} navigate={navigate} onLogout={() => setSession(null)}> <Shell actor={session.actor} build={session.build} route={route} navigate={navigate} onLogout={() => setSession(null)}>
<Routes route={route} navigate={navigate} /> <Routes route={route} navigate={navigate} />
</Shell> </Shell>
</PermissionsProvider> </PermissionsProvider>

View file

@ -41,12 +41,14 @@ export function BootScreen() {
export function Shell({ export function Shell({
actor, actor,
build,
route, route,
navigate, navigate,
onLogout, onLogout,
children children
}: { }: {
actor: string; actor: string;
build?: { commit: string; short_commit: string; dirty: boolean; build_time: string };
route: RouteState; route: RouteState;
navigate: Navigate; navigate: Navigate;
onLogout: () => void; onLogout: () => void;
@ -143,6 +145,11 @@ export function Shell({
</nav> </nav>
<div className="sidebar-status"> <div className="sidebar-status">
<span className="sidebar-label">{"Version: O7"}</span> <span className="sidebar-label">{"Version: O7"}</span>
{build?.short_commit && (
<span className="sidebar-label sidebar-build" title={build.commit + (build.dirty ? " (uncommitted changes)" : "")}>
{`Build: ${build.short_commit}${build.dirty ? "+" : ""}`}
</span>
)}
</div> </div>
</aside> </aside>
<div className="workspace"> <div className="workspace">

View file

@ -263,6 +263,13 @@ a {
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.sidebar-build {
font-weight: 500;
text-transform: none;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
opacity: 0.7;
}
.nav-list { .nav-list {
display: grid; display: grid;
gap: 4px; gap: 4px;

View file

@ -584,6 +584,14 @@ export type AdminSession = {
// comment. Used by Server Settings' Restart/Update flow to detect a // comment. Used by Server Settings' Restart/Update flow to detect a
// genuinely new admin process after asking it to bounce. // genuinely new admin process after asking it to bounce.
boot_id?: string; boot_id?: string;
// This admin binary's own build -- shown under "Version" in the sidebar
// footer so an operator can tell which build is actually running.
build?: {
commit: string;
short_commit: string;
dirty: boolean;
build_time: string;
};
}; };
export type AdminLoginResult = AdminSession & { export type AdminLoginResult = AdminSession & {

View file

@ -387,9 +387,13 @@ func (m *Manager) goBuild(ctx context.Context, outPath, pkg string) (string, err
// --- high-level actions ---------------------------------------------------- // --- high-level actions ----------------------------------------------------
// Restart rebuilds and relaunches only bin/owpengram-server (the MTProto // Restart rebuilds BOTH bin/owpengram-server and bin/owpengram-admin-panel
// data-plane process) -- never the admin binary currently handling this // from the current working tree (no git pull -- see Update for that) and
// request. Returns a combined build/relaunch log for the admin UI. // relaunches owpengram-server, which then bounces the admin panel onto its
// freshly built binary. Never self-restarts the admin process handling this
// request directly -- see HandlePendingAdminRestart's doc comment for why
// that handoff happens from the newly launched server instead. Returns a
// combined build/relaunch log for the admin UI.
func (m *Manager) Restart(ctx context.Context) (string, error) { func (m *Manager) Restart(ctx context.Context) (string, error) {
st := m.loadState() st := m.loadState()
if pidAlive(st.ServerPID) { if pidAlive(st.ServerPID) {
@ -399,7 +403,7 @@ func (m *Manager) Restart(ctx context.Context) (string, error) {
if err != nil { if err != nil {
return dockerLog, err return dockerLog, err
} }
buildLog, err := m.buildServer(ctx) buildLog, err := m.buildBoth(ctx)
fullLog := dockerLog + "\n" + buildLog fullLog := dockerLog + "\n" + buildLog
if err != nil { if err != nil {
return fullLog, fmt.Errorf("build failed: %w", err) return fullLog, fmt.Errorf("build failed: %w", err)
@ -416,13 +420,11 @@ func (m *Manager) Restart(ctx context.Context) (string, error) {
if err := m.saveState(st); err != nil { if err := m.saveState(st); err != nil {
return fullLog, fmt.Errorf("save state: %w", err) return fullLog, fmt.Errorf("save state: %w", err)
} }
return fullLog + fmt.Sprintf("\nowpengram-server relaunched, pid=%d. Admin panel will restart shortly.\n", pid), nil return fullLog + fmt.Sprintf("\nowpengram-server relaunched, pid=%d. Admin panel will restart shortly onto its freshly built binary.\n", pid), nil
} }
// Update runs git pull, ensures Docker infrastructure is up, rebuilds both // Update is Restart plus a `git pull --ff-only` first, so a fresh checkout
// binaries, then does the same server-only relaunch as Restart -- including // gets built instead of whatever's already on disk.
// asking that new process to bounce the admin panel too, now onto its
// freshly built binary.
func (m *Manager) Update(ctx context.Context) (string, error) { func (m *Manager) Update(ctx context.Context) (string, error) {
pullLog, err := m.GitPull(ctx) pullLog, err := m.GitPull(ctx)
if err != nil { if err != nil {