fixed info about api layer in admin page

This commit is contained in:
onysd 2026-09-01 19:12:32 +03:00
parent 62d8b53030
commit d2f4e11390
11 changed files with 78 additions and 30 deletions

View file

@ -386,6 +386,7 @@ LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.ed
LEFT JOIN auth a ON a.user_id = u.id LEFT JOIN auth a ON a.user_id = u.id
LEFT JOIN account_passwords ap ON ap.user_id = u.id LEFT JOIN account_passwords ap ON ap.user_id = u.id
WHERE NOT u.is_bot WHERE NOT u.is_bot
AND NOT (u.id = ANY($8::bigint[]))
AND ( AND (
u.id = $1 u.id = $1
OR u.phone LIKE $2 OR u.phone LIKE $2
@ -396,7 +397,7 @@ WHERE NOT u.is_bot
) )
AND ($5::bigint = 0 OR (COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), u.id) < (to_timestamp(($5::double precision) / 1000000.0), $6::bigint)) AND ($5::bigint = 0 OR (COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz), u.id) < (to_timestamp(($5::double precision) / 1000000.0), $6::bigint))
ORDER BY COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz) DESC, u.id DESC ORDER BY COALESCE(a.last_active_at, '0001-01-01 00:00:00+00'::timestamptz) DESC, u.id DESC
LIMIT $7`, id, phonePrefix, substring, term, beforeActiveUS, beforeID, limit+1) LIMIT $7`, id, phonePrefix, substring, term, beforeActiveUS, beforeID, limit+1, domain.SystemUserIDs())
if err != nil { if err != nil {
return nil, false, fmt.Errorf("search accounts: %w", err) return nil, false, fmt.Errorf("search accounts: %w", err)
} }
@ -837,19 +838,20 @@ WITH auth AS (
SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at, SELECT u.id, u.phone, u.username, u.first_name, u.last_name, u.created_at, u.updated_at,
COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, u.scam, u.fake, COALESCE(r.frozen, false), COALESCE(r.reason, ''), u.verified, u.scam, u.fake,
COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint, COALESCE(EXTRACT(EPOCH FROM u.premium_expires_at), 0)::bigint,
auth.last_active_at, auth.device_count, COALESCE(auth.last_active_at, u.created_at), COALESCE(auth.device_count, 0),
COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username, COALESCE(NULLIF(u.username, ''), p.username_lower, '') AS display_username,
COALESCE(ap.login_email, ''), COALESCE(ap.login_email, ''),
`+accountCollectibleUsernamesColumn+` AS collectibles `+accountCollectibleUsernamesColumn+` AS collectibles
FROM users u FROM users u
JOIN auth ON auth.user_id = u.id LEFT JOIN auth ON auth.user_id = u.id
LEFT JOIN account_restrictions r ON r.user_id = u.id LEFT JOIN account_restrictions r ON r.user_id = u.id
LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable LEFT JOIN peer_usernames p ON p.peer_type = 'user' AND p.peer_id = u.id AND p.editable
LEFT JOIN account_passwords ap ON ap.user_id = u.id LEFT JOIN account_passwords ap ON ap.user_id = u.id
WHERE NOT u.is_bot WHERE NOT u.is_bot
AND ($1::bigint = 0 OR (auth.last_active_at, u.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint)) AND NOT (u.id = ANY($4::bigint[]))
ORDER BY auth.last_active_at DESC, u.id DESC AND ($1::bigint = 0 OR (COALESCE(auth.last_active_at, u.created_at), u.id) < (to_timestamp(($1::double precision) / 1000000.0), $2::bigint))
LIMIT $3`, beforeActiveUS, beforeID, limit+1) ORDER BY COALESCE(auth.last_active_at, u.created_at) DESC, u.id DESC
LIMIT $3`, beforeActiveUS, beforeID, limit+1, domain.SystemUserIDs())
if err != nil { if err != nil {
return nil, false, fmt.Errorf("list accounts: %w", err) return nil, false, fmt.Errorf("list accounts: %w", err)
} }

View file

@ -18,6 +18,7 @@ import (
"time" "time"
"github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tg"
"github.com/iamxvbaba/td/tlprofile"
"telesrv/internal/admin" "telesrv/internal/admin"
"telesrv/internal/domain" "telesrv/internal/domain"
@ -292,6 +293,22 @@ func (s *server) handleAPILogout(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"ok": true}) writeJSON(w, http.StatusOK, map[string]any{"ok": true})
} }
// supportedTLLayers returns every MTProto TL schema layer this server binary
// can admit and encode for, oldest first. Probes tlprofile.ResolveProfile
// (the vendored td package's registry) rather than hardcoding a range here,
// so this stays correct as new profiles get added upstream without a
// second place to remember to update. tg.Layer is always the newest one and
// is included by construction, since ResolveProfile(tg.Layer) must succeed.
func supportedTLLayers() []int {
var layers []int
for n := 1; n <= tg.Layer; n++ {
if _, ok := tlprofile.ResolveProfile(n); ok {
layers = append(layers, n)
}
}
return layers
}
// handleSession is what the panel asks on load. It reports the permissions the // handleSession is what the panel asks on load. It reports the permissions the
// 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.
@ -307,10 +324,12 @@ 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,
// api_layer is the MTProto TL schema layer this server binary speaks // api_layers is every MTProto TL schema layer this server binary can
// (tg.Layer), shown in the sidebar footer above the build/commit line // actually admit and encode for (see tlprofile.ResolveProfile in the
// so an operator can tell at a glance which protocol layer is live. // vendored td package) -- the server is multi-layer (a client on an
"api_layer": tg.Layer, // older supported layer still works, not just the newest one), so
// the sidebar shows the whole supported set, not just tg.Layer.
"api_layers": supportedTLLayers(),
// build is this admin binary's own commit -- shown under "Version" // build is this admin binary's own commit -- shown under "Version"
// in the sidebar footer so an operator can tell at a glance which // in the sidebar footer so an operator can tell at a glance which
// build is actually running, independent of the app version string. // build is actually running, independent of the app version string.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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-D-ZS9t5z.js"></script> <script type="module" crossorigin src="/assets/index-D9dyWksN.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bkel0mh-.css"> <link rel="stylesheet" crossorigin href="/assets/index-DUxLVnzO.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} apiLayer={session.api_layer} build={session.build} route={route} navigate={navigate} onLogout={() => setSession(null)}> <Shell actor={session.actor} apiLayers={session.api_layers} 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

@ -24,6 +24,28 @@ import { type Navigate, type RouteState, routeTitle } from "../routing";
import { ThemeSwitch } from "../theme"; import { ThemeSwitch } from "../theme";
import { AppLink } from "./AppLink"; import { AppLink } from "./AppLink";
// Compresses a sorted (or unsorted) list of layer numbers into run-length
// ranges for the compact sidebar label, e.g. [225,226,227,228,229] -> "225-229",
// or [225,226,228] -> "225-226, 228" if the server ever supports a
// non-contiguous set. The full list is still always shown in the tooltip.
function formatLayerRanges(layers: number[]): string {
const sorted = [...layers].sort((a, b) => a - b);
const parts: string[] = [];
let start = sorted[0];
let prev = sorted[0];
for (let i = 1; i <= sorted.length; i++) {
const current = sorted[i];
if (current === prev + 1) {
prev = current;
continue;
}
parts.push(start === prev ? `${start}` : `${start}-${prev}`);
start = current;
prev = current;
}
return parts.join(", ");
}
export function BootScreen() { export function BootScreen() {
return ( return (
<div className="boot-screen"> <div className="boot-screen">
@ -41,7 +63,7 @@ export function BootScreen() {
export function Shell({ export function Shell({
actor, actor,
apiLayer, apiLayers,
build, build,
route, route,
navigate, navigate,
@ -49,7 +71,7 @@ export function Shell({
children children
}: { }: {
actor: string; actor: string;
apiLayer?: number; apiLayers?: number[];
build?: { commit: string; short_commit: string; dirty: boolean; build_time: string }; build?: { commit: string; short_commit: string; dirty: boolean; build_time: string };
route: RouteState; route: RouteState;
navigate: Navigate; navigate: Navigate;
@ -223,8 +245,10 @@ 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>
{typeof apiLayer === "number" && ( {apiLayers && apiLayers.length > 0 && (
<span className="sidebar-label sidebar-api-layer">{`API layer: ${apiLayer}`}</span> <span className="sidebar-label sidebar-api-layer" title={`Layers: ${apiLayers.join(", ")}`}>
{`API layers: ${formatLayerRanges(apiLayers)}`}
</span>
)} )}
{build?.short_commit && ( {build?.short_commit && (
<span className="sidebar-label sidebar-build" title={build.commit + (build.dirty ? " (uncommitted changes)" : "")}> <span className="sidebar-label sidebar-build" title={build.commit + (build.dirty ? " (uncommitted changes)" : "")}>

View file

@ -265,6 +265,7 @@ a {
letter-spacing: 0.04em; letter-spacing: 0.04em;
} }
.sidebar-api-layer,
.sidebar-build { .sidebar-build {
font-weight: 500; font-weight: 500;
text-transform: none; text-transform: none;

View file

@ -584,9 +584,11 @@ 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;
// The MTProto TL schema layer this server binary speaks -- shown in the // Every MTProto TL schema layer this server binary can admit and encode
// sidebar footer above the build/commit line. // for (oldest first) -- the server is multi-layer, so this is the whole
api_layer?: number; // supported set, not just the newest one. Shown in the sidebar footer
// above the build/commit line.
api_layers?: number[];
// This admin binary's own build -- shown under "Version" in the sidebar // This admin binary's own build -- shown under "Version" in the sidebar
// footer so an operator can tell which build is actually running. // footer so an operator can tell which build is actually running.
build?: { build?: {