Move user administration to its own page; add Project Super User
User accounts lived in the Admin Console, which is admins-only. Project admins
need to create the accounts on their own jobs without an app admin on the phone,
so accounts move to a new User Directory page and a new role carries the right.
server/auth.py, server/app.py
New permissions role `project_super_user`, between admin and project_admin:
everything a project admin may do, plus user administration SCOPED to the
projects they hold the role on. Four limits make it safe to hand out, all
enforced server-side:
* Scope comes from projects, not the job title. It resolves per membership
(managed_project_ids), so an ordinary account can hold it on one job via
ProjectMember.role, and a super user demoted on one job administers
nobody there. No projects, no authority.
* Account-level changes (password, disable, rename, permissions, delete)
require EXCLUSIVE scope: refused when the target is also on a project the
caller does not administer, because those changes are global. The
directory renders such rows read-only with the reason.
* No admin or super-user targets, and neither role can be granted by a
super user -- that is the line that stops it becoming app-wide control.
* PUT .../projects rebuilds only the caller's own slice; memberships on
projects they do not administer are left untouched. A payload that simply
omits them must not cut someone off a job the caller cannot see.
Creating requires naming at least one of your own projects: an account with
none would be one the creator instantly cannot manage.
/api/auth/users is now scoped rather than admin-only, and carries a per-row
`manageable` verdict plus the reason. Non-managers get a contact card only --
a project user has no business reading colleagues' login history. New
/api/auth/user-scope tells the page what it may offer. Administrative
password resets are now audited; they were the one account change that left
no trace. Settings, feature flags and the auto-add rule stay admin-only.
While here: one definition of "is a user manager", derived from the managed
set. An account-role-only version disagreed with the scoped one and locked
per-project super users out of routes they were entitled to.
html/users.html, html/users.js
The directory: three renderings from one page -- admin (everything), super
user (controls per row, read-only where scope is shared), everyone else (a
read-only directory of the people on their own projects).
html/console.css, html/console-util.js
Extracted from admin.html/admin.js so both console pages share them. A
divergent jsq() is an XSS and a divergent role list offers permissions the
server refuses, so neither may exist twice.
html/wp-sidenav.{js,css}
Global nav drawer, role-gated, carrying ?project= across links. Mounted on
the field view (which had no way to anywhere) plus both console pages.
No migration: users.role is already String(20) and the new value fits.
Verified: 93 scope/gate tests, 29 live HTTP tests through the real dependency
stack, 33 static JS checks. Not verified in a browser -- no JS engine on this
machine -- so users.html and field.html want one manual load.
server/smoketest.py still fails with 401s. Pre-existing: it has no login code,
so auth_gate refuses it. Confirmed unchanged by stashing this work.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
244
html/admin.html
244
html/admin.html
@@ -13,198 +13,45 @@
|
||||
<meta name="theme-color" content="#161616">
|
||||
<link rel="stylesheet" href="theme-light.css">
|
||||
<link rel="stylesheet" href="wp-chrome.css">
|
||||
<link rel="stylesheet" href="console.css">
|
||||
<link rel="stylesheet" href="wp-sidenav.css">
|
||||
<style>
|
||||
/* ══ TOKENS ══════════════════════════════════════════════════════════════
|
||||
The console is the only page in the suite that is mostly dense tables, so
|
||||
it carries its own sheet. The palette, the square corners and the type are
|
||||
Carbon's — the same ones theme-light.css sets — so it still reads as one
|
||||
product with the rest of the suite. Two scales do all the spacing and all
|
||||
the control sizing; nothing in here should invent its own. */
|
||||
:root{ --bg:#f4f4f4; --surface:#fff; --border:#e0e0e0; --border-strong:#8d8d8d; --text:#161616;
|
||||
--muted:#525252; --dim:#8d8d8d; --accent:#0f62fe; --accent-hover:#0353e9; --accent-soft:#edf5ff;
|
||||
--green:#198038; --green-bg:#defbe6;
|
||||
--red:#da1e28; --red-bg:#fff1f1; --amber:#8e6a00; --amber-bg:#fdf6dd;
|
||||
--head-bg:#f4f4f4; --zebra:#fafafa; --row-hover:#eef0f2;
|
||||
--mono:'IBM Plex Mono','Cascadia Mono',Consolas,monospace;
|
||||
--s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:20px; --s6:28px;
|
||||
--ctl:32px; /* every button / input / select that sits in a form row */
|
||||
--ctl-sm:26px; } /* every control that sits inside a table cell */
|
||||
*{ box-sizing:border-box; }
|
||||
body{ margin:0; font-family:'IBM Plex Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; background:var(--bg); color:var(--text); }
|
||||
/* Page-specific only — the tokens, cards, controls, tables, banners and modal
|
||||
live in console.css, shared with the User Directory. What stays here is what
|
||||
only this page has: the per-card scroll boxes admin.js paints tables into, the
|
||||
column exceptions for those tables, and the admins-only notice.
|
||||
|
||||
/* ══ PAGE ════════════════════════════════════════════════════════════════
|
||||
1240px, not 860: the user table is nine columns wide and at 860 it spilled
|
||||
straight out of its own white card. Wide enough for that table, still a
|
||||
readable measure for the prose, which is capped separately. */
|
||||
.wrap{ max-width:1240px; margin:0 auto; padding:var(--s6) var(--s5) 80px; }
|
||||
h1{ font-size:20px; line-height:1.2; margin:0 0 2px; }
|
||||
.sub{ color:var(--muted); font-size:13px; line-height:1.5; margin:0 0 var(--s3); max-width:96ch; }
|
||||
a.home{ color:var(--accent); font-size:13px; text-decoration:none; white-space:nowrap; }
|
||||
a.home:hover{ text-decoration:underline; }
|
||||
These are addressed by ID because admin.js emits the tables without per-cell
|
||||
classes. */
|
||||
|
||||
/* ══ CARDS ═══════════════════════════════════════════════════════════════ */
|
||||
.card{ background:var(--surface); border:1px solid var(--border); border-radius:0;
|
||||
padding:var(--s4) var(--s5) var(--s5); margin-bottom:var(--s4); }
|
||||
/* One card header everywhere: small uppercase accent label on a hairline.
|
||||
admin.js also emits h2 for sub-sections inside a card (Localization
|
||||
defaults, Step views, Actions) with an inline margin-top — the same
|
||||
treatment reads correctly as a divider there, so both get it. */
|
||||
.card h2{ font-size:12px; font-weight:600; letter-spacing:.08em; text-transform:uppercase;
|
||||
color:var(--accent); margin:0 0 var(--s3); padding-bottom:var(--s2); border-bottom:1px solid var(--border); }
|
||||
.wrap code{ font-family:var(--mono); font-size:.92em; background:var(--bg); padding:1px 4px; }
|
||||
/* These start life as empty divs that admin.js fills on demand, so they only earn
|
||||
their gap once they are actually saying something. */
|
||||
#projects-banner:not(:empty), #defmem-banner:not(:empty){ margin-bottom:var(--s3); }
|
||||
|
||||
/* ══ CONTROLS ════════════════════════════════════════════════════════════
|
||||
Every button, input and select in a form row is exactly --ctl tall, so a
|
||||
toolbar is one clean band instead of a ragged one. */
|
||||
button{ font:inherit; font-size:13px; font-weight:600; line-height:1; white-space:nowrap;
|
||||
height:var(--ctl); padding:0 var(--s3); border-radius:0; cursor:pointer;
|
||||
border:1px solid var(--border-strong); background:#fff; color:var(--text); }
|
||||
button:hover{ border-color:var(--accent); color:var(--accent); }
|
||||
button:focus-visible{ outline:2px solid var(--accent); outline-offset:-3px; }
|
||||
button:disabled, button:disabled:hover{ color:var(--dim); border-color:var(--border); background:#fff; cursor:default; }
|
||||
button.primary{ background:var(--accent); border-color:var(--accent); color:#fff; }
|
||||
button.primary:hover{ background:var(--accent-hover); border-color:var(--accent-hover); color:#fff; }
|
||||
button.danger{ border-color:var(--red); color:var(--red); }
|
||||
button.danger:hover{ background:var(--red-bg); border-color:var(--red); color:var(--red); }
|
||||
.row{ display:flex; gap:var(--s2); flex-wrap:wrap; align-items:center; }
|
||||
/* The filter / search / button strip at the top of a card. */
|
||||
.toolbar{ display:flex; gap:var(--s2); flex-wrap:wrap; align-items:center; margin:0 0 var(--s3); }
|
||||
.toolbar + .banner{ margin-top:0; }
|
||||
.urow{ display:flex; gap:var(--s2); flex-wrap:wrap; align-items:center; }
|
||||
/* Checkboxes are excluded: they are drawn by the platform and want none of a
|
||||
text field's height, padding or border. */
|
||||
.toolbar input:not([type=checkbox]), .toolbar select,
|
||||
.urow input:not([type=checkbox]), .urow select{
|
||||
height:var(--ctl); padding:0 var(--s2); font:inherit; font-size:13px; line-height:normal;
|
||||
border:1px solid var(--border-strong); border-radius:0; background:#fff; color:var(--text); }
|
||||
.toolbar select, .urow select{ cursor:pointer; padding-right:var(--s1); }
|
||||
.toolbar input:focus-visible, .toolbar select:focus-visible,
|
||||
.urow input:focus-visible, .urow select:focus-visible{ outline:2px solid var(--accent); outline-offset:-2px; }
|
||||
.toolbar > input{ flex:1 1 240px; min-width:150px; }
|
||||
.urow input:not([type=checkbox]){ flex:1 1 140px; min-width:0; }
|
||||
/* Inline checkbox + label, sized to sit on the same line as the buttons. */
|
||||
.chk{ display:inline-flex; align-items:center; gap:var(--s2); height:var(--ctl); padding:0 var(--s1);
|
||||
font-size:13px; color:var(--muted); white-space:nowrap; cursor:pointer; }
|
||||
.chk input{ width:16px; height:16px; margin:0; accent-color:var(--accent); cursor:pointer; }
|
||||
|
||||
/* ══ FEEDBACK: banners, notes, console output, key/value ═════════════════ */
|
||||
.banner{ margin:var(--s3) 0 0; padding:9px var(--s3); border-radius:0; font-size:13px; font-weight:600;
|
||||
line-height:1.4; border:1px solid var(--border); border-left:3px solid var(--border-strong);
|
||||
background:var(--surface); color:var(--text); }
|
||||
.banner.ok{ background:var(--green-bg); color:var(--green); border-color:#a7f0ba; border-left-color:var(--green); }
|
||||
.banner.bad{ background:var(--red-bg); color:var(--red); border-color:#ffd7d9; border-left-color:var(--red); }
|
||||
/* These three start life as empty divs that admin.js fills on demand, so they
|
||||
only earn their gap once they are actually saying something. */
|
||||
#users-banner:not(:empty), #projects-banner:not(:empty), #defmem-banner:not(:empty){ margin-bottom:var(--s3); }
|
||||
/* --muted, not --dim: #8d8d8d on white is 3.3:1, under the 4.5:1 floor at 12px,
|
||||
and #features-box / #settings-box are themselves .note — their primary toggle
|
||||
labels inherit this colour. */
|
||||
.note{ font-size:12px; line-height:1.55; color:var(--muted); margin-top:var(--s2); }
|
||||
.note strong, .note em{ color:var(--text); }
|
||||
pre.out{ background:#0f1525; color:#d7e0f5; border-radius:0; padding:var(--s3) var(--s4); font-family:var(--mono);
|
||||
font-size:12px; line-height:1.55; white-space:pre-wrap; max-height:340px; overflow:auto; margin:var(--s3) 0 0; }
|
||||
pre.out .p{ color:#56d364; font-weight:700; } pre.out .f{ color:#ff7b72; font-weight:700; }
|
||||
table.kv{ border-collapse:collapse; font-size:13px; margin-top:var(--s2); }
|
||||
table.kv th{ text-align:left; padding:var(--s1) var(--s5) var(--s1) 0; color:var(--muted); font-weight:600; white-space:nowrap; }
|
||||
table.kv td{ padding:var(--s1) 0; font-variant-numeric:tabular-nums; font-weight:700; color:var(--text); }
|
||||
|
||||
/* ══ DATA TABLES ═════════════════════════════════════════════════════════
|
||||
table.users is the name admin.js already emits; table.grid is the same
|
||||
object under the shared name. One rule set serves both, so existing markup
|
||||
picks up the dense styling without being rewritten. border-collapse is
|
||||
separate rather than collapse because a collapsed border does not travel
|
||||
with a sticky header. */
|
||||
table.grid, table.users{ width:100%; border-collapse:separate; border-spacing:0;
|
||||
font-size:13px; color:var(--text); background:var(--surface); }
|
||||
table.grid th, table.users th{ position:sticky; top:0; z-index:2; background:var(--head-bg);
|
||||
text-align:left; padding:var(--s2) var(--s3); white-space:nowrap;
|
||||
font-size:11px; font-weight:600; letter-spacing:.04em; text-transform:uppercase; color:var(--muted);
|
||||
box-shadow:inset 0 -1px 0 var(--border); }
|
||||
/* Cells never wrap: a wrapped cell turns one user into a 100px tall band and
|
||||
the table stops reading as rows. Anything genuinely long truncates (.ell)
|
||||
or is exempted by name further down. */
|
||||
table.grid td, table.users td{ padding:var(--s1) var(--s3); border-bottom:1px solid var(--border);
|
||||
vertical-align:middle; white-space:nowrap; }
|
||||
table.grid tbody tr:last-child td, table.users tbody tr:last-child td{ border-bottom:none; }
|
||||
table.grid tbody tr:nth-child(even) td, table.users tbody tr:nth-child(even) td{ background:var(--zebra); }
|
||||
/* A neutral hover, not --accent-soft: that is .tag.admin's fill, and an "all
|
||||
projects" pill sitting on its own colour disappears the moment you hover it. */
|
||||
table.grid tbody tr:hover td, table.users tbody tr:hover td{ background:var(--row-hover); }
|
||||
/* Truncation has to hang off a block INSIDE the cell. max-width on a <td> is
|
||||
advisory under table-layout:auto — the cell just grows to fit and the ellipsis
|
||||
never appears, which is the usual reason this trick looks like it works in the
|
||||
stylesheet and doesn't on the page. admin.js emits <td class="ell"><span>. */
|
||||
.ell{ max-width:240px; }
|
||||
.ell > span{ display:block; max-width:240px; overflow:hidden; text-overflow:ellipsis;
|
||||
white-space:nowrap; }
|
||||
/* Every action cell admin.js renders is a .cellactions, and it must not wrap:
|
||||
unwrapped, the three buttons stack and the row grows fourfold — which is what
|
||||
the console looked like before this pass. */
|
||||
.cellactions{ display:flex; flex-wrap:nowrap; align-items:center; gap:var(--s1); white-space:nowrap; }
|
||||
/* Controls that live in a cell are one step smaller, which is what keeps a
|
||||
row at ~34px instead of ~100px. .chk is form-row sized by default, so it needs
|
||||
saying again here or the default-members rows stand 6px taller than the rest. */
|
||||
button.mini{ height:var(--ctl-sm); padding:0 var(--s2); font-size:12px; }
|
||||
table.grid td .chk, table.users td .chk{ height:var(--ctl-sm); }
|
||||
select.role-select{ height:var(--ctl-sm); max-width:170px; padding:0 var(--s1) 0 var(--s2);
|
||||
font:inherit; font-size:12px; border:1px solid var(--border-strong); border-radius:0;
|
||||
background:#fff; color:var(--text); cursor:pointer; }
|
||||
select.role-select:hover{ border-color:var(--accent); }
|
||||
select.role-select.is-admin{ color:var(--accent); border-color:var(--accent); font-weight:600; }
|
||||
.tag{ display:inline-block; padding:1px 8px; border-radius:11px; font-size:11px; font-weight:600;
|
||||
line-height:1.55; white-space:nowrap; vertical-align:middle; }
|
||||
.tag.admin{ background:var(--accent-soft); color:var(--accent); }
|
||||
.tag.user{ background:#e8e8e8; color:var(--muted); }
|
||||
.tag.on{ background:var(--green-bg); color:var(--green); }
|
||||
.tag.off{ background:var(--red-bg); color:var(--red); }
|
||||
.tag.archived{ background:var(--amber-bg); color:var(--amber); }
|
||||
.me-tag{ font-size:11px; color:var(--dim); margin-left:6px; white-space:nowrap; }
|
||||
|
||||
/* A wide table scrolls inside its own box so the page never scrolls sideways,
|
||||
and the capped height is what gives the sticky header something to do. Every
|
||||
container admin.js paints a table into is one, so a sticky header always has
|
||||
a scrollport of its own rather than sliding up behind the app bar. */
|
||||
.tscroll, #users-table, #comments-admin, #audit-admin, #notif-box, #usage-admin,
|
||||
#projects-table, #defmem-table{
|
||||
/* Every container admin.js paints a table into is a scrollport of its own, so a
|
||||
sticky header always has something to stick to rather than sliding up behind
|
||||
the app bar. Same rule as console.css's .tscroll. */
|
||||
#comments-admin, #audit-admin, #notif-box, #usage-admin, #projects-table, #defmem-table{
|
||||
overflow:auto; max-height:min(70vh,640px); overscroll-behavior:contain; }
|
||||
/* If admin.js wraps its table in its own .tscroll, the outer box steps aside
|
||||
so one table never ends up with two scrollbars. */
|
||||
#users-table:has(.tscroll), #comments-admin:has(.tscroll), #audit-admin:has(.tscroll),
|
||||
#notif-box:has(.tscroll), #usage-admin:has(.tscroll), #projects-table:has(.tscroll),
|
||||
#defmem-table:has(.tscroll){
|
||||
/* If admin.js wraps its table in its own .tscroll, the outer box steps aside so
|
||||
one table never ends up with two scrollbars. */
|
||||
#comments-admin:has(.tscroll), #audit-admin:has(.tscroll), #notif-box:has(.tscroll),
|
||||
#usage-admin:has(.tscroll), #projects-table:has(.tscroll), #defmem-table:has(.tscroll){
|
||||
overflow:visible; max-height:none; }
|
||||
|
||||
/* Column exceptions, addressed by card because admin.js emits these tables
|
||||
without per-cell classes. Email is the one user cell long enough to stretch
|
||||
a row, so it truncates; comment text and audit detail are the two columns
|
||||
you are actually here to read, so they wrap inside a sane width instead. */
|
||||
#users-table table.users td:nth-child(3){ max-width:230px; overflow:hidden; text-overflow:ellipsis; }
|
||||
#comments-admin table.users td:nth-child(5){ white-space:normal; min-width:260px; max-width:640px; }
|
||||
#audit-admin table.users td:nth-child(6){ white-space:normal; max-width:420px; }
|
||||
/* Comment text and audit detail are the two columns you are actually here to
|
||||
read, so they wrap inside a sane width instead of truncating. */
|
||||
#comments-admin table td:nth-child(5){ white-space:normal; min-width:260px; max-width:640px; }
|
||||
#audit-admin table td:nth-child(6){ white-space:normal; max-width:420px; }
|
||||
|
||||
/* ══ GATES & WARNINGS ════════════════════════════════════════════════════ */
|
||||
.gate-overlay{ position:fixed; inset:0; background:var(--bg); display:flex; align-items:center; justify-content:center; padding:var(--s5); z-index:9999; }
|
||||
.gate-box{ background:var(--surface); border:1px solid var(--border); border-radius:0; padding:var(--s6); max-width:380px; width:100%; box-shadow:0 8px 30px rgba(20,30,50,.12); }
|
||||
.gate-box h2{ margin:0 0 var(--s1); padding:0; border:0; font-size:17px; text-transform:none; letter-spacing:0; color:var(--text); }
|
||||
.gate-box p{ color:var(--muted); font-size:13px; margin:0 0 var(--s4); }
|
||||
.gate-box input{ width:100%; height:var(--ctl); padding:0 var(--s3); font:inherit; font-size:14px; border:1px solid var(--border-strong); border-radius:0; margin-bottom:var(--s3); }
|
||||
.gate-msg{ color:var(--red); font-size:12px; min-height:16px; margin-bottom:var(--s2); }
|
||||
.secwarn{ background:var(--amber-bg); color:var(--amber); border:1px solid var(--amber); border-radius:0; padding:9px 13px; font-size:12px; margin-bottom:var(--s4); }
|
||||
/* The denial notice is a sentence, not a table — don't stretch it to 1240px. */
|
||||
#admin-denied .card{ max-width:560px; }
|
||||
.gate-box input{ width:100%; height:var(--ctl); padding:0 var(--s3); font:inherit; font-size:14px;
|
||||
border:1px solid var(--border-strong); border-radius:0; margin-bottom:var(--s3); }
|
||||
|
||||
/* ══ NARROW SCREENS ══════════════════════════════════════════════════════
|
||||
The page itself must never scroll sideways; the wide tables scroll inside
|
||||
their own box instead, and there they get the full page height to do it. */
|
||||
@media (max-width:900px){
|
||||
.wrap{ padding:var(--s4) var(--s3) 60px; }
|
||||
.card{ padding:var(--s3) var(--s4) var(--s4); }
|
||||
.toolbar > input{ flex:1 1 100%; }
|
||||
.tscroll, #users-table, #comments-admin, #audit-admin, #notif-box, #usage-admin,
|
||||
#projects-table, #defmem-table{ max-height:none; }
|
||||
}
|
||||
@media (max-width:620px){
|
||||
.urow input, .urow select, .urow button{ flex:1 1 100%; }
|
||||
#comments-admin, #audit-admin, #notif-box, #usage-admin, #projects-table, #defmem-table{
|
||||
max-height:none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -241,29 +88,14 @@
|
||||
<div class="banner" id="health-banner">—</div>
|
||||
</div>
|
||||
|
||||
<!-- USER ADMINISTRATION -->
|
||||
<!-- USER ADMINISTRATION — moved out to its own page -->
|
||||
<div class="card">
|
||||
<h2>User administration</h2>
|
||||
<div class="sub">Login accounts for the portal. Requires an <strong>admin</strong> role on your own account.</div>
|
||||
<div class="toolbar"><button onclick="loadUsers()">Refresh users</button></div>
|
||||
<div id="users-banner"></div>
|
||||
<div id="users-table"></div>
|
||||
|
||||
<h2 style="margin-top:var(--s6)">Add a user</h2>
|
||||
<div class="urow">
|
||||
<input id="nu-username" placeholder="Username *" autocomplete="off">
|
||||
<input id="nu-fullname" placeholder="Full name" autocomplete="off">
|
||||
<input id="nu-email" placeholder="Email" autocomplete="off">
|
||||
<select id="nu-role" title="Permissions — what this account may do">
|
||||
<option value="project_user">Project User</option>
|
||||
<option value="project_admin">Project Admin</option>
|
||||
<option value="admin">Administrator</option>
|
||||
</select>
|
||||
<select id="nu-project-role" title="Job function on the project"></select>
|
||||
<input id="nu-password" type="password" placeholder="Password (min 12)" autocomplete="new-password">
|
||||
<button class="primary" onclick="createUser()">Create user</button>
|
||||
</div>
|
||||
<div id="users-create-msg" class="note"></div>
|
||||
<h2>User accounts</h2>
|
||||
<div class="sub">Login accounts, permissions and project access now live on the
|
||||
<strong>User Directory</strong> page. They moved because user administration is no longer
|
||||
admin-only: a <strong>Project Super User</strong> creates and manages the accounts on the
|
||||
projects they administer, and they must never be sent through this console to do it.</div>
|
||||
<div class="toolbar"><a class="home" href="users.html"><button class="primary">Open the User Directory →</button></a></div>
|
||||
</div>
|
||||
|
||||
<!-- PROJECTS (ARCHIVE / UNARCHIVE) -->
|
||||
@@ -286,8 +118,10 @@
|
||||
<h2>Default members on new projects</h2>
|
||||
<div class="sub">Everyone flagged here is added automatically to every project created from now on,
|
||||
with the role chosen here. It does not touch projects that already exist — for those, use
|
||||
<strong>Project access</strong> in the user table above. Administrators are listed with nothing
|
||||
to set: they already reach every project.</div>
|
||||
<strong>Project access</strong> on the <a class="home" href="users.html">User Directory</a>.
|
||||
Administrators are listed with nothing to set: they already reach every project. This card stays
|
||||
in the console because it is a rule about <em>every</em> future project, including the ones a
|
||||
Project Super User has no part in — so only an admin sets it.</div>
|
||||
<div class="toolbar"><button onclick="loadDefaultMembers()">Refresh</button></div>
|
||||
<div id="defmem-banner"></div>
|
||||
<div id="defmem-table"><div class="note">Click “Refresh” to load.</div></div>
|
||||
@@ -377,7 +211,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="console-util.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<script src="wp-chrome.js"></script>
|
||||
<script src="wp-sidenav.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user