Admin Messages: CSV export of the whole filtered list
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m14s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 11s
CI / ui (pull_request) Successful in 33s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m14s
A right-aligned 'Export CSV ↓' link in the filter row downloads /_gm/messages.csv with the active filters (game / sender / name / ext masks), exporting every matching message (capped at 100k) regardless of the page window — columns time, source, sender_id, sender, ip, message, game_id.
This commit is contained in:
@@ -74,6 +74,7 @@ h1 { font-size: 1.4rem; margin: 0 0 0.4rem; }
|
||||
.subnav a.active { color: var(--ink); }
|
||||
|
||||
.form { display: flex; flex-wrap: wrap; gap: 0.6rem; align-items: end; margin-top: 0.4rem; }
|
||||
.form .export { margin-left: auto; align-self: center; color: var(--accent); white-space: nowrap; }
|
||||
.form.col { flex-direction: column; align-items: stretch; max-width: 540px; }
|
||||
.form label { display: flex; flex-direction: column; gap: 0.2rem; font-size: 0.85rem; color: var(--ink-dim); }
|
||||
.form input, .form select, .form textarea {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<input name="name" value="{{.NameMask}}" placeholder="sender name mask (* ?)">
|
||||
<input name="ext" value="{{.ExtMask}}" placeholder="sender external id mask (* ?)">
|
||||
<button type="submit">Filter</button>
|
||||
<a class="export" href="/_gm/messages.csv?{{.FilterQuery}}">Export CSV ↓</a>
|
||||
</form>
|
||||
{{if or .GameID .UserID}}
|
||||
<p class="note">Filtered{{if .GameID}} to game <a href="/_gm/games/{{.GameID}}">{{.GameID}}</a>{{end}}{{if .UserID}} from <a href="/_gm/users/{{.UserID}}">sender</a>{{end}} · <a href="/_gm/messages">clear</a></p>
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -53,6 +54,7 @@ func (s *Server) registerConsole(router *gin.Engine) {
|
||||
gm.GET("/complaints/:id", s.consoleComplaintDetail)
|
||||
gm.POST("/complaints/:id/resolve", s.consoleResolveComplaint)
|
||||
gm.GET("/messages", s.consoleMessages)
|
||||
gm.GET("/messages.csv", s.consoleMessagesCSV)
|
||||
gm.GET("/dictionary", s.consoleDictionary)
|
||||
gm.POST("/dictionary/reload", s.consoleReloadDictionary)
|
||||
gm.POST("/dictionary/changes/apply", s.consoleApplyChanges)
|
||||
@@ -186,6 +188,38 @@ func (s *Server) consoleMessages(c *gin.Context) {
|
||||
s.renderConsole(c, "messages", "messages", "Messages", view)
|
||||
}
|
||||
|
||||
// adminMessagesExportCap bounds the CSV export row count (the moderated chat volume is small).
|
||||
const adminMessagesExportCap = 100000
|
||||
|
||||
// consoleMessagesCSV exports the whole filtered chat-message list (ignoring pagination) as a
|
||||
// CSV download, for offline moderation review.
|
||||
func (s *Server) consoleMessagesCSV(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
gameID, _ := uuid.Parse(strings.TrimSpace(c.Query("game")))
|
||||
userID, _ := uuid.Parse(strings.TrimSpace(c.Query("user")))
|
||||
filter := social.AdminMessageFilter{
|
||||
GameID: gameID,
|
||||
SenderID: userID,
|
||||
NameMask: c.Query("name"),
|
||||
ExtMask: c.Query("ext"),
|
||||
}
|
||||
items, err := s.social.AdminListMessages(ctx, filter, adminMessagesExportCap, 0)
|
||||
if err != nil {
|
||||
s.consoleError(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", `attachment; filename="messages.csv"`)
|
||||
w := csv.NewWriter(c.Writer)
|
||||
_ = w.Write([]string{"time", "source", "sender_id", "sender", "ip", "message", "game_id"})
|
||||
for _, m := range items {
|
||||
_ = w.Write([]string{
|
||||
fmtTime(m.CreatedAt), m.Source, m.SenderID.String(), m.SenderName, m.SenderIP, m.Body, m.GameID.String(),
|
||||
})
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
// consoleUserDetail renders one account with its stats, identities and games.
|
||||
func (s *Server) consoleUserDetail(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
Reference in New Issue
Block a user