Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
13d9780d
Commit
13d9780d
authored
Apr 20, 2026
by
IanShaw027
Browse files
feat: expose user activity timestamps in admin list
parent
e9de839d
Changes
7
Show whitespace changes
Inline
Side-by-side
backend/internal/handler/dto/mappers.go
View file @
13d9780d
...
@@ -21,6 +21,8 @@ func UserFromServiceShallow(u *service.User) *User {
...
@@ -21,6 +21,8 @@ func UserFromServiceShallow(u *service.User) *User {
Concurrency
:
u
.
Concurrency
,
Concurrency
:
u
.
Concurrency
,
Status
:
u
.
Status
,
Status
:
u
.
Status
,
AllowedGroups
:
u
.
AllowedGroups
,
AllowedGroups
:
u
.
AllowedGroups
,
LastLoginAt
:
u
.
LastLoginAt
,
LastActiveAt
:
u
.
LastActiveAt
,
CreatedAt
:
u
.
CreatedAt
,
CreatedAt
:
u
.
CreatedAt
,
UpdatedAt
:
u
.
UpdatedAt
,
UpdatedAt
:
u
.
UpdatedAt
,
BalanceNotifyEnabled
:
u
.
BalanceNotifyEnabled
,
BalanceNotifyEnabled
:
u
.
BalanceNotifyEnabled
,
...
...
backend/internal/handler/dto/types.go
View file @
13d9780d
...
@@ -15,6 +15,8 @@ type User struct {
...
@@ -15,6 +15,8 @@ type User struct {
Concurrency
int
`json:"concurrency"`
Concurrency
int
`json:"concurrency"`
Status
string
`json:"status"`
Status
string
`json:"status"`
AllowedGroups
[]
int64
`json:"allowed_groups"`
AllowedGroups
[]
int64
`json:"allowed_groups"`
LastLoginAt
*
time
.
Time
`json:"last_login_at,omitempty"`
LastActiveAt
*
time
.
Time
`json:"last_active_at,omitempty"`
CreatedAt
time
.
Time
`json:"created_at"`
CreatedAt
time
.
Time
`json:"created_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
UpdatedAt
time
.
Time
`json:"updated_at"`
...
...
backend/internal/handler/dto/user_mapper_activity_test.go
0 → 100644
View file @
13d9780d
package
dto
import
(
"testing"
"time"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/require"
)
func
TestUserFromServiceAdmin_MapsActivityTimestamps
(
t
*
testing
.
T
)
{
t
.
Parallel
()
lastLoginAt
:=
time
.
Date
(
2026
,
time
.
April
,
20
,
10
,
0
,
0
,
0
,
time
.
UTC
)
lastActiveAt
:=
lastLoginAt
.
Add
(
15
*
time
.
Minute
)
out
:=
UserFromServiceAdmin
(
&
service
.
User
{
ID
:
42
,
Email
:
"admin@example.com"
,
Username
:
"admin"
,
Role
:
service
.
RoleAdmin
,
Status
:
service
.
StatusActive
,
LastLoginAt
:
&
lastLoginAt
,
LastActiveAt
:
&
lastActiveAt
,
})
require
.
NotNil
(
t
,
out
)
require
.
NotNil
(
t
,
out
.
LastLoginAt
)
require
.
NotNil
(
t
,
out
.
LastActiveAt
)
require
.
WithinDuration
(
t
,
lastLoginAt
,
*
out
.
LastLoginAt
,
time
.
Second
)
require
.
WithinDuration
(
t
,
lastActiveAt
,
*
out
.
LastActiveAt
,
time
.
Second
)
}
frontend/src/i18n/locales/en.ts
View file @
13d9780d
...
@@ -1391,6 +1391,8 @@ export default {
...
@@ -1391,6 +1391,8 @@ export default {
usage
:
'
Usage
'
,
usage
:
'
Usage
'
,
concurrency
:
'
Concurrency
'
,
concurrency
:
'
Concurrency
'
,
status
:
'
Status
'
,
status
:
'
Status
'
,
lastLogin
:
'
Last Login
'
,
lastActive
:
'
Last Active
'
,
created
:
'
Created
'
,
created
:
'
Created
'
,
actions
:
'
Actions
'
actions
:
'
Actions
'
},
},
...
...
frontend/src/i18n/locales/zh.ts
View file @
13d9780d
...
@@ -1417,6 +1417,8 @@ export default {
...
@@ -1417,6 +1417,8 @@ export default {
usage
:
'
用量
'
,
usage
:
'
用量
'
,
concurrency
:
'
并发数
'
,
concurrency
:
'
并发数
'
,
status
:
'
状态
'
,
status
:
'
状态
'
,
lastLogin
:
'
最后登录
'
,
lastActive
:
'
最后使用
'
,
created
:
'
创建时间
'
,
created
:
'
创建时间
'
,
actions
:
'
操作
'
actions
:
'
操作
'
},
},
...
...
frontend/src/types/index.ts
View file @
13d9780d
...
@@ -47,6 +47,8 @@ export interface User {
...
@@ -47,6 +47,8 @@ export interface User {
balance_notify_threshold
:
number
|
null
balance_notify_threshold
:
number
|
null
balance_notify_extra_emails
:
NotifyEmailEntry
[]
balance_notify_extra_emails
:
NotifyEmailEntry
[]
subscriptions
?:
UserSubscription
[]
// User's active subscriptions
subscriptions
?:
UserSubscription
[]
// User's active subscriptions
last_login_at
?:
string
|
null
last_active_at
?:
string
|
null
created_at
:
string
created_at
:
string
updated_at
:
string
updated_at
:
string
}
}
...
...
frontend/src/views/admin/UsersView.vue
View file @
13d9780d
...
@@ -455,6 +455,18 @@
...
@@ -455,6 +455,18 @@
<span
class=
"text-sm text-gray-500 dark:text-dark-400"
>
{{
formatDateTime
(
value
)
}}
</span>
<span
class=
"text-sm text-gray-500 dark:text-dark-400"
>
{{
formatDateTime
(
value
)
}}
</span>
</
template
>
</
template
>
<
template
#cell-last_login_at=
"{ value }"
>
<span
class=
"text-sm text-gray-500 dark:text-dark-400"
>
{{
value
?
formatDateTime
(
value
)
:
'
-
'
}}
</span>
</
template
>
<
template
#cell-last_active_at=
"{ value }"
>
<span
class=
"text-sm text-gray-500 dark:text-dark-400"
>
{{
value
?
formatDateTime
(
value
)
:
'
-
'
}}
</span>
</
template
>
<
template
#cell-actions=
"{ row }"
>
<
template
#cell-actions=
"{ row }"
>
<div
class=
"flex items-center gap-1"
>
<div
class=
"flex items-center gap-1"
>
<!-- Edit Button -->
<!-- Edit Button -->
...
@@ -700,6 +712,8 @@ const allColumns = computed<Column[]>(() => [
...
@@ -700,6 +712,8 @@ const allColumns = computed<Column[]>(() => [
{
key
:
'
usage
'
,
label
:
t
(
'
admin.users.columns.usage
'
),
sortable
:
false
},
{
key
:
'
usage
'
,
label
:
t
(
'
admin.users.columns.usage
'
),
sortable
:
false
},
{
key
:
'
concurrency
'
,
label
:
t
(
'
admin.users.columns.concurrency
'
),
sortable
:
true
},
{
key
:
'
concurrency
'
,
label
:
t
(
'
admin.users.columns.concurrency
'
),
sortable
:
true
},
{
key
:
'
status
'
,
label
:
t
(
'
admin.users.columns.status
'
),
sortable
:
true
},
{
key
:
'
status
'
,
label
:
t
(
'
admin.users.columns.status
'
),
sortable
:
true
},
{
key
:
'
last_login_at
'
,
label
:
t
(
'
admin.users.columns.lastLogin
'
),
sortable
:
true
},
{
key
:
'
last_active_at
'
,
label
:
t
(
'
admin.users.columns.lastActive
'
),
sortable
:
true
},
{
key
:
'
created_at
'
,
label
:
t
(
'
admin.users.columns.created
'
),
sortable
:
true
},
{
key
:
'
created_at
'
,
label
:
t
(
'
admin.users.columns.created
'
),
sortable
:
true
},
{
key
:
'
actions
'
,
label
:
t
(
'
admin.users.columns.actions
'
),
sortable
:
false
}
{
key
:
'
actions
'
,
label
:
t
(
'
admin.users.columns.actions
'
),
sortable
:
false
}
])
])
...
@@ -714,7 +728,7 @@ const toggleableColumns = computed(() =>
...
@@ -714,7 +728,7 @@ const toggleableColumns = computed(() =>
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
const
hiddenColumns
=
reactive
<
Set
<
string
>>
(
new
Set
())
// Default hidden columns (columns hidden by default on first load)
// Default hidden columns (columns hidden by default on first load)
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
notes
'
,
'
groups
'
,
'
subscriptions
'
,
'
usage
'
,
'
concurrency
'
]
const
DEFAULT_HIDDEN_COLUMNS
=
[
'
notes
'
,
'
groups
'
,
'
subscriptions
'
,
'
usage
'
,
'
concurrency
'
,
'
last_login_at
'
,
'
last_active_at
'
]
// localStorage key for column settings
// localStorage key for column settings
const
HIDDEN_COLUMNS_KEY
=
'
user-hidden-columns
'
const
HIDDEN_COLUMNS_KEY
=
'
user-hidden-columns
'
...
@@ -787,7 +801,7 @@ const searchQuery = ref('')
...
@@ -787,7 +801,7 @@ const searchQuery = ref('')
const
USER_SORT_STORAGE_KEY
=
'
admin-users-table-sort
'
const
USER_SORT_STORAGE_KEY
=
'
admin-users-table-sort
'
const
loadInitialSortState
=
():
{
sort_by
:
string
;
sort_order
:
'
asc
'
|
'
desc
'
}
=>
{
const
loadInitialSortState
=
():
{
sort_by
:
string
;
sort_order
:
'
asc
'
|
'
desc
'
}
=>
{
const
fallback
=
{
sort_by
:
'
created_at
'
,
sort_order
:
'
desc
'
as
'
asc
'
|
'
desc
'
}
const
fallback
=
{
sort_by
:
'
created_at
'
,
sort_order
:
'
desc
'
as
'
asc
'
|
'
desc
'
}
const
sortable
=
new
Set
([
'
email
'
,
'
id
'
,
'
username
'
,
'
role
'
,
'
balance
'
,
'
concurrency
'
,
'
status
'
,
'
created_at
'
])
const
sortable
=
new
Set
([
'
email
'
,
'
id
'
,
'
username
'
,
'
role
'
,
'
balance
'
,
'
concurrency
'
,
'
status
'
,
'
last_login_at
'
,
'
last_active_at
'
,
'
created_at
'
])
try
{
try
{
const
raw
=
localStorage
.
getItem
(
USER_SORT_STORAGE_KEY
)
const
raw
=
localStorage
.
getItem
(
USER_SORT_STORAGE_KEY
)
if
(
!
raw
)
return
fallback
if
(
!
raw
)
return
fallback
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment