Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
aisbf
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
nexlab
aisbf
Commits
72013505
Commit
72013505
authored
May 12, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add dedicated market admin page
parent
09f195d6
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
344 additions
and
42 deletions
+344
-42
admin.py
aisbf/routes/dashboard/admin.py
+0
-1
market.py
aisbf/routes/dashboard/market.py
+46
-2
admin_market.html
templates/dashboard/admin_market.html
+66
-0
admin_payment_settings.html
templates/dashboard/admin_payment_settings.html
+7
-39
test_dashboard_admin_market_management.py
tests/routes/test_dashboard_admin_market_management.py
+225
-0
No files found.
aisbf/routes/dashboard/admin.py
View file @
72013505
...
...
@@ -1510,7 +1510,6 @@ async def dashboard_admin_payment_settings(request: Request):
"session"
:
request
.
session
,
"currency_symbol"
:
DatabaseRegistry
.
get_config_database
()
.
get_currency_settings
()
.
get
(
'currency_symbol'
,
'$'
),
"market_settings"
:
DatabaseRegistry
.
get_config_database
()
.
get_market_settings
(),
"market_listings"
:
DatabaseRegistry
.
get_config_database
()
.
list_market_listings
(
active_only
=
False
),
}
)
...
...
aisbf/routes/dashboard/market.py
View file @
72013505
...
...
@@ -719,17 +719,50 @@ async def api_toggle_market_listing(request: Request, listing_id: int):
@
router
.
get
(
'/dashboard/admin/market'
,
response_class
=
HTMLResponse
)
async
def
dashboard_admin_market
(
request
:
Request
):
async
def
dashboard_admin_market
(
request
:
Request
,
q
:
str
=
''
,
source_type
:
str
=
''
,
active_filter
:
str
=
''
,
online_filter
:
str
=
''
,
owner_username
:
str
=
''
,
page
:
int
=
1
,
limit
:
int
=
25
,
):
auth_check
=
require_admin
(
request
)
if
auth_check
:
return
auth_check
db
=
DatabaseRegistry
.
get_config_database
()
listings
=
db
.
list_market_listings
(
active_only
=
False
)
page
=
max
(
page
,
1
)
limit
=
max
(
1
,
min
(
limit
,
100
))
result
=
db
.
list_market_listings_paginated
(
page
=
page
,
limit
=
limit
,
search
=
q
or
None
,
source_type
=
source_type
or
None
,
active_filter
=
active_filter
or
None
,
online_filter
=
online_filter
or
None
,
owner_username
=
owner_username
or
None
,
)
listings
=
result
[
'items'
]
enriched_listings
=
[]
for
listing
in
listings
:
listing_copy
=
dict
(
listing
)
_apply_listing_derived_fields
(
listing_copy
,
db
)
enriched_listings
.
append
(
listing_copy
)
total
=
int
(
result
.
get
(
'total'
)
or
0
)
total_pages
=
max
((
total
+
limit
-
1
)
//
limit
,
1
)
if
page
>
total_pages
:
page
=
total_pages
filters
=
{
'q'
:
q
,
'source_type'
:
source_type
,
'active_filter'
:
active_filter
,
'online_filter'
:
online_filter
,
'owner_username'
:
owner_username
,
'page'
:
page
,
'limit'
:
limit
,
}
return
_templates
.
TemplateResponse
(
request
=
request
,
name
=
'dashboard/admin_market.html'
,
...
...
@@ -737,5 +770,16 @@ async def dashboard_admin_market(request: Request):
'request'
:
request
,
'session'
:
request
.
session
,
'market_listings'
:
enriched_listings
,
'filters'
:
filters
,
'pagination'
:
{
'page'
:
page
,
'limit'
:
limit
,
'total'
:
total
,
'total_pages'
:
total_pages
,
'has_prev'
:
page
>
1
,
'has_next'
:
page
<
total_pages
,
'prev_page'
:
page
-
1
,
'next_page'
:
page
+
1
,
},
},
)
templates/dashboard/admin_market.html
View file @
72013505
...
...
@@ -4,6 +4,54 @@
{% block content %}
<h2
style=
"margin-bottom: 20px;"
><i
class=
"fas fa-store-alt"
style=
"color: var(--color-link);"
></i>
Market Administration
</h2>
<div
style=
"background: var(--bg-panel); border: 1px solid var(--color-border); border-radius: 10px; padding: 16px; margin-bottom: 16px;"
>
<form
method=
"get"
style=
"display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; align-items:end;"
>
<div>
<label
for=
"market-search"
style=
"display:block; margin-bottom: 6px; color: var(--color-text);"
>
Search
</label>
<input
id=
"market-search"
type=
"text"
name=
"q"
value=
"{{ filters.q }}"
placeholder=
"Search listings, owner, provider, model"
style=
"width:100%; padding: 10px; border:1px solid var(--color-border); border-radius: 8px; background: var(--bg-page); color: var(--color-text);"
>
</div>
<div>
<label
for=
"source-type"
style=
"display:block; margin-bottom: 6px; color: var(--color-text);"
>
Source Type
</label>
<select
id=
"source-type"
name=
"source_type"
style=
"width:100%; padding: 10px; border:1px solid var(--color-border); border-radius: 8px; background: var(--bg-page); color: var(--color-text);"
>
<option
value=
""
>
All sources
</option>
<option
value=
"provider"
{%
if
filters
.
source_type =
=
'
provider
'
%}
selected
{%
endif
%}
>
Provider
</option>
<option
value=
"model"
{%
if
filters
.
source_type =
=
'
model
'
%}
selected
{%
endif
%}
>
Model
</option>
<option
value=
"rotation"
{%
if
filters
.
source_type =
=
'
rotation
'
%}
selected
{%
endif
%}
>
Rotation
</option>
<option
value=
"autoselect"
{%
if
filters
.
source_type =
=
'
autoselect
'
%}
selected
{%
endif
%}
>
Autoselect
</option>
</select>
</div>
<div>
<label
for=
"active-filter"
style=
"display:block; margin-bottom: 6px; color: var(--color-text);"
>
Active State
</label>
<select
id=
"active-filter"
name=
"active_filter"
style=
"width:100%; padding: 10px; border:1px solid var(--color-border); border-radius: 8px; background: var(--bg-page); color: var(--color-text);"
>
<option
value=
""
>
All states
</option>
<option
value=
"active"
{%
if
filters
.
active_filter =
=
'
active
'
%}
selected
{%
endif
%}
>
Active
</option>
<option
value=
"inactive"
{%
if
filters
.
active_filter =
=
'
inactive
'
%}
selected
{%
endif
%}
>
Inactive
</option>
</select>
</div>
<div>
<label
for=
"online-filter"
style=
"display:block; margin-bottom: 6px; color: var(--color-text);"
>
Availability
</label>
<select
id=
"online-filter"
name=
"online_filter"
style=
"width:100%; padding: 10px; border:1px solid var(--color-border); border-radius: 8px; background: var(--bg-page); color: var(--color-text);"
>
<option
value=
""
>
All
</option>
<option
value=
"online"
{%
if
filters
.
online_filter =
=
'
online
'
%}
selected
{%
endif
%}
>
Online
</option>
<option
value=
"offline"
{%
if
filters
.
online_filter =
=
'
offline
'
%}
selected
{%
endif
%}
>
Offline
</option>
</select>
</div>
<div>
<label
for=
"owner-username"
style=
"display:block; margin-bottom: 6px; color: var(--color-text);"
>
Owner
</label>
<input
id=
"owner-username"
type=
"text"
name=
"owner_username"
value=
"{{ filters.owner_username }}"
placeholder=
"Filter by owner"
style=
"width:100%; padding: 10px; border:1px solid var(--color-border); border-radius: 8px; background: var(--bg-page); color: var(--color-text);"
>
</div>
<div>
<label
for=
"limit"
style=
"display:block; margin-bottom: 6px; color: var(--color-text);"
>
Per Page
</label>
<select
id=
"limit"
name=
"limit"
style=
"width:100%; padding: 10px; border:1px solid var(--color-border); border-radius: 8px; background: var(--bg-page); color: var(--color-text);"
>
{% for option in [10, 25, 50, 100] %}
<option
value=
"{{ option }}"
{%
if
filters
.
limit =
=
option
%}
selected
{%
endif
%}
>
{{ option }}
</option>
{% endfor %}
</select>
</div>
<input
type=
"hidden"
name=
"page"
value=
"1"
>
<button
type=
"submit"
class=
"btn"
style=
"background: var(--color-link); color: white;"
>
Apply
</button>
</form>
</div>
<div
style=
"background: var(--bg-panel); border: 1px solid var(--color-border); border-radius: 10px; padding: 16px; overflow:auto;"
>
<table
style=
"width:100%; border-collapse: collapse;"
>
<thead>
...
...
@@ -34,8 +82,26 @@
<td
style=
"padding:10px; text-align:right;"
>
{{ (listing.stats or {}).total_requests or 0 }}
</td>
<td
style=
"padding:10px; text-align:center;"
>
{{ 'Yes' if listing.is_active else 'No' }}
</td>
</tr>
{% else %}
<tr>
<td
colspan=
"10"
style=
"padding:16px; text-align:center; color: var(--color-muted);"
>
No market listings match the current filters.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div
style=
"display:flex; justify-content:space-between; align-items:center; gap: 12px; margin-top: 16px; flex-wrap: wrap;"
>
<div
style=
"color: var(--color-text);"
>
Showing page {{ pagination.page }} of {{ pagination.total_pages }} ({{ pagination.total }} listings)
</div>
<div
style=
"display:flex; gap: 8px; flex-wrap: wrap;"
>
{% if pagination.has_prev %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.prev_page }}"
style=
"background: var(--bg-accent); color: var(--color-text);"
>
Previous
</a>
{% endif %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.page }}"
style=
"background: var(--color-link); color: white;"
>
Current Page
</a>
{% if pagination.has_next %}
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}?q={{ filters.q|urlencode }}&source_type={{ filters.source_type|urlencode }}&active_filter={{ filters.active_filter|urlencode }}&online_filter={{ filters.online_filter|urlencode }}&owner_username={{ filters.owner_username|urlencode }}&limit={{ filters.limit }}&page={{ pagination.next_page }}"
style=
"background: var(--bg-accent); color: var(--color-text);"
>
Next
</a>
{% endif %}
</div>
</div>
{% endblock %}
templates/dashboard/admin_payment_settings.html
View file @
72013505
...
...
@@ -45,47 +45,15 @@
</div>
<div
style=
"background: var(--bg-panel); border: 2px solid var(--color-link); border-radius: 8px; padding: 20px; margin-bottom: 20px;"
>
<h3
style=
"margin: 0 0
20
px 0; color: var(--color-link);"
>
<h3
style=
"margin: 0 0
12
px 0; color: var(--color-link);"
>
<i
class=
"fas fa-store-alt me-2"
></i>
Market Administration
</h3>
<div
style=
"overflow:auto; border: 1px solid var(--color-border); border-radius: 8px; background: var(--bg-page);"
>
<table
style=
"width:100%; border-collapse: collapse;"
>
<thead>
<tr
style=
"background: var(--bg-accent);"
>
<th
style=
"padding:10px; text-align:left;"
>
ID
</th>
<th
style=
"padding:10px; text-align:left;"
>
Title
</th>
<th
style=
"padding:10px; text-align:left;"
>
Owner
</th>
<th
style=
"padding:10px; text-align:left;"
>
Source
</th>
<th
style=
"padding:10px; text-align:left;"
>
Status
</th>
<th
style=
"padding:10px; text-align:right;"
>
1M Tokens
</th>
<th
style=
"padding:10px; text-align:right;"
>
1K Requests
</th>
<th
style=
"padding:10px; text-align:right;"
>
Revenue
</th>
<th
style=
"padding:10px; text-align:right;"
>
Requests
</th>
<th
style=
"padding:10px; text-align:center;"
>
Active
</th>
</tr>
</thead>
<tbody>
{% for listing in market_listings %}
<tr
style=
"border-top:1px solid var(--color-border);"
>
<td
style=
"padding:10px;"
>
{{ listing.id }}
</td>
<td
style=
"padding:10px;"
>
{{ listing.title }}
</td>
<td
style=
"padding:10px;"
>
{{ listing.owner_username }}
</td>
<td
style=
"padding:10px;"
>
{{ listing.source_type }} / {{ listing.source_id }}
</td>
<td
style=
"padding:10px;"
>
{{ 'Online' if listing.online else 'Offline' }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ listing.price_per_million_tokens }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ listing.price_per_1000_requests }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ '%.2f'|format((listing.stats or {}).gross_revenue or 0) }}
</td>
<td
style=
"padding:10px; text-align:right;"
>
{{ (listing.stats or {}).total_requests or 0 }}
</td>
<td
style=
"padding:10px; text-align:center;"
>
{{ 'Yes' if listing.is_active else 'No' }}
</td>
</tr>
{% else %}
<tr>
<td
colspan=
"10"
style=
"padding:16px; text-align:center; color: var(--color-muted);"
>
No market listings yet.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p
style=
"margin: 0 0 16px 0; color: var(--color-text);"
>
Review, search, filter, and moderate exported market listings on the dedicated administration page.
</p>
<a
class=
"btn"
href=
"{{ url_for(request, '/dashboard/admin/market') }}"
style=
"background: var(--color-link); color: white;"
>
<i
class=
"fas fa-arrow-right me-2"
></i>
Open Market Administration
</a>
</div>
<!-- Global Currency Settings -->
...
...
tests/routes/test_dashboard_admin_market_management.py
0 → 100644
View file @
72013505
import
json
import
sys
from
base64
import
b64encode
from
pathlib
import
Path
from
fastapi.responses
import
HTMLResponse
from
fastapi.testclient
import
TestClient
from
itsdangerous
import
TimestampSigner
from
jinja2
import
Environment
,
FileSystemLoader
,
select_autoescape
from
aisbf.routes.dashboard
import
admin
as
dashboard_admin
from
aisbf.routes.dashboard
import
market
as
dashboard_market
sys
.
path
.
insert
(
0
,
str
(
Path
(
__file__
)
.
resolve
()
.
parents
[
2
]))
from
main
import
app
class
TemplateCapture
:
def
__init__
(
self
):
self
.
calls
=
[]
self
.
_env
=
Environment
(
loader
=
FileSystemLoader
(
str
(
Path
(
__file__
)
.
resolve
()
.
parents
[
2
]
/
"templates"
)),
autoescape
=
select_autoescape
([
"html"
,
"xml"
]),
)
self
.
_env
.
globals
[
"url_for"
]
=
lambda
request
,
path
,
**
kwargs
:
path
def
TemplateResponse
(
self
,
*
args
,
**
kwargs
):
request
=
kwargs
[
"request"
]
name
=
kwargs
[
"name"
]
context
=
kwargs
[
"context"
]
self
.
calls
.
append
({
"request"
:
request
,
"name"
:
name
,
"context"
:
context
})
template
=
self
.
_env
.
get_template
(
name
)
return
HTMLResponse
(
template
.
render
(
**
context
))
class
MarketAdminDbStub
:
def
__init__
(
self
):
self
.
current_market_settings
=
{
"enabled"
:
True
,
"allow_user_publish"
:
True
,
"allow_admin_publish"
:
True
,
"allow_import"
:
True
,
}
self
.
last_paginated_call
=
None
self
.
items
=
[
{
"id"
:
7
,
"title"
:
"Alice Provider"
,
"owner_username"
:
"alice"
,
"source_type"
:
"provider"
,
"source_id"
:
"alice-provider"
,
"provider_id"
:
"alice-provider"
,
"model_id"
:
None
,
"price_per_million_tokens"
:
1.5
,
"price_per_1000_requests"
:
0.2
,
"stats"
:
{
"gross_revenue"
:
42.0
,
"total_requests"
:
12
},
"is_active"
:
True
,
"metadata"
:
{},
}
]
self
.
total
=
1
def
get_market_settings
(
self
):
return
dict
(
self
.
current_market_settings
)
def
get_currency_settings
(
self
):
return
{
"currency_symbol"
:
"$"
}
def
list_market_listings_paginated
(
self
,
**
kwargs
):
self
.
last_paginated_call
=
kwargs
return
{
"items"
:
list
(
self
.
items
),
"total"
:
self
.
total
,
}
def
get_provider_disabled_until
(
self
,
owner_user_id
,
provider_id
):
return
None
def
get_provider_usage
(
self
,
owner_user_id
,
provider_id
):
return
{
"usage_data"
:
{
"ok"
:
True
}}
def
get_user_by_id
(
self
,
user_id
):
return
{
"display_name"
:
"Alice"
}
def
get_market_vote_summary
(
self
,
listing_id
):
return
{
"listing"
:
{
"upvotes"
:
0
,
"downvotes"
:
0
,
"score"
:
0
},
"provider"
:
{
"upvotes"
:
0
,
"downvotes"
:
0
,
"score"
:
0
},
"model"
:
{
"upvotes"
:
0
,
"downvotes"
:
0
,
"score"
:
0
},
"user"
:
{
"upvotes"
:
0
,
"downvotes"
:
0
,
"score"
:
0
},
}
def
get_market_listing_stats
(
self
,
listing_id
):
return
{
"gross_revenue"
:
42.0
,
"total_requests"
:
12
}
class
RegistryStub
:
def
__init__
(
self
,
db
):
self
.
_db
=
db
def
get_config_database
(
self
):
return
self
.
_db
def
_find_session_secret
()
->
str
:
for
middleware
in
app
.
user_middleware
:
kwargs
=
getattr
(
middleware
,
"kwargs"
,
{})
secret_key
=
kwargs
.
get
(
"secret_key"
)
if
secret_key
:
return
secret_key
raise
AssertionError
(
"Session middleware secret key not found"
)
def
_set_session_cookie
(
client
:
TestClient
,
data
:
dict
)
->
None
:
signer
=
TimestampSigner
(
_find_session_secret
())
serialized
=
b64encode
(
json
.
dumps
(
data
)
.
encode
(
"utf-8"
))
signed
=
signer
.
sign
(
serialized
)
.
decode
(
"utf-8"
)
client
.
cookies
.
set
(
"session"
,
signed
)
def
_login_as_admin
(
client
:
TestClient
)
->
None
:
_set_session_cookie
(
client
,
{
"logged_in"
:
True
,
"username"
:
"admin"
,
"role"
:
"admin"
,
"user_id"
:
None
,
"expires_at"
:
4102444800
,
},
)
def
test_payment_settings_shows_market_admin_link_not_embedded_table
(
monkeypatch
):
db
=
MarketAdminDbStub
()
templates
=
TemplateCapture
()
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
monkeypatch
.
setattr
(
dashboard_admin
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
monkeypatch
.
setattr
(
dashboard_admin
,
"_templates"
,
templates
)
response
=
client
.
get
(
"/dashboard/admin/payment-settings"
)
assert
response
.
status_code
==
200
assert
templates
.
calls
[
-
1
][
"name"
]
==
"dashboard/admin_payment_settings.html"
assert
"Open Market Administration"
in
response
.
text
assert
"Review, search, filter, and moderate exported market listings"
in
response
.
text
assert
"<table style=
\"
width:100
%
; border-collapse: collapse;
\"
>"
not
in
response
.
text
def
test_admin_market_page_supports_search_filters_and_pagination
(
monkeypatch
):
db
=
MarketAdminDbStub
()
db
.
total
=
25
templates
=
TemplateCapture
()
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
monkeypatch
.
setattr
(
dashboard_market
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
monkeypatch
.
setattr
(
dashboard_market
,
"_templates"
,
templates
)
response
=
client
.
get
(
"/dashboard/admin/market"
,
params
=
{
"q"
:
"alice"
,
"source_type"
:
"provider"
,
"active_filter"
:
"active"
,
"online_filter"
:
"online"
,
"owner_username"
:
"alice"
,
"page"
:
2
,
"limit"
:
10
,
},
)
assert
response
.
status_code
==
200
assert
db
.
last_paginated_call
==
{
"page"
:
2
,
"limit"
:
10
,
"search"
:
"alice"
,
"source_type"
:
"provider"
,
"active_filter"
:
"active"
,
"online_filter"
:
"online"
,
"owner_username"
:
"alice"
,
}
assert
templates
.
calls
[
-
1
][
"name"
]
==
"dashboard/admin_market.html"
assert
templates
.
calls
[
-
1
][
"context"
][
"filters"
]
==
{
"q"
:
"alice"
,
"source_type"
:
"provider"
,
"active_filter"
:
"active"
,
"online_filter"
:
"online"
,
"owner_username"
:
"alice"
,
"page"
:
2
,
"limit"
:
10
,
}
assert
'value="alice"'
in
response
.
text
assert
'value="provider" selected'
in
response
.
text
assert
'value="active" selected'
in
response
.
text
assert
'value="online" selected'
in
response
.
text
assert
'page=2'
in
response
.
text
def
test_admin_market_page_clamps_page_to_last_result_page
(
monkeypatch
):
db
=
MarketAdminDbStub
()
db
.
items
=
[]
db
.
total
=
1
templates
=
TemplateCapture
()
client
=
TestClient
(
app
)
_login_as_admin
(
client
)
monkeypatch
.
setattr
(
dashboard_market
,
"DatabaseRegistry"
,
RegistryStub
(
db
))
monkeypatch
.
setattr
(
dashboard_market
,
"_templates"
,
templates
)
response
=
client
.
get
(
"/dashboard/admin/market"
,
params
=
{
"q"
:
"missing"
,
"page"
:
9
,
"limit"
:
1
,
},
)
assert
response
.
status_code
==
200
assert
templates
.
calls
[
-
1
][
"context"
][
"pagination"
][
"page"
]
==
1
assert
templates
.
calls
[
-
1
][
"context"
][
"pagination"
][
"total_pages"
]
==
1
assert
"No market listings match the current filters."
in
response
.
text
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