{"id":50508887,"url":"https://github.com/hwesselmann/ranking-info-rails","last_synced_at":"2026-06-02T18:31:16.381Z","repository":{"id":40007117,"uuid":"253183782","full_name":"hwesselmann/ranking-info-rails","owner":"hwesselmann","description":"A small web-app to help browse and analyze the German Tennis rankings.","archived":false,"fork":false,"pushed_at":"2026-05-25T18:47:11.000Z","size":4943,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-25T20:27:09.908Z","etag":null,"topics":["rails","ruby","tennis"],"latest_commit_sha":null,"homepage":"https://www.ranking-info.net","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hwesselmann.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-04-05T07:58:58.000Z","updated_at":"2026-05-25T18:42:58.000Z","dependencies_parsed_at":"2024-04-05T20:38:21.755Z","dependency_job_id":null,"html_url":"https://github.com/hwesselmann/ranking-info-rails","commit_stats":null,"previous_names":["hwesselmann/ranking-info"],"tags_count":52,"template":false,"template_full_name":null,"purl":"pkg:github/hwesselmann/ranking-info-rails","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hwesselmann%2Franking-info-rails","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hwesselmann%2Franking-info-rails/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hwesselmann%2Franking-info-rails/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hwesselmann%2Franking-info-rails/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hwesselmann","download_url":"https://codeload.github.com/hwesselmann/ranking-info-rails/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hwesselmann%2Franking-info-rails/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33833277,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-02T02:00:07.132Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rails","ruby","tennis"],"created_at":"2026-06-02T18:31:15.791Z","updated_at":"2026-06-02T18:31:16.374Z","avatar_url":"https://github.com/hwesselmann.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ranking-info  \n\n[![test and sca](https://github.com/hwesselmann/ranking-info/actions/workflows/test.yaml/badge.svg)](https://github.com/hwesselmann/ranking-info/actions/workflows/test.yaml)\n[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=hwesselmann_ranking-info\u0026metric=ncloc)](https://sonarcloud.io/summary/new_code?id=hwesselmann_ranking-info)\n[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=hwesselmann_ranking-info\u0026metric=coverage)](https://sonarcloud.io/summary/new_code?id=hwesselmann_ranking-info)\n[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=hwesselmann_ranking-info\u0026metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=hwesselmann_ranking-info)\n\nThis is a small web-application aiming at providing accessible information on the German national tennis youth and adult rankings.\n\n## Prerequisites  \n\nThis application is developed using\n\n* Ruby 4.0.5\n* Rails 8.1.3\n* Node.js 24 (for asset compilation)\n* Tailwind CSS v4 (for styling, via yarn)\n* SQLite3 (for development and test)\n* PostgreSQL (for production)\n* Bundler (Ruby gem management)\n* Yarn (JavaScript package management)\n\n## Setup  \n\nTo set up a development environment, run the following steps before starting the server:\n\n```bash\n# Install Ruby gems\nbundle install\n\n# Install JavaScript packages (also copies FontAwesome webfonts)\nyarn install\n\n# Run database migrations\nbin/rails db:migrate\n\n# Build JavaScript and CSS assets\nyarn build\nyarn build:css\n```\n\nThen start the development server:\n\n```bash\nbin/rails server\n```\n\nTo automatically rebuild assets on changes, run the following in separate terminals alongside the server:\n\n```bash\nyarn build --watch\nyarn build:css --watch\n```\n\n## Tests\n\nTo run the test suite:\n\n```bash\nbin/rails test\n```\n\nThe project also includes Guard for running tests automatically on code changes. Start it from the project folder with:\n\n```bash\nguard\n```\n\n## API Documentation (Swagger UI)\n\nThe application exposes a REST API documented with Swagger UI, available at `/api-docs` when the server is running (e.g. `http://localhost:3000/api-docs`).\n\nAll API endpoints require Bearer token authentication. Requests without a valid token receive a `401 Unauthorized` response.\n\n### Configuring Bearer Tokens\n\nTokens can be provided in two ways:\n\n**Via environment variable** (single token, suitable for development or simple deployments):\n\n```bash\nAPI_BEARER_TOKEN=your_secret_token bin/rails server\n```\n\n**Via Rails credentials** (array of tokens, suitable for production):\n\n```bash\nbin/rails credentials:edit\n```\n\nAdd the following structure:\n\n```yaml\napi:\n  tokens:\n    - your_first_token\n    - your_second_token\n```\n\n### Using Bearer Tokens in Swagger UI\n\n1. Open `/api-docs` in your browser.\n2. Click the **Authorize** button (lock icon) at the top right of the page.\n3. Enter your token in the **Value** field using the format `Bearer your_secret_token`.\n4. Click **Authorize**, then **Close**.\n\nSubsequent requests made through the Swagger UI will include the token automatically.\n\n### Rate Limiting\n\nAll API endpoints are rate-limited to **1000 requests per hour**, keyed on the `Authorization` header value. Requests that exceed the limit receive a `429 Too Many Requests` response:\n\n```json\n{ \"error\": \"Too Many Requests\" }\n```\n\nThe counter resets automatically after one hour. If no `Authorization` header is present, the limit is applied per IP address instead.\n\n### Bruno Collection\n\nA [Bruno](https://www.usebruno.com/) collection for manual API testing is located in the `bruno/` directory. Open the folder in Bruno, select the **local** environment, and set your `apiToken`. All three endpoints are pre-configured; optional query parameters are included but disabled and can be toggled per request.\n\n### Available Endpoints\n\n#### Listings\n\n```\nGET /api/v1/listings/:quarter/:age_group_slug\n```\n\nReturns a paginated ranking list for a given quarter and age group. Each entry includes a HAL-style `links.self` pointing to the player detail endpoint. See `/api-docs` for all query parameters (federation, club, pagination, etc.).\n\n#### Players\n\n```\nGET /api/v1/players?lastname=Mustermann\u0026yob=2008\n```\n\nSearches for players by lastname (case-insensitive, partial match). The optional `yob` parameter (year of birth, YYYY) narrows results to players born in that year. Returns 400 if `lastname` is missing, 404 if no match is found.\n\n```\nGET /api/v1/players/:dtb_id\n```\n\nReturns basic player data and the full quarterly ranking history across all age groups. Returns 404 if the DTB ID is unknown.\n\n## Import file format  \n\nTo import data into the system, create a CSV file from the official ranking list PDF using a tool like Tabula. The file must be in the format:\n\n\u003e ranking-position, lastname, firstname, nationality, dtb_id, federation, club, score\n\nThis is the default column order Tabula produces. An example file can be found in `test/fixtures/files`.\n\n## Scheduled filesystem import\n\nThe application automatically scans an import folder for new CSV ranking files and imports them on a configurable schedule. The Solid Queue supervisor must be running for scheduled jobs to execute:\n\n```bash\nbin/jobs\n```\n\n**Import folder**\n\nPlace ranking CSV files in the import folder. Files already recorded in the import history are skipped automatically. Files are never deleted after import.\n\nIf an import fails, the error is appended to `error.log` in the import folder. Subsequent errors are separated from previous ones by a blank line.\n\n**Configuration**\n\n| Variable | Description | Default |\n|---|---|---|\n| `IMPORT_FOLDER` | Absolute path to the folder containing ranking CSV files | `storage/import/` inside the application root |\n| `IMPORT_SCHEDULE` | Cron expression for how often the folder is scanned | `0 12 * * *` (daily at 12:00) |\n\nExample with custom settings:\n\n```bash\nIMPORT_FOLDER=/data/rankings IMPORT_SCHEDULE=\"0 6 * * *\" bin/jobs\n```\n\n## Caching\n\nThe application uses **Solid Cache** (database-backed) as the cache store in production. The cache table is part of the main database and is created automatically by `bin/rails db:migrate`.\n\nAPI responses include HTTP `ETag` headers. Clients that send a matching `If-None-Match` header receive a `304 Not Modified` response without a body, avoiding redundant data transfer. ETags are invalidated automatically whenever new ranking data is imported.\n\nThe maximum cache size is configured to 256 MB in `config/cache.yml`.\n\n## Deploy  \n\n### Docker\n\nThe project includes a Dockerfile and a docker-compose configuration for a production environment. Adapt `docker-compose.yml` to your needs, build the image, then start the stack:\n\n```bash\nDATABASE_PASSWORD=your_password docker-compose up\n```\n\nAfter the stack is running, run database migrations:\n\nThe application is then available at `http://127.0.0.1:3000`.\n\n### Standalone\n\nSet the following environment variables to populate the imprint on the about page (or hardcode them in `config/config.yml`):\n\n| Variable | Description |\n|---|---|\n| `DOMAIN` | Domain the application is deployed to |\n| `IMPRINT_NAME` | Name for the imprint |\n| `IMPRINT_STREET` | Street and street number for the imprint |\n| `IMPRINT_ZIPCODE` | Zip code for the imprint |\n| `IMPRINT_CITY` | City for the imprint |\n| `IMPRINT_PHONE` | Phone number for the imprint |\n| `IMPRINT_MAIL` | Email address for the imprint |\n\n\n[![SonarQube Cloud](https://sonarcloud.io/images/project_badges/sonarcloud-light.svg)](https://sonarcloud.io/summary/new_code?id=hwesselmann_ranking-info)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhwesselmann%2Franking-info-rails","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhwesselmann%2Franking-info-rails","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhwesselmann%2Franking-info-rails/lists"}