{"openapi": "3.1.0", "info": {"title": "WinnerWeb", "version": "0.2.0", "description": "Family step race using IST dates and cumulative daily totals. No API authentication. Django admin requires a staff login."}, "servers": [{"url": "/winnerweb"}], "security": [], "paths": {"/profiles/{phone_id}/": {"parameters": [{"name": "phone_id", "in": "path", "required": true, "schema": {"type": "string", "minLength": 1, "maxLength": 128}}], "get": {"operationId": "getProfile", "description": "Fetch the name and saved Cloudinary photo URL.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Profile"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "Unknown phone", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}, "put": {"operationId": "upsertProfile", "description": "Create/update a profile. Save uploads locally, then upload to Cloudinary. An omitted photo preserves the existing photo. The response includes the saved URL.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Profile"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "503": {"description": "Image upload unavailable; existing profile preserved", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}, "requestBody": {"required": true, "content": {"multipart/form-data": {"schema": {"type": "object", "required": ["name"], "properties": {"name": {"type": "string", "minLength": 1, "maxLength": 100}, "photo": {"type": "string", "format": "binary", "description": "Optional JPEG, PNG or WebP, at most 5 MiB."}}}}}}}}, "/steps/": {"post": {"operationId": "upsertDailySteps", "description": "Overwrite today's IST total and set source=automated. Last submission processed wins, even with a lower total or earlier client timestamp. Past/future IST dates are rejected. Requires an existing profile.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DailySteps"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "Unknown phone; onboard first", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}, "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StepsInput"}}}}}}, "/steps/manual/": {"post": {"operationId": "upsertManualDailySteps", "description": "Overwrite today's IST total from app Settings and set source=manual. Server determines date and timestamp. No date or weekly override. A later automated or manual submission replaces this value. Preserves the recorded phone model. Requires onboarding first.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DailySteps"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}, "404": {"description": "Unknown phone", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}, "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ManualStepsInput"}}}}}}, "/leaderboard/daily/": {"get": {"operationId": "getDailyLeaderboard", "description": "All profiles, including zero-step participants, sorted by steps descending, then phone ID. Photo URLs are fetched through profiles only. Weekly means seven IST dates beginning with start_date, inclusive.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DailyLeaderboard"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}, "parameters": [{"name": "date", "in": "query", "required": true, "schema": {"type": "string", "format": "date"}}]}}, "/leaderboard/weekly/": {"get": {"operationId": "getWeeklyLeaderboard", "description": "All profiles, including zero-step participants, sorted by steps descending, then phone ID. Photo URLs are fetched through profiles only. Weekly means seven IST dates beginning with start_date, inclusive.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Leaderboard"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}, "parameters": [{"name": "start_date", "in": "query", "required": true, "schema": {"type": "string", "format": "date"}}]}}, "/openapi.json": {"get": {"operationId": "getWinnerWebOpenAPI", "description": "WinnerWeb-only OpenAPI specification.", "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "Invalid request", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}}}}}, "components": {"schemas": {"Profile": {"type": "object", "required": ["phone_id", "name", "photo_url", "created_at", "updated_at"], "properties": {"phone_id": {"type": "string", "minLength": 1, "maxLength": 128}, "name": {"type": "string", "minLength": 1, "maxLength": 100}, "photo_url": {"type": ["string", "null"], "format": "uri"}, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"}}}, "StepsInput": {"type": "object", "additionalProperties": false, "required": ["phone_id", "steps", "timestamp", "phone_model"], "properties": {"phone_id": {"type": "string", "minLength": 1, "maxLength": 128}, "steps": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807}, "timestamp": {"type": "string", "format": "date-time", "description": "Offset-aware reporting time; must belong to today in IST."}, "phone_model": {"type": "string", "minLength": 1, "maxLength": 200}}}, "ManualStepsInput": {"type": "object", "additionalProperties": false, "required": ["phone_id", "steps"], "properties": {"phone_id": {"type": "string", "minLength": 1, "maxLength": 128}, "steps": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807}}}, "DailySteps": {"type": "object", "required": ["phone_id", "date", "steps", "timestamp", "phone_model", "source", "created_at", "updated_at"], "properties": {"phone_id": {"type": "string", "minLength": 1, "maxLength": 128}, "steps": {"type": "integer", "minimum": 0, "maximum": 9223372036854775807}, "timestamp": {"type": "string", "format": "date-time", "description": "Offset-aware reporting time; must belong to today in IST."}, "phone_model": {"type": "string", "maxLength": 200}, "source": {"type": "string", "enum": ["manual", "automated"]}, "date": {"type": "string", "format": "date"}, "created_at": {"type": "string", "format": "date-time"}, "updated_at": {"type": "string", "format": "date-time"}}}, "Leaderboard": {"type": "object", "required": ["start_date", "end_date", "timezone", "participants"], "properties": {"start_date": {"type": "string", "format": "date"}, "end_date": {"type": "string", "format": "date"}, "timezone": {"const": "Asia/Kolkata"}, "participants": {"type": "array", "items": {"type": "object", "required": ["rank", "phone_id", "name", "steps"], "properties": {"rank": {"type": "integer", "minimum": 1}, "phone_id": {"type": "string", "minLength": 1, "maxLength": 128}, "name": {"type": "string", "minLength": 1, "maxLength": 100}, "steps": {"type": "integer", "minimum": 0}}}}}}, "DailyLeaderboard": {"type": "object", "required": ["start_date", "end_date", "timezone", "participants"], "properties": {"start_date": {"type": "string", "format": "date"}, "end_date": {"type": "string", "format": "date"}, "timezone": {"const": "Asia/Kolkata"}, "participants": {"type": "array", "items": {"type": "object", "required": ["rank", "phone_id", "name", "steps", "source"], "properties": {"rank": {"type": "integer", "minimum": 1}, "phone_id": {"type": "string", "minLength": 1, "maxLength": 128}, "name": {"type": "string", "minLength": 1, "maxLength": 100}, "steps": {"type": "integer", "minimum": 0}, "source": {"type": ["string", "null"], "enum": ["manual", "automated", null], "description": "Null when no reading exists for this day."}}}}}}, "Error": {"type": "object", "required": ["error"], "properties": {"error": {"type": "string"}}}}}}