fix admin

This commit is contained in:
tdurieux
2026-05-03 22:29:01 +02:00
parent 6096cb0744
commit db2ac5307d
6 changed files with 429 additions and 114 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+199 -29
View File
@@ -1037,21 +1037,53 @@ a:hover {
}
.status-bar {
background-color: var(--main-bg-color);
padding: 8px 6px;
display: flex;
align-items: center;
gap: 12px;
background-color: var(--canvas-bg-color);
padding: 10px 14px;
margin: 0;
border-bottom: 1px solid var(--border-color);
border-radius: 0;
flex-shrink: 0;
}
.status-bar-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.status-bar-actions .btn {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--paper-card);
border: 1px solid var(--border-color);
color: var(--color);
border-radius: 6px;
padding: 4px 10px;
font-size: 12.5px;
min-height: 30px;
}
.status-bar-actions .btn:hover {
background: var(--hover-bg-color);
color: var(--color);
}
.paths {
flex: 1 1 auto;
min-width: 0;
padding: 0;
margin: 0;
background-color: initial;
border: none;
border-radius: 0;
align-items: center;
overflow-x: auto;
white-space: nowrap;
flex-wrap: nowrap;
}
.paths::-webkit-scrollbar { display: none; }
.paths a {
color: var(--color);
@@ -1988,22 +2020,27 @@ code {
font-size: 16px; /* Prevents iOS zoom */
}
/* File explorer mobile layout */
.leftCol {
max-height: 40vh;
border-bottom: 2px solid var(--border-color);
}
/* Status bar wrapping */
.status-bar {
flex-wrap: wrap;
gap: 4px;
gap: 6px;
padding: 8px 10px;
}
.status-bar .btn {
font-size: 12px;
padding: 3px 8px;
min-height: 28px;
padding: 4px 10px;
min-height: 32px;
}
.status-bar .paths {
font-size: 11.5px;
overflow-x: auto;
flex-wrap: nowrap;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
}
.status-bar .paths::-webkit-scrollbar { display: none; }
.status-bar-actions {
flex-shrink: 0;
gap: 4px;
}
/* Home hero section */
@@ -2155,27 +2192,160 @@ code {
}
}
/* Mobile toggle for file explorer sidebar */
.sidebar-toggle {
display: none;
/* ===== Explorer page (paper) ===== */
.explorer-page {
display: flex;
height: 100%;
width: 100%;
padding: 8px;
text-align: center;
background: var(--sidebar-bg-color);
border: 1px solid var(--border-color);
color: var(--color);
cursor: pointer;
font-size: 14px;
background: var(--canvas-bg-color);
position: relative;
}
.explorer-main {
flex: 1 1 auto;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--canvas-bg-color);
}
.explorer-content {
flex: 1 1 auto;
min-height: 0;
width: 100%;
overflow: auto;
}
@media (max-width: 767px) {
.sidebar-toggle {
display: block;
}
.leftCol-head,
.leftCol-foot,
.leftCol-close {
display: none;
}
.leftCol-body {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
.leftCol-foot {
padding: 8px 12px;
border-top: 1px solid var(--border-color);
background: var(--paper-bg-alt);
}
.leftCol-foot .last-update {
border-top: none;
padding: 0;
}
.leftCol-backdrop { display: none; }
.leftCol.collapsed {
display: none !important;
.paper-inline-warning {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 8px;
padding: 10px 12px;
background: rgba(138, 107, 30, 0.08);
border: 1px solid rgba(138, 107, 30, 0.35);
border-radius: 8px;
color: var(--color);
font-size: 12.5px;
line-height: 1.4;
}
.paper-inline-warning i { color: #8A6B1E; margin-top: 2px; flex-shrink: 0; }
.dark-mode .paper-inline-warning i { color: #FFD37A; }
@media (min-width: 768px) {
.leftCol-body { padding: 8px 4px; }
}
/* Mobile / tablet toggle */
.sidebar-toggle {
display: none;
align-items: center;
gap: 8px;
position: fixed;
bottom: 18px;
left: 18px;
z-index: 1100;
padding: 10px 14px;
background: var(--color);
color: var(--canvas-bg-color);
border: 1px solid var(--color);
border-radius: 999px;
font-family: var(--font-sans);
font-size: 13px;
font-weight: 500;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
cursor: pointer;
outline: none;
}
.sidebar-toggle:hover { background: var(--primary-hover-bg); }
.sidebar-toggle:focus,
.sidebar-toggle:focus-visible { outline: none; box-shadow: 0 6px 20px rgba(0,0,0,0.15); }
@media (max-width: 991px) {
.sidebar-toggle { display: inline-flex; }
/* Sidebar becomes a slide-in drawer */
.leftCol {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(86vw, 340px);
max-width: 340px;
z-index: 1200;
border: none;
border-right: 1px solid var(--border-color);
transform: translateX(0);
transition: transform 0.22s ease;
box-shadow: 4px 0 24px rgba(0,0,0,0.18);
background: var(--sidebar-bg-color);
}
.leftCol.collapsed {
transform: translateX(-105%);
box-shadow: none;
pointer-events: none;
}
.leftCol-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--border-color);
background: var(--paper-bg-alt);
}
.leftCol-eyebrow {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-muted);
}
.leftCol-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 0;
background: transparent;
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--ink-muted);
cursor: pointer;
}
.leftCol-close:hover { color: var(--color); border-color: var(--color); }
.leftCol-foot { display: block; }
.leftCol-body { padding: 4px 0; }
.leftCol-backdrop {
display: block;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.32);
z-index: 1150;
backdrop-filter: blur(2px);
}
.dark-mode .leftCol-backdrop { background: rgba(0,0,0,0.5); }
}
/* ===== Admin Interface (Paper style) ===== */
+85 -71
View File
@@ -1,85 +1,99 @@
<div class="container-fluid h-100">
<div class="row h-100">
<button
class="sidebar-toggle"
ng-show="files.length"
ng-click="sidebarCollapsed = !sidebarCollapsed"
>
<i class="fas" ng-class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
{{sidebarCollapsed ? 'Show Files' : 'Hide Files'}}
</button>
<div
class="leftCol shadow p-1 overflow-auto"
ng-show="files.length"
ng-class="{'collapsed': sidebarCollapsed}"
>
<div class="explorer-page" ng-init="sidebarCollapsed = window.matchMedia && window.matchMedia('(max-width: 767px)').matches">
<button
class="sidebar-toggle"
ng-show="files.length"
ng-click="sidebarCollapsed = !sidebarCollapsed"
aria-label="{{sidebarCollapsed ? 'Show files' : 'Hide files'}}"
>
<i class="fas" ng-class="sidebarCollapsed ? 'fa-folder-open' : 'fa-times'"></i>
<span ng-bind="sidebarCollapsed ? 'Files' : 'Close'"></span>
</button>
<div class="leftCol" ng-show="files.length" ng-class="{'collapsed': sidebarCollapsed}">
<div class="leftCol-head">
<span class="leftCol-eyebrow">Files</span>
<button class="leftCol-close" ng-click="sidebarCollapsed = true" aria-label="Close files">
<i class="fas fa-times"></i>
</button>
</div>
<div class="leftCol-body">
<div
ng-if="options.truncatedFolders.length > 0"
class="alert alert-warning small p-2 mb-2"
class="paper-inline-warning"
role="alert"
>
<i class="fas fa-exclamation-triangle"></i>
{{ 'WARNINGS.repo_truncated' | translate }}
</div>
<tree class="files" file="files"></tree>
<div class="bottom column">
<div
class="last-update"
data-toggle="tooltip"
data-placement="top"
title="{{options.lastUpdateDate}}"
</div>
<div class="leftCol-foot">
<span
class="last-update"
data-toggle="tooltip"
data-placement="top"
title="{{options.lastUpdateDate}}"
>
Updated {{options.lastUpdateDate|date}}
</span>
</div>
</div>
<div
class="leftCol-backdrop"
ng-show="files.length && !sidebarCollapsed"
ng-click="sidebarCollapsed = true"
></div>
<div class="explorer-main">
<div class="status-bar">
<ol class="breadcrumb paths" aria-label="Path">
<li class="breadcrumb-item" ng-repeat="p in paths" ng-bind="p"></li>
</ol>
<div class="status-bar-actions">
<a
ng-if="options.isAdmin || options.isOwner"
ng-href="/anonymize/{{repoId}}"
class="btn btn-sm"
aria-label="Edit"
><i class="far fa-edit"></i><span class="d-none d-md-inline"> Edit</span></a
>
<a
ng-show="content != null"
ng-href="{{url}}"
target="__self"
class="btn btn-sm"
aria-label="Raw"
><i class="fas fa-file-alt"></i><span class="d-none d-md-inline"> Raw</span></a
>
<a
ng-show="content != null"
ng-href="{{url}}&download=true"
target="__self"
class="btn btn-sm"
aria-label="Download"
><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a
>
<a
ng-if="options.download"
ng-href="/api/repo/{{repoId}}/zip"
target="__self"
class="btn btn-sm"
aria-label="Download ZIP"
><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> ZIP</span></a
>
<a
ng-if="options.hasWebsite"
ng-href="/w/{{repoId}}/"
target="__self"
class="btn btn-sm"
aria-label="Website"
><i class="fas fa-globe"></i><span class="d-none d-md-inline"> Website</span></a
>
Last Update: {{options.lastUpdateDate|date}}
</div>
</div>
</div>
<div class="col-md h-100 overflow-auto p-0 d-flex flex-column">
<div class="d-flex align-content-between status-bar shadow">
<ol class="flex-grow-1 breadcrumb paths">
<li class="breadcrumb-item" ng-repeat="p in paths" ng-bind="p">
Loading...
</li>
</ol>
<div class="d-flex flex-wrap" style="gap: 4px">
<a
ng-if="options.isAdmin || options.isOwner"
ng-href="/anonymize/{{repoId}}"
class="btn btn-sm"
>Edit</a
>
<a
ng-show="content != null"
ng-href="{{url}}"
target="__self"
class="btn btn-sm"
>Raw</a
>
<a
ng-show="content != null"
ng-href="{{url}}&download=true"
target="__self"
class="btn btn-sm"
><i class="fas fa-download"></i><span class="d-none d-md-inline"> Download</span></a
>
<a
ng-if="options.download"
ng-href="/api/repo/{{repoId}}/zip"
target="__self"
class="btn btn-sm"
><i class="fas fa-file-archive"></i><span class="d-none d-md-inline"> ZIP</span></a
>
<a
ng-if="options.hasWebsite"
ng-href="/w/{{repoId}}/"
target="__self"
class="btn btn-sm"
>Website</a
>
</div>
</div>
<div class="align-items-stretch h-100 w-100 overflow-auto">
<ng-include src="'./partials/pageView.htm'"></ng-include>
</div>
<div class="explorer-content">
<ng-include src="'./partials/pageView.htm'"></ng-include>
</div>
</div>
</div>
+58 -6
View File
@@ -18,7 +18,8 @@ angular
$scope.repositories = [];
$scope.total = -1;
$scope.totalPage = 0;
$scope.query = {
const reposAdminPrefsKey = "admin.repos.filterPrefs";
const reposAdminDefaults = {
page: 1,
limit: 25,
sort: "lastView",
@@ -29,6 +30,11 @@ angular
error: true,
preparing: true,
};
const savedReposAdminPrefs = loadFilterPrefs(reposAdminPrefsKey) || {};
$scope.query = Object.assign({}, reposAdminDefaults, savedReposAdminPrefs, {
page: 1,
search: "",
});
$scope.removeCache = (repo) => {
$http.delete("/api/admin/repos/" + repo.repoId).then(
@@ -85,6 +91,8 @@ angular
() => {
clearTimeout(timeClear);
timeClear = setTimeout(getRepositories, 500);
const { page, search, ...persisted } = $scope.query;
saveFilterPrefs(reposAdminPrefsKey, persisted);
},
true
);
@@ -108,12 +116,18 @@ angular
$scope.users = [];
$scope.total = -1;
$scope.totalPage = 0;
$scope.query = {
const usersAdminPrefsKey = "admin.users.filterPrefs";
const usersAdminDefaults = {
page: 1,
limit: 25,
sort: "username",
search: "",
};
const savedUsersAdminPrefs = loadFilterPrefs(usersAdminPrefsKey) || {};
$scope.query = Object.assign({}, usersAdminDefaults, savedUsersAdminPrefs, {
page: 1,
search: "",
});
function getUsers() {
$http.get("/api/admin/users", { params: $scope.query }).then(
@@ -136,6 +150,8 @@ angular
() => {
clearTimeout(timeClear);
timeClear = setTimeout(getUsers, 500);
const { page, search, ...persisted } = $scope.query;
saveFilterPrefs(usersAdminPrefsKey, persisted);
},
true
);
@@ -159,10 +175,38 @@ angular
$scope.userInfo;
$scope.repositories = [];
$scope.search = "";
$scope.filters = {
status: { ready: true, expired: false, removed: false },
const adminUserPrefsKey = "admin.user.filterPrefs";
const adminUserDefaults = {
filters: { status: { ready: true, expired: false, removed: false } },
orderBy: "-anonymizeDate",
};
$scope.orderBy = "-anonymizeDate";
const savedAdminUserPrefs = loadFilterPrefs(adminUserPrefsKey) || {};
$scope.filters = {
status: Object.assign(
{},
adminUserDefaults.filters.status,
(savedAdminUserPrefs.filters && savedAdminUserPrefs.filters.status) || {}
),
};
$scope.orderBy = savedAdminUserPrefs.orderBy || adminUserDefaults.orderBy;
$scope.$watch("orderBy", () => {
saveFilterPrefs(adminUserPrefsKey, {
filters: $scope.filters,
orderBy: $scope.orderBy,
});
});
$scope.$watch(
"filters",
() => {
saveFilterPrefs(adminUserPrefsKey, {
filters: $scope.filters,
orderBy: $scope.orderBy,
});
},
true
);
$scope.repoFiler = (repo) => {
if ($scope.filters.status[repo.status] == false) return false;
@@ -311,12 +355,18 @@ angular
$scope.conferences = [];
$scope.total = -1;
$scope.totalPage = 0;
$scope.query = {
const confAdminPrefsKey = "admin.conferences.filterPrefs";
const confAdminDefaults = {
page: 1,
limit: 25,
sort: "name",
search: "",
};
const savedConfAdminPrefs = loadFilterPrefs(confAdminPrefsKey) || {};
$scope.query = Object.assign({}, confAdminDefaults, savedConfAdminPrefs, {
page: 1,
search: "",
});
function getConferences() {
$http.get("/api/admin/conferences", { params: $scope.query }).then(
@@ -339,6 +389,8 @@ angular
() => {
clearTimeout(timeClear);
timeClear = setTimeout(getConferences, 500);
const { page, search, ...persisted } = $scope.query;
saveFilterPrefs(confAdminPrefsKey, persisted);
},
true
);
+69 -7
View File
@@ -861,11 +861,45 @@ angular
$scope.items = [];
$scope.search = "";
$scope.typeFilter = "all";
$scope.filters = {
status: { ready: true, expired: true, removed: false },
const dashboardPrefsKey = "dashboard.filterPrefs";
const dashboardPrefDefaults = {
typeFilter: "all",
filters: { status: { ready: true, expired: true, removed: false } },
orderBy: "-anonymizeDate",
};
$scope.orderBy = "-anonymizeDate";
const savedDashboardPrefs = loadFilterPrefs(dashboardPrefsKey) || {};
$scope.typeFilter = savedDashboardPrefs.typeFilter || dashboardPrefDefaults.typeFilter;
$scope.filters = {
status: Object.assign(
{},
dashboardPrefDefaults.filters.status,
(savedDashboardPrefs.filters && savedDashboardPrefs.filters.status) || {}
),
};
$scope.orderBy = savedDashboardPrefs.orderBy || dashboardPrefDefaults.orderBy;
$scope.$watchGroup(
["typeFilter", "orderBy"],
() => {
saveFilterPrefs(dashboardPrefsKey, {
typeFilter: $scope.typeFilter,
filters: $scope.filters,
orderBy: $scope.orderBy,
});
}
);
$scope.$watch(
"filters",
() => {
saveFilterPrefs(dashboardPrefsKey, {
typeFilter: $scope.typeFilter,
filters: $scope.filters,
orderBy: $scope.orderBy,
});
},
true
);
function getQuota() {
$http.get("/api/user/quota").then((res) => {
@@ -2030,10 +2064,38 @@ angular
$scope.conferences = [];
$scope.search = "";
$scope.filters = {
status: { ready: true, expired: false, removed: false },
const conferencesPrefsKey = "conferences.filterPrefs";
const conferencesPrefDefaults = {
filters: { status: { ready: true, expired: false, removed: false } },
orderBy: "name",
};
$scope.orderBy = "name";
const savedConferencesPrefs = loadFilterPrefs(conferencesPrefsKey) || {};
$scope.filters = {
status: Object.assign(
{},
conferencesPrefDefaults.filters.status,
(savedConferencesPrefs.filters && savedConferencesPrefs.filters.status) || {}
),
};
$scope.orderBy = savedConferencesPrefs.orderBy || conferencesPrefDefaults.orderBy;
$scope.$watch("orderBy", () => {
saveFilterPrefs(conferencesPrefsKey, {
filters: $scope.filters,
orderBy: $scope.orderBy,
});
});
$scope.$watch(
"filters",
() => {
saveFilterPrefs(conferencesPrefsKey, {
filters: $scope.filters,
orderBy: $scope.orderBy,
});
},
true
);
$scope.removeConference = function (conf) {
if (
+17
View File
@@ -1,3 +1,20 @@
function loadFilterPrefs(key) {
try {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch (e) {
return null;
}
}
function saveFilterPrefs(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
/* localStorage unavailable or quota exceeded */
}
}
function humanFileSize(bytes, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;