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
// than letting them walk into a 403.
func (s *server) handleSession(w http.ResponseWriter, r *http.Request) {
build := currentBuildMetadata()
writeJSON(w, http.StatusOK, map[string]any{
"actor": actorFromContext(r.Context()),
"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
// from "the old one is just slow to respond".
"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")
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
}
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 type="module" crossorigin src="/assets/index-qfdwgPZE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dc3900m_.css">
<script type="module" crossorigin src="/assets/index-CgFYD-Jw.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-hA2EpjuH.css">
</head>
<body>
<div id="root"></div>

View file

@ -42,7 +42,7 @@ export function App() {
return (
<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} />
</Shell>
</PermissionsProvider>

View file

@ -41,12 +41,14 @@ export function BootScreen() {
export function Shell({
actor,
build,
route,
navigate,
onLogout,
children
}: {
actor: string;
build?: { commit: string; short_commit: string; dirty: boolean; build_time: string };
route: RouteState;
navigate: Navigate;
onLogout: () => void;
@ -143,6 +145,11 @@ export function Shell({
</nav>
<div className="sidebar-status">
<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>
</aside>
<div className="workspace">

View file

@ -263,6 +263,13 @@ a {
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 {
display: grid;
gap: 4px;

View file

@ -584,6 +584,14 @@ export type AdminSession = {
// comment. Used by Server Settings' Restart/Update flow to detect a
// genuinely new admin process after asking it to bounce.
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 & {

View file

@ -387,9 +387,13 @@ func (m *Manager) goBuild(ctx context.Context, outPath, pkg string) (string, err
// --- high-level actions ----------------------------------------------------
// Restart rebuilds and relaunches only bin/owpengram-server (the MTProto
// data-plane process) -- never the admin binary currently handling this
// request. Returns a combined build/relaunch log for the admin UI.
// Restart rebuilds BOTH bin/owpengram-server and bin/owpengram-admin-panel
// from the current working tree (no git pull -- see Update for that) and
// 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) {
st := m.loadState()
if pidAlive(st.ServerPID) {
@ -399,7 +403,7 @@ func (m *Manager) Restart(ctx context.Context) (string, error) {
if err != nil {
return dockerLog, err
}
buildLog, err := m.buildServer(ctx)
buildLog, err := m.buildBoth(ctx)
fullLog := dockerLog + "\n" + buildLog
if err != nil {
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 {
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
// binaries, then does the same server-only relaunch as Restart -- including
// asking that new process to bounce the admin panel too, now onto its
// freshly built binary.
// Update is Restart plus a `git pull --ff-only` first, so a fresh checkout
// gets built instead of whatever's already on disk.
func (m *Manager) Update(ctx context.Context) (string, error) {
pullLog, err := m.GitPull(ctx)
if err != nil {