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
1 change: 1 addition & 0 deletions code/backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ func main() {
})
app.POST("/user", apiCfg.HandlerUser)
app.POST("/log", auth.MiddlewareAuth(apiCfg.LogHandler))
app.GET("/logs", auth.MiddlewareAuth(apiCfg.GetUserLogsHandler))
app.GET("/parsers", apiCfg.ListParsersHandler)
app.GET("/parsers/:id", apiCfg.GetParserHandler)
app.PATCH("/parsers/:id", apiCfg.UpdateParserHandler)
Expand Down
84 changes: 84 additions & 0 deletions code/backend/internal/api/get_user_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package api

import (
"net/http"
"strconv"

"github.com/EleventhHour-Projects/GoLogGo/code/backend/internal/auth"
"github.com/nottechdm/notnet/pkg/notnet"
)

// GetUserLogsHandler returns paginated requests + stats for the authenticated user.
// GET /logs?page=1&limit=25&status=all
func (api *Config) GetUserLogsHandler(req *notnet.Request, res *notnet.Response, claims *auth.CustomClaims) error {
ctx := req.HTTPRequest.Context()

// Parse query params
query := req.HTTPRequest.URL.Query()
page, _ := strconv.ParseInt(query.Get("page"), 10, 64)
if page < 1 {
page = 1
}
limit, _ := strconv.ParseInt(query.Get("limit"), 10, 64)
if limit < 1 || limit > 100 {
limit = 25
}
status := query.Get("status") // e.g. "completed", "pending", "" / "all"

skip := (page - 1) * limit

// Fetch stats
stats, err := api.Reqs.GetUserStats(ctx, claims.UserID)
if err != nil {
return res.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to fetch user stats",
})
}

// Fetch paginated requests
reqs, total, err := api.Reqs.GetUserRequests(ctx, claims.UserID, limit, skip, status)
if err != nil {
return res.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to fetch requests",
})
}

// For completed requests, fetch their parsed logs
type ReqRow struct {
ID string `json:"id"`
Payload string `json:"payload"`
Status string `json:"status"`
Attempts int `json:"attempts"`
CreatedAt interface{} `json:"createdAt"`
UpdatedAt interface{} `json:"updatedAt"`
Logs interface{} `json:"logs,omitempty"`
}

rows := make([]ReqRow, 0, len(reqs))
for _, r := range reqs {
row := ReqRow{
ID: r.ID.Hex(),
Payload: string(r.Payload),
Status: string(r.Status),
Attempts: r.Attempts,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
}
if r.Status == "completed" {
logs, err := api.Reqs.GetLogsByReqID(ctx, r.ID)
if err == nil {
row.Logs = logs
}
}
rows = append(rows, row)
}

return res.JSON(http.StatusOK, map[string]interface{}{
"stats": stats,
"requests": rows,
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) / limit,
})
}
135 changes: 135 additions & 0 deletions code/backend/internal/database/get_user_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package database

import (
"context"
"time"

"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
)

// ReqWithLogs is a joined view of a request and its parsed logs.
type ReqWithLogs struct {
Req Req `json:"req"`
Logs []Log `json:"logs"`
}

// GetUserRequests returns paginated requests belonging to a user, newest first.
// Pass a zero ObjectID to get requests across all users (admin use).
func (m *MongoDB) GetUserRequests(
ctx context.Context,
userID bson.ObjectID,
limit, skip int64,
status string,
) ([]Req, int64, error) {
col := m.Collections[CollectionRequests]

filter := bson.M{"userId": userID}
if status != "" && status != "all" {
filter["status"] = ReqestStatus(status)
}

total, err := col.CountDocuments(ctx, filter)
if err != nil {
return nil, 0, err
}

opts := options.Find().
SetSort(bson.D{{Key: "createdAt", Value: -1}}).
SetLimit(limit).
SetSkip(skip)

cursor, err := col.Find(ctx, filter, opts)
if err != nil {
return nil, 0, err
}
defer cursor.Close(ctx)

var reqs []Req
for cursor.Next(ctx) {
var r Req
if err := cursor.Decode(&r); err != nil {
return nil, 0, err
}
reqs = append(reqs, r)
}
if reqs == nil {
reqs = []Req{}
}
return reqs, total, cursor.Err()
}

// GetLogsByReqID returns all parsed log entries for a given request ID.
func (m *MongoDB) GetLogsByReqID(ctx context.Context, reqID bson.ObjectID) ([]Log, error) {
col := m.Collections[CollectionLogs]

cursor, err := col.Find(ctx, bson.M{"reqId": reqID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

var logs []Log
for cursor.Next(ctx) {
var l Log
if err := cursor.Decode(&l); err != nil {
return nil, err
}
logs = append(logs, l)
}
if logs == nil {
logs = []Log{}
}
return logs, cursor.Err()
}

// GetUserStats returns counts of requests per status for a given user.
type UserStats struct {
Total int64 `json:"total"`
Pending int64 `json:"pending"`
Processing int64 `json:"processing"`
WaitingParser int64 `json:"waitingParser"`
Completed int64 `json:"completed"`
Failed int64 `json:"failed"`
LastActivityAt time.Time `json:"lastActivityAt"`
}

func (m *MongoDB) GetUserStats(ctx context.Context, userID bson.ObjectID) (*UserStats, error) {
col := m.Collections[CollectionRequests]

statuses := []ReqestStatus{
StatusPending, StatusProcessing, StatusWaitingParser, StatusCompleted, StatusFailed,
}

stats := &UserStats{}
for _, s := range statuses {
n, err := col.CountDocuments(ctx, bson.M{"userId": userID, "status": s})
if err != nil {
return nil, err
}
switch s {
case StatusPending:
stats.Pending = n
case StatusProcessing:
stats.Processing = n
case StatusWaitingParser:
stats.WaitingParser = n
case StatusCompleted:
stats.Completed = n
case StatusFailed:
stats.Failed = n
}
stats.Total += n
}

// Grab the most recent request timestamp
var latest struct {
CreatedAt time.Time `bson:"createdAt"`
}
opts := options.FindOne().SetSort(bson.D{{Key: "createdAt", Value: -1}})
if err := col.FindOne(ctx, bson.M{"userId": userID}, opts).Decode(&latest); err == nil {
stats.LastActivityAt = latest.CreatedAt
}

return stats, nil
}
48 changes: 48 additions & 0 deletions code/frontend/gologgo-client/app/api/logs/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,51 @@ export async function POST(request) {
);
}
}

export async function GET(request) {
try {
const cookieStore = await cookies();
const tokenCookie = cookieStore.get('tokenString');
const token = tokenCookie?.value;

if (!token) {
return NextResponse.json(
{ error: 'Unauthorized: No token found' },
{ status: 401 }
);
}

// Forward query params (page, limit, status) to the Go backend
const { searchParams } = new URL(request.url);
const queryString = searchParams.toString();

const backendRes = await fetch(
`${BACKEND_URL}/logs${queryString ? `?${queryString}` : ''}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
// No caching — always fresh
cache: 'no-store',
}
);

if (!backendRes.ok) {
const errorText = await backendRes.text();
return NextResponse.json(
{ error: 'Failed to fetch logs from backend', details: errorText },
{ status: backendRes.status }
);
}

const data = await backendRes.json();
return NextResponse.json(data);
} catch (error) {
console.error('Error in GET /api/logs:', error);
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
}
}
2 changes: 2 additions & 0 deletions code/frontend/gologgo-client/app/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Analytics } from '@vercel/analytics/next'
import { Toaster } from 'sonner'
import './globals.css'

export const metadata = {
Expand Down Expand Up @@ -40,6 +41,7 @@ export default function RootLayout({ children }) {
</head>
<body className="antialiased" suppressHydrationWarning>
{children}
<Toaster richColors position="bottom-right" />
{process.env.NODE_ENV === 'production' && <Analytics />}
</body>
</html>
Expand Down
7 changes: 4 additions & 3 deletions code/frontend/gologgo-client/components/log-dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
X,
} from 'lucide-react'
import { ThemeToggle } from '@/components/theme-toggle'
import { toast } from 'sonner'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
Expand Down Expand Up @@ -66,14 +67,14 @@ export function LogDashboard() {
});
if (res.ok) {
setRawLogs('');
alert('Logs successfully sent for processing!');
toast.success('Logs successfully sent for processing!');
} else {
const errorData = await res.json().catch(() => ({}));
alert(`Failed to process logs: ${errorData.error || 'Unknown error'}`);
toast.error(`Failed to process logs: ${errorData.error || 'Unknown error'}`);
}
} catch (err) {
console.error(err);
alert('An error occurred while sending logs.');
toast.error('An error occurred while sending logs.');
} finally {
setIsProcessing(false);
}
Expand Down
Loading
Loading