Skip to content

Conversation

@alyssaditroia
Copy link
Contributor

Updates the dojo stats function and updates the stats dashboard to simplify the displayed stats. The get_dojo_stats() function now uses direct SQL queries, removed chart and trend data. The stat dashboard has been redesigned to fit the updated results of the stats function.

(Award logic is unchanged; I do not have any awards data locally)

Backend stats calculation:

  • Replaced stats and trend calculation with raw SQL queries for total solves, users, and recent solves.
  • Removed chart/trend logic and related.
  • Query time reduced from ~800ms to < ~400ms using my local mock data with ~2M solves and 6,000 users.

Frontend dashboard redesign:

  • Removed chart and trend display, restructured dashboard to show total solves, challenges, recent solves, unique hackers, and awards.
  • Award icons and emojis are now displayed in the dashboard, and the chart component/script is removed.
  • Removed duplicate award display from the main dojo page since it is now shown in the dashboard.

UI and styling improvements:

  • Updated stat card layout
  • Added award styling
  • Updated recent activity/award display
  • Removed chart/trend styles.
Screenshot 2025-10-21 at 2 17 11 PM

@codecov
Copy link

codecov bot commented Oct 22, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@alyssaditroia alyssaditroia marked this pull request as ready for review November 11, 2025 06:15
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 17 to 27
challenge_ids = [c.challenge_id for c in dojo.challenges]

stats = db.session.execute(
text("""
SELECT
COUNT(DISTINCT user_id) as total_users,
COUNT(*) as total_solves
FROM submissions
WHERE type = 'correct'
AND challenge_id = ANY(:challenge_ids)
"""),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve visibility/admin filters in dojo stats queries

The new raw SQL in get_dojo_stats() now counts all correct submissions for the dojo’s challenge IDs without the filters that dojo.solves() previously applied. Admin solves, hidden users, challenges that aren’t yet visible, and optional challenges all contribute to total_users and total_solves, whereas the old implementation excluded them by default. This inflates the dashboard statistics and can leak activity for hidden or unreleased challenges. Consider reproducing the filter set from dojo.solves() so the numbers match what the rest of the site treats as public progress.

Useful? React with 👍 / 👎.

Comment on lines 17 to 28
challenge_ids = [c.challenge_id for c in dojo.challenges]

stats = db.session.execute(
text("""
SELECT
COUNT(DISTINCT user_id) as total_users,
COUNT(*) as total_solves
FROM submissions
WHERE type = 'correct'
AND challenge_id = ANY(:challenge_ids)
"""),
{"challenge_ids": challenge_ids}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle dojos without challenges before executing ANY() query

When a dojo has no challenges, challenge_ids is an empty list. Executing challenge_id = ANY(:challenge_ids) with an empty list causes PostgreSQL to raise cannot determine type of empty array, resulting in a 500 when the stats page is rendered for a freshly created dojo. The previous implementation simply returned zeros in this case. Guard against an empty list (return zeros early or cast the parameter to a typed empty array) before running the SQL.

Useful? React with 👍 / 👎.

@alyssaditroia
Copy link
Contributor Author

Updated get_dojo_stats and ran testing by adding synthetic users with solves. Two indexes were added to optimize these properly. I have provided my test results showing execution time with and without the new indexes:

Note

Tested against a dojo locally with 730k synthetic solves.

Total Solves:

Using DISTINCT & Without New Indexes

  • Execution Time: 420.089 ms

Using DISTINCT & With New Indexes

  • Execution Time: 272.466 ms

Using GROUP BY & Without New Indexes

  • Execution Time: 140.768 ms

Using GROUP BY & With New Indexes

  • Execution Time: 125.814 ms

Recent Solves:

Without New Index

  • Execution Time: 100.309 ms

With New Indexes

  • Execution Time: 0.384 ms

Total New Indexes: 2

image

Note

Performance improvements are dependent on adding 2 indexes.
I do not have awards added.
Still need to get "Hacking Now" working.

Synthetic Users + Solves:

from datetime import datetime, timedelta

NUM_USERS = 3000
BATCH     = 200
PREFIX    = "synthetic"
MAX_DAYS  = 90

chals = Challenges.query.order_by(Challenges.id.asc()).all()
total = len(chals)
now = datetime.now()

solves_to_insert = []

for i in range(NUM_USERS):
    is_hidden = (i % 100 == 0)
    is_admin = (i % 500 == 0)
    
    u = Users(
        name=f"{PREFIX}-{i}",
        email=f"{PREFIX}-{i}@email.com",
        hidden=is_hidden,
        type="admin" if is_admin else "user"
    )
    db.session.add(u)
    
    pct = (abs(hash(f"{i}-seed-42")) % 100) / 100.0
    k = int(pct * total)
    
    if k > 0:
        for j in range(k):
            days_ago = abs(hash(f"solve-{i}-{j}")) % MAX_DAYS
            solve_time = now - timedelta(days=days_ago)
            chal_idx = abs(hash(f"chal-{i}-{j}")) % total
            
            solves_to_insert.append((solve_time, u, chals[chal_idx]))

db.session.commit()
print(f"Created {NUM_USERS} users")

solves_to_insert.sort(key=lambda x: x[0])

for idx, (solve_time, user, challenge) in enumerate(solves_to_insert):
    db.session.add(Solves(challenge=challenge, user=user, date=solve_time))
    
    if (idx + 1) % BATCH == 0:
        db.session.commit()
        print(f"Committed {idx + 1} solves")

db.session.commit()
print(f"Done. Created {len(solves_to_insert)} solves dated within the last {MAX_DAYS} days.")

@ConnorNelson
Copy link
Member

CREATE INDEX idx_submissions_date_desc ON submissions (date DESC); is super helpful for figuring out the most recent N solves for some challenge (or module/dojo).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants