Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions admin/ai_usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,23 @@ func AIUsageHandler(w http.ResponseWriter, r *http.Request) {
uptime := time.Since(summary.Since).Round(time.Minute)

var sb strings.Builder
sb.WriteString(`<h2>Claude API Usage</h2>`)
sb.WriteString(`<style>
.ai-usage-table { width:100%; border-collapse:collapse; font-size:0.85rem; }
.ai-usage-table th, .ai-usage-table td { padding:6px 8px; white-space:nowrap; }
.ai-usage-table td:first-child { word-break:break-all; white-space:normal; }
@media (max-width: 600px) {
.ai-usage-table { font-size:0.72rem; }
.ai-usage-table th, .ai-usage-table td { padding:4px 3px; }
}
</style>`)
sb.WriteString(fmt.Sprintf(`<p>Tracking since %s (%s ago)</p>`, summary.Since.Format("2006-01-02 15:04"), uptime))
sb.WriteString(fmt.Sprintf(`<p><strong>Total: %d calls, est $%.4f</strong></p>`, summary.TotalCalls, summary.TotalCost/100))

// Usage by caller table
sb.WriteString(`<h3>Usage by Caller</h3>`)
sb.WriteString(`<table><thead><tr>
<th>Caller</th><th>Calls</th><th>Input Tokens</th><th>Output Tokens</th>
<th>Cache Read</th><th>Cache Write</th><th>Est Cost</th>
sb.WriteString(`<h3>By Caller</h3>`)
sb.WriteString(`<div style="overflow-x:auto;"><table class="ai-usage-table"><thead><tr>
<th>Caller</th><th>Calls</th><th>In</th><th>Out</th>
<th>Cache R</th><th>Cache W</th><th>Cost</th>
</tr></thead><tbody>`)

for _, cu := range summary.ByCaller {
Expand All @@ -40,27 +48,27 @@ func AIUsageHandler(w http.ResponseWriter, r *http.Request) {
cu.CacheReadTokens, cu.CacheCreationTokens, cu.TotalCostCents/100))
}

sb.WriteString(`</tbody></table>`)
sb.WriteString(`</tbody></table></div>`)

// Recent calls
sb.WriteString(`<h3>Recent Calls</h3>`)
sb.WriteString(`<table><thead><tr>
sb.WriteString(`<div style="overflow-x:auto;"><table class="ai-usage-table"><thead><tr>
<th>Time</th><th>Caller</th><th>Model</th><th>In</th><th>Out</th><th>Cache</th><th>Cost</th>
</tr></thead><tbody>`)

for _, r := range summary.RecentCalls {
cache := ""
if r.CacheReadTokens > 0 {
cache = fmt.Sprintf("read:%d", r.CacheReadTokens)
cache = fmt.Sprintf("r:%d", r.CacheReadTokens)
} else if r.CacheCreationTokens > 0 {
cache = fmt.Sprintf("write:%d", r.CacheCreationTokens)
cache = fmt.Sprintf("w:%d", r.CacheCreationTokens)
}
sb.WriteString(fmt.Sprintf(`<tr><td>%s</td><td>%s</td><td>%s</td><td>%d</td><td>%d</td><td>%s</td><td>$%.4f</td></tr>`,
r.Timestamp.Format("15:04:05"), r.Caller, r.Model,
r.InputTokens, r.OutputTokens, cache, r.EstimatedCostCents/100))
}

sb.WriteString(`</tbody></table>`)
sb.WriteString(`</tbody></table></div>`)

html := app.RenderHTMLForRequest("Admin", "AI Usage", sb.String(), r)
w.Write([]byte(html))
Expand Down
17 changes: 17 additions & 0 deletions apps/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,23 @@ func GetPublicApps() []*App {
return list
}

// GetAppsByAuthor returns all public apps by a given author ID, sorted by name.
func GetAppsByAuthor(authorID string) []*App {
mutex.RLock()
defer mutex.RUnlock()

var list []*App
for _, a := range apps {
if a.AuthorID == authorID && a.Public {
list = append(list, a)
}
}
sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
return list
}

// SearchApps searches for apps by query string.
func SearchApps(query string) []*App {
query = strings.ToLower(query)
Expand Down
13 changes: 13 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ func main() {
return result
}
user.LinkifyContent = blog.Linkify
user.GetUserApps = func(authorID string) []user.UserApp {
appList := apps.GetAppsByAuthor(authorID)
result := make([]user.UserApp, len(appList))
for i, a := range appList {
result[i] = user.UserApp{
Slug: a.Slug,
Name: a.Name,
Description: a.Description,
Icon: a.Icon,
}
}
return result
}

// Wire admin → blog callbacks (avoids blog importing admin)
admin.GetNewAccountBlog = blog.GetNewAccountBlogPosts
Expand Down
32 changes: 31 additions & 1 deletion user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ type UserPost struct {
// GetUserPosts returns posts by author name. Wired from main.go.
var GetUserPosts func(authorName string) []UserPost

// UserApp is a simplified app representation for profile rendering.
type UserApp struct {
Slug string
Name string
Description string
Icon string
}

// GetUserApps returns public apps by author ID. Wired from main.go.
var GetUserApps func(authorID string) []UserApp

// LinkifyContent converts URLs in text to clickable links. Wired from main.go.
var LinkifyContent func(text string) string

Expand Down Expand Up @@ -339,8 +350,27 @@ func Handler(w http.ResponseWriter, r *http.Request) {
messageLink = fmt.Sprintf(`<p class="mt-4"><a href="/mail?compose=true&to=%s">Send a message</a></p>`, acc.ID)
}

// Apps section removed
// Apps section
appsSection := ""
if GetUserApps != nil {
userApps := GetUserApps(acc.ID)
if len(userApps) > 0 {
var appsSB strings.Builder
appsSB.WriteString(fmt.Sprintf(`<h3 class="mb-5">Apps (%d)</h3>`, len(userApps)))
for _, a := range userApps {
icon := a.Icon
if icon == "" {
icon = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>`
}
desc := a.Description
if len(desc) > 80 {
desc = desc[:80] + "..."
}
appsSB.WriteString(fmt.Sprintf(`<div class="post-item"><h3><a href="/apps/%s/run">%s %s</a></h3><p class="info">%s</p></div>`, a.Slug, icon, a.Name, desc))
}
appsSection = appsSB.String()
}
}

// Build the profile page content
content := fmt.Sprintf(`<div class="max-w-xl">
Expand Down
Loading