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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable user-facing changes to this project will be documented in this file.

## Unreleased

- The public Wall of Shame and validator wallet endpoints no longer reveal the profile identity (name, avatar, profile link) of operators who set their account to non-visible; the validator still appears, identified only by its on-chain operator address (dedbeae)
- New Wall of Shame page under Validators publicly flags every active validator that isn't reporting metrics or logs in the last five minutes. The page lists all active validators (SHAME rows first) with their moniker, addresses, operator profile link, network badge, and binary ON/SHAME badges for both metrics and logs. Backed by a five-minute cron that cross-references on-chain active validators against our observability stack (98d083e)
- Stewards reviewing submissions can now copy a compact AI-ready bundle (user, contribution, mission, state, submitter notes, evidence URLs, staff reply, proposal, and internal CRM notes) from a copy icon next to each submission's title; the copy aborts with a warning if internal notes can't be loaded so the clipboard payload is never silently incomplete. Evidence URL types gain an admin-editable "Allow duplicate" flag that exempts URLs of that type (for example shared GitHub repositories) from duplicate detection across both manual and automated review (ac68ec7)
- Contributions explorer now shows highlighted contributions again, the page footer is flush to the bottom and CTAs users to submit a contribution, and the highlights section collapses to a single compact line when empty so contributions get more room. The Submit Contribution form no longer shows misleading "Please add a description" errors for evidence URLs, detects URL types correctly when users omit `https://`, and disables the submit button when a required GitHub or X URL has no linked social account (b249787)
Expand Down
32 changes: 16 additions & 16 deletions backend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,19 +220,19 @@ GET /api/auth/verify/
POST /api/auth/logout/

# Users
GET /api/v1/users/
GET /api/v1/users/ (requires auth)
GET /api/v1/users/me/ (requires auth)
PATCH /api/v1/users/me/ (requires auth, only name)
GET /api/v1/users/{address}/
GET /api/v1/users/by-address/{address}/
GET /api/v1/users/validators/
GET /api/v1/users/{address}/ (requires auth)
GET /api/v1/users/by-address/{address}/ (requires auth)
GET /api/v1/users/validators/ (requires auth)
POST /api/v1/users/link_x_account/ (requires auth, awards 20 pts for linking X)
POST /api/v1/users/link_discord_account/ (requires auth, awards 20 pts for linking Discord)

# Contributions
GET /api/v1/contributions/
GET /api/v1/contributions/ (requires auth)
POST /api/v1/contributions/ (requires auth)
GET /api/v1/contributions/{id}/
GET /api/v1/contributions/{id}/ (requires auth)
PATCH /api/v1/contributions/{id}/ (requires auth)
DELETE /api/v1/contributions/{id}/ (requires auth)

Expand All @@ -242,26 +242,26 @@ POST /api/v1/submissions/{id}/appeal/ (requires auth, owner-only, one
POST /api/v1/submissions/{id}/add-evidence/ (requires auth, owner-only)

# Contribution Types
GET /api/v1/contribution-types/
GET /api/v1/contribution-types/{id}/
GET /api/v1/contribution-types/statistics/
GET /api/v1/contribution-types/ (requires auth)
GET /api/v1/contribution-types/{id}/ (requires auth)
GET /api/v1/contribution-types/statistics/ (requires auth)

# Leaderboard
GET /api/v1/leaderboard/
GET /api/v1/leaderboard/stats/
GET /api/v1/leaderboard/user_stats/by-address/{address}/
GET /api/v1/leaderboard/ (requires auth)
GET /api/v1/leaderboard/stats/ (requires auth)
GET /api/v1/leaderboard/user_stats/by-address/{address}/ (requires auth)

# Multipliers
GET /api/v1/multipliers/
GET /api/v1/multipliers/ (requires auth)
GET /api/v1/multiplier-periods/

# Validators - Wall of Shame
POST /api/v1/validators/wallets/sync-grafana/ (cron-protected, X-Cron-Token, background)
GET /api/v1/validators/wallets/wall-of-shame/ (public, cached 60s, ?network= filter)

# Steward Submissions (public metrics)
GET /api/v1/steward-submissions/stats/ (public - aggregate stats)
GET /api/v1/steward-submissions/daily-metrics/ (public - time-series data)
# Steward Submissions
GET /api/v1/steward-submissions/stats/ (requires steward)
GET /api/v1/steward-submissions/daily-metrics/ (requires steward)

# AI Review Agent
GET /api/v1/ai-review/
Expand Down
6 changes: 3 additions & 3 deletions backend/api/metrics_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework import permissions, status
from django.core.cache import cache
from django.db.models import Count, Q, Min
from django.utils import timezone
Expand All @@ -19,7 +19,6 @@ class ActiveValidatorsView(APIView):
Get active validators based on their first uptime contribution.
Returns data points showing validator activation over time with continuous dates.
"""

def get(self, request):
from django.db.models.functions import TruncDate
from datetime import date, timedelta
Expand Down Expand Up @@ -91,7 +90,6 @@ class ContributionTypesStatsView(APIView):
"""
Get time series data showing how many contribution types have been assigned on each date.
"""

def get(self, request):
from django.db.models.functions import TruncDate
from datetime import date, timedelta
Expand Down Expand Up @@ -169,6 +167,7 @@ class ParticipantsGrowthView(APIView):
require `user.visible=True`, matching the Dashboard `/leaderboard/stats/`
definitions so the time series and the live counts agree.
"""
permission_classes = [permissions.AllowAny]

EXCLUDED_BUILDER_SLUGS = ('builder-welcome', 'builder')

Expand Down Expand Up @@ -307,6 +306,7 @@ class TestnetMetricsView(APIView):
explorer hosts don't serve CORS headers, so we proxy from Django and
cache the aggregate KPIs for a short window.
"""
permission_classes = [permissions.AllowAny]

EXPLORER_BASE_URLS = {
'asimov': 'https://explorer-asimov.genlayer.com',
Expand Down
74 changes: 53 additions & 21 deletions backend/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.test import TestCase
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient

from builders.models import Builder
Expand All @@ -13,40 +14,71 @@
class ParticipantsGrowthViewTests(TestCase):
def setUp(self):
self.client = APIClient()
self.validator_category = Category.objects.create(
name='Validator',
slug='validator'
self.authenticated_user = User.objects.create_user(
email='metrics@example.com',
password='pass',
address='0x9999999999999999999999999999999999999999',
)
self.client.force_authenticate(user=self.authenticated_user)
self.validator_category, _ = Category.objects.get_or_create(
slug='validator',
defaults={'name': 'Validator'},
)
self.builder_category = Category.objects.create(
name='Builder',
slug='builder'
self.builder_category, _ = Category.objects.get_or_create(
slug='builder',
defaults={'name': 'Builder'},
)
self.waitlist_type = ContributionType.objects.create(
name='Validator Waitlist',
self.waitlist_type, _ = ContributionType.objects.get_or_create(
slug='validator-waitlist',
category=self.validator_category
defaults={
'name': 'Validator Waitlist',
'category': self.validator_category,
},
)
self.builder_welcome_type = ContributionType.objects.create(
name='Builder Welcome',
self.builder_welcome_type, _ = ContributionType.objects.get_or_create(
slug='builder-welcome',
category=self.builder_category
defaults={
'name': 'Builder Welcome',
'category': self.builder_category,
},
)
self.builder_real_type = ContributionType.objects.create(
name='Builder Submission',
self.builder_real_type, _ = ContributionType.objects.get_or_create(
slug='builder-submission',
category=self.builder_category
defaults={
'name': 'Builder Submission',
'category': self.builder_category,
},
)
self.validator_real_type = ContributionType.objects.create(
name='Uptime',
self.validator_real_type, _ = ContributionType.objects.get_or_create(
slug='uptime',
category=self.validator_category
defaults={
'name': 'Uptime',
'category': self.validator_category,
},
)
self.validator_graduation_type = ContributionType.objects.create(
name='Validator',
self.validator_graduation_type, _ = ContributionType.objects.get_or_create(
slug='validator',
category=self.validator_category
defaults={
'name': 'Validator',
'category': self.validator_category,
},
)

def test_participants_growth_allows_public_metrics_page(self):
self.client.force_authenticate(user=None)

response = self.client.get('/api/v1/metrics/participants-growth/')

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('data', response.data)

def test_testnet_metrics_allows_public_metrics_page(self):
self.client.force_authenticate(user=None)

response = self.client.get('/api/v1/metrics/testnet-kpis/?network=unknown')

self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def _create_user(self, email, address):
return User.objects.create_user(
email=email,
Expand Down
124 changes: 80 additions & 44 deletions backend/contributions/migrations/0037_seed_featured_content.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,105 @@
from django.db import migrations


def get_seed_user(User, email, fallback_email, name, address):
user = User.objects.filter(email=email).first()
if user:
return user

user, _ = User.objects.get_or_create(
email=fallback_email,
defaults={
'name': name,
'username': name,
'address': address,
},
)
return user


def seed_featured_content(apps, schema_editor):
User = apps.get_model('users', 'User')
FeaturedContent = apps.get_model('contributions', 'FeaturedContent')

albert = User.objects.get(email='albert@genlayer.foundation') # cognocracy
ivan = User.objects.get(email='ivan@genlayer.foundation') # raskovsky
albert = get_seed_user(
User,
email='albert@genlayer.foundation',
fallback_email='cognocracy@seed.genlayer.com',
name='cognocracy',
address='0x00000000000000000000000000000000000000a1',
)
ivan = get_seed_user(
User,
email='ivan@genlayer.foundation',
fallback_email='raskovsky@seed.genlayer.com',
name='raskovsky',
address='0x00000000000000000000000000000000000000a2',
)

FeaturedContent.objects.create(
FeaturedContent.objects.update_or_create(
content_type='hero',
title='Argue.fun Launch',
description='Deploy intelligent contracts, run validators, and earn GenLayer Points on the latest testnet.',
subtitle='cognocracy',
user=albert,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117991/tally/featured_1_hero_1772117989.png',
hero_image_public_id='tally/featured_1_hero_1772117989',
url='',
is_active=True,
order=0,
defaults={
'description': 'Deploy intelligent contracts, run validators, and earn GenLayer Points on the latest testnet.',
'subtitle': 'cognocracy',
'user': albert,
'hero_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117991/tally/featured_1_hero_1772117989.png',
'hero_image_public_id': 'tally/featured_1_hero_1772117989',
'url': '',
'is_active': True,
'order': 0,
},
)

FeaturedContent.objects.create(
FeaturedContent.objects.update_or_create(
content_type='build',
title='Argue.fun',
description='',
subtitle='',
user=albert,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117992/tally/featured_2_hero_1772117992.png',
hero_image_public_id='tally/featured_2_hero_1772117992',
user_profile_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117994/tally/featured_2_avatar_1772117993.png',
user_profile_image_public_id='tally/featured_2_avatar_1772117993',
url='',
is_active=True,
order=0,
defaults={
'description': '',
'subtitle': '',
'user': albert,
'hero_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117992/tally/featured_2_hero_1772117992.png',
'hero_image_public_id': 'tally/featured_2_hero_1772117992',
'user_profile_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117994/tally/featured_2_avatar_1772117993.png',
'user_profile_image_public_id': 'tally/featured_2_avatar_1772117993',
'url': '',
'is_active': True,
'order': 0,
},
)

FeaturedContent.objects.create(
FeaturedContent.objects.update_or_create(
content_type='build',
title='Internet Court',
description='',
subtitle='',
user=ivan,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117994/tally/featured_3_hero_1772117994.png',
hero_image_public_id='tally/featured_3_hero_1772117994',
user_profile_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117995/tally/featured_3_avatar_1772117995.png',
user_profile_image_public_id='tally/featured_3_avatar_1772117995',
url='',
is_active=True,
order=1,
defaults={
'description': '',
'subtitle': '',
'user': ivan,
'hero_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117994/tally/featured_3_hero_1772117994.png',
'hero_image_public_id': 'tally/featured_3_hero_1772117994',
'user_profile_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117995/tally/featured_3_avatar_1772117995.png',
'user_profile_image_public_id': 'tally/featured_3_avatar_1772117995',
'url': '',
'is_active': True,
'order': 1,
},
)

FeaturedContent.objects.create(
FeaturedContent.objects.update_or_create(
content_type='build',
title='Rally',
description='',
subtitle='',
user=ivan,
hero_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117996/tally/featured_4_hero_1772117995.png',
hero_image_public_id='tally/featured_4_hero_1772117995',
user_profile_image_url='https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117996/tally/featured_4_avatar_1772117996.png',
user_profile_image_public_id='tally/featured_4_avatar_1772117996',
url='',
is_active=True,
order=2,
defaults={
'description': '',
'subtitle': '',
'user': ivan,
'hero_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117996/tally/featured_4_hero_1772117995.png',
'hero_image_public_id': 'tally/featured_4_hero_1772117995',
'user_profile_image_url': 'https://res.cloudinary.com/dfqmoeawa/image/upload/v1772117996/tally/featured_4_avatar_1772117996.png',
'user_profile_image_public_id': 'tally/featured_4_avatar_1772117996',
'url': '',
'is_active': True,
'order': 2,
},
)
Comment thread
JoaquinBN marked this conversation as resolved.


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ def restore_validator_waitlist_points(apps, schema_editor):
class Migration(migrations.Migration):

dependencies = [
('builders', '0001_initial'),
('contributions', '0050_evidence_url_types'),
('leaderboard', '0014_add_referral_points_model'),
('users', '0014_add_referral_system'),
('validators', '0001_initial'),
]

operations = [
Expand Down
Loading
Loading