From 98fc7fb0625606d0af2b305544234b82d7887c8a Mon Sep 17 00:00:00 2001 From: marekl11 Date: Thu, 3 Sep 2026 23:35:08 +0200 Subject: [PATCH 01/18] fix(readme): point CI badges and license link at the current files The CI badges referenced backend_build.yml and frontend_build.yml on master; the workflows are backend_build_check.yml and frontend_build_check.yml and the default branch is develop, so both badges rendered as "no status". The license link pointed at LICENSE.md on master, but the file is LICENSE on develop. --- Readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 298b205f7..7b689a462 100644 --- a/Readme.md +++ b/Readme.md @@ -17,13 +17,13 @@
-| **CI/CD** | | [![Backend Build](https://github.com/hotosm/fair/actions/workflows/backend_build.yml/badge.svg?branch=master)](https://github.com/hotosm/fair/actions/workflows/backend_build.yml) [![Frontend Build](https://github.com/hotosm/fair/actions/workflows/frontend_build.yml/badge.svg?branch=master)](https://github.com/hotosm/fair/actions/workflows/frontend_build.yml) | +| **CI/CD** | | [![Backend Build](https://github.com/hotosm/fair/actions/workflows/backend_build_check.yml/badge.svg?branch=develop)](https://github.com/hotosm/fair/actions/workflows/backend_build_check.yml) [![Frontend Build](https://github.com/hotosm/fair/actions/workflows/frontend_build_check.yml/badge.svg?branch=develop)](https://github.com/hotosm/fair/actions/workflows/frontend_build_check.yml) | | :--- | :--- | :--- | | **Tech Stack** | | ![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django) ![React](https://img.shields.io/badge/React-20232A.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![TensorFlow](https://img.shields.io/badge/TensorFlow-FF6F00.svg?style=for-the-badge&logo=TensorFlow&logoColor=white) ![PyTorch](https://img.shields.io/badge/PyTorch-EE4C2C.svg?style=for-the-badge&logo=PyTorch&logoColor=white) | | **Code Style** | | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://pre-commit.com) [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) | | **Community** | | [![Slack](https://img.shields.io/badge/Slack-Join%20the%20community!-d63f3f?style=for-the-badge&logo=slack&logoColor=white)](https://slack.hotosm.org) [![All Contributors](https://img.shields.io/badge/all-contributors-red.svg?logo=github)](https://github.com/hotosm/fAIr/graphs/contributors) | | **Submodules** | | [![fairpredictor Repo](https://img.shields.io/badge/fairpredictor-GitHub-blue.svg?logo=github)](https://github.com/hotosm/fairpredictor) [![fair-utilities Repo](https://img.shields.io/badge/fair--utilities-GitHub-green.svg?logo=github)](https://github.com/hotosm/fair-utilities) | -| **Other Info** | | [![Roadmap](https://img.shields.io/badge/Roadmap-View%20Plan-blue.svg)](https://github.com/orgs/hotosm/projects/40) [![Figma Design](https://img.shields.io/badge/Figma-View%20Plan-red.svg)](https://www.figma.com/design/1WXme5jfWV4tCaY9Rcaekk/fAIr-Project---UIUX-Team?node-id=2772-7466&p=f) [![Features Plan](https://img.shields.io/badge/Features-Plan-orange.svg)](https://docs.google.com/document/d/1A8kRG-Tw922bV8CTz4HXjCpp7cZmHSpehLOQQF1EXao/edit?usp=sharing) [![License](https://img.shields.io/github/license/hotosm/fair.svg)](https://github.com/hotosm/fair/blob/main/LICENSE.md) | +| **Other Info** | | [![Roadmap](https://img.shields.io/badge/Roadmap-View%20Plan-blue.svg)](https://github.com/orgs/hotosm/projects/40) [![Figma Design](https://img.shields.io/badge/Figma-View%20Plan-red.svg)](https://www.figma.com/design/1WXme5jfWV4tCaY9Rcaekk/fAIr-Project---UIUX-Team?node-id=2772-7466&p=f) [![Features Plan](https://img.shields.io/badge/Features-Plan-orange.svg)](https://docs.google.com/document/d/1A8kRG-Tw922bV8CTz4HXjCpp7cZmHSpehLOQQF1EXao/edit?usp=sharing) [![License](https://img.shields.io/github/license/hotosm/fair.svg)](https://github.com/hotosm/fair/blob/develop/LICENSE) |
From 7d3cd2caef4df89b9136bd1dc6b9082504620ee5 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Fri, 4 Sep 2026 05:09:46 +0100 Subject: [PATCH 02/18] docs: update all refs master --> main --- .github/workflows/backend_build_check.yml | 2 +- .github/workflows/frontend_build_check.yml | 6 +++--- .github/workflows/publish_container_image.yml | 2 +- .github/workflows/test_backend_build.yml | 4 ++-- CONTRIBUTING.md | 2 +- Readme.md => README.md | 4 ++-- docs/User-Manual-for-fAIr.md | 2 +- frontend/src/utils/geo/geometry-utils.ts | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) rename Readme.md => README.md (96%) diff --git a/.github/workflows/backend_build_check.yml b/.github/workflows/backend_build_check.yml index e45e9db5e..115e6720b 100644 --- a/.github/workflows/backend_build_check.yml +++ b/.github/workflows/backend_build_check.yml @@ -2,7 +2,7 @@ name: Build Docker Images on: pull_request: branches: - - master + - main - develop paths: - "backend/**" diff --git a/.github/workflows/frontend_build_check.yml b/.github/workflows/frontend_build_check.yml index 30470d505..900c6f9db 100644 --- a/.github/workflows/frontend_build_check.yml +++ b/.github/workflows/frontend_build_check.yml @@ -2,12 +2,12 @@ name: Frontend Build on: push: - branches: [master, develop] + branches: [main, develop] paths: - "frontend/**" - ".github/workflows/frontend_build.yml" pull_request: - branches: [master, develop] + branches: [main, develop] paths: - "frontend/**" - ".github/workflows/frontend_build.yml" @@ -55,7 +55,7 @@ jobs: run: pnpm run build # Build-only checks for PRs. Push happens in docker_publish_image.yml on - # merge to master/stage/develop. + # merge to main/stage/develop. docker-build-dev: needs: [frontend-unit-test] uses: hotosm/gh-workflows/.github/workflows/image_build.yml@4.0.3 diff --git a/.github/workflows/publish_container_image.yml b/.github/workflows/publish_container_image.yml index 845aceefe..22974aa8a 100644 --- a/.github/workflows/publish_container_image.yml +++ b/.github/workflows/publish_container_image.yml @@ -3,7 +3,7 @@ name: Build and Publish Docker Images on: push: branches: - - master + - main - stage - develop paths-ignore: diff --git a/.github/workflows/test_backend_build.yml b/.github/workflows/test_backend_build.yml index 495dd9c63..0e1c5e317 100644 --- a/.github/workflows/test_backend_build.yml +++ b/.github/workflows/test_backend_build.yml @@ -3,7 +3,7 @@ name: Backend lint and tests on: push: branches: - - master + - main - develop paths: - "backend/**" @@ -11,7 +11,7 @@ on: - ".pre-commit-config.yaml" pull_request: branches: - - master + - main - develop paths: - "backend/**" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 138867bce..babb99ee5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,5 +57,5 @@ Create pull requests (PRs) for changes that you think are needed to the document ## :handshake: Thank you -Thank you very much in advance for your contributions!! Please ensure you refer to our [Code of Conduct](https://github.com/hotosm/fAIr/blob/master/docs/Code-of-Conduct.md). +Thank you very much in advance for your contributions!! Please ensure you refer to our [Code of Conduct](https://github.com/hotosm/fAIr/blob/main/docs/Code-of-Conduct.md). If you've read the guidelines, but you are still not sure how to contribute on Github, please reach out to us via our Slack #geospatial-tech-and-innovation. diff --git a/Readme.md b/README.md similarity index 96% rename from Readme.md rename to README.md index 298b205f7..9364e2d8d 100644 --- a/Readme.md +++ b/README.md @@ -17,7 +17,7 @@
-| **CI/CD** | | [![Backend Build](https://github.com/hotosm/fair/actions/workflows/backend_build.yml/badge.svg?branch=master)](https://github.com/hotosm/fair/actions/workflows/backend_build.yml) [![Frontend Build](https://github.com/hotosm/fair/actions/workflows/frontend_build.yml/badge.svg?branch=master)](https://github.com/hotosm/fair/actions/workflows/frontend_build.yml) | +| **CI/CD** | | [![Backend Build](https://github.com/hotosm/fAIr/actions/workflows/test_backend_build.yml/badge.svg?branch=main)](https://github.com/hotosm/fAIr/actions/workflows/test_backend_build.yml) [![Frontend Build](https://github.com/hotosm/fAIr/actions/workflows/frontend_build_check.yml/badge.svg?branch=main)](https://github.com/hotosm/fAIr/actions/workflows/frontend_build_check.yml) | | :--- | :--- | :--- | | **Tech Stack** | | ![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django) ![React](https://img.shields.io/badge/React-20232A.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![TensorFlow](https://img.shields.io/badge/TensorFlow-FF6F00.svg?style=for-the-badge&logo=TensorFlow&logoColor=white) ![PyTorch](https://img.shields.io/badge/PyTorch-EE4C2C.svg?style=for-the-badge&logo=PyTorch&logoColor=white) | | **Code Style** | | [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://pre-commit.com) [![Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) | @@ -118,7 +118,7 @@ Checkout Docker Installation [docs](./docs/Docker-installation.md) ## Get involved! -- Start by reading our [Code of conduct](https://github.com/hotosm/fAIr/blob/master/docs/Code-of-Conduct.md) +- Start by reading our [Code of conduct](https://github.com/hotosm/fAIr/blob/main/docs/Code-of-Conduct.md) - Get familiar with our [contributor guidelines](CONTRIBUTING.md) explaining the different ways in which you can support this project! We need your help! # Licenses diff --git a/docs/User-Manual-for-fAIr.md b/docs/User-Manual-for-fAIr.md index eb6a982d4..dd4b323de 100644 --- a/docs/User-Manual-for-fAIr.md +++ b/docs/User-Manual-for-fAIr.md @@ -212,7 +212,7 @@ https://github.com/hotosm/fAIr/assets/97789856/f4eae99f-e9a2-4424-806f-fdfce80e6 ## Help and Support If you encounter any issues or need assistance while using fAIr, you can access the following resources: -- Check the [FAQs](https://github.com/hotosm/fAIr/blob/master/docs/FAQ.md) . +- Check the [FAQs](https://github.com/hotosm/fAIr/blob/main/docs/FAQ.md) . - Join our Slack channel. ## Thank you diff --git a/frontend/src/utils/geo/geometry-utils.ts b/frontend/src/utils/geo/geometry-utils.ts index 80d30f7bc..e23fd6912 100644 --- a/frontend/src/utils/geo/geometry-utils.ts +++ b/frontend/src/utils/geo/geometry-utils.ts @@ -70,7 +70,7 @@ export const getGeoJSONFeatureBounds = (geojsonFeature: Feature): BBOX => { return bboxPolygon(geojsonFeature) as BBOX; }; -// Ref - https://github.com/hotosm/fAIr/blob/master/frontend/src/utils.js +// Ref - https://github.com/hotosm/fAIr/blob/main/frontend/src/utils.js /** * Converts degrees to radians. From e60e0949afe8d1ab9e22b0eb0b0a9274b485d631 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Fri, 4 Sep 2026 06:04:41 +0100 Subject: [PATCH 03/18] ci: fix up invalid references to workflows --- .github/workflows/backend_build_check.yml | 2 +- .github/workflows/frontend_build_check.yml | 4 ++-- .github/workflows/publish_container_image.yml | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/backend_build_check.yml b/.github/workflows/backend_build_check.yml index 115e6720b..9eaffe6af 100644 --- a/.github/workflows/backend_build_check.yml +++ b/.github/workflows/backend_build_check.yml @@ -6,7 +6,7 @@ on: - develop paths: - "backend/**" - - ".github/workflows/docker_build.yml" + - ".github/workflows/backend_build_check.yml" jobs: # Backend API image. amd64-only (GDAL + source-built tippecanoe make arm64 diff --git a/.github/workflows/frontend_build_check.yml b/.github/workflows/frontend_build_check.yml index 900c6f9db..413b1e8c8 100644 --- a/.github/workflows/frontend_build_check.yml +++ b/.github/workflows/frontend_build_check.yml @@ -5,12 +5,12 @@ on: branches: [main, develop] paths: - "frontend/**" - - ".github/workflows/frontend_build.yml" + - ".github/workflows/frontend_build_check.yml" pull_request: branches: [main, develop] paths: - "frontend/**" - - ".github/workflows/frontend_build.yml" + - ".github/workflows/frontend_build_check.yml" jobs: frontend-unit-test: diff --git a/.github/workflows/publish_container_image.yml b/.github/workflows/publish_container_image.yml index 22974aa8a..ea3d4c628 100644 --- a/.github/workflows/publish_container_image.yml +++ b/.github/workflows/publish_container_image.yml @@ -7,9 +7,8 @@ on: - stage - develop paths-ignore: - - ".github/workflows/backend_build.yml" - - ".github/workflows/frontend_build.yml" - - ".github/workflows/frontend_build_push.yml" + - ".github/workflows/backend_build_check.yml" + - ".github/workflows/frontend_build_check.yml" release: types: [released] workflow_dispatch: From 9f90047b4958d1c12ad76a6af397abdf6921fcf6 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Fri, 4 Sep 2026 06:06:15 +0100 Subject: [PATCH 04/18] ci: remove husky from frontend --- frontend/.husky/pre-commit | 3 --- frontend/package.json | 4 +--- frontend/pnpm-lock.yaml | 10 ---------- 3 files changed, 1 insertion(+), 16 deletions(-) delete mode 100755 frontend/.husky/pre-commit diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit deleted file mode 100755 index 5ecd813c5..000000000 --- a/frontend/.husky/pre-commit +++ /dev/null @@ -1,3 +0,0 @@ -cd frontend -pnpm format -pnpm build \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 8b3fd529d..8f2cfe623 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,8 +11,7 @@ "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", "test": "vitest", - "coverage": "vitest run --coverage", - "prepare": "cd .. && husky frontend/.husky" + "coverage": "vitest run --coverage" }, "dependencies": { "@hookform/resolvers": "^5.4.0", @@ -79,7 +78,6 @@ "eslint-plugin-react-refresh": "^0.4.12", "eslint-plugin-tailwindcss": "^3.17.5", "globals": "^15.10.0", - "husky": "^9.1.7", "jsdom": "^26.0.0", "postcss": "^8.4.47", "prettier": "3.3.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7aa82a6cf..3cef6693e 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -199,9 +199,6 @@ importers: globals: specifier: ^15.10.0 version: 15.10.0 - husky: - specifier: ^9.1.7 - version: 9.1.7 jsdom: specifier: ^26.0.0 version: 26.0.0 @@ -2206,11 +2203,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5923,8 +5915,6 @@ snapshots: transitivePeerDependencies: - supports-color - husky@9.1.7: {} - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 From 886fa9ea8798470a12b943cc939adbfe36e708ef Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Fri, 4 Sep 2026 06:21:30 +0100 Subject: [PATCH 05/18] ci: run oxfmt over whole codebase - styling --- .github/ISSUE_TEMPLATE/bug_report.md | 13 +- .github/ISSUE_TEMPLATE/feature_request.md | 5 +- .github/workflows/deploy_frontend_s3.yml | 6 +- .github/workflows/test_backend_build.yml | 4 +- .pre-commit-config.yaml | 13 ++ CONTRIBUTING.md | 2 +- backend/ARCHITECTURE.md | 87 ++++---- backend/README.md | 198 +++++++++--------- backend/pyproject.toml | 90 ++++---- docker-compose.dev.yml | 8 +- docker-compose.hotreload.yml | 3 +- docker-compose.yml | 8 +- docs/About.md | 7 +- docs/Code-of-Conduct.md | 28 ++- docs/Docker-installation.md | 24 +-- docs/FAQ.md | 1 + docs/Home.md | 11 +- docs/Infra.md | 5 + docs/Release.md | 2 +- docs/User-Manual-for-fAIr.md | 130 +++--------- docs/assets/fair-updates.json | 12 +- docs/decisions/infra/0001-mlops.md | 32 ++- docs/deployment.md | 61 +++--- docs/infra/dev.md | 10 +- frontend/README.md | 4 +- frontend/eslint.config.js | 44 ++-- frontend/index.html | 48 ++--- frontend/package.json | 4 +- frontend/pnpm-workspace.yaml | 6 +- frontend/prettier.config.mjs | 2 +- frontend/src/app/index.tsx | 6 +- frontend/src/app/providers/auth-provider.tsx | 23 +- .../src/app/providers/models-provider.tsx | 64 ++---- frontend/src/app/providers/tour-provider.tsx | 16 +- frontend/src/app/router.tsx | 123 ++++------- frontend/src/app/routes/about.tsx | 4 +- frontend/src/app/routes/ai-predictions.tsx | 21 +- .../app/routes/datasets/dataset-detail.tsx | 17 +- .../src/app/routes/datasets/dataset-list.tsx | 6 +- frontend/src/app/routes/learn.tsx | 33 +-- .../src/app/routes/models/confirmation.tsx | 21 +- frontend/src/app/routes/models/feedbacks.tsx | 12 +- .../app/routes/models/model-details-card.tsx | 21 +- .../src/app/routes/models/models-list.tsx | 85 ++------ frontend/src/app/routes/not-found.tsx | 10 +- frontend/src/app/routes/profile/datasets.tsx | 15 +- .../routes/profile/offline-predictions.tsx | 53 +---- frontend/src/app/routes/profile/overview.tsx | 5 +- frontend/src/app/routes/profile/settings.tsx | 102 +++------ frontend/src/app/routes/resources.tsx | 4 +- frontend/src/app/routes/start-mapping.tsx | 158 ++++---------- frontend/src/app/routes/try-fair.tsx | 77 ++----- .../learn/video-player-modal.test.tsx | 4 +- .../components/errors/under-construction.tsx | 5 +- .../landing/about-fair/about-fair.tsx | 4 +- .../core-features/core-features.module.css | 11 +- .../landing/core-values/core-values.tsx | 32 +-- .../src/components/landing/cta/cta.module.css | 6 +- frontend/src/components/landing/kpi/kpi.tsx | 16 +- .../components/landing/tagline/tagline.tsx | 4 +- .../src/components/layouts/footer/footer.tsx | 14 +- .../components/layouts/model-forms-layout.tsx | 39 +--- .../components/layouts/navbar/nav-logo.tsx | 12 +- .../src/components/layouts/navbar/navbar.tsx | 59 ++---- .../layouts/navbar/user-profile.tsx | 12 +- .../src/components/layouts/root-layout.tsx | 4 +- .../layouts/user-profile-layout.tsx | 5 +- frontend/src/components/learn/guide-card.tsx | 15 +- .../components/learn/learn-course-card.tsx | 26 +-- frontend/src/components/learn/update-card.tsx | 14 +- .../components/learn/update-video-player.tsx | 4 +- .../map/controls/fit-to-bounds-control.tsx | 16 +- .../components/map/controls/layer-control.tsx | 18 +- .../components/map/controls/zoom-control.tsx | 5 +- .../components/map/layers/google-basemap.tsx | 5 +- .../components/map/layers/tile-boundaries.tsx | 12 +- frontend/src/components/map/map.tsx | 12 +- .../components/map/setups/setup-terra-draw.ts | 10 +- frontend/src/components/seo/head.tsx | 4 +- .../components/shared/dataset-explorer.tsx | 20 +- .../shared/fair-process/fair-process.tsx | 19 +- frontend/src/components/shared/faqs/faqs.tsx | 10 +- .../shared/filters/clear-filters.tsx | 12 +- .../shared/filters/ordering-filter.tsx | 18 +- .../shared/filters/search-filter.tsx | 19 +- .../shared/form/xyz-tile-server-input.tsx | 21 +- .../src/components/shared/hot-tracking.tsx | 9 +- .../src/components/shared/layout-toggle.tsx | 4 +- .../components/shared/modals/delete-modal.tsx | 16 +- .../shared/modals/file-upload-dialog.tsx | 63 ++---- .../src/components/shared/model-explorer.tsx | 81 ++----- frontend/src/components/shared/pagination.tsx | 5 +- .../src/components/shared/section-header.tsx | 6 +- .../src/components/shared/show-map-toggle.tsx | 4 +- .../src/components/shared/training-logs.tsx | 8 +- .../shared/training-status-badge.tsx | 4 +- .../ui/animated-beam/animated-beam.tsx | 38 +--- .../src/components/ui/button/back-button.tsx | 6 +- frontend/src/components/ui/button/button.tsx | 4 +- .../src/components/ui/carousel/carousel.tsx | 5 +- .../src/components/ui/codeblock/codeblock.tsx | 6 +- frontend/src/components/ui/copy-button.tsx | 4 +- .../components/ui/data-table/data-table.tsx | 5 +- .../components/ui/drawer/mobile-drawer.tsx | 17 +- .../src/components/ui/dropdown/dropdown.tsx | 33 +-- .../ui/form/checkbox-group/checkbox-group.tsx | 8 +- .../ui/form/date-picker/date-picker.tsx | 18 +- .../ui/form/help-text/help-text.tsx | 7 +- .../src/components/ui/form/input/input.tsx | 19 +- .../ui/form/radio-group/radio-group.tsx | 9 +- .../src/components/ui/form/select/select.tsx | 4 +- .../src/components/ui/form/switch/switch.tsx | 6 +- .../ui/form/text-area/text-area.tsx | 11 +- .../ui/icons/advanced-guage-icon.tsx | 7 +- .../components/ui/icons/arrow-back-icon.tsx | 7 +- .../components/ui/icons/arrow-move-icon.tsx | 7 +- .../components/ui/icons/basic-guage-icon.tsx | 7 +- .../ui/icons/book-template-icon.tsx | 7 +- frontend/src/components/ui/icons/bot-icon.tsx | 7 +- .../src/components/ui/icons/calender-icon.tsx | 11 +- .../src/components/ui/icons/category-icon.tsx | 7 +- .../components/ui/icons/chat-bubble-icon.tsx | 7 +- .../src/components/ui/icons/check-icon.tsx | 7 +- .../components/ui/icons/chevron-down-icon.tsx | 11 +- .../ui/icons/choose-imagery-icon.tsx | 15 +- .../components/ui/icons/clipboard-icon.tsx | 7 +- .../ui/icons/cloud-download-icon.tsx | 7 +- .../src/components/ui/icons/cloud-icon.tsx | 7 +- .../src/components/ui/icons/database-icon.tsx | 7 +- .../src/components/ui/icons/delete-icon.tsx | 7 +- .../ui/icons/desktop-cursor-icon.tsx | 7 +- .../components/ui/icons/desktop-flow-icon.tsx | 7 +- .../components/ui/icons/direction-icon.tsx | 7 +- .../components/ui/icons/directory-icon.tsx | 7 +- .../src/components/ui/icons/download-icon.tsx | 7 +- .../src/components/ui/icons/draw-icon.tsx | 7 +- .../src/components/ui/icons/elipsis-icon.tsx | 7 +- .../components/ui/icons/empty-grid-icon.tsx | 18 +- .../ui/icons/external-link-icon.tsx | 7 +- .../src/components/ui/icons/file-icon.tsx | 7 +- .../components/ui/icons/file-upload-icon.tsx | 7 +- .../ui/icons/filled-calendar-icon.tsx | 7 +- .../components/ui/icons/filled-flag-icon.tsx | 7 +- .../ui/icons/filled-location-icon.tsx | 7 +- .../src/components/ui/icons/filter-icon.tsx | 7 +- .../components/ui/icons/fullscreen-icon.tsx | 7 +- .../components/ui/icons/geolocation-icon.tsx | 7 +- .../src/components/ui/icons/info-icon.tsx | 7 +- .../ui/icons/intermediate-guage-icon.tsx | 7 +- .../components/ui/icons/layer-stack-icon.tsx | 7 +- .../components/ui/icons/legend-book-icon.tsx | 7 +- .../components/ui/icons/locate-grid-icon.tsx | 9 +- frontend/src/components/ui/icons/map-icon.tsx | 7 +- .../src/components/ui/icons/maximize-icon.tsx | 7 +- .../src/components/ui/icons/minimize-icon.tsx | 7 +- .../ui/icons/no-training-area-icon.tsx | 18 +- .../ui/icons/notification-bell-icon.tsx | 7 +- .../components/ui/icons/parameters-icon.tsx | 7 +- frontend/src/components/ui/icons/pen-icon.tsx | 7 +- .../src/components/ui/icons/people-icon.tsx | 7 +- .../src/components/ui/icons/points-icons.tsx | 16 +- .../src/components/ui/icons/polygon-icon.tsx | 32 +-- .../components/ui/icons/predictions-icon.tsx | 7 +- .../ui/icons/production-checkmark.tsx | 7 +- frontend/src/components/ui/icons/ram-icon.tsx | 7 +- .../src/components/ui/icons/reset-icon.tsx | 7 +- .../src/components/ui/icons/save-icon.tsx | 7 +- .../src/components/ui/icons/settings-icon.tsx | 7 +- .../src/components/ui/icons/shield-icon.tsx | 7 +- .../components/ui/icons/snow-flake-icon.tsx | 7 +- .../src/components/ui/icons/sort-icon.tsx | 7 +- .../ui/icons/square-shadow-icon.tsx | 7 +- .../src/components/ui/icons/star-icon.tsx | 7 +- .../components/ui/icons/star-stack-icon.tsx | 7 +- frontend/src/components/ui/icons/tag-icon.tsx | 7 +- .../components/ui/icons/tags-info-icon.tsx | 7 +- .../src/components/ui/icons/text-icon.tsx | 7 +- .../icons/try-fair-prediction-output-icon.tsx | 5 +- .../components/ui/icons/upload-arrow-icon.tsx | 7 +- .../src/components/ui/icons/upload-icon.tsx | 7 +- .../components/ui/icons/wavy-check-icon.tsx | 4 +- .../ui/icons/youtube-play-circle-icon.tsx | 7 +- .../components/ui/icons/youtube-play-icon.tsx | 7 +- .../src/components/ui/icons/zoom-in-icon.tsx | 7 +- frontend/src/components/ui/image/image.tsx | 4 +- frontend/src/components/ui/navbar/navbar.tsx | 9 +- .../src/components/ui/spinner/spinner.tsx | 4 +- frontend/src/components/ui/tab-group.tsx | 5 +- frontend/src/components/ui/table/table.tsx | 76 +++---- .../src/components/ui/tooltip/tooltip.tsx | 8 +- frontend/src/config/__tests__/config.test.ts | 4 +- frontend/src/config/env.ts | 42 ++-- frontend/src/config/index.ts | 125 ++++------- frontend/src/constants/site-tour.ts | 22 +- frontend/src/constants/toast-notifications.ts | 6 +- .../src/constants/ui-contents/auth-content.ts | 3 +- .../constants/ui-contents/learn-content.ts | 6 +- .../constants/ui-contents/models-content.ts | 39 ++-- .../ui-contents/try-fair-contents.ts | 3 +- .../ui-contents/user-profile-content.ts | 9 +- .../features/ai-predictions/api/factory.ts | 5 +- .../components/ai-prediction-card.tsx | 9 +- .../ai-prediction-detail-dialog.tsx | 23 +- .../components/ai-prediction-details-info.tsx | 4 +- .../components/ai-predictions-filters.tsx | 31 +-- .../components/ai-predictions-grid.tsx | 9 +- .../components/ai-predictions-list.tsx | 24 +-- .../components/ai-predictions-map.tsx | 38 +--- .../components/ai-predictions-table.tsx | 15 +- .../hooks/use-ai-predictions.tsx | 7 +- frontend/src/features/datasets/api/factory.ts | 18 +- .../src/features/datasets/api/get-datasets.ts | 8 +- .../datasets/components/dataset-area-map.tsx | 29 +-- .../datasets/components/dataset-card.tsx | 4 +- .../components/dataset-list-skeleton.tsx | 5 +- .../datasets/components/datasets-map.tsx | 38 +--- .../dialogs/dataset-details-edit-dialog.tsx | 11 +- .../drawers/dataset-aoi-edit-drawer.tsx | 7 +- .../features/datasets/hooks/use-datasets.ts | 8 +- .../datasets/hooks/use-query-params.ts | 42 ++-- .../mapswipe/api/mapswipe-projects.ts | 4 +- .../components/project-creation-dialog.tsx | 27 +-- .../components/project-results-map.tsx | 10 +- .../components/project-status-badge.tsx | 4 +- .../components/project-status-dialog.tsx | 56 ++--- .../components/project-success-dialog.tsx | 10 +- .../mapswipe/hooks/use-mapswipe-project.ts | 7 +- .../model-creation/api/create-trainings.ts | 11 +- .../model-creation/api/delete-trainings.ts | 4 +- .../features/model-creation/api/factory.ts | 15 +- .../model-creation/api/get-trainings.ts | 24 +-- .../dialogs/training-area-tour-dialog.tsx | 30 +-- .../map-layers/mask-bounds-layer.tsx | 14 +- .../model-details/model-description-input.tsx | 35 +--- .../model-details/model-details.tsx | 38 +--- .../model-details/model-name-input.tsx | 27 +-- .../components/model-summary.tsx | 7 +- .../components/progress-bar.tsx | 15 +- .../components/progress-buttons.tsx | 19 +- .../components/step-heading.tsx | 16 +- .../training-area/open-area-map.tsx | 10 +- .../training-area/training-area-item.tsx | 74 ++----- .../training-area/training-area-list.tsx | 16 +- .../training-area/training-area-map.tsx | 41 ++-- .../training-area/training-area.tsx | 28 +-- .../training-area/training-labels-offset.tsx | 70 ++----- .../training-dataset/create-new.tsx | 39 +--- .../training-dataset/select-existing.tsx | 15 +- .../training-dataset-form.tsx | 63 ++---- .../training-dataset/training-dataset.tsx | 14 +- .../training-settings-form.tsx | 97 ++------- .../training-settings/training-settings.tsx | 4 +- .../model-creation/hooks/use-models.ts | 18 +- .../hooks/use-training-areas.ts | 36 +--- .../hooks/use-training-datasets.ts | 10 +- frontend/src/features/models/api/factory.ts | 23 +- .../src/features/models/api/get-trainings.ts | 16 +- .../features/models/api/update-trainings.ts | 15 +- .../models/components/accuracy-display.tsx | 15 +- .../dialogs/mobile-filters-dialog.tsx | 21 +- .../dialogs/model-details-update-dialog.tsx | 9 +- .../dialogs/model-enhancement-dialog.tsx | 27 +-- .../components/dialogs/model-files-dialog.tsx | 15 +- .../dialogs/training-details-dialog.tsx | 6 +- .../dialogs/training-settings-dialog.tsx | 50 +---- .../models/components/directory-tree.tsx | 58 +---- .../components/filters/category-filter.tsx | 6 +- .../components/filters/date-range-filter.tsx | 20 +- .../components/filters/status-filter.tsx | 4 +- .../components/maps/feedbacks-layer.tsx | 18 +- .../models/components/maps/feedbacks-map.tsx | 12 +- .../models/components/maps/models-map.tsx | 38 +--- .../components/maps/training-area-map.tsx | 29 +-- .../features/models/components/model-card.tsx | 18 +- .../models/components/model-detail-item.tsx | 8 +- .../models/components/model-detail-user.tsx | 8 +- .../models/components/model-details-info.tsx | 39 +--- .../components/model-details-properties.tsx | 116 +++------- .../components/model-details-section.tsx | 8 +- .../models/components/model-feedbacks.tsx | 4 +- .../models/components/model-not-found.tsx | 48 +---- .../components/training-area-drawer.tsx | 12 +- .../components/training-history-table.tsx | 77 ++----- .../src/features/models/hooks/use-models.ts | 24 +-- .../src/features/models/hooks/use-training.ts | 10 +- frontend/src/features/models/layouts/grid.tsx | 10 +- .../src/features/models/layouts/table.tsx | 18 +- .../offline-prediction-request-dialog.tsx | 40 +--- ...line-prediction-request-success-dialog.tsx | 10 +- .../components/header/header.tsx | 35 +--- .../components/header/logo-with-dropdown.tsx | 6 +- .../components/header/model-action.tsx | 30 +-- .../header/model-details-info-button.tsx | 10 +- .../components/header/model-details-info.tsx | 24 +-- .../header/model-predictions-tracker.tsx | 8 +- .../map/layers/all-prediction-layer.tsx | 9 +- .../map/layers/prediction-raster-layer.tsx | 6 +- .../components/map/legend-control.tsx | 45 +--- .../start-mapping/components/map/map.tsx | 45 +--- .../map/predicted-feature-popup.tsx | 66 ++---- .../components/mobile-drawer.tsx | 37 +--- .../components/model-settings.tsx | 57 ++--- ...imagery-source-selector-trigger-button.tsx | 4 +- .../imagery-source-selector.tsx | 30 +-- .../model-selector-trigger-button.tsx | 24 +-- .../replicable-models/model-selector.tsx | 19 +- .../start-mapping/hooks/use-feedbacks.ts | 7 +- .../hooks/use-model-predictions.ts | 7 +- .../features/try-fair/api/features-to-map.ts | 12 +- .../src/features/try-fair/api/hot-imagery.ts | 47 ++--- .../features/try-fair/api/map-large-area.ts | 5 +- frontend/src/features/try-fair/api/stac.ts | 8 +- .../dropdowns/feature-to-map-dropdown.tsx | 45 +--- .../__tests__/choose-imagery-source.test.tsx | 25 +-- .../__tests__/custom-imagery-form.test.tsx | 7 +- .../__tests__/imagery-location-modal.test.tsx | 34 +-- .../__tests__/imagery-search-panel.test.tsx | 8 +- .../__tests__/location-search.test.tsx | 52 +---- .../__tests__/oam-imagery-map.test.tsx | 10 +- .../imagery/choose-imagery-source.tsx | 4 +- .../imagery/custom-imagery-form.tsx | 13 +- .../imagery/imagery-location-modal.tsx | 22 +- .../imagery/imagery-modal-map.layers.ts | 45 +--- .../imagery/imagery-search-panel.tsx | 38 +--- .../components/imagery/location-search.tsx | 9 +- .../components/imagery/oam-imagery-map.tsx | 7 +- .../components/map/chloropleth-legend.tsx | 7 +- .../components/map/draggable-grid.tsx | 18 +- .../components/map/grid-off-screen-nudge.tsx | 5 +- .../map/try-fair-download-button.tsx | 38 +--- .../components/map/try-fair-layer-control.tsx | 45 +--- .../try-fair/components/map/try-fair-map.tsx | 9 +- .../map/try-fair-prediction-results.tsx | 21 +- .../components/modals/share-project-modal.tsx | 8 +- .../components/modals/sign-in-prompt.tsx | 4 +- .../components/model-picker-modal.tsx | 44 ++-- .../model-picker/feature-to-map-list.tsx | 12 +- .../model-picker/imagery-preview-card.tsx | 8 +- .../model-picker/model-picker-badges.tsx | 6 +- .../model-picker/recent-imageries-list.tsx | 28 +-- .../start-mapping/export-map-results.tsx | 31 +-- .../start-mapping/map-large-area-modal.tsx | 13 +- .../map-large-area-request-success-dialog.tsx | 11 +- .../components/try-fair-nav-links.tsx | 8 +- .../try-fair/components/try-fair-sidebar.tsx | 38 +--- .../use-imagery-mapping-model.test.ts | 5 +- .../__tests__/use-imagery-modal-map.test.ts | 9 +- .../__tests__/use-try-fair-params.test.tsx | 19 +- .../try-fair/hooks/use-base-models.tsx | 8 +- .../try-fair/hooks/use-fair-predict.tsx | 16 +- .../features/try-fair/hooks/use-grid-drag.ts | 35 +--- .../hooks/use-grid-screen-geometry.ts | 14 +- .../try-fair/hooks/use-grid-visibility.ts | 16 +- .../try-fair/hooks/use-imagery-country.ts | 16 +- .../hooks/use-imagery-mapping-model.ts | 36 ++-- .../try-fair/hooks/use-map-large-area.ts | 39 ++-- .../features/try-fair/hooks/use-oam-item.ts | 8 +- .../try-fair/hooks/use-recent-imageries.ts | 8 +- .../features/try-fair/hooks/use-tile-grid.ts | 16 +- .../try-fair/hooks/use-try-fair-imagery.ts | 45 ++-- .../try-fair/hooks/use-try-fair-params.tsx | 29 +-- .../try-fair/hooks/use-try-fair-tour.ts | 34 +-- .../src/features/try-fair/utils/common.tsx | 22 +- .../src/features/try-fair/utils/helpers.ts | 40 +--- .../src/features/try-fair/utils/models.ts | 7 +- .../try-fair/utils/start-mapping-store.ts | 3 +- .../src/features/try-fair/utils/tile-math.ts | 21 +- .../user-profile/api/offline-predictions.ts | 5 +- .../features/user-profile/api/predictions.ts | 17 +- .../notifications/notification-item.tsx | 15 +- .../notifications/notifications-panel.tsx | 25 +-- .../notifications/user-notifications.tsx | 20 +- .../mapswipe-project-active.tsx | 16 +- .../offline-prediction-card.tsx | 20 +- .../offline-prediction-list-skeleton.tsx | 5 +- .../offline-predictions-actions.tsx | 32 ++- .../offline-predictions-table.tsx | 20 +- .../predictions-results-drawer.tsx | 9 +- .../publish-prediction-flow.tsx | 4 +- .../components/profile-navigation-tabs.tsx | 38 ++-- .../components/profile-overview.tsx | 15 +- .../components/profile-section-header.tsx | 4 +- .../components/profile-statistics.tsx | 3 +- .../components/training-logs-dialog.tsx | 6 +- .../components/user-training-history.tsx | 4 +- .../user-profile/hooks/use-notifications.ts | 16 +- .../user-profile/hooks/use-predictions.ts | 42 ++-- .../user-profile/hooks/use-user-profile.ts | 8 +- .../src/hooks/__tests__/use-login.test.ts | 9 +- frontend/src/hooks/use-break-point.ts | 9 +- frontend/src/hooks/use-browser-type.ts | 4 +- frontend/src/hooks/use-history.ts | 5 +- frontend/src/hooks/use-layer-order.ts | 5 +- frontend/src/hooks/use-map-instance.ts | 9 +- frontend/src/hooks/use-map-layer.ts | 14 +- frontend/src/hooks/use-tileservice.ts | 45 +--- frontend/src/hooks/use-toast-notification.ts | 7 +- frontend/src/hooks/use-tooltip-visibility.ts | 5 +- frontend/src/services/__tests__/auth.test.ts | 28 +-- frontend/src/services/api-routes.ts | 28 +-- frontend/src/services/react-query.ts | 17 +- frontend/src/store/model-prediction-store.ts | 34 ++- frontend/src/types/common.ts | 5 +- .../src/utils/__tests__/date-utils.test.ts | 17 +- .../__tests__/geo/geometry-utils.test.ts | 30 +-- .../src/utils/__tests__/regex-utils.test.ts | 80 ++----- frontend/src/utils/date-utils.ts | 6 +- frontend/src/utils/general-utils.ts | 32 +-- frontend/src/utils/geo/geo-utils.ts | 14 +- frontend/src/utils/geo/geojson-to-osm.ts | 12 +- frontend/src/utils/geo/geometry-utils.ts | 55 ++--- frontend/src/utils/geo/map-utils.ts | 10 +- frontend/src/utils/number-utils.ts | 3 +- frontend/src/utils/regex-utils.ts | 7 +- frontend/src/utils/string-utils.ts | 6 +- frontend/src/vite-env.d.ts | 10 +- frontend/test-setup.ts | 8 +- frontend/tsconfig.app.json | 2 +- frontend/vite.config.mts | 8 +- infra/systemd/Readme.md | 2 +- 420 files changed, 2122 insertions(+), 6025 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d1ba2bc04..75801b9c9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- **Describe the bug** @@ -12,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,9 +24,10 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491ef..d883b8f2d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,9 @@ --- name: Feature request about: Suggest an idea for this project -title: '' +title: "" labels: enhancement -assignees: '' - +assignees: "" --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/deploy_frontend_s3.yml b/.github/workflows/deploy_frontend_s3.yml index 3b4368d1c..3e7e6da52 100644 --- a/.github/workflows/deploy_frontend_s3.yml +++ b/.github/workflows/deploy_frontend_s3.yml @@ -5,8 +5,8 @@ on: branches: - develop paths: - - 'frontend/**' - - '.github/workflows/deploy_frontend_s3.yml' + - "frontend/**" + - ".github/workflows/deploy_frontend_s3.yml" workflow_dispatch: permissions: @@ -79,4 +79,4 @@ jobs: DISTRIBUTION_ID=$(aws cloudfront list-distributions --query "DistributionList.Items[?Aliases.Items && contains(Aliases.Items, 'fair.hotosm.org')].Id | [0]" --output text) if [ "$DISTRIBUTION_ID" != "None" ] && [ -n "$DISTRIBUTION_ID" ]; then aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" --paths "/*" - fi \ No newline at end of file + fi diff --git a/.github/workflows/test_backend_build.yml b/.github/workflows/test_backend_build.yml index 0e1c5e317..cc7f3cad5 100644 --- a/.github/workflows/test_backend_build.yml +++ b/.github/workflows/test_backend_build.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-dependency-glob: "backend/uv.lock" @@ -56,7 +56,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-dependency-glob: "backend/uv.lock" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 547d04d70..0da27fc67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,19 @@ repos: files: ^backend/(pyproject\.toml|uv\.lock)$ pass_filenames: false + # Autoformat: YAML, JSON, Markdown, etc. + - repo: local + hooks: + - id: oxfmt + name: oxfmt + files: ^ + # oxfmt batches it's runs, so using !/path/to/file in args can cause issues + exclude: ^(?:CHANGELOG\.md|frontend/pnpm-lock\.yaml|chart) + entry: oxfmt --no-error-on-unmatched-pattern --write + language: node + "types": [text] + additional_dependencies: ["oxfmt@0.35.0"] + - id: pytest name: pytest (backend) entry: uv run --directory backend pytest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index babb99ee5..2a35d9770 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Before you start contributing code, set up your local development environment: 2. **Set up Docker environment** (recommended): ```bash - docker compose up + docker compose up cp backend/.env.example backend/.env docker compose up api worker ``` diff --git a/backend/ARCHITECTURE.md b/backend/ARCHITECTURE.md index 24763e386..bbe0e372b 100644 --- a/backend/ARCHITECTURE.md +++ b/backend/ARCHITECTURE.md @@ -8,7 +8,7 @@ For the cluster topology, deployment manifests, and ops notes specific to the Ku ## 1. End-to-end user flow -The user makes five calls. +The user makes five calls. ```mermaid flowchart LR @@ -20,19 +20,19 @@ flowchart LR What happens behind each step: -| Step | What the backend does | Output | -| --- | --- | --- | -| **1. AOI** | Validates polygon, stores in postgres. | Row in `datasets_aoi`. | -| **2. Build dataset** | Async worker downloads OAM tiles + OSM labels for the AOI, uploads chips + `labels.geojson` to MinIO, registers a STAC item under `datasets/`. | Built STAC dataset. Poll `GET /datasets/{id}/` until `status=built`. | +| Step | What the backend does | Output | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| **1. AOI** | Validates polygon, stores in postgres. | Row in `datasets_aoi`. | +| **2. Build dataset** | Async worker downloads OAM tiles + OSM labels for the AOI, uploads chips + `labels.geojson` to MinIO, registers a STAC item under `datasets/`. | Built STAC dataset. Poll `GET /datasets/{id}/` until `status=built`. | | **3. Submit training** | Async worker submits a ZenML pipeline. ZenML schedules an orchestrator + step pods on the autoscaling ml pool (`split -> train -> eval -> onnx`). Worker polls ZenML status into the DB every 30s. | Trained `weights.pt` + `model.onnx` in MinIO. Poll `GET /trainings/{id}/` until `status=completed`. Tail with `GET /trainings/runs/{run_id}/logs/`. | -| **4. Promote** | API builds a versioned STAC `local-models/` item from the run's hyperparameters + asset URLs, validates against the base-model's `fair:hyperparameters_spec`, publishes. | `local_model_stac_id`. | -| **5. Predict** | Async worker downloads chips for the requested bbox, submits an inference pipeline, then post-processes the geojson into `.fgb` + `.pmtiles` via tippecanoe. | Three presigned URLs at `GET /predictions/{id}/result/` once `results_ready=true`. | +| **4. Promote** | API builds a versioned STAC `local-models/` item from the run's hyperparameters + asset URLs, validates against the base-model's `fair:hyperparameters_spec`, publishes. | `local_model_stac_id`. | +| **5. Predict** | Async worker downloads chips for the requested bbox, submits an inference pipeline, then post-processes the geojson into `.fgb` + `.pmtiles` via tippecanoe. | Three presigned URLs at `GET /predictions/{id}/result/` once `results_ready=true`. | ### Key invariants - **Datasets, base-models, and local-models are STAC items.** The postgres tables (`datasets_dataset`, `modelregistry_localmodel`) are thin pointers that hold ownership + lifecycle state. Per-version metadata (assets, hyperparameter specs, mlm:training image, etc.) lives only in STAC. - **Training and prediction are async.** The `POST /…/submit/` endpoints return 202 + a row whose `zenml_run_id` is null until the worker actually submits the pipeline. Status flow: `initializing -> submitted -> provisioning -> running -> completed | failed | …`. -- **`results_ready` on a Prediction is a separate flag from `status=completed`.** The worker's `post_run` step generates `.fgb` and `.pmtiles` from the geojson via tippecanoe *after* ZenML reports completion; until that finishes, `/predictions/{id}/result/` returns 409. +- **`results_ready` on a Prediction is a separate flag from `status=completed`.** The worker's `post_run` step generates `.fgb` and `.pmtiles` from the geojson via tippecanoe _after_ ZenML reports completion; until that finishes, `/predictions/{id}/result/` returns 409. - **Publish is the only step that writes a versioned local-model STAC item.** It validates `mlm:hyperparameters` (logged by the training pipeline) against the base-model's `fair:hyperparameters_spec`. Any logged key not declared in the spec causes a 500 — the spec must stay in sync with the keys the training image actually emits. --- @@ -128,6 +128,7 @@ erDiagram datetime last_polled_at } ``` + --- ## 3. API reference @@ -136,38 +137,38 @@ All endpoints live under `/api/v1/`. Writes and owner-scoped reads require `Auth ### Core flow -| Method | Path | Purpose | -| --- | --- | --- | -| GET | `/health/` | Liveness/readiness | -| POST | `/aois/` | Create an AOI polygon (GeoJSON Feature) | -| GET | `/aois/` | List AOIs (bbox-filterable) | -| POST | `/datasets/build/` | Enqueue a dataset build job. `aoi_ids`, `source_imagery` (TMS), `zoom`, `label_tasks`, `label_classes`, `keywords` (allowed: `building`, `road`, `tree`, `water`, `landuse`), `label_type`, `geometry_type` | -| GET | `/datasets/{id}/?expand=stac` | Inspect dataset, with STAC metadata + presigned `chips`/`labels` URLs once `status=built` | -| POST | `/datasets/{id}/{publish,unpublish}/` | Toggle dataset `visibility` (anonymous read) | -| GET | `/local-models/` | List local models (filterable by `status`, `visibility`, `user`) | -| GET | `/local-models/{id}/runs/` | List ZenML pipeline runs that produced this model | -| POST | `/local-models/{id}/{publish,unpublish}/` | Toggle local-model `visibility` (anonymous read) | -| POST | `/trainings/submit/` | Enqueue a finetune. `base_model_stac_id`, `dataset_stac_id`, `model_name`, `overrides` (must match base-model's `fair:hyperparameters_spec`) | -| GET | `/trainings/{id}/` | Run state including `zenml_run_id` | -| GET | `/trainings/runs/{run_id}/status/` | Force-poll ZenML; refreshes the DB row | -| GET | `/trainings/runs/{run_id}/logs/?tail=N&step=name` | Tail orchestrator or step logs | -| POST | `/trainings/runs/{run_id}/cancel/?graceful=true` | Stop a running pipeline | -| POST | `/trainings/{id}/publish/` | Promote a completed run to a versioned local-model. Validates against base-model spec; writes a new STAC item under `local-models/`. Returns `local_model_stac_id` | -| POST | `/predictions/submit/` | Enqueue inference. `model_stac_id`, `image_uri` (TMS), `bbox`, `zoom`, `params`, `remove_osm` | -| GET | `/predictions/{id}/` | Status + assets (presigned) once `results_ready=true` | -| GET | `/predictions/{id}/result/` | Just the three presigned URLs (geojson / fgb / pmtiles); 409 until `results_ready` | -| GET | `/predictions/runs/{run_id}/{status,logs}/` | Same shape as trainings | -| POST | `/predictions/{id}/{publish,unpublish}/` | Toggle prediction `visibility` (anonymous read of result) | +| Method | Path | Purpose | +| ------ | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| GET | `/health/` | Liveness/readiness | +| POST | `/aois/` | Create an AOI polygon (GeoJSON Feature) | +| GET | `/aois/` | List AOIs (bbox-filterable) | +| POST | `/datasets/build/` | Enqueue a dataset build job. `aoi_ids`, `source_imagery` (TMS), `zoom`, `label_tasks`, `label_classes`, `keywords` (allowed: `building`, `road`, `tree`, `water`, `landuse`), `label_type`, `geometry_type` | +| GET | `/datasets/{id}/?expand=stac` | Inspect dataset, with STAC metadata + presigned `chips`/`labels` URLs once `status=built` | +| POST | `/datasets/{id}/{publish,unpublish}/` | Toggle dataset `visibility` (anonymous read) | +| GET | `/local-models/` | List local models (filterable by `status`, `visibility`, `user`) | +| GET | `/local-models/{id}/runs/` | List ZenML pipeline runs that produced this model | +| POST | `/local-models/{id}/{publish,unpublish}/` | Toggle local-model `visibility` (anonymous read) | +| POST | `/trainings/submit/` | Enqueue a finetune. `base_model_stac_id`, `dataset_stac_id`, `model_name`, `overrides` (must match base-model's `fair:hyperparameters_spec`) | +| GET | `/trainings/{id}/` | Run state including `zenml_run_id` | +| GET | `/trainings/runs/{run_id}/status/` | Force-poll ZenML; refreshes the DB row | +| GET | `/trainings/runs/{run_id}/logs/?tail=N&step=name` | Tail orchestrator or step logs | +| POST | `/trainings/runs/{run_id}/cancel/?graceful=true` | Stop a running pipeline | +| POST | `/trainings/{id}/publish/` | Promote a completed run to a versioned local-model. Validates against base-model spec; writes a new STAC item under `local-models/`. Returns `local_model_stac_id` | +| POST | `/predictions/submit/` | Enqueue inference. `model_stac_id`, `image_uri` (TMS), `bbox`, `zoom`, `params`, `remove_osm` | +| GET | `/predictions/{id}/` | Status + assets (presigned) once `results_ready=true` | +| GET | `/predictions/{id}/result/` | Just the three presigned URLs (geojson / fgb / pmtiles); 409 until `results_ready` | +| GET | `/predictions/runs/{run_id}/{status,logs}/` | Same shape as trainings | +| POST | `/predictions/{id}/{publish,unpublish}/` | Toggle prediction `visibility` (anonymous read of result) | ### Schema endpoints -| Method | Path | -| --- | --- | -| GET | `/api/schema/` (raw OpenAPI) | -| GET | `/api/docs/` (Swagger UI) | -| GET | `/api/redoc/` (ReDoc) | +| Method | Path | +| ------ | ---------------------------- | +| GET | `/api/schema/` (raw OpenAPI) | +| GET | `/api/docs/` (Swagger UI) | +| GET | `/api/redoc/` (ReDoc) | -### User test +### User test Send these one at a time from Swagger (`/api/docs/`) or any HTTP client. Every request needs `Authorization: Bearer `. Substitute the IDs returned from each step into the next. @@ -178,11 +179,15 @@ Send these one at a time from Swagger (`/api/docs/`) or any HTTP client. Every r "type": "Feature", "geometry": { "type": "Polygon", - "coordinates": [[ - [85.51678, 27.63133], [85.52323, 27.63133], - [85.52323, 27.63743], [85.51678, 27.63743], - [85.51678, 27.63133] - ]] + "coordinates": [ + [ + [85.51678, 27.63133], + [85.52323, 27.63133], + [85.52323, 27.63743], + [85.51678, 27.63743], + [85.51678, 27.63133] + ] + ] }, "properties": { "dataset": null } } diff --git a/backend/README.md b/backend/README.md index 52b07e5bf..2d3f41e33 100644 --- a/backend/README.md +++ b/backend/README.md @@ -18,10 +18,10 @@ run Django outside it: cd .. docker compose up -d postgres minio stac mlflow zenml cd backend -just setup -cp env_example .env +just setup +cp env_example .env just migrate -just run +just run just worker tasks ``` @@ -37,19 +37,19 @@ OpenAPI schema at `/api/schema/`, Swagger UI at `/api/docs/`, ReDoc at `/api/red ### Core Django -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `DEBUG` | no | `false` | Django debug mode. In prod (`false`), `SECRET_KEY` must be at least 32 chars and not contain `dev` or `unsafe`. | -| `SECRET_KEY` | yes | (none) | Django secret. Strict length/strength check when `DEBUG=false`. | -| `DATABASE_URL` | yes | (none) | Postgres URL. Scheme must be `postgres`, `postgresql`, or `postgis`. | -| `DATABASE_SSL_MODE` | no | `null` | psycopg `sslmode`. Set to `require` for hosted Postgres. | -| `ALLOWED_HOSTS` | no | `[]` | Comma-separated list. | -| `CSRF_TRUSTED_ORIGINS` | no | `[]` | Comma-separated list. | -| `CORS_ALLOWED_ORIGINS` | no | `[]` | Comma-separated list. | -| `SECURE_SSL_REDIRECT` | no | `true` | Force HTTPS redirect at the Django layer. | -| `FRONTEND_URL` | yes | (none) | Public URL of the SPA. Used in emails and CORS. | -| `API_BASE_URL` | yes | (none) | Public URL of this backend (e.g. `http://localhost:8000/api/v1`). | -| `HOSTNAME` | no | `127.0.0.1` | Used by the OpenAPI server URL. | +| Name | Required | Default | Description | +| ---------------------- | -------- | ----------- | --------------------------------------------------------------------------------------------------------------- | +| `DEBUG` | no | `false` | Django debug mode. In prod (`false`), `SECRET_KEY` must be at least 32 chars and not contain `dev` or `unsafe`. | +| `SECRET_KEY` | yes | (none) | Django secret. Strict length/strength check when `DEBUG=false`. | +| `DATABASE_URL` | yes | (none) | Postgres URL. Scheme must be `postgres`, `postgresql`, or `postgis`. | +| `DATABASE_SSL_MODE` | no | `null` | psycopg `sslmode`. Set to `require` for hosted Postgres. | +| `ALLOWED_HOSTS` | no | `[]` | Comma-separated list. | +| `CSRF_TRUSTED_ORIGINS` | no | `[]` | Comma-separated list. | +| `CORS_ALLOWED_ORIGINS` | no | `[]` | Comma-separated list. | +| `SECURE_SSL_REDIRECT` | no | `true` | Force HTTPS redirect at the Django layer. | +| `FRONTEND_URL` | yes | (none) | Public URL of the SPA. Used in emails and CORS. | +| `API_BASE_URL` | yes | (none) | Public URL of this backend (e.g. `http://localhost:8000/api/v1`). | +| `HOSTNAME` | no | `127.0.0.1` | Used by the OpenAPI server URL. | ### Authentication @@ -57,19 +57,19 @@ OpenAPI schema at `/api/schema/`, Swagger UI at `/api/docs/`, ReDoc at `/api/red `GET` on datasets, local-models, and predictions is open to anonymous callers for rows with `visibility="public"`. Owner-scoped lifecycle data (AOIs, trainings, feedback, notifications) and every write require Bearer auth. -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `AUTH_PROVIDER` | no | `hanko` | One of `hanko`, `dev`. | -| `FAIR_DEV_TOKEN` | when `AUTH_PROVIDER=dev` | `null` | Static dev token. Generate with `openssl rand -hex 32`. Never commit. | -| `HANKO_API_URL` | when `AUTH_PROVIDER=hanko` | `null` | Hanko backend URL. | -| `COOKIE_SECRET` | when `AUTH_PROVIDER=hanko` | `null` | Used to verify Hanko-signed cookies. | -| `COOKIE_DOMAIN` | no | `null` | Cookie scope domain. | -| `COOKIE_SECURE` | no | `null` | Force `Secure` cookie flag. | -| `JWT_AUDIENCE` | no | `null` | Expected `aud` claim. | -| `LOGIN_URL` | no | `https://login.hotosm.org` | HOT login portal URL. | -| `LOGIN_INTERNAL_API_KEY` | no | `null` | Server-to-server key against the login portal. | -| `LOGIN_BACKEND_URL` | no | `null` | Login portal backend URL. | -| `OSM_LOGIN_REDIRECT_URI` | no | `null` | Required only when Hanko's "connect existing OSM account" flow is enabled. | +| Name | Required | Default | Description | +| ------------------------ | -------------------------- | -------------------------- | -------------------------------------------------------------------------- | +| `AUTH_PROVIDER` | no | `hanko` | One of `hanko`, `dev`. | +| `FAIR_DEV_TOKEN` | when `AUTH_PROVIDER=dev` | `null` | Static dev token. Generate with `openssl rand -hex 32`. Never commit. | +| `HANKO_API_URL` | when `AUTH_PROVIDER=hanko` | `null` | Hanko backend URL. | +| `COOKIE_SECRET` | when `AUTH_PROVIDER=hanko` | `null` | Used to verify Hanko-signed cookies. | +| `COOKIE_DOMAIN` | no | `null` | Cookie scope domain. | +| `COOKIE_SECURE` | no | `null` | Force `Secure` cookie flag. | +| `JWT_AUDIENCE` | no | `null` | Expected `aud` claim. | +| `LOGIN_URL` | no | `https://login.hotosm.org` | HOT login portal URL. | +| `LOGIN_INTERNAL_API_KEY` | no | `null` | Server-to-server key against the login portal. | +| `LOGIN_BACKEND_URL` | no | `null` | Login portal backend URL. | +| `OSM_LOGIN_REDIRECT_URI` | no | `null` | Required only when Hanko's "connect existing OSM account" flow is enabled. | ### fair-py-ops (ZenML + STAC) @@ -77,105 +77,105 @@ OpenAPI schema at `/api/schema/`, Swagger UI at `/api/docs/`, ReDoc at `/api/red the `zenml` library itself when it opens a connection, so both sets point at the same server. Authenticate with either an API key or a username and password. -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `FAIR_ZENML_STORE_URL` | yes (at runtime) | `null` | URL of the deployed ZenML server. Optional at boot, raises loud at first call site. | -| `FAIR_STAC_API_URL` | yes (at runtime) | `null` | URL of the STAC API root (eoapi-stac-fastapi). Trailing slashes are stripped. | -| `FAIR_STAC_API_KEY` | prod | `null` | Bearer token for the STAC Transactions extension. | -| `ZENML_STORE_URL` | yes (at runtime) | `null` | Same server as `FAIR_ZENML_STORE_URL`. | -| `ZENML_STORE_API_KEY` | one of the two | `null` | Service-account key. Mint with `zenml service-account create fair-backend`. | -| `ZENML_STORE_USERNAME` | one of the two | `null` | Username, paired with `ZENML_STORE_PASSWORD`. The compose stack's default user is `default` with an empty password. | -| `ZENML_STORE_PASSWORD` | with username | `null` | Password for the above. | +| Name | Required | Default | Description | +| ---------------------- | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------- | +| `FAIR_ZENML_STORE_URL` | yes (at runtime) | `null` | URL of the deployed ZenML server. Optional at boot, raises loud at first call site. | +| `FAIR_STAC_API_URL` | yes (at runtime) | `null` | URL of the STAC API root (eoapi-stac-fastapi). Trailing slashes are stripped. | +| `FAIR_STAC_API_KEY` | prod | `null` | Bearer token for the STAC Transactions extension. | +| `ZENML_STORE_URL` | yes (at runtime) | `null` | Same server as `FAIR_ZENML_STORE_URL`. | +| `ZENML_STORE_API_KEY` | one of the two | `null` | Service-account key. Mint with `zenml service-account create fair-backend`. | +| `ZENML_STORE_USERNAME` | one of the two | `null` | Username, paired with `ZENML_STORE_PASSWORD`. The compose stack's default user is `default` with an empty password. | +| `ZENML_STORE_PASSWORD` | with username | `null` | Password for the above. | ### Object storage (S3 / MinIO) -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `BUCKET_NAME` | yes (at runtime) | `null` | S3 / MinIO bucket. | -| `PARENT_BUCKET_FOLDER` | no | `dev` | Per-environment prefix inside the bucket. | -| `AWS_REGION` | no | `us-east-1` | S3 region. | -| `AWS_ACCESS_KEY_ID` | yes (at runtime) | `null` | | -| `AWS_SECRET_ACCESS_KEY` | yes (at runtime) | `null` | | -| `AWS_ENDPOINT_URL` | no | `null` | Set for non-AWS S3 (MinIO, Cloudflare R2, etc.). | -| `PRESIGNED_URL_EXPIRY` | no | `900` | Presigned-URL TTL in seconds. | +| Name | Required | Default | Description | +| ----------------------- | ---------------- | ----------- | ------------------------------------------------ | +| `BUCKET_NAME` | yes (at runtime) | `null` | S3 / MinIO bucket. | +| `PARENT_BUCKET_FOLDER` | no | `dev` | Per-environment prefix inside the bucket. | +| `AWS_REGION` | no | `us-east-1` | S3 region. | +| `AWS_ACCESS_KEY_ID` | yes (at runtime) | `null` | | +| `AWS_SECRET_ACCESS_KEY` | yes (at runtime) | `null` | | +| `AWS_ENDPOINT_URL` | no | `null` | Set for non-AWS S3 (MinIO, Cloudflare R2, etc.). | +| `PRESIGNED_URL_EXPIRY` | no | `900` | Presigned-URL TTL in seconds. | ### Rate limits + database pool DRF throttle scopes use `/`, e.g. `1000/h`. Use the Django-native pool OR PgBouncer, not both. -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `USER_RATE_LIMIT` | no | `1000/h` | Authenticated requests. | -| `ANON_RATE_LIMIT` | no | `100/h` | Anonymous requests. | -| `TRAINING_RATE_LIMIT` | no | `10/h` | Training submission throttle. | -| `PREDICTION_RATE_LIMIT` | no | `50/h` | Prediction submission throttle. | -| `DB_POOL_MIN_SIZE` | no | `4` | Django 5.1+ native PostgreSQL connection pool minimum. | -| `DB_POOL_MAX_SIZE` | no | `20` | Pool maximum. | -| `DB_POOL_TIMEOUT` | no | `30` | Seconds to wait for a free connection. | +| Name | Required | Default | Description | +| ----------------------- | -------- | -------- | ------------------------------------------------------ | +| `USER_RATE_LIMIT` | no | `1000/h` | Authenticated requests. | +| `ANON_RATE_LIMIT` | no | `100/h` | Anonymous requests. | +| `TRAINING_RATE_LIMIT` | no | `10/h` | Training submission throttle. | +| `PREDICTION_RATE_LIMIT` | no | `50/h` | Prediction submission throttle. | +| `DB_POOL_MIN_SIZE` | no | `4` | Django 5.1+ native PostgreSQL connection pool minimum. | +| `DB_POOL_MAX_SIZE` | no | `20` | Pool maximum. | +| `DB_POOL_TIMEOUT` | no | `30` | Seconds to wait for a free connection. | ### OSM raw-data API -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `RAW_DATA_API_URL` | no | `https://api-prod.raw-data.hotosm.org/v1` | HOT Raw Data API root. | +| Name | Required | Default | Description | +| ------------------ | -------- | ----------------------------------------- | ---------------------- | +| `RAW_DATA_API_URL` | no | `https://api-prod.raw-data.hotosm.org/v1` | HOT Raw Data API root. | ### Mapswipe Off by default. When `ENABLE_MAPSWIPE=false`, `POST /api/v1/predictions//mapswipe/` returns 503. The `/api/v1/health/` endpoint reports `mapswipe.reachable` when enabled. -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `ENABLE_MAPSWIPE` | no | `false` | Master toggle. | -| `MAPSWIPE_BACKEND_URL` | no | `null` | Mapswipe backend URL. | -| `MAPSWIPE_MANAGER_URL` | no | `null` | Mapswipe manager URL. | -| `MAPSWIPE_WEB_URL` | no | `null` | Mapswipe web app URL. | -| `MAPSWIPE_CSRFTOKEN_KEY` | when `ENABLE_MAPSWIPE=true` | `null` | CSRF cookie name on the Mapswipe backend. | -| `MAPSWIPE_FB_AUTH_URL` | when `ENABLE_MAPSWIPE=true` | `null` | Firebase auth endpoint. | -| `MAPSWIPE_FB_USERNAME` | when `ENABLE_MAPSWIPE=true` | `null` | Firebase service-account username. | -| `MAPSWIPE_FB_PASSWORD` | when `ENABLE_MAPSWIPE=true` | `null` | Firebase service-account password. | -| `MAPSWIPE_TUTORIAL_ID` | no | `37` | Mapswipe tutorial ID injected into pushed projects. | -| `MAPSWIPE_ORGANIZATION_ID` | no | `4` | Mapswipe organization ID. | -| `MAPSWIPE_VERIFICATION_NUMBER` | no | `3` | Number of crowd verifications required per tile. | -| `MAPSWIPE_POLL_INTERVAL` | no | `10` | Seconds between Mapswipe push-status polls. | -| `MAPSWIPE_POLL_TIMEOUT` | no | `600` | Maximum total seconds to wait for a Mapswipe push. | +| Name | Required | Default | Description | +| ------------------------------ | --------------------------- | ------- | --------------------------------------------------- | +| `ENABLE_MAPSWIPE` | no | `false` | Master toggle. | +| `MAPSWIPE_BACKEND_URL` | no | `null` | Mapswipe backend URL. | +| `MAPSWIPE_MANAGER_URL` | no | `null` | Mapswipe manager URL. | +| `MAPSWIPE_WEB_URL` | no | `null` | Mapswipe web app URL. | +| `MAPSWIPE_CSRFTOKEN_KEY` | when `ENABLE_MAPSWIPE=true` | `null` | CSRF cookie name on the Mapswipe backend. | +| `MAPSWIPE_FB_AUTH_URL` | when `ENABLE_MAPSWIPE=true` | `null` | Firebase auth endpoint. | +| `MAPSWIPE_FB_USERNAME` | when `ENABLE_MAPSWIPE=true` | `null` | Firebase service-account username. | +| `MAPSWIPE_FB_PASSWORD` | when `ENABLE_MAPSWIPE=true` | `null` | Firebase service-account password. | +| `MAPSWIPE_TUTORIAL_ID` | no | `37` | Mapswipe tutorial ID injected into pushed projects. | +| `MAPSWIPE_ORGANIZATION_ID` | no | `4` | Mapswipe organization ID. | +| `MAPSWIPE_VERIFICATION_NUMBER` | no | `3` | Number of crowd verifications required per tile. | +| `MAPSWIPE_POLL_INTERVAL` | no | `10` | Seconds between Mapswipe push-status polls. | +| `MAPSWIPE_POLL_TIMEOUT` | no | `600` | Maximum total seconds to wait for a Mapswipe push. | ### Email Only checked when `DEBUG=false`. -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `EMAIL_HOST` | no | `smtp.gmail.com` | SMTP host. | -| `EMAIL_PORT` | no | `587` | SMTP port. | -| `EMAIL_USE_TLS` | no | `true` | STARTTLS. | -| `EMAIL_USE_SSL` | no | `false` | Implicit TLS (mutually exclusive with `EMAIL_USE_TLS`). | -| `EMAIL_HOST_USER` | no | `""` | SMTP username. | -| `EMAIL_HOST_PASSWORD` | no | `""` | SMTP password. | -| `DEFAULT_FROM_EMAIL` | no | `no-reply@fair.hotosm.org` | `From:` header for outbound mail. | +| Name | Required | Default | Description | +| --------------------- | -------- | -------------------------- | ------------------------------------------------------- | +| `EMAIL_HOST` | no | `smtp.gmail.com` | SMTP host. | +| `EMAIL_PORT` | no | `587` | SMTP port. | +| `EMAIL_USE_TLS` | no | `true` | STARTTLS. | +| `EMAIL_USE_SSL` | no | `false` | Implicit TLS (mutually exclusive with `EMAIL_USE_TLS`). | +| `EMAIL_HOST_USER` | no | `""` | SMTP username. | +| `EMAIL_HOST_PASSWORD` | no | `""` | SMTP password. | +| `DEFAULT_FROM_EMAIL` | no | `no-reply@fair.hotosm.org` | `From:` header for outbound mail. | ### Logging + pagination -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `LOG_PATH` | no | `./logs` | Directory for rotating log files. | -| `DEFAULT_PAGINATION_SIZE` | no | `50` | DRF page size. | -| `SESSION_COOKIE_AGE` | no | `3600` | Session cookie TTL in seconds. | -| `CACHE_TIMEOUT_MINUTES` | no | `5` | Default LocMem cache TTL for non-STAC entries. | -| `LOG_LINE_STREAM_TRUNCATE_VALUE` | no | `10` | Per-step log-line truncation factor for streamed pipeline logs. | +| Name | Required | Default | Description | +| -------------------------------- | -------- | -------- | --------------------------------------------------------------- | +| `LOG_PATH` | no | `./logs` | Directory for rotating log files. | +| `DEFAULT_PAGINATION_SIZE` | no | `50` | DRF page size. | +| `SESSION_COOKIE_AGE` | no | `3600` | Session cookie TTL in seconds. | +| `CACHE_TIMEOUT_MINUTES` | no | `5` | Default LocMem cache TTL for non-STAC entries. | +| `LOG_LINE_STREAM_TRUNCATE_VALUE` | no | `10` | Per-step log-line truncation factor for streamed pipeline logs. | ### Operational tuning All have safe defaults; only set to override. -| Name | Required | Default | Description | -|------|----------|---------|-------------| -| `HEALTH_PROBE_TIMEOUT` | no | `2.0` | Seconds each HTTP probe waits before marking a dependency unreachable in `GET /api/v1/health/`. | -| `PREDICTION_SYNC_INTERVAL` | no | `15` | Seconds between prediction-status poll re-enqueues. | -| `TRAINING_SYNC_INTERVAL` | no | `30` | Seconds between training-status poll re-enqueues. | -| `STAC_CACHE_TTL` | no | `300` | Seconds the LocMem cache holds a STAC item before re-fetching. Raise in prod for lower STAC load; lower in dev to see property changes faster. | -| `STAC_BULK_FETCH_WORKERS` | no | `16` | Thread pool size for parallel STAC item fetches when `?expand=stac` is used on list endpoints. | -| `PMTILES_MIN_ZOOM` | no | `10` | Min zoom passed to `tippecanoe` for PMTiles generation. | -| `PMTILES_MAX_ZOOM` | no | `20` | Max zoom passed to `tippecanoe` for PMTiles generation. | +| Name | Required | Default | Description | +| -------------------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `HEALTH_PROBE_TIMEOUT` | no | `2.0` | Seconds each HTTP probe waits before marking a dependency unreachable in `GET /api/v1/health/`. | +| `PREDICTION_SYNC_INTERVAL` | no | `15` | Seconds between prediction-status poll re-enqueues. | +| `TRAINING_SYNC_INTERVAL` | no | `30` | Seconds between training-status poll re-enqueues. | +| `STAC_CACHE_TTL` | no | `300` | Seconds the LocMem cache holds a STAC item before re-fetching. Raise in prod for lower STAC load; lower in dev to see property changes faster. | +| `STAC_BULK_FETCH_WORKERS` | no | `16` | Thread pool size for parallel STAC item fetches when `?expand=stac` is used on list endpoints. | +| `PMTILES_MIN_ZOOM` | no | `10` | Min zoom passed to `tippecanoe` for PMTiles generation. | +| `PMTILES_MAX_ZOOM` | no | `20` | Max zoom passed to `tippecanoe` for PMTiles generation. | ## Worker process diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 2b43bb01a..ee16d23e1 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -9,33 +9,33 @@ authors = [{ name = "HOTOSM", email = "sysadmin@hotosm.org" }] maintainers = [{ name = "Kshitij Raj Sharma", email = "krschap@proton.me" }] dependencies = [ - "adrf>=0.1.12", - "boto3>=1.40", - "dj-database-url>=2.3", - "django>=6.0,<6.1", - "django-cors-headers>=4.6", - "django-filter>=24.3", - "django-tasks>=0.12", - "django-tasks-db>=0.5", - "djangorestframework>=3.15", - "djangorestframework-gis>=1.1", - "drf-spectacular>=0.29", - "fair-py-ops[k8s]==0.3.6", - "geomltoolkits>=2.1.0", - "geopandas>=1.0", - "gpxpy>=1.6", - "gunicorn>=23.0", - "hotosm-auth[django]==0.2.12", - "httpx>=0.28.1", - "numpy>=2.0", - "psycopg-pool>=3.3.1", - "psycopg[binary]>=3.2", - "pydantic>=2.10", - "pydantic-settings>=2.7", - "pyogrio>=0.10", - "python-ulid>=3.1", - "shapely>=2.0", - "whitenoise>=6.6", + "adrf>=0.1.12", + "boto3>=1.40", + "dj-database-url>=2.3", + "django>=6.0,<6.1", + "django-cors-headers>=4.6", + "django-filter>=24.3", + "django-tasks>=0.12", + "django-tasks-db>=0.5", + "djangorestframework>=3.15", + "djangorestframework-gis>=1.1", + "drf-spectacular>=0.29", + "fair-py-ops[k8s]==0.3.6", + "geomltoolkits>=2.1.0", + "geopandas>=1.0", + "gpxpy>=1.6", + "gunicorn>=23.0", + "hotosm-auth[django]==0.2.12", + "httpx>=0.28.1", + "numpy>=2.0", + "psycopg-pool>=3.3.1", + "psycopg[binary]>=3.2", + "pydantic>=2.10", + "pydantic-settings>=2.7", + "pyogrio>=0.10", + "python-ulid>=3.1", + "shapely>=2.0", + "whitenoise>=6.6", ] [dependency-groups] @@ -51,12 +51,7 @@ override-dependencies = ["pyjwt[crypto]>=2.8.0,<3"] [tool.ruff] target-version = "py312" line-length = 100 -extend-exclude = [ - "**/migrations/**", - "shared/integrations/mapswipe.py", - "tests/**", - "login/**", -] +extend-exclude = ["**/migrations/**", "shared/integrations/mapswipe.py", "tests/**", "login/**"] [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "RUF"] @@ -77,12 +72,7 @@ deprecated = "ignore" missing-argument = "ignore" [tool.ty.src] -exclude = [ - "**/migrations/**", - "**/tests/**", - "shared/integrations/mapswipe.py", - "login/**", -] +exclude = ["**/migrations/**", "**/tests/**", "shared/integrations/mapswipe.py", "login/**"] [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings" @@ -91,17 +81,17 @@ addopts = "-ra --strict-markers" [tool.coverage.run] source = [ - "datasets", - "feedback", - "login", - "modelregistry", - "notifications", - "prediction", - "shared", - "system", - "training", - "workspace", - "config", + "datasets", + "feedback", + "login", + "modelregistry", + "notifications", + "prediction", + "shared", + "system", + "training", + "workspace", + "config", ] omit = ["**/migrations/**", "**/tests/**"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 04fa36584..3e770dbfc 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -41,7 +41,13 @@ services: api: restart: unless-stopped - entrypoint: ["sh", "-c", 'zenml init && zenml stack set fair-compose && python manage.py collectstatic --noinput && exec "$$@"', "--"] + entrypoint: + [ + "sh", + "-c", + 'zenml init && zenml stack set fair-compose && python manage.py collectstatic --noinput && exec "$$@"', + "--", + ] command: - gunicorn - --bind=0.0.0.0:8000 diff --git a/docker-compose.hotreload.yml b/docker-compose.hotreload.yml index dd79b5d8c..d74f7fd71 100644 --- a/docker-compose.hotreload.yml +++ b/docker-compose.hotreload.yml @@ -1,9 +1,8 @@ -# Opt-in local hot reload: bind-mounts ./backend into the api and worker +# Opt-in local hot reload: bind-mounts ./backend into the api and worker # TODO : add frontend support # Enable by appending this file to COMPOSE_FILE, e.g.: # COMPOSE_FILE=docker-compose.yml:docker-compose.hotreload.yml docker compose up - services: api: volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 1704ecb65..ba11f9c2a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,7 +114,13 @@ services: ports: - "${STAC_PORT:-8082}:8080" healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8080/_mgmt/ping')"] + test: + [ + "CMD", + "python", + "-c", + "import urllib.request; urllib.request.urlopen('http://localhost:8080/_mgmt/ping')", + ] interval: 5s timeout: 3s retries: 30 diff --git a/docs/About.md b/docs/About.md index 7046bf58c..b1d6726cc 100644 --- a/docs/About.md +++ b/docs/About.md @@ -1,6 +1,6 @@ # fAIr -## :open_book: History +## :open_book: History We recognized the Open Cities Challenge for building segmentation mid-2020, and then around the end of 2020, HOT conducted research in collaboration with the Netherland Red Cross. Last year, HOT contributed to an academic research project investigating the capability of UAV imagery to be used for AI-assisted mapping on refugees camps in Africa, which proved that the use of localised AI models produces higher prediction accuracy in comparison to wide trained models. @@ -10,8 +10,9 @@ In March 2022, we participated in an AI for Social good seminar in Frankfurt, Ge ![Screenshot+2022-11-02+at+14 09 49-94d2eb](https://github.com/hotosm/fAIr/assets/97789856/39222563-13cc-4813-80f4-982c9afa6491) ## Glossary + fAIr is the product name. How come?: - + f: for freedom and free and open-source software @@ -24,7 +25,9 @@ AI models: AI is wide term and it includes lots of approaches and techniques. In


+ ## What , How and for Whom? + Unlike other AI data producers, fAIr is an intuitive, fair and open-source AI-assisted mapping tool where AI models are created and trained by the people living and working in the local communities. By working with the local communities (and getting constant feedback on the models), we strive to eliminate model biases as we ensure the models are relevant to the communities where the maps are being created to improve the conditions of the people living there. ![OAM+pics](https://github.com/hotosm/fAIr/assets/97789856/c01a25fa-2a32-49a8-876e-0ab8f540766b) diff --git a/docs/Code-of-Conduct.md b/docs/Code-of-Conduct.md index ba6a53a4a..1ef2bfdd7 100644 --- a/docs/Code-of-Conduct.md +++ b/docs/Code-of-Conduct.md @@ -9,38 +9,36 @@ The HOT community principles are: - **Be friendly and patient.** Be generous and kind in both giving and accepting critique. Critique is a natural and important part of our culture. Good critiques are kind, respectful, clear, and constructive, focused on goals and requirements rather than personal preferences. You are expected to give and receive criticism with grace. Be considerate in speech and actions, and actively seek to acknowledge and respect the boundaries of fellow attendees. - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. Some examples of behavior that contributes to creating a positive environment include: + - Using welcoming and inclusive language. - - Using welcoming and inclusive language. + - Being respectful of differing viewpoints and experiences. - - Being respectful of differing viewpoints and experiences. + - Gracefully accepting constructive criticism. - - Gracefully accepting constructive criticism. + - Showing empathy towards other community members. - - Showing empathy towards other community members. - - - Placing collective interest before your own interest. + - Placing collective interest before your own interest. - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language. - **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the HOT community should be respectful when dealing with other members as well as with people outside the HOT community. - **Be careful in your word choice.** We are a global community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to: + - Violent threats or language directed against another person. - - Violent threats or language directed against another person. - - - Discriminatory jokes and language. + - Discriminatory jokes and language. - - Posting sexually explicit or violent material. + - Posting sexually explicit or violent material. - - Posting (or threatening to post) other people's personally identifying information ("doxing"). + - Posting (or threatening to post) other people's personally identifying information ("doxing"). - - Personal insults, especially those using racist or sexist terms. + - Personal insults, especially those using racist or sexist terms. - - Unwelcome sexual attention. + - Unwelcome sexual attention. - - Advocating for, or encouraging, any of the above behavior. + - Advocating for, or encouraging, any of the above behavior. - - Repeated harassment of others. In general, if someone asks you to stop, then stop. + - Repeated harassment of others. In general, if someone asks you to stop, then stop. - **Assume all communications are positive.** Always remain polite, and assume good faith. It is surprisingly easy to misunderstand each other, be it online or in person, particularly in such a culturally diverse setting as ours. Misunderstandings are particularly easy to arise when we are in a rush, or otherwise distracted. Please ask clarifying questions before assuming that a communication was inappropriate. diff --git a/docs/Docker-installation.md b/docs/Docker-installation.md index 98644f13e..8896d044b 100644 --- a/docs/Docker-installation.md +++ b/docs/Docker-installation.md @@ -37,18 +37,18 @@ it at a stack on other ports with `--api`, `--stac`, and `--minio`. ## Services -| Service | URL | Credentials | -| --- | --- | --- | -| fAIr frontend and API | | Bearer `dev-token` | -| Swagger UI | | | -| ReDoc | | | -| OpenAPI schema | | | -| Health probes | | | -| ZenML | | `default`, empty password | -| STAC | | | -| MLflow | | | -| MinIO console | | `minioadmin` / `minioadmin` | -| PostgreSQL | `localhost:5434` | `admin` / `password` | +| Service | URL | Credentials | +| --------------------- | -------------------------------------- | --------------------------- | +| fAIr frontend and API | | Bearer `dev-token` | +| Swagger UI | | | +| ReDoc | | | +| OpenAPI schema | | | +| Health probes | | | +| ZenML | | `default`, empty password | +| STAC | | | +| MLflow | | | +| MinIO console | | `minioadmin` / `minioadmin` | +| PostgreSQL | `localhost:5434` | `admin` / `password` | All v1 routes are under `/api/v1/`. Versioning uses DRF `NamespaceVersioning`, so `request.version` is set per request and `/api/v2/` is one URL line away when diff --git a/docs/FAQ.md b/docs/FAQ.md index e4bfea66d..4917f8840 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,6 +1,7 @@ # Frequently Asked Questions (FAQs) ## Users + **Q : What is fAIr?** A : fAIr is an open-source toolkit developed by the Humanitarian OpenStreetMap Team (HOT) that enables the integration of artificial intelligence (AI) into humanitarian mapping workflows. It provides AI models and tools to automate mapping tasks, improving efficiency and accuracy. diff --git a/docs/Home.md b/docs/Home.md index 72b0ce628..7e7f8f974 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -3,12 +3,13 @@ ## _**What are the challenges that mappers experience while utilizing AI data on OpenStreetMap?**_ - ### Overview + fAIr does mapping in the same way as human mappers using HOTs Tasking Manager. It looks at UAV imagery and produces map data that can be added to the OSM. Tests show a 100% speedup compared to manual mapping. It uses Artificial Intelligence (AI) to accomplish this. fAIr is developed by the Humanitarian OpenStreetMap Team and all the software is free and open source. Before fAIr is used it needs to be fine-tuned by training on high quality map data for a small representative part of the geographical region where it is to be used - + - ### Quality of AI Data One of the primary challenges faced by mappers when using AI data on OpenStreetMap is the quality of the data. The accuracy and completeness of AI data depends on the quality of the training data used to train the AI model. If the training data is biased or incomplete, the AI model will produce inaccurate results and this has happened in some cases especially with roads. Mappers must therefore carefully assess the quality of the AI data before adding it to OSM. - ### Data integration process. @@ -17,9 +18,9 @@ The use of AI technology in mapping requires technical expertise, which can be a significant challenge for mappers who may not have the necessary skills. For instance, beginer Mappers can not be recommended or trained to use these AI tools as they may mess the entire area. Therefore frequent trainings are needed for mappers to be equipped with adquet skills of handling such tasks. - ### Imagery offsets The AI technologies for example AI data from Microsoft was and is generated using Bing Imagery which in several cases is older compared to the recent Imageries like Maxar Premium. The imposes a big challenge in aligning the AI data with the latest imageries. -
-
+
+
### Here the HOT’s open AI-assisted mapping service: fAIr comes to rescue . -The fAIr tool is also an open-source mapping tool with AI assistance, and the AI models it uses are developed and trained by people who reside in and work in nearby towns. To know more about fAIr check the **About** page. - + +The fAIr tool is also an open-source mapping tool with AI assistance, and the AI models it uses are developed and trained by people who reside in and work in nearby towns. To know more about fAIr check the **About** page. diff --git a/docs/Infra.md b/docs/Infra.md index 57ef766e4..48e44f275 100644 --- a/docs/Infra.md +++ b/docs/Infra.md @@ -4,6 +4,7 @@ Our standard deployment process for other apps is [here](https://docs.hotosm.org/devops/deployment-process) fAIr differs slightly, because we have: + - Versioning of both software, as well as AI models. - A dedicated dev instance EC2 for easier development with all components. @@ -11,6 +12,7 @@ Currently model development happens in the `fAIr-models` repo, but this might eventually move to the `fAIr` monorepo. The model flow works like this: + - Each model dir has a `stac-item.json`. These point at the moving `dev-inference` image tag, and only seed a STAC the first time it starts up (on dev, or a brand new prod). @@ -27,6 +29,7 @@ The model flow works like this: > [!NOTE] > The Environment +> > - Single EC2, lightweight k3s cluster. > - Manually updated / synced with dev. > - Model registration in STAC etc is all manual. @@ -45,6 +48,7 @@ The model flow works like this: > [!NOTE] > The Environment +> > - Runs all the same components as production, but > start up via PR from `staging` --> `main`. > - The components run inside the `fair-staging` @@ -70,6 +74,7 @@ The model flow works like this: > [!NOTE] > The Environment +> > - Runs through tagged releases on Github, where ArgoCD > picks up the latest helm chart tag and deploys. diff --git a/docs/Release.md b/docs/Release.md index bd080f427..13284dfaf 100644 --- a/docs/Release.md +++ b/docs/Release.md @@ -1,4 +1,4 @@ -We use [commitizen](https://pypi.org/project/commitizen/) to manage our release version +We use [commitizen](https://pypi.org/project/commitizen/) to manage our release version - Install commitizen ```bash diff --git a/docs/User-Manual-for-fAIr.md b/docs/User-Manual-for-fAIr.md index dd4b323de..e3b709df5 100644 --- a/docs/User-Manual-for-fAIr.md +++ b/docs/User-Manual-for-fAIr.md @@ -1,4 +1,5 @@ # User Manual for fAIr +
This manual is a step by step guide for the community project managers on how to get started with the fAIr. @@ -8,9 +9,10 @@ This manual is a step by step guide for the community project managers on how to - [Steps to start access your project and Start mapping](#steps-to-start-access-your-project-and-start-mapping) - [Steps to make changes to the prediction of a particular model(even if you are not a project manager)](#steps-to-make-changes-to-the-prediction-of-a-particular-model) - [Help and Support](#help-and-support) -- [Thank you note](#thank-you) +- [Thank you note](#thank-you) ## Prerequisites + - Stable Internet connection - Knowledge on mapping . If you are new to mapping we suggest you to read [this](https://tasks.hotosm.org/learn/map) . - Very basic knowledge on training AI Datasets and Models. @@ -18,120 +20,73 @@ This manual is a step by step guide for the community project managers on how to ## Video Tutorial - https://github.com/hotosm/fAIr/assets/97789856/47121891-b21a-43c0-bb60-1e03f5222c10 - - - https://github.com/hotosm/fAIr/assets/97789856/d7d86a6f-e492-4169-8443-d9924cb10e54 - - - https://github.com/hotosm/fAIr/assets/97789856/f68def4f-6b0b-4870-801c-0fac16713249 - - - - https://github.com/hotosm/fAIr/assets/97789856/3da0771d-0346-4042-9049-4f321c27ba8d - - - - https://github.com/hotosm/fAIr/assets/97789856/1e2efaf0-f0c2-4331-a290-566434db5db3 - - - - - https://github.com/hotosm/fAIr/assets/97789856/a667433b-5d50-4160-b65c-192aaeae79af - - - https://github.com/hotosm/fAIr/assets/97789856/2dcae84f-b873-42b4-a9bf-93f589b563f4 - - - https://github.com/hotosm/fAIr/assets/97789856/7e29d4ec-65a1-4aef-ab67-42efb94eba6d - - - - https://github.com/hotosm/fAIr/assets/97789856/fe3f55e6-d175-423f-ba71-10283671e0cb - - - - https://github.com/hotosm/fAIr/assets/97789856/47d0a5fe-c606-4168-b401-4b34d70a3a0e - - - https://github.com/hotosm/fAIr/assets/97789856/43327f2d-e807-46e5-a1db-a8f58091eebd - - https://github.com/hotosm/fAIr/assets/97789856/1113b858-67a1-4ec9-8895-a955e7cd9063 - - https://github.com/hotosm/fAIr/assets/97789856/35ea2f68-92ef-4d8d-bfaf-fb05ec507d5a - - - https://github.com/hotosm/fAIr/assets/97789856/f4eae99f-e9a2-4424-806f-fdfce80e66dd - - ## Steps to create a project in fAIr 1. Go to [fAIr](https://fair-dev.hotosm.org/) . 2. First Login & Click on the button Start Creating Dataset.To create a new training dataset, start by clicking on the 'Start Creating Dataset' button. -2 + 2 3. Click on the button Create New.After clicking on 'Start Creating Dataset', click on the 'Create New' button to create a new dataset. -3 + 3 > A dataset would be a list of area of interests (AIOs) and its labels. OpenStreetMap data can be downlaoded automatically and used as initial > labels. It is our responsibility as model creators to make sure labels align with the feature before proceeding to training phase. 4. Click on the input field , Give your Dataset Name.To name your dataset, click on the input field and type in your desired name & click on the 'Create Training Dataset'. -4 + 4 5. Find Image to Train.After creating Dataset You will see following screen , Now Open Open Aerial Map in New Tab , https://openaerialmap.org/. -5 + 5 -6. Find Drone Image and Copy TMS URL.Look for your area and find good Drone Image for training . After finding , Click on Copy Image URL TMS. -6 +6. Find Drone Image and Copy TMS URL.Look for your area and find good Drone Image for training . After finding , Click on Copy Image URL TMS. + 6 7. Paste TMS To Open Aerial Imagery Tab.Paste your TMS URL that you have copied from Open Aerial Map Drone Image. -7 + 7 8. Zoom to Layer and Visualize Image.Click on Zoom to layer next to your Image name in OAM Block Top Right side of screen. -8 + 8 -9. Create Area of Interest for Training.Click on top left map buttons below zoom , To create AOI , AOI will be used to create labels. +9. Create Area of Interest for Training.Click on top left map buttons below zoom , To create AOI , AOI will be used to create labels. -> Labels are Buildings that will be acting as input for model inside AOI. +> Labels are Buildings that will be acting as input for model inside AOI. 9 10. Fetch Exisiting OSM Buildings in your Area of Interest.Click on Fetch OSM Data button next to OSM Logo Inside List of Area of Interest on Right side. -10 + 10 11. Visualize each Buildings to check their accuracy.Zoom to Level 20 in Map to see OSM Buildings that you have just fetched. -11 - + 11 12. Correct your labels.For training it is crucial that buildings are aligned exactly on Drone image, More good your input is more good your output will be. If you want to edit it Click on OSM Logo and Fix buildings / labels in your AOI. @@ -139,57 +94,44 @@ https://github.com/hotosm/fAIr/assets/97789856/f4eae99f-e9a2-4424-806f-fdfce80e6 12 - 13. Upload your fixes.You should see your AOI in ID , Fix your labels and Upload your changes to OSM. -13 - - + 13 14. Come Back to fAIr , Fetch OSM Labels.After you do your changes on OSM , Comeback to fAIr and fetch new labels, Give it a few min to update our database ( Click on button next to OSM Logo on AOI to fetch). -14 - + 14 15. Create Model for you Dataset.Click on View Models on your dataset page , You will see this page. Click on Create New. -15 + 15 16. Provide Model Metadata.Give your model name , Choose Your Dataset from Dropdown , Select BaseModel : RAMP is default for now , Click on Create AI Model. -16 - + 16 17. Create Training for Your Model.After creating Model for your dataset you will see following page. From here You can submit Trainings for your model. Give your epochs , Batch size and Zoom level for Training . -17 - + 17 18. Submit Your Training.Click on Submit Training Request and slide down, you will see your training listed there , You can check its status by clicking on info, Based on your dataset , AOI , your parameter model training may take time you can check progress on status. SUBMITTED , RUNNING , FINISHED . - You can see your model accuracy and use it after it is finished. If it fails you can check the reason for it and adapt accordingly. -18 - + You can see your model accuracy and use it after it is finished. If it fails you can check the reason for it and adapt accordingly. + 18 19. Check info of your Trainings.Click on i icon button next to your training to visualize current terminal and process of your training , It will display accuracy validation graph after training is finished. -19 - + 19 20. Finished Training.You can visualize your trainings accuracy and it's graph after it is finished like this. -20 - - -21. Publish Your Training.Once you are statisfied accuracy and want to visualize its prediction you need to publish the training. You can run multiple trainings for same model to find best performing checkpoint, Each training will result different checkpoint. You can always publish another training. Click on PUblish Training button to Publish Model. -21 + 20 +21. Publish Your Training.Once you are statisfied accuracy and want to visualize its prediction you need to publish the training. You can run multiple trainings for same model to find best performing checkpoint, Each training will result different checkpoint. You can always publish another training. Click on PUblish Training button to Publish Model. + 21 -## Steps to start access your project and Start mapping +## Steps to start access your project and Start mapping 1. Start Mapping.Once Model is Published it will be listed here on Model page as Published Training ID , Click on Start Mapping to See its Prediction. -1 1 - + 1 1 2. Visualize Your Model's Prediction.Zoom to the area you want to see predictions and Click Detect to Run your Published training Model. It will load the model and Run live predictions. -1 2 - + 1 2 3. Bring Predictions to OSM.Your Predictions will be visualized on Map, Now you can bring them to OSM Modify them remove bad predictions and Push it back to OSM. fAIr should be able to have feedback loop when user discards the prediction or modifies it ( it is work in Prrogress) , you can launch JOSM with prediction data though. -1 3 - + 1 3 ## Steps to make changes to the prediction of a particular model @@ -201,21 +143,18 @@ https://github.com/hotosm/fAIr/assets/97789856/f4eae99f-e9a2-4424-806f-fdfce80e6 4. All the feedbacks submitted need to be approved/validated by the project manager. - > Note: 'task' refers to each section of the map enclosed in the dotted > lines and each task has a corresponding number tag. - - - - - ## Help and Support + If you encounter any issues or need assistance while using fAIr, you can access the following resources: + - Check the [FAQs](https://github.com/hotosm/fAIr/blob/main/docs/FAQ.md) . - Join our Slack channel. -## Thank you +## Thank you + We are excited to have you join our community of passionate mappers and volunteers. fAIr is an AI powerful platform developed by the Humanitarian OpenStreetMap Team (HOT) that provides AI models and tools to automate mapping tasks, improving efficiency and accuracy. With fAIr, you have the opportunity to make a real impact by mapping areas that are in need of support. Your contributions help create detailed and up-to-date maps that aid organizations and communities in their efforts to respond to crises, plan infrastructure, and improve the lives of people around the world. @@ -229,4 +168,3 @@ Thank you for being part of fAIr. Your mapping efforts are invaluable, and we ap Happy mapping! The fAIr Team - diff --git a/docs/assets/fair-updates.json b/docs/assets/fair-updates.json index 74c13918c..5ec5f9133 100644 --- a/docs/assets/fair-updates.json +++ b/docs/assets/fair-updates.json @@ -1,5 +1,11 @@ { - "videos": [ - {"id": 1, "name":"Prediction Request Feature", "url":"https://www.youtube.com/watch?v=jRDBiSAgm3c", "slug":"prediction-request-feature", "date":"09-02-2026"} - ] + "videos": [ + { + "id": 1, + "name": "Prediction Request Feature", + "url": "https://www.youtube.com/watch?v=jRDBiSAgm3c", + "slug": "prediction-request-feature", + "date": "09-02-2026" + } + ] } diff --git a/docs/decisions/infra/0001-mlops.md b/docs/decisions/infra/0001-mlops.md index 9891ce22d..fadd8a008 100644 --- a/docs/decisions/infra/0001-mlops.md +++ b/docs/decisions/infra/0001-mlops.md @@ -38,38 +38,46 @@ The stack: ### Why ZenML **Good fit for a small team** + - Works out-of-the-box on Kubernetes without needing Kubeflow or a heavy control plane. - Provides a ready-made ML workflow layer so we don’t have to build one ourselves. **Lower maintenance than DIY** + - Gives us pipeline orchestration, artifact storage, and run metadata in one system. - Reduces the amount of custom glue code we would otherwise need to maintain. **Reproducibility by default** + - Tracks code, environments, artifacts, and pipeline versions automatically. - Builds lineage between inputs, runs, and outputs without manual effort. **Easy integration with fAIr** + - Can be triggered programmatically via the ZenML Python client, keeping a clean boundary: - - fAIr = user workflows + - fAIr = user workflows - ZenML = ML infrastructure **Works with our stack** + - Native integrations with S3, MLflow, WandB, and model serving tools (Seldon/KServe). - Runs directly on Kubernetes using ZenML’s built-in executor. **STAC integration should be simple** + - ZenML will be our source of truth for ML run metadata and artifacts. - We will publish selected outputs to pgSTAC via a simple post-run hook or final pipeline step. ### Why not the others **Metaflow:** + - No automatic environment capture (depends on team discipline) - Reproducibility is manual, not enforced - We'd be building what ZenML already provides **Flyte:** + - Heavier control plane (FlyteAdmin, FlyteConsole, separate database) - More complex to operate than ZenML - Built for larger scale than we currently need @@ -81,6 +89,7 @@ Flyte is excellent but overkill for our current needs. ### When we'd reconsider We'd move to **Flyte** if: + - We scale to 100+ runs per day - We need multi-cluster execution across regions - We add separate teams needing strict isolation @@ -90,6 +99,7 @@ We'd move to **Flyte** if: ## Architecture **Training & data processing:** + - Runs as ZenML pipelines - Executes on Kubernetes (via ZenML's native orchestrator) - Reads input data from S3/STAC @@ -97,25 +107,27 @@ We'd move to **Flyte** if: - All metadata tracked automatically **Metadata registration:** + - ZenML tracks all pipeline/artifact metadata internally - Post-run hook publishes summary to pgSTAC for geospatial discovery - Optional: track experiments in MLflow/WandB via ZenML integrations **Inference:** + - Deploy to Seldon/KServe via ZenML model deployer - Or export to ONNX for client-side execution ## Trade-offs -- ✅ Automatic reproducibility without team discipline required -- ✅ Minimal custom code to maintain -- ✅ Built for small teams without platform engineers -- ✅ First-class integrations with our stack (S3, Seldon, MLflow) -- ✅ Easy programmatic triggering from fAIr -- ✅ Full lineage tracking and metadata management built-in -- ✅ Clean separation: fAIr handles user workflows, ZenML handles ML infrastructure -- ✅ Can migrate to Flyte later if needed (similar abstractions) +- ✅ Automatic reproducibility without team discipline required +- ✅ Minimal custom code to maintain +- ✅ Built for small teams without platform engineers +- ✅ First-class integrations with our stack (S3, Seldon, MLflow) +- ✅ Easy programmatic triggering from fAIr +- ✅ Full lineage tracking and metadata management built-in +- ✅ Clean separation: fAIr handles user workflows, ZenML handles ML infrastructure +- ✅ Can migrate to Flyte later if needed (similar abstractions) - ❌ Adds MySQL database to maintain -- ❌ Learning curve for ZenML concepts (stacks, materializers) +- ❌ Learning curve for ZenML concepts (stacks, materializers) - ❌ Slightly more opinionated than raw Metaflow - ❌ ZenML's Kubernetes orchestrator is less mature than Argo Workflows diff --git a/docs/deployment.md b/docs/deployment.md index 4505994cc..f561cd83b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -2,48 +2,51 @@ Checklist for the deployments: **Pre-Deployment Checklist** -* Perform sanity check on the dev env with model creation , predictions , dataset creation ! -* Backup the Database , `python manage.py backup_db myoutputpath/backup.sql` -* Check and verify any database migration exists for the release if they do kindly verify the migrations files are stored in the git itself , production uses migrations from the repo +- Perform sanity check on the dev env with model creation , predictions , dataset creation ! +- Backup the Database , `python manage.py backup_db myoutputpath/backup.sql` +- Check and verify any database migration exists for the release if they do kindly verify the migrations files are stored in the git itself , production uses migrations from the repo **Release Checklist** -* Release fAIr utilities -* Release fairpredictor -* Verify the pypi releases for utilities and predictor -* Make PR to include new versions from utilities and - predictor & finally Release fAIr -- Document the env variable changes that are required for this release as compared to the previous version +- Release fAIr utilities +- Release fairpredictor +- Verify the pypi releases for utilities and predictor +- Make PR to include new versions from utilities and + predictor & finally Release fAIr -Make sure you always follow this order because : new version of fAIr utilities and fAIrpredictor should be included in the fAIr backend envs, hence it can only be done after first two release . A PR would be required to bump it to new versions and docker images should be built for the prediciton and new release of fAIr should include those versions from utilties and predictor ! +* Document the env variable changes that are required for this release as compared to the previous version + +Make sure you always follow this order because : new version of fAIr utilities and fAIrpredictor should be included in the fAIr backend envs, hence it can only be done after first two release . A PR would be required to bump it to new versions and docker images should be built for the prediciton and new release of fAIr should include those versions from utilties and predictor ! **Database Migration Checklist** -Make sure you have backups available in case things go wrong ! -* Login to SSH -* Login as admin -* Checkout to release -* Activate virtualenv (source fAIr/backend/.venv/bin/activate) -* Verify changes -* Run `python manage.py migrate` +Make sure you have backups available in case things go wrong ! + +- Login to SSH +- Login as admin +- Checkout to release +- Activate virtualenv (source fAIr/backend/.venv/bin/activate) +- Verify changes +- Run `python manage.py migrate` **Deployment Checklist** -* Verify PYPI packages are available: - + hot-fair-utilities (https://pypi.org/project/hot-fair-utilities/) - + fairpredictor (https://pypi.org/project/fairpredictor/) +- Verify PYPI packages are available: + - hot-fair-utilities (https://pypi.org/project/hot-fair-utilities/) + - fairpredictor (https://pypi.org/project/fairpredictor/) -* Verify Docker images for fAIr production are built and deployed -* [worker](https://github.com/hotosm/fAIr/pkgs/container/fair/worker) , [api](https://github.com/hotosm/fAIr/pkgs/container/fair/api) & [offline-predictor](https://github.com/hotosm/fAIr/pkgs/container/fair-offline-predictor ) images should be built and pointed to latest release -* Verify Docker image for [fairpredictor](https://github.com/hotosm/fairpredictor/pkgs/container/fairpredictor) is built and deployed -- Now create new task definition for api , worker , predictor and prediction worker , Verify the env variable changes if there are any -- Deploy the services +- Verify Docker images for fAIr production are built and deployed +- [worker](https://github.com/hotosm/fAIr/pkgs/container/fair/worker) , [api](https://github.com/hotosm/fAIr/pkgs/container/fair/api) & [offline-predictor](https://github.com/hotosm/fAIr/pkgs/container/fair-offline-predictor) images should be built and pointed to latest release +- Verify Docker image for [fairpredictor](https://github.com/hotosm/fairpredictor/pkgs/container/fairpredictor) is built and deployed +* Now create new task definition for api , worker , predictor and prediction worker , Verify the env variable changes if there are any +* Deploy the services **Verification Checklist** -* Verify all the workers are appearing in flower -* Verify the full workflow in frontend ! -- Check the cache for the s3 (frontend cloudfront) -* Verify Matomo after deployment +- Verify all the workers are appearing in flower +- Verify the full workflow in frontend ! + +* Check the cache for the s3 (frontend cloudfront) +- Verify Matomo after deployment diff --git a/docs/infra/dev.md b/docs/infra/dev.md index fa54749c9..ff34c2744 100644 --- a/docs/infra/dev.md +++ b/docs/infra/dev.md @@ -15,11 +15,11 @@ branch: CI builds the images on every push, and a redeploy pulls them. Everything lives in `/opt/fAIr-app` (a `develop` checkout): -| File | Purpose | -|---|---| -| `docker-compose.yml` | base stack (api, worker, postgres, minio, stac, mlflow, zenml, frontend) | -| `docker-compose.dev.yml` | dev override: Caddy ingress, restart policies, the inline Caddyfile | -| `.env` | all runtime config and secrets (not in git) | +| File | Purpose | +| ------------------------ | ------------------------------------------------------------------------ | +| `docker-compose.yml` | base stack (api, worker, postgres, minio, stac, mlflow, zenml, frontend) | +| `docker-compose.dev.yml` | dev override: Caddy ingress, restart policies, the inline Caddyfile | +| `.env` | all runtime config and secrets (not in git) | `.env` sets `COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml`, so plain `docker compose` commands pick up both files. The stack is managed by the diff --git a/frontend/README.md b/frontend/README.md index 483f9ac2c..bb486281e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -116,7 +116,7 @@ Here's an overview of the folder structure: │ ├── utils/ - Utility functions, application content and constants. │ └── main.tsx - Entry point of the React app. ├── docs/ - ARD documentation for some of the decisions made for the app. -└── vercel.json - To prevent the custom 404 page from Vercel when a route is visited. (This is just for the demo site deployed on Vercel.) +└── vercel.json - To prevent the custom 404 page from Vercel when a route is visited. (This is just for the demo site deployed on Vercel.) └── ... Other configuration files like tsconfig.json, vite.config.mts etc. ``` @@ -192,4 +192,4 @@ fAIr also bundles portions of the following open source software. - [Maplibre GL JS (BSD-3-Clause)](https://github.com/maplibre/maplibre-gl-js). - [PMTiles (BSD-3-Clause)](https://github.com/protomaps/PMTiles). - [React Medium Image Zoom (BSD-3-Clause)](https://github.com/rpearce/react-medium-image-zoom). -- Map fonts from [Maplibre Demo Tiles](https://github.com/maplibre/demotiles). \ No newline at end of file +- Map fonts from [Maplibre Demo Tiles](https://github.com/maplibre/demotiles). diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 6b191c342..424a6311b 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -1,42 +1,36 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from '@typescript-eslint/eslint-plugin'; -import prettierPlugin from 'eslint-plugin-prettier'; -import prettierConfig from 'eslint-config-prettier'; - +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import prettierPlugin from "eslint-plugin-prettier"; +import prettierConfig from "eslint-config-prettier"; export default [ - { - ignores: ['dist'], - files: ['**/*.{ts,tsx}'], + ignores: ["dist"], + files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, - sourceType: 'module', + sourceType: "module", globals: globals.browser, - parser: '@typescript-eslint/parser', + parser: "@typescript-eslint/parser", }, plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - '@tanstack/query': '@tanstack/query', - 'prettier': prettierPlugin, + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + "@tanstack/query": "@tanstack/query", + prettier: prettierPlugin, }, rules: { ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - 'prettier/prettier': 'error', - + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "prettier/prettier": "error", }, }, js.configs.recommended, ...tseslint.configs.recommended, - 'plugin:@tanstack/eslint-plugin-query/recommended', - 'plugin:tailwindcss/recommended', + "plugin:@tanstack/eslint-plugin-query/recommended", + "plugin:tailwindcss/recommended", prettierConfig, ]; diff --git a/frontend/index.html b/frontend/index.html index b29b66314..4ed803e83 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,31 +1,29 @@ + + + + + + + + + + + + + + - - - - - - - - - - - - - - + HOT fAIr - HOT fAIr + + + - - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/frontend/package.json b/frontend/package.json index 8f2cfe623..fd824b81c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", - "private": true, "version": "0.0.0", + "private": true, "type": "module", "scripts": { "dev": "vite", @@ -14,10 +14,10 @@ "coverage": "vitest run --coverage" }, "dependencies": { + "@awesome.me/webawesome": "3.11.0", "@hookform/resolvers": "^5.4.0", "@hotosm/hanko-auth": "^0.5.2", "@hotosm/ui": "^2.1.0", - "@awesome.me/webawesome": "3.11.0", "@reactour/tour": "^3.7.0", "@shoelace-style/shoelace": "^2.17.1", "@tanstack/react-query": "^5.59.0", diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml index 373098350..62eed984d 100644 --- a/frontend/pnpm-workspace.yaml +++ b/frontend/pnpm-workspace.yaml @@ -2,8 +2,8 @@ # ignored by pnpm >= 11, which silently drops these overrides from the lockfile # and breaks --frozen-lockfile in CI (ERR_PNPM_LOCKFILE_CONFIG_MISMATCH). overrides: - cross-spawn@>=7.0.0 <7.0.5: '>=7.0.5' - '@eslint/plugin-kit@<0.2.3': '>=0.2.3' + cross-spawn@>=7.0.0 <7.0.5: ">=7.0.5" + "@eslint/plugin-kit@<0.2.3": ">=0.2.3" # onlyBuiltDependencies is the pnpm 10 spelling of allowBuilds (pnpm 11); # both are needed while CI runs pnpm 10 and devs run pnpm 11. @@ -13,4 +13,4 @@ allowBuilds: esbuild: true minimumReleaseAgeExclude: - - '@hotosm/ui@1.0.0 || 1.0.1 || 2.0.0 || 2.1.0' + - "@hotosm/ui@1.0.0 || 1.0.1 || 2.0.0 || 2.1.0" diff --git a/frontend/prettier.config.mjs b/frontend/prettier.config.mjs index 6016970d1..51ebb5777 100644 --- a/frontend/prettier.config.mjs +++ b/frontend/prettier.config.mjs @@ -8,5 +8,5 @@ export default { trailingComma: "all", printWidth: 80, useTabs: false, - endOfLine: "auto" + endOfLine: "auto", }; diff --git a/frontend/src/app/index.tsx b/frontend/src/app/index.tsx index f2e35ee18..c3c8ba80e 100644 --- a/frontend/src/app/index.tsx +++ b/frontend/src/app/index.tsx @@ -1,11 +1,7 @@ import { AppRouter } from "@/app/router"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { showErrorToast } from "@/utils"; -import { - QueryCache, - QueryClient, - QueryClientProvider, -} from "@tanstack/react-query"; +import { QueryCache, QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { TourProvider } from "@reactour/tour"; import { APP_TOUR_STEPS } from "@/constants/site-tour"; import { AuthProvider } from "./providers/auth-provider"; diff --git a/frontend/src/app/providers/auth-provider.tsx b/frontend/src/app/providers/auth-provider.tsx index efe607fbe..c2ac1c8da 100644 --- a/frontend/src/app/providers/auth-provider.tsx +++ b/frontend/src/app/providers/auth-provider.tsx @@ -47,8 +47,7 @@ type AuthProviderProps = { export const AuthProvider: React.FC = ({ children }) => { const { getValue, setValue, removeValue } = useLocalStorage(); - const { getSessionValue, removeSessionValue, setSessionValue } = - useSessionStorage(); + const { getSessionValue, removeSessionValue, setSessionValue } = useSessionStorage(); const isTryFairPage = location.pathname.includes(APPLICATION_ROUTES.TRY_FAIR); @@ -61,9 +60,7 @@ export const AuthProvider: React.FC = ({ children }) => { const [user, setUser] = useState(undefined); const isAuthenticated = - AUTH_PROVIDER === "hanko" - ? user !== undefined - : user !== undefined && token !== undefined; + AUTH_PROVIDER === "hanko" ? user !== undefined : user !== undefined && token !== undefined; /** * Set token globally to eliminate the need to rewrite it. @@ -73,9 +70,7 @@ export const AuthProvider: React.FC = ({ children }) => { if (AUTH_PROVIDER === "hanko" && !IS_DEV) { apiClient.defaults.withCredentials = true; } else { - apiClient.defaults.headers.common["Authorization"] = token - ? `Bearer ${token}` - : null; + apiClient.defaults.headers.common["Authorization"] = token ? `Bearer ${token}` : null; } const handleRedirection = () => { @@ -91,9 +86,7 @@ export const AuthProvider: React.FC = ({ children }) => { * To show the login success after completing redirection if any. */ useEffect(() => { - const loginSuccessful = getSessionValue( - HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY, - ); + const loginSuccessful = getSessionValue(HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY); if (loginSuccessful == "success") { showSuccessToast(TOAST_NOTIFICATIONS.loginSuccess); removeSessionValue(HOT_FAIR_LOGIN_SUCCESSFUL_SESSION_KEY); @@ -138,9 +131,7 @@ export const AuthProvider: React.FC = ({ children }) => { if (response.ok) { const userData = await response.json(); if (!userData.img_url) { - const hankoUser = JSON.parse( - localStorage.getItem("hotosm-auth-user") || "{}", - ); + const hankoUser = JSON.parse(localStorage.getItem("hotosm-auth-user") || "{}"); if (hankoUser.avatarUrl) { userData.img_url = hankoUser.avatarUrl; } @@ -275,9 +266,7 @@ export const AuthProvider: React.FC = ({ children }) => { .then((res) => (res.ok ? res.json() : Promise.reject())) .then((userData) => { if (!userData.img_url) { - const hankoUser = JSON.parse( - localStorage.getItem("hotosm-auth-user") || "{}", - ); + const hankoUser = JSON.parse(localStorage.getItem("hotosm-auth-user") || "{}"); if (hankoUser.avatarUrl) { userData.img_url = hankoUser.avatarUrl; } diff --git a/frontend/src/app/providers/models-provider.tsx b/frontend/src/app/providers/models-provider.tsx index e43549c3c..dfd13d0ae 100644 --- a/frontend/src/app/providers/models-provider.tsx +++ b/frontend/src/app/providers/models-provider.tsx @@ -5,21 +5,11 @@ import { MODELS_ROUTES, TOAST_NOTIFICATIONS, } from "@/constants"; -import { - BASE_MODELS, - TileServiceType, - TrainingDatasetOption, - TrainingType, -} from "@/enums"; +import { BASE_MODELS, TileServiceType, TrainingDatasetOption, TrainingType } from "@/enums"; import { HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY } from "@/config"; import { LngLatBoundsLike } from "maplibre-gl"; -import { - useLocation, - useNavigate, - useParams, - useSearchParams, -} from "react-router-dom"; +import { useLocation, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { useModelDetails } from "@/features/models/hooks/use-models"; import { UseMutationResult } from "@tanstack/react-query"; import { useLocalStorage } from "@/hooks/use-storage"; @@ -175,8 +165,7 @@ const initialFormState: FormData = { [MODEL_CREATION_FORM_NAME.MODEL_NAME]: "", [MODEL_CREATION_FORM_NAME.MODEL_DESCRIPTION]: "", [MODEL_CREATION_FORM_NAME.BASE_MODELS]: BASE_MODELS.RAMP, - [MODEL_CREATION_FORM_NAME.TRAINING_DATASET_OPTION]: - TrainingDatasetOption.USE_EXISTING, + [MODEL_CREATION_FORM_NAME.TRAINING_DATASET_OPTION]: TrainingDatasetOption.USE_EXISTING, // create new dataset form [MODEL_CREATION_FORM_NAME.DATASET_NAME]: "", [MODEL_CREATION_FORM_NAME.TMS_URL]: "", @@ -265,9 +254,7 @@ export const ModelsProvider: React.FC<{ */ const datasetId = searchParams.get(DatasetURLParams.DATASET_ID); const datasetName = searchParams.get(DatasetURLParams.DATASET_NAME); - const datasetSourceImagery = searchParams.get( - DatasetURLParams.DATASET_SOURCE_IMAGERY, - ); + const datasetSourceImagery = searchParams.get(DatasetURLParams.DATASET_SOURCE_IMAGERY); const { setValue, removeValue, getValue } = useLocalStorage(); const storedFormData = getValue(HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY); const [formData, setFormData] = useState( @@ -287,10 +274,7 @@ export const ModelsProvider: React.FC<{ ) => { setFormData((prev) => { const updatedData = { ...prev, [field]: value }; - setValue( - HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY, - JSON.stringify(updatedData), - ); + setValue(HOT_FAIR_MODEL_CREATION_LOCAL_STORAGE_KEY, JSON.stringify(updatedData)); return updatedData; }); }; @@ -311,8 +295,7 @@ export const ModelsProvider: React.FC<{ const isModelOwner = isAuthenticated && data?.user?.osm_id === user?.osm_id; // Will be used in the route validator component to delay the redirection for a while until the data are retrieved - const validateEditMode = - formData.selectedTrainingDatasetId !== "" && formData.tmsURL !== ""; + const validateEditMode = formData.selectedTrainingDatasetId !== "" && formData.tmsURL !== ""; useEffect(() => { if (isError && error) { @@ -338,10 +321,7 @@ export const ModelsProvider: React.FC<{ // Prefill dataset id if available in the URL. useEffect(() => { if (datasetId && datasetName && datasetSourceImagery) { - handleChange( - MODEL_CREATION_FORM_NAME.SELECTED_TRAINING_DATASET_ID, - datasetId, - ); + handleChange(MODEL_CREATION_FORM_NAME.SELECTED_TRAINING_DATASET_ID, datasetId); handleChange(MODEL_CREATION_FORM_NAME.DATASET_NAME, datasetName); handleChange(MODEL_CREATION_FORM_NAME.TMS_URL, datasetSourceImagery); } @@ -358,23 +338,11 @@ export const ModelsProvider: React.FC<{ if (!isEditMode || isPending || !data || isError) return; handleChange(MODEL_CREATION_FORM_NAME.BASE_MODELS, data.base_model); - handleChange( - MODEL_CREATION_FORM_NAME.MODEL_DESCRIPTION, - data.description ?? "", - ); + handleChange(MODEL_CREATION_FORM_NAME.MODEL_DESCRIPTION, data.description ?? ""); handleChange(MODEL_CREATION_FORM_NAME.MODEL_NAME, data.name ?? ""); - handleChange( - MODEL_CREATION_FORM_NAME.SELECTED_TRAINING_DATASET_ID, - data.dataset.id, - ); - handleChange( - MODEL_CREATION_FORM_NAME.DATASET_NAME, - data.dataset.name ?? "", - ); - handleChange( - MODEL_CREATION_FORM_NAME.TMS_URL, - data.dataset.source_imagery ?? "", - ); + handleChange(MODEL_CREATION_FORM_NAME.SELECTED_TRAINING_DATASET_ID, data.dataset.id); + handleChange(MODEL_CREATION_FORM_NAME.DATASET_NAME, data.dataset.name ?? ""); + handleChange(MODEL_CREATION_FORM_NAME.TMS_URL, data.dataset.source_imagery ?? ""); handleChange( MODEL_CREATION_FORM_NAME.TILESERVICE_TYPE, getTileServerTypeFromURL(data.dataset.source_imagery ?? ""), @@ -484,9 +452,7 @@ export const ModelsProvider: React.FC<{ const hasAOIsWithGeometry = useMemo( () => formData.trainingAreas.length > 0 && - formData.trainingAreas.every( - (aoi: TTrainingAreaFeature) => aoi.geometry !== null, - ), + formData.trainingAreas.every((aoi: TTrainingAreaFeature) => aoi.geometry !== null), [formData], ); @@ -563,11 +529,7 @@ export const ModelsProvider: React.FC<{ ], ); - return ( - - {children} - - ); + return {children}; }; export const useModelsContext = () => useContext(ModelsContext); diff --git a/frontend/src/app/providers/tour-provider.tsx b/frontend/src/app/providers/tour-provider.tsx index 8c13942aa..ff0863a75 100644 --- a/frontend/src/app/providers/tour-provider.tsx +++ b/frontend/src/app/providers/tour-provider.tsx @@ -1,10 +1,4 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useState, -} from "react"; +import React, { createContext, useCallback, useContext, useEffect, useState } from "react"; import { useLocalStorage } from "@/hooks/use-storage"; import { useTour } from "@reactour/tour"; @@ -39,9 +33,7 @@ type AppTourProviderProps = { children: React.ReactNode; }; -export const AppTourProvider: React.FC = ({ - children, -}) => { +export const AppTourProvider: React.FC = ({ children }) => { const { setIsOpen, setCurrentStep, setSteps } = useTour(); const { getValue, setValue } = useLocalStorage(); const [showTourModal, setShowTourModal] = useState(false); @@ -53,9 +45,7 @@ export const AppTourProvider: React.FC = ({ setIsOpen(true); // @ts-expect-error bad types definition setSteps((prevSteps) => { - const visibleSteps = APP_TOUR_STEPS.filter((step) => - document.querySelector(step.selector), - ); + const visibleSteps = APP_TOUR_STEPS.filter((step) => document.querySelector(step.selector)); return visibleSteps; }); setCurrentStep(0); diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 119ca440e..31ae75b48 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -1,18 +1,9 @@ import { APPLICATION_ROUTES } from "@/constants"; import { isNavigationRouteEnabled } from "@/constants/general"; import { MainErrorFallback } from "@/components/errors"; -import { - ModelFormsLayout, - RootLayout, - UserProfileLayout, -} from "@/components/layouts"; +import { ModelFormsLayout, RootLayout, UserProfileLayout } from "@/components/layouts"; import { ProtectedRoute } from "@/components/shared"; -import { - Navigate, - RouterProvider, - createBrowserRouter, - useLocation, -} from "react-router-dom"; +import { Navigate, RouterProvider, createBrowserRouter, useLocation } from "react-router-dom"; import { ModelsProvider } from "@/app/providers/models-provider"; import { NuqsAdapter } from "nuqs/adapters/react-router/v6"; @@ -21,11 +12,7 @@ const RouteAvailabilityGuard = ({ children }: React.PropsWithChildren) => { if (!isNavigationRouteEnabled(location.pathname)) { return ( - + ); } @@ -95,9 +82,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.MODEL_DETAILS, lazy: async () => { - const { ModelDetailsPage } = await import( - "@/app/routes/models/model-details-card" - ); + const { ModelDetailsPage } = await import("@/app/routes/models/model-details-card"); return { Component: () => ( @@ -111,9 +96,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.MODEL_FEEDBACKS, lazy: async () => { - const { ModelFeedbacksPage } = await import( - "@/app/routes/models/feedbacks" - ); + const { ModelFeedbacksPage } = await import("@/app/routes/models/feedbacks"); return { Component: () => ( @@ -126,9 +109,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.MODELS, lazy: async () => { - const { ModelsPage } = await import( - "@/app/routes/models/models-list" - ); + const { ModelsPage } = await import("@/app/routes/models/models-list"); return { Component: () => , }; @@ -141,9 +122,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.AI_PREDICTIONS, lazy: async () => { - const { AIPredictionsPage } = await import( - "@/app/routes/ai-predictions" - ); + const { AIPredictionsPage } = await import("@/app/routes/ai-predictions"); return { Component: () => , }; @@ -166,9 +145,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL, lazy: async () => { - const { ModelDetailsFormPage } = await import( - "@/app/routes/models/model-details-form" - ); + const { ModelDetailsFormPage } = + await import("@/app/routes/models/model-details-form"); return { Component: () => , }; @@ -177,9 +155,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL_TRAINING_DATASET, lazy: async () => { - const { ModelTrainingDatasetPage } = await import( - "@/app/routes/models/training-dataset" - ); + const { ModelTrainingDatasetPage } = + await import("@/app/routes/models/training-dataset"); return { Component: () => , }; @@ -188,9 +165,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL_TRAINING_AREA, lazy: async () => { - const { ModelTrainingAreaPage } = await import( - "@/app/routes/models/training-area" - ); + const { ModelTrainingAreaPage } = await import("@/app/routes/models/training-area"); return { Component: () => , }; @@ -199,9 +174,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL_TRAINING_SETTINGS, lazy: async () => { - const { ModelTrainingSettingsPage } = await import( - "@/app/routes/models/training-settings" - ); + const { ModelTrainingSettingsPage } = + await import("@/app/routes/models/training-settings"); return { Component: () => , }; @@ -210,9 +184,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL_SUMMARY, lazy: async () => { - const { ModelSummaryPage } = await import( - "@/app/routes/models/summary" - ); + const { ModelSummaryPage } = await import("@/app/routes/models/summary"); return { Component: () => , }; @@ -221,9 +193,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.CREATE_NEW_MODEL_CONFIRMATION, lazy: async () => { - const { ModelConfirmationPage } = await import( - "@/app/routes/models/confirmation" - ); + const { ModelConfirmationPage } = await import("@/app/routes/models/confirmation"); return { Component: () => , }; @@ -239,9 +209,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.EDIT_MODEL_DETAILS, lazy: async () => { - const { ModelDetailsFormPage } = await import( - "@/app/routes/models/model-details-form" - ); + const { ModelDetailsFormPage } = + await import("@/app/routes/models/model-details-form"); return { Component: () => , }; @@ -250,9 +219,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.EDIT_MODEL_TRAINING_DATASET, lazy: async () => { - const { ModelTrainingDatasetPage } = await import( - "@/app/routes/models/training-dataset" - ); + const { ModelTrainingDatasetPage } = + await import("@/app/routes/models/training-dataset"); return { Component: () => , }; @@ -261,9 +229,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.EDIT_MODEL_TRAINING_AREA, lazy: async () => { - const { ModelTrainingAreaPage } = await import( - "@/app/routes/models/training-area" - ); + const { ModelTrainingAreaPage } = await import("@/app/routes/models/training-area"); return { Component: () => , }; @@ -272,9 +238,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.EDIT_MODEL_TRAINING_SETTINGS, lazy: async () => { - const { ModelTrainingSettingsPage } = await import( - "@/app/routes/models/training-settings" - ); + const { ModelTrainingSettingsPage } = + await import("@/app/routes/models/training-settings"); return { Component: () => , }; @@ -283,9 +248,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.EDIT_MODEL_SUMMARY, lazy: async () => { - const { ModelSummaryPage } = await import( - "@/app/routes/models/summary" - ); + const { ModelSummaryPage } = await import("@/app/routes/models/summary"); return { Component: () => , }; @@ -294,9 +257,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.EDIT_MODEL_CONFIRMATION, lazy: async () => { - const { ModelConfirmationPage } = await import( - "@/app/routes/models/confirmation" - ); + const { ModelConfirmationPage } = await import("@/app/routes/models/confirmation"); return { Component: () => , }; @@ -313,9 +274,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.DATASET_DETAILS, lazy: async () => { - const { TrainingDatasetsDetailPage } = await import( - "@/app/routes/datasets/dataset-detail" - ); + const { TrainingDatasetsDetailPage } = + await import("@/app/routes/datasets/dataset-detail"); return { Component: () => , }; @@ -328,9 +288,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.DATASETS, lazy: async () => { - const { DatasetExplorerPage } = await import( - "@/app/routes/datasets/dataset-list" - ); + const { DatasetExplorerPage } = await import("@/app/routes/datasets/dataset-list"); return { Component: () => , }; @@ -347,9 +305,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.START_MAPPING, lazy: async () => { - const { StartMappingPage } = await import( - "@/app/routes/start-mapping" - ); + const { StartMappingPage } = await import("@/app/routes/start-mapping"); return { Component: () => ( @@ -376,9 +332,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.PROFILE_BASE, lazy: async () => { - const { UserProfileOverviewPage } = await import( - "@/app/routes/profile/overview" - ); + const { UserProfileOverviewPage } = await import("@/app/routes/profile/overview"); return { Component: () => , }; @@ -387,9 +341,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.PROFILE_SETTINGS, lazy: async () => { - const { UserProfileSettingsPage } = await import( - "@/app/routes/profile/settings" - ); + const { UserProfileSettingsPage } = await import("@/app/routes/profile/settings"); return { Component: () => , }; @@ -398,9 +350,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.PROFILE_MODELS, lazy: async () => { - const { UserModelsPage } = await import( - "@/app/routes/profile/models" - ); + const { UserModelsPage } = await import("@/app/routes/profile/models"); return { Component: () => , }; @@ -409,9 +359,7 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.PROFILE_DATASETS, lazy: async () => { - const { UserProfileDatasetsPage } = await import( - "@/app/routes/profile/datasets" - ); + const { UserProfileDatasetsPage } = await import("@/app/routes/profile/datasets"); return { Component: () => , }; @@ -420,9 +368,8 @@ const router = createBrowserRouter([ { path: APPLICATION_ROUTES.PROFILE_OFFLINE_PREDICTIONS, lazy: async () => { - const { UserProfileOfflinePredictionsPage } = await import( - "@/app/routes/profile/offline-predictions" - ); + const { UserProfileOfflinePredictionsPage } = + await import("@/app/routes/profile/offline-predictions"); return { Component: () => , }; diff --git a/frontend/src/app/routes/about.tsx b/frontend/src/app/routes/about.tsx index bce29670e..a1fbc0bec 100644 --- a/frontend/src/app/routes/about.tsx +++ b/frontend/src/app/routes/about.tsx @@ -14,9 +14,7 @@ export const AboutPage = () => {

{ABOUT_PAGE_CONTENT.heroHeading.firstSegment}{" "} - - {ABOUT_PAGE_CONTENT.heroHeading.secondSegment} - {" "} + {ABOUT_PAGE_CONTENT.heroHeading.secondSegment}{" "} {ABOUT_PAGE_CONTENT.heroHeading.thirdSegment}{" "}

diff --git a/frontend/src/app/routes/ai-predictions.tsx b/frontend/src/app/routes/ai-predictions.tsx index cb191cf9b..a1f996781 100644 --- a/frontend/src/app/routes/ai-predictions.tsx +++ b/frontend/src/app/routes/ai-predictions.tsx @@ -10,10 +10,7 @@ import { MapSwipeProjectResultMapDrawer } from "@/features/mapswipe/components/p import PageHeader from "@/features/models/components/header"; import { MapswipeProjectStatusDialog } from "@/features/mapswipe/components/project-status-dialog"; -import { - useScrollToElement, - useScrollToTop, -} from "@/hooks/use-scroll-to-element"; +import { useScrollToElement, useScrollToTop } from "@/hooks/use-scroll-to-element"; import { LayoutView } from "@/enums"; import { AIPredictionsListLayout } from "@/features/ai-predictions/components/ai-predictions-table"; import { Spinner } from "@/components/ui/spinner"; @@ -49,9 +46,7 @@ export const AIPredictionsPage = () => { openDialog: openMapSwipeProjectStatusDialog, closeDialog: closeMapSwipeProjectStatusDialog, } = useDialog(); - const [mapSwipeResultsPmtiles, setMapSwipeResultsPmtiles] = useState< - string | null - >(null); + const [mapSwipeResultsPmtiles, setMapSwipeResultsPmtiles] = useState(null); const { isOpened: isMapSwipeProjectResultMapOpened, @@ -75,8 +70,7 @@ export const AIPredictionsPage = () => { setMapSwipeResultsPmtiles(null); openMapSwipeProjectStatusDialog(); }; - const [activePrediction, setActivePrediction] = - useState(null); + const [activePrediction, setActivePrediction] = useState(null); const { isOpened: isPredictionResultOpened, @@ -119,10 +113,7 @@ export const AIPredictionsPage = () => { />
- {isMapDataPending || - isMapDataError || - !mapData || - mapData.features.length === 0 ? ( + {isMapDataPending || isMapDataError || !mapData || mapData.features.length === 0 ? (
@@ -186,9 +177,7 @@ export const AIPredictionsPage = () => { isOpen={isMapswipeDialogOpen} onClose={closeMapSwipeProjectStatusDialog} mapSwipeProjectId={activePrediction.mapswipe_id ?? ""} - handleMapSwipeProjectResultMapModal={ - handleMapSwipeProjectResultMapModal - } + handleMapSwipeProjectResultMapModal={handleMapSwipeProjectResultMapModal} /> )} diff --git a/frontend/src/app/routes/datasets/dataset-detail.tsx b/frontend/src/app/routes/datasets/dataset-detail.tsx index fc05d8ca6..f4c1a8051 100644 --- a/frontend/src/app/routes/datasets/dataset-detail.tsx +++ b/frontend/src/app/routes/datasets/dataset-detail.tsx @@ -78,11 +78,7 @@ export const TrainingDatasetsDetailPage = () => { {showEditOptions && ( <> - + { {data.models_count > 1 ? "models" : "model"}

- Created by:{" "} - {data.user.username} + Created by: {data.user.username}

- Last Modified:{" "} - {formatDate(data.last_modified)} + Last Modified: {formatDate(data.last_modified)}

@@ -138,10 +132,7 @@ export const TrainingDatasetsDetailPage = () => { }} className="!w-fit" /> - + {/* Edit Dropdown */}
{showEditOptions && ( diff --git a/frontend/src/app/routes/datasets/dataset-list.tsx b/frontend/src/app/routes/datasets/dataset-list.tsx index e02b8e1fd..263664fcc 100644 --- a/frontend/src/app/routes/datasets/dataset-list.tsx +++ b/frontend/src/app/routes/datasets/dataset-list.tsx @@ -15,11 +15,7 @@ export const DatasetExplorerPage = () => { disableCreateButton isTrainingDataset /> - + ); diff --git a/frontend/src/app/routes/learn.tsx b/frontend/src/app/routes/learn.tsx index 1fe690c32..c3f1b4660 100644 --- a/frontend/src/app/routes/learn.tsx +++ b/frontend/src/app/routes/learn.tsx @@ -51,37 +51,21 @@ export const LearnPage = () => {

{LEARN_PAGE_CONTENT.heroHeading.firstSegment}{" "} - - {LEARN_PAGE_CONTENT.heroHeading.secondSegment} - {" "} + {LEARN_PAGE_CONTENT.heroHeading.secondSegment}{" "} {LEARN_PAGE_CONTENT.heroHeading.thirdSegment}{" "} - - {LEARN_PAGE_CONTENT.heroHeading.fourthSegment} - {" "} + {LEARN_PAGE_CONTENT.heroHeading.fourthSegment}{" "} {LEARN_PAGE_CONTENT.heroHeading.fifthSegment}{" "} - - {LEARN_PAGE_CONTENT.heroHeading.sixthSegment} - {" "} + {LEARN_PAGE_CONTENT.heroHeading.sixthSegment}{" "} {LEARN_PAGE_CONTENT.heroHeading.seventhSegment}

-

- {LEARN_PAGE_CONTENT.heroDescription} -

+

{LEARN_PAGE_CONTENT.heroDescription}

- fAIr Values + fAIr Values
-
+
{ carouselClassName="updates-carousel" renderSkeleton={(index) => } renderItem={(update) => ( - handleVideoSelect(update)} - /> + handleVideoSelect(update)} /> )} />
diff --git a/frontend/src/app/routes/models/confirmation.tsx b/frontend/src/app/routes/models/confirmation.tsx index d9f88f57d..4ff6414e9 100644 --- a/frontend/src/app/routes/models/confirmation.tsx +++ b/frontend/src/app/routes/models/confirmation.tsx @@ -24,18 +24,9 @@ export const ModelConfirmationPage = () => { }, []); return ( -
+
- + Model Creation Success Icon

Model {modelId} is {isEditMode ? "Updated" : "Created"}! @@ -51,15 +42,11 @@ export const ModelConfirmationPage = () => { title={MODELS_CONTENT.modelCreation.confirmation.buttons.goToModel} nativeAnchor={false} > - +

- These are the rejected mapping results for this training by - users. Some have comments attached to them. + These are the rejected mapping results for this training by users. Some have + comments attached to them.

diff --git a/frontend/src/app/routes/models/model-details-card.tsx b/frontend/src/app/routes/models/model-details-card.tsx index 85374b03a..4a10d98e1 100644 --- a/frontend/src/app/routes/models/model-details-card.tsx +++ b/frontend/src/app/routes/models/model-details-card.tsx @@ -71,24 +71,17 @@ export const ModelDetailsPage = () => { openTrainingAreaDrawer={openDialog} trainingDataset={data?.dataset} /> - + {!data?.published_training ? (
- Model training in progress + Model training in progress

- Model training is not activated yet. Properties will be - available after a successful and activated training. + Model training is not activated yet. Properties will be available after a successful + and activated training.

) : ( - + )}
@@ -103,9 +96,7 @@ export const ModelDetailsPage = () => {
{/* mobile */}
{ return (
- +
- {modelsMapDataIsPending || - modelsMapDataIsError || - mapData.features.length === 0 ? ( + {modelsMapDataIsPending || modelsMapDataIsError || mapData.features.length === 0 ? (
) : ( - + )}
@@ -106,21 +83,11 @@ export const ModelsPage = () => { if (query[SEARCH_PARAMS.layout] === LayoutView.LIST) { return (
- +
); } - return ( - - ); + return ; }; return ( @@ -143,10 +110,7 @@ export const ModelsPage = () => { {/* Mobile filters */} @@ -159,11 +123,7 @@ export const ModelsPage = () => { disabled={Boolean(mapViewIsActive)} />
- + {/* Desktop */}
@@ -179,11 +139,7 @@ export const ModelsPage = () => {
{/* Mobile */}
- +
{isPending ? ( @@ -193,23 +149,12 @@ export const ModelsPage = () => {

{data?.count}{" "} - { - MODELS_CONTENT.models.modelsList.sortingAndPaginationSection - .modelCountSuffix - } + {MODELS_CONTENT.models.modelsList.sortingAndPaginationSection.modelCountSuffix}

- +
- +
{ const modelNotFound = fromPath.includes(APPLICATION_ROUTES.MODELS); - const trainingDatasetNotFound = fromPath.includes( - APPLICATION_ROUTES.DATASETS, - ); + const trainingDatasetNotFound = fromPath.includes(APPLICATION_ROUTES.DATASETS); const fallbackRedirectPath = modelNotFound ? APPLICATION_ROUTES.MODELS @@ -49,11 +47,7 @@ export const PageNotFound = () => { 404 {/* Icon */} - + { const { user } = useAuth(); - const { - data, - isError, - isPending, - isPlaceholderData, - refetch, - query, - updateQuery, - } = useDatasetsQueryParams(user.osm_id); + const { data, isError, isPending, isPlaceholderData, refetch, query, updateQuery } = + useDatasetsQueryParams(user.osm_id); return ( <> @@ -25,9 +18,7 @@ export const UserProfileDatasetsPage = () => {
{/* Section heading */}
- + { const { user } = useAuth(); - const { - data, - isError, - isPending, - isPlaceholderData, - query, - updateQuery, - refetch, - } = useOfflinePredictionsQueryParams(user.osm_id); + const { data, isError, isPending, isPlaceholderData, query, updateQuery, refetch } = + useOfflinePredictionsQueryParams(user.osm_id); const { isOpened, openDialog, closeDialog } = useDialog(); const { isOpened: isPredictionResultOpened, @@ -54,12 +47,9 @@ export const UserProfileOfflinePredictionsPage = () => { } = useDialog(); const [activeTaskId, setActiveTaskId] = useState(null); - const [activePrediction, setActivePrediction] = - useState(null); + const [activePrediction, setActivePrediction] = useState(null); - const [MapSwipeResultsPmtiles, setMapSwipeResultsPmtiles] = useState< - string | null - >(null); + const [MapSwipeResultsPmtiles, setMapSwipeResultsPmtiles] = useState(null); const handleTrainingLogsModal = (taskId: string) => { setActiveTaskId(taskId); openDialog(); @@ -80,9 +70,7 @@ export const UserProfileOfflinePredictionsPage = () => { openMapSwipeProjectStatusDialog(); setMapSwipeResultsPmtiles(null); }; - const handleCreateOrViewMapSwipeProject = ( - prediction: TOfflinePrediction, - ) => { + const handleCreateOrViewMapSwipeProject = (prediction: TOfflinePrediction) => { const mapSwipeProjectExists = prediction.mapswipe_project_id; if (!mapSwipeProjectExists) { openMapSwipeProjectCreationDialog(); @@ -106,9 +94,7 @@ export const UserProfileOfflinePredictionsPage = () => { isOpen={isMapSwipeProjectStatusDialogOpened} onClose={closeMapSwipeProjectStatusDialog} mapSwipeProjectId={activePrediction.mapswipe_project_id as string} - handleMapSwipeProjectResultMapModal={ - handleMapSwipeProjectResultMapModal - } + handleMapSwipeProjectResultMapModal={handleMapSwipeProjectResultMapModal} /> )} @@ -134,11 +120,7 @@ export const UserProfileOfflinePredictionsPage = () => { /> )} {activeTaskId && ( - + )}
@@ -158,12 +140,7 @@ export const UserProfileOfflinePredictionsPage = () => { {data?.count} prediction {data?.count && data?.count > 1 ? "s" : ""}

- +
{ scrollToTopOnPageSwitch />
- +
{query[SEARCH_PARAMS.layout] === LayoutView.LIST ? ( @@ -199,9 +172,7 @@ export const UserProfileOfflinePredictionsPage = () => { isPending={isPending} handleTrainingLogsModal={handleTrainingLogsModal} handlePredictionResultModal={handlePredictionResultModal} - handleCreateOrViewMapSwipeProject={ - handleCreateOrViewMapSwipeProject - } + handleCreateOrViewMapSwipeProject={handleCreateOrViewMapSwipeProject} /> ) : ( { refetch={refetch} handleTrainingLogsModal={handleTrainingLogsModal} handlePredictionResultModal={handlePredictionResultModal} - handleCreateOrViewMapSwipeProject={ - handleCreateOrViewMapSwipeProject - } + handleCreateOrViewMapSwipeProject={handleCreateOrViewMapSwipeProject} /> )}
diff --git a/frontend/src/app/routes/profile/overview.tsx b/frontend/src/app/routes/profile/overview.tsx index 63da76eba..26d528c24 100644 --- a/frontend/src/app/routes/profile/overview.tsx +++ b/frontend/src/app/routes/profile/overview.tsx @@ -1,9 +1,6 @@ import { Head } from "@/components/seo"; import { USER_PROFILE_PAGE_CONTENT } from "@/constants/ui-contents/user-profile-content"; -import { - ProfileStatistics, - UserTrainingHistory, -} from "@/features/user-profile/components"; +import { ProfileStatistics, UserTrainingHistory } from "@/features/user-profile/components"; export const UserProfileOverviewPage = () => { return ( diff --git a/frontend/src/app/routes/profile/settings.tsx b/frontend/src/app/routes/profile/settings.tsx index 307677275..e7b6055aa 100644 --- a/frontend/src/app/routes/profile/settings.tsx +++ b/frontend/src/app/routes/profile/settings.tsx @@ -18,12 +18,10 @@ import { useState } from "react"; export const UserProfileSettingsPage = () => { const { user, setUser } = useAuth(); const [email, setEmail] = useState(user?.email || ""); - const [validity, setValidity] = useState<{ valid: boolean; message: string }>( - { - valid: true, - message: "", - }, - ); + const [validity, setValidity] = useState<{ valid: boolean; message: string }>({ + valid: true, + message: "", + }); const [showForm, setShowForm] = useState(user?.email.length === 0); @@ -43,8 +41,7 @@ export const UserProfileSettingsPage = () => { ), }); - const [isNotificationPending, setIsNotificationPending] = - useState(false); + const [isNotificationPending, setIsNotificationPending] = useState(false); const { mutate: updateEmail } = useUpdateUserProfile({ mutationConfig: { @@ -62,21 +59,17 @@ export const UserProfileSettingsPage = () => { }, }); - const { - mutate: requestEmailVerification, - isPending: emailVerificationRequestIsPending, - } = useEmailVerification({ - mutationConfig: { - onSuccess: () => { - showSuccessToast( - "Email verification instructions has been sent to your email address.", - ); + const { mutate: requestEmailVerification, isPending: emailVerificationRequestIsPending } = + useEmailVerification({ + mutationConfig: { + onSuccess: () => { + showSuccessToast("Email verification instructions has been sent to your email address."); + }, + onError: (error) => { + showErrorToast(error); + }, }, - onError: (error) => { - showErrorToast(error); - }, - }, - }); + }); const { mutate: updateNotifications } = useUpdateUserProfile({ mutationConfig: { @@ -106,29 +99,21 @@ export const UserProfileSettingsPage = () => { {showForm && (
setEmail(e.target.value)} validationStateUpdateCallback={(e) => setValidity(e)} /> - {validity.message && ( - {validity.message} - )} + {validity.message && {validity.message}}
@@ -146,12 +130,7 @@ export const UserProfileSettingsPage = () => { {!user.email_verified && user?.email.length > 0 && (
-

- { - USER_PROFILE_PAGE_CONTENT.settings.form - .emailNotVerifiedMessage - } -

+

{USER_PROFILE_PAGE_CONTENT.settings.form.emailNotVerifiedMessage}

@@ -172,9 +148,7 @@ export const UserProfileSettingsPage = () => {
{USER_PROFILE_PAGE_CONTENT.settings.notifications.notificationTypes.map( @@ -184,9 +158,7 @@ export const UserProfileSettingsPage = () => { className={`flex justify-between items-center transition-opacity duration-200 ${user.email.length === 0 && notification.key !== USER_PROFILE_PAGE_CONTENT.settings.notifications.notificationKeys.webTrainingNotification ? "opacity-50" : ""}`} >
-

- {notification.label} -

+

{notification.label}

{notification.description}

@@ -196,18 +168,14 @@ export const UserProfileSettingsPage = () => { isNotificationPending || (!user.email_verified && notification.key !== - USER_PROFILE_PAGE_CONTENT.settings.notifications - .notificationKeys.webTrainingNotification) || + USER_PROFILE_PAGE_CONTENT.settings.notifications.notificationKeys + .webTrainingNotification) || (user.email.length === 0 && notification.key !== - USER_PROFILE_PAGE_CONTENT.settings.notifications - .notificationKeys.webTrainingNotification) - } - checked={ - notifications[ - notification.key as keyof typeof notifications - ] + USER_PROFILE_PAGE_CONTENT.settings.notifications.notificationKeys + .webTrainingNotification) } + checked={notifications[notification.key as keyof typeof notifications]} handleSwitchChange={(e) => { const updatedNotifications = { ...notifications, @@ -216,8 +184,7 @@ export const UserProfileSettingsPage = () => { setIsNotificationPending(true); updateNotifications( { - newsletter_subscription: - updatedNotifications.monthlyNewsletter, + newsletter_subscription: updatedNotifications.monthlyNewsletter, notifications_delivery_methods: [ ...(updatedNotifications.emailTrainingNotification ? [NotificationDeliveryMethod.MAIL] @@ -232,10 +199,7 @@ export const UserProfileSettingsPage = () => { setNotifications(updatedNotifications); }, onError: (error) => { - showErrorToast( - error, - "Error updating notifications", - ); + showErrorToast(error, "Error updating notifications"); }, }, ); @@ -256,9 +220,7 @@ export const UserProfileSettingsPage = () => { const SectionHeader = ({ sectionTitle }: { sectionTitle: string }) => { return (
-

- {sectionTitle} -

+

{sectionTitle}

); }; diff --git a/frontend/src/app/routes/resources.tsx b/frontend/src/app/routes/resources.tsx index 92397d730..8c91166e6 100644 --- a/frontend/src/app/routes/resources.tsx +++ b/frontend/src/app/routes/resources.tsx @@ -17,9 +17,7 @@ export const ResourcesPage = () => {

{RESOURCES_PAGE_CONTENT.hero.firstSegment}{" "} - - {RESOURCES_PAGE_CONTENT.hero.secondSegment} - {" "} + {RESOURCES_PAGE_CONTENT.hero.secondSegment}{" "} {RESOURCES_PAGE_CONTENT.hero.thirdSegment}{" "}

diff --git a/frontend/src/app/routes/start-mapping.tsx b/frontend/src/app/routes/start-mapping.tsx index 71d5e3686..7ac2398a1 100644 --- a/frontend/src/app/routes/start-mapping.tsx +++ b/frontend/src/app/routes/start-mapping.tsx @@ -1,15 +1,6 @@ import useScreenSize from "@/hooks/use-screen-size"; -import { - APPLICATION_ROUTES, - START_MAPPING_PAGE_CONTENT, - TOAST_NOTIFICATIONS, -} from "@/constants"; -import { - DrawControl, - FitToBounds, - LayerControl, - ZoomLevel, -} from "@/components/map"; +import { APPLICATION_ROUTES, START_MAPPING_PAGE_CONTENT, TOAST_NOTIFICATIONS } from "@/constants"; +import { DrawControl, FitToBounds, LayerControl, ZoomLevel } from "@/components/map"; import { Head } from "@/components/seo"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useMapInstance } from "@/hooks/use-map-instance"; @@ -46,12 +37,7 @@ import { ImagerySourceSelector } from "@/features/start-mapping/components/repli import { useDialog } from "@/hooks/use-dialog"; import { useModelPredictionStore } from "@/store/model-prediction-store"; import { ModelSelector } from "@/features/start-mapping/components/replicable-models/model-selector"; -import { - BASE_MODELS, - DrawingModes, - TileServiceType, - ToolTipPlacement, -} from "@/enums"; +import { BASE_MODELS, DrawingModes, TileServiceType, ToolTipPlacement } from "@/enums"; import { useTileservice } from "@/hooks/use-tileservice"; import { ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, @@ -99,9 +85,7 @@ const defaultQuery = { [SEARCH_PARAMS.maxAngleChange]: 15, }; -const getMergedQueryFromSearchParams = ( - params: URLSearchParams, -): TQueryParams => { +const getMergedQueryFromSearchParams = (params: URLSearchParams): TQueryParams => { return { [SEARCH_PARAMS.orthogonalize]: params.get(SEARCH_PARAMS.orthogonalize) !== null @@ -132,8 +116,7 @@ const getMergedQueryFromSearchParams = ( [SEARCH_PARAMS.imagery]: params.get(SEARCH_PARAMS.imagery) ?? undefined, [SEARCH_PARAMS.predictionModelCheckpoint]: params.get(SEARCH_PARAMS.predictionModelCheckpoint) ?? undefined, - [SEARCH_PARAMS.tileserver]: - params.get(SEARCH_PARAMS.tileserver) ?? undefined, + [SEARCH_PARAMS.tileserver]: params.get(SEARCH_PARAMS.tileserver) ?? undefined, [SEARCH_PARAMS.mode]: params.get(SEARCH_PARAMS.mode) ?? undefined, }; }; @@ -141,10 +124,7 @@ const getMergedQueryFromSearchParams = ( export const StartMappingPage = () => { const { modelId } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); - const { map, mapContainerRef, setDrawingMode, terraDraw } = useMapInstance( - false, - true, - ); + const { map, mapContainerRef, setDrawingMode, terraDraw } = useMapInstance(false, true); const { isSmallViewport } = useScreenSize(); const { goBack } = useHistory(); @@ -155,30 +135,23 @@ export const StartMappingPage = () => { clearFeatures, } = useModelPredictionStore(); - const [offlinePredictionAOI, setOfflinePredictionAOI] = - useState(null); + const [offlinePredictionAOI, setOfflinePredictionAOI] = useState(null); const acceptedFeatures = useMemo( - () => - modelPredictions.filter( - (f) => f.properties.status === PredictedFeatureStatus.ACCEPTED, - ), + () => modelPredictions.filter((f) => f.properties.status === PredictedFeatureStatus.ACCEPTED), [modelPredictions], ); const navigate = useNavigate(); - const [openMobileDrawer, setOpenMobileDrawer] = - useState(isSmallViewport); + const [openMobileDrawer, setOpenMobileDrawer] = useState(isSmallViewport); useEffect(() => { setOpenMobileDrawer(isSmallViewport); }, [isSmallViewport]); - const [ - customPredictionModelCheckpointPath, - setCustomPredictionModelCheckpointPath, - ] = useState(""); + const [customPredictionModelCheckpointPath, setCustomPredictionModelCheckpointPath] = + useState(""); const updateQuery = useCallback( (newParams: TQueryParams) => { @@ -190,11 +163,7 @@ export const StartMappingPage = () => { for (const [key, value] of Object.entries(updated)) { // Skip keys equal to default or null/undefined // This is to prevent the same settings from repeating in the url - if ( - value !== undefined && - value !== null && - value !== defaultQuery[key] - ) { + if (value !== undefined && value !== null && value !== defaultQuery[key]) { updatedParams.set(key, String(value)); } } @@ -232,11 +201,9 @@ export const StartMappingPage = () => { updateQuery({ [SEARCH_PARAMS.imagery]: newValue }); }; - const [predictionModelCheckpoint, setPredictionModelCheckpoint] = - useState(""); + const [predictionModelCheckpoint, setPredictionModelCheckpoint] = useState(""); - const predictionModel = - searchParams.get(SEARCH_PARAMS.model) ?? PredictionModel.DEFAULT; + const predictionModel = searchParams.get(SEARCH_PARAMS.model) ?? PredictionModel.DEFAULT; const setPredictionModel = (newValue: string) => { updateQuery({ [SEARCH_PARAMS.model]: newValue }); @@ -308,15 +275,10 @@ export const StartMappingPage = () => { * Only update the checkpoint if the modelInfo is available and * the predictionModel is not set or is set to the default model. */ - if ( - modelInfo && - (!predictionModel || predictionModel === PredictionModel.DEFAULT) - ) { + if (modelInfo && (!predictionModel || predictionModel === PredictionModel.DEFAULT)) { setPredictionModelCheckpoint(constructModelCheckpointPath(modelInfo)); } else if (predictionModel && predictionModel !== PredictionModel.CUSTOM) { - setPredictionModelCheckpoint( - FAIR_BASE_MODELS_PATH[predictionModel as BASE_MODELS], - ); + setPredictionModelCheckpoint(FAIR_BASE_MODELS_PATH[predictionModel as BASE_MODELS]); } }, [predictionModel, modelInfo]); @@ -353,8 +315,7 @@ export const StartMappingPage = () => { useEffect(() => { if (customPredictionModelCheckpointPath) { updateQuery({ - [SEARCH_PARAMS.predictionModelCheckpoint]: - customPredictionModelCheckpointPath, + [SEARCH_PARAMS.predictionModelCheckpoint]: customPredictionModelCheckpointPath, }); } else { updateQuery({ [SEARCH_PARAMS.predictionModelCheckpoint]: undefined }); @@ -412,10 +373,7 @@ export const StartMappingPage = () => { /** * Check if the offline prediction AOI is valid. */ - const isOfflineMode = useMemo( - () => currentMode === MapMode.OFFLINE, - [currentMode], - ); + const isOfflineMode = useMemo(() => currentMode === MapMode.OFFLINE, [currentMode]); /** * Handle the finish event of the TerraDraw instance. @@ -555,9 +513,7 @@ export const StartMappingPage = () => { ...(modelPredictions.length > 0 ? [ { - value: - START_MAPPING_PAGE_CONTENT.map.controls.legendControl - .predictionResults, + value: START_MAPPING_PAGE_CONTENT.map.controls.legendControl.predictionResults, subLayers: [ ALL_MODEL_PREDICTIONS_FILL_LAYER_ID, ALL_MODEL_PREDICTIONS_OUTLINE_LAYER_ID, @@ -624,12 +580,7 @@ export const StartMappingPage = () => { const handleFeaturesDownloadToJOSM = useCallback( (features: Feature[]) => { if (!map || !modelInfo?.dataset) return; - openInJOSM( - modelInfo.dataset.name, - modelInfo.dataset.source_imagery, - features, - true, - ); + openInJOSM(modelInfo.dataset.name, modelInfo.dataset.source_imagery, features, true); }, [map, modelInfo], ); @@ -665,28 +616,21 @@ export const StartMappingPage = () => { name: START_MAPPING_PAGE_CONTENT.buttons.download.options.acceptedFeatures( isSmallViewport ? "Accepted" : "Download accepted", ), - value: - START_MAPPING_PAGE_CONTENT.buttons.download.options.acceptedFeatures( - isSmallViewport ? "Accepted" : "Download accepted", - ), + value: START_MAPPING_PAGE_CONTENT.buttons.download.options.acceptedFeatures( + isSmallViewport ? "Accepted" : "Download accepted", + ), onClick: handleAcceptedFeaturesDownload, showOnMobile: true, }, { - name: START_MAPPING_PAGE_CONTENT.buttons.download.options - .openAllFeaturesInJOSM, - value: - START_MAPPING_PAGE_CONTENT.buttons.download.options - .openAllFeaturesInJOSM, + name: START_MAPPING_PAGE_CONTENT.buttons.download.options.openAllFeaturesInJOSM, + value: START_MAPPING_PAGE_CONTENT.buttons.download.options.openAllFeaturesInJOSM, onClick: handleAllFeaturesDownloadToJOSM, showOnMobile: false, }, { - name: START_MAPPING_PAGE_CONTENT.buttons.download.options - .openAcceptedFeaturesInJOSM, - value: - START_MAPPING_PAGE_CONTENT.buttons.download.options - .openAcceptedFeaturesInJOSM, + name: START_MAPPING_PAGE_CONTENT.buttons.download.options.openAcceptedFeaturesInJOSM, + value: START_MAPPING_PAGE_CONTENT.buttons.download.options.openAcceptedFeaturesInJOSM, onClick: handleAcceptedFeaturesDownloadToJOSM, showOnMobile: false, }, @@ -820,12 +764,8 @@ export const StartMappingPage = () => { setPredictionModel={setPredictionModel} predictionModelCheckpoint={predictionModelCheckpoint} setPredictionModelCheckpoint={setPredictionModelCheckpoint} - customPredictionModelCheckpointPath={ - customPredictionModelCheckpointPath - } - setCustomPredictionModelCheckpointPath={ - setCustomPredictionModelCheckpointPath - } + customPredictionModelCheckpointPath={customPredictionModelCheckpointPath} + setCustomPredictionModelCheckpointPath={setCustomPredictionModelCheckpointPath} defaultPredictionModel={modelInfo?.name} isMobile /> @@ -870,12 +810,8 @@ export const StartMappingPage = () => { setPredictionModel={setPredictionModel} predictionModelCheckpoint={predictionModelCheckpoint} setPredictionModelCheckpoint={setPredictionModelCheckpoint} - customPredictionModelCheckpointPath={ - customPredictionModelCheckpointPath - } - setCustomPredictionModelCheckpointPath={ - setCustomPredictionModelCheckpointPath - } + customPredictionModelCheckpointPath={customPredictionModelCheckpointPath} + setCustomPredictionModelCheckpointPath={setCustomPredictionModelCheckpointPath} openModelSelectionDialog={handlePredictionModelDialogOpen} setTileServiceType={setTileServiceType} setTileserverURL={setTileserverURL} @@ -889,9 +825,7 @@ export const StartMappingPage = () => { isSmallViewport={isSmallViewport} isOfflineMode={isOfflineMode} hasDrawnAOI={hasDrawnAOI} - openOfflinePredictionRequestDialog={ - handleOfflinePredictionRequestDialogOpen - } + openOfflinePredictionRequestDialog={handleOfflinePredictionRequestDialogOpen} /> )} {/* Mobile bottom sheet */} @@ -914,12 +848,8 @@ export const StartMappingPage = () => { setPredictionModel={setPredictionModel} predictionModelCheckpoint={predictionModelCheckpoint} setPredictionModelCheckpoint={setPredictionModelCheckpoint} - customPredictionModelCheckpointPath={ - customPredictionModelCheckpointPath - } - setCustomPredictionModelCheckpointPath={ - setCustomPredictionModelCheckpointPath - } + customPredictionModelCheckpointPath={customPredictionModelCheckpointPath} + setCustomPredictionModelCheckpointPath={setCustomPredictionModelCheckpointPath} setTileServiceType={setTileServiceType} setTileserverURL={setTileserverURL} tileServiceTypeValidity={tileServiceTypeValidity} @@ -932,9 +862,7 @@ export const StartMappingPage = () => { isSmallViewport={isSmallViewport} isOfflineMode={isOfflineMode} hasDrawnAOI={hasDrawnAOI} - openOfflinePredictionRequestDialog={ - handleOfflinePredictionRequestDialogOpen - } + openOfflinePredictionRequestDialog={handleOfflinePredictionRequestDialogOpen} stopMappingFn={stopMapping} />
@@ -966,9 +894,7 @@ export const StartMappingPage = () => { > @@ -977,22 +903,14 @@ export const StartMappingPage = () => { onClick={openFileUploadDialog} disabled={hasDrawnAOI} > - +
)}
- +
diff --git a/frontend/src/app/routes/try-fair.tsx b/frontend/src/app/routes/try-fair.tsx index 904730cd8..593331d80 100644 --- a/frontend/src/app/routes/try-fair.tsx +++ b/frontend/src/app/routes/try-fair.tsx @@ -1,11 +1,6 @@ import { Head } from "@/components/seo"; import { TRY_FAIR_PAGE_CONTENT } from "@/constants/ui-contents/try-fair-contents"; -import { - ImagerySource, - ModelType, - TryFairMapOutputType, - TryFairResolution, -} from "@/enums"; +import { ImagerySource, ModelType, TryFairMapOutputType, TryFairResolution } from "@/enums"; import { TryFairMap } from "@/features/try-fair/components/map/try-fair-map"; import { TryFairSidebar } from "@/features/try-fair/components/try-fair-sidebar"; import { ModelPickerContent } from "@/features/try-fair/components/model-picker-modal"; @@ -13,19 +8,13 @@ import { getSelectedModel } from "@/features/try-fair/utils/models"; import { useMapInstance } from "@/hooks/use-map-instance"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTryFairParams } from "@/features/try-fair/hooks/use-try-fair-params"; -import { - useStacBaseModels, - useStacLocalModels, -} from "@/features/try-fair/hooks/use-base-models"; +import { useStacBaseModels, useStacLocalModels } from "@/features/try-fair/hooks/use-base-models"; import { BBOX } from "@/types"; import { MapLargeAreaModal } from "@/features/try-fair/components/start-mapping/map-large-area-modal"; import { useFairPredict } from "@/features/try-fair/hooks/use-fair-predict"; import { useTryFairImagery } from "@/features/try-fair/hooks/use-try-fair-imagery"; import { useTryFairTour } from "@/features/try-fair/hooks/use-try-fair-tour"; -import { - BaseModelStacItem, - getInferenceParams, -} from "@/features/try-fair/api/stac"; +import { BaseModelStacItem, getInferenceParams } from "@/features/try-fair/api/stac"; import { useImageryMappingModel } from "@/features/try-fair/hooks/use-imagery-mapping-model"; import useScreenSize from "@/hooks/use-screen-size"; import { MobileDrawer } from "@/components/ui/drawer"; @@ -72,8 +61,7 @@ export const TryFairPage = () => { setOutputType: state.setOutputType, })), ); - const { closeGuidedTour, openGuidedTour, recordMapRun } = - useTryFairTour(isSmallViewport); + const { closeGuidedTour, openGuidedTour, recordMapRun } = useTryFairTour(isSmallViewport); const { modelId, @@ -103,18 +91,11 @@ export const TryFairPage = () => { const { recentImageries, addRecentImagery } = useRecentImageries(); const { models: allModels, loading: modelsLoading } = useStacBaseModels(); - const { models: localModels, loading: localModelLoading } = - useStacLocalModels(); + const { models: localModels, loading: localModelLoading } = useStacLocalModels(); - const models = useMemo( - () => [...allModels, ...localModels], - [allModels, localModels], - ); + const models = useMemo(() => [...allModels, ...localModels], [allModels, localModels]); - const selectedModel = useMemo( - () => getSelectedModel(models, modelId), - [models, modelId], - ); + const selectedModel = useMemo(() => getSelectedModel(models, modelId), [models, modelId]); const { imageryBounds, imageryCenter, @@ -185,14 +166,11 @@ export const TryFairPage = () => { map.dragRotate.disable(); map.touchZoomRotate.disable(); map.touchPitch.disable(); - map.fitBounds( - [latestBBox[0], latestBBox[1], latestBBox[2], latestBBox[3]], - { - padding: 40, - duration: GRID_ZOOM_IN_DURATION, - essential: true, - }, - ); + map.fitBounds([latestBBox[0], latestBBox[1], latestBBox[2], latestBBox[3]], { + padding: 40, + duration: GRID_ZOOM_IN_DURATION, + essential: true, + }); map.once("moveend", () => { map.dragPan.enable(); map.scrollZoom.enable(); @@ -206,8 +184,7 @@ export const TryFairPage = () => { }, [latestBBox, map]); // Map Large Area (Export → Map Large Area). Opens when downloadType is set to - const [isLargeAreaSuccessDialogOpen, setIsLargeAreaSuccessDialogOpen] = - useState(false); + const [isLargeAreaSuccessDialogOpen, setIsLargeAreaSuccessDialogOpen] = useState(false); const handleLargeAreaSubmit = () => { setDownloadType(""); setIsLargeAreaSuccessDialogOpen(true); @@ -240,9 +217,7 @@ export const TryFairPage = () => { // Reset confidence threshold to model's spec default if available const infParams = getInferenceParams(model); - const confidenceParam = infParams.find( - (p) => p.key === "confidence_threshold", - ); + const confidenceParam = infParams.find((p) => p.key === "confidence_threshold"); if (confidenceParam && typeof confidenceParam.spec.default === "number") { setConfidence(confidenceParam.spec.default); } @@ -269,13 +244,7 @@ export const TryFairPage = () => { }, []); const handleMap = useCallback(() => { - if ( - hasNoModelsForFeature || - !modelForMapping || - !mappingModelId || - !modelUri || - !latestBBox - ) + if (hasNoModelsForFeature || !modelForMapping || !mappingModelId || !modelUri || !latestBBox) return; closeGuidedTour(); @@ -345,16 +314,10 @@ export const TryFairPage = () => { setSeletedImagery(selection); setMode(ModelType.IMAGERY); setImagery({ - url: - selection.source === ImagerySource.CUSTOM ? selection.tileUrl : null, + url: selection.source === ImagerySource.CUSTOM ? selection.tileUrl : null, tileServiceType: - selection.source === ImagerySource.CUSTOM - ? selection.tileServiceType - : null, - oamItemId: - selection.source === ImagerySource.OPEN_AERIAL_MAP - ? selection.item.id - : null, + selection.source === ImagerySource.CUSTOM ? selection.tileServiceType : null, + oamItemId: selection.source === ImagerySource.OPEN_AERIAL_MAP ? selection.item.id : null, }); // Invalidate the last prediction so the Map button re-enables and stale // predictions clear when the imagery changes. @@ -365,9 +328,7 @@ export const TryFairPage = () => { // Add to recent imageries list. const isOam = selection.source === ImagerySource.OPEN_AERIAL_MAP; const bounds = selection.bounds ?? null; - const center = bounds - ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] - : null; + const center = bounds ? [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2] : null; const addEntry = (country = "", countryCode = "") => { addRecentImagery({ diff --git a/frontend/src/components/__tests__/learn/video-player-modal.test.tsx b/frontend/src/components/__tests__/learn/video-player-modal.test.tsx index c06529569..fd5b45532 100644 --- a/frontend/src/components/__tests__/learn/video-player-modal.test.tsx +++ b/frontend/src/components/__tests__/learn/video-player-modal.test.tsx @@ -12,9 +12,7 @@ describe("VideoPlayerModal", () => { }; it("renders the video player modal", () => { - render( - , - ); + render(); // Check if the iframe is rendered const iframe = screen.getByTitle("Test Video"); diff --git a/frontend/src/components/errors/under-construction.tsx b/frontend/src/components/errors/under-construction.tsx index 36fca1646..70435cf31 100644 --- a/frontend/src/components/errors/under-construction.tsx +++ b/frontend/src/components/errors/under-construction.tsx @@ -10,10 +10,7 @@ export const PageUnderConstruction = () => {

- diff --git a/frontend/src/components/landing/about-fair/about-fair.tsx b/frontend/src/components/landing/about-fair/about-fair.tsx index 56dde0a9b..de3b03917 100644 --- a/frontend/src/components/landing/about-fair/about-fair.tsx +++ b/frontend/src/components/landing/about-fair/about-fair.tsx @@ -8,9 +8,7 @@ export const WhatIsFAIR = () => {

{SHARED_CONTENT.homepage.aboutTitle}

-

- {SHARED_CONTENT.homepage.aboutContent} -

+

{SHARED_CONTENT.homepage.aboutContent}

AI Icon diff --git a/frontend/src/components/landing/core-features/core-features.module.css b/frontend/src/components/landing/core-features/core-features.module.css index d6c90e7c6..1e8a463cc 100644 --- a/frontend/src/components/landing/core-features/core-features.module.css +++ b/frontend/src/components/landing/core-features/core-features.module.css @@ -26,12 +26,11 @@ display: flex; align-items: center; justify-content: center; - --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), - 0 4px 6px -4px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), - 0 4px 6px -4px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), - var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: + 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: + var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .coreFeatureIcon { diff --git a/frontend/src/components/landing/core-values/core-values.tsx b/frontend/src/components/landing/core-values/core-values.tsx index 30a199549..aad564539 100644 --- a/frontend/src/components/landing/core-values/core-values.tsx +++ b/frontend/src/components/landing/core-values/core-values.tsx @@ -40,17 +40,13 @@ export const Corevalues = () => {

- - {SHARED_CONTENT.homepage.coreValues.sectionTitle.thirdSegment} - + {SHARED_CONTENT.homepage.coreValues.sectionTitle.thirdSegment}

{SHARED_CONTENT.homepage.coreValues.sectionTitle.fourthSegment}

- - {SHARED_CONTENT.homepage.coreValues.sectionTitle.fifthSegment} - + {SHARED_CONTENT.homepage.coreValues.sectionTitle.fifthSegment}

@@ -67,12 +63,8 @@ export const Corevalues = () => { className={styles.image} /> {/* The rectangles */} -
-
+
+
@@ -84,12 +76,8 @@ export const Corevalues = () => { {/* Humans not replaced */}
-

- {SHARED_CONTENT.homepage.coreValues.humansNotReplaced.title} -

-

- {SHARED_CONTENT.homepage.coreValues.humansNotReplaced.description} -

+

{SHARED_CONTENT.homepage.coreValues.humansNotReplaced.title}

+

{SHARED_CONTENT.homepage.coreValues.humansNotReplaced.description}

{ className={styles.image} /> {/* The rectangles */} -
-
+
+
diff --git a/frontend/src/components/landing/cta/cta.module.css b/frontend/src/components/landing/cta/cta.module.css index 5f198eca5..2ffa6e6de 100644 --- a/frontend/src/components/landing/cta/cta.module.css +++ b/frontend/src/components/landing/cta/cta.module.css @@ -59,11 +59,7 @@ top: -90px; right: 0; left: 0; - background-image: radial-gradient( - circle, - var(--hot-fair-color-primary) 1px, - transparent 1px - ); + background-image: radial-gradient(circle, var(--hot-fair-color-primary) 1px, transparent 1px); background-size: var(--sl-spacing-2x-small) var(--sl-spacing-2x-small); } diff --git a/frontend/src/components/landing/kpi/kpi.tsx b/frontend/src/components/landing/kpi/kpi.tsx index 2b08c081e..b888dd6cd 100644 --- a/frontend/src/components/landing/kpi/kpi.tsx +++ b/frontend/src/components/landing/kpi/kpi.tsx @@ -3,12 +3,7 @@ import { KPI_STATS_CACHE_TIME_MS } from "@/config"; import { SHARED_CONTENT } from "@/constants"; import { useQuery } from "@tanstack/react-query"; import { useState, useEffect, FC } from "react"; -import { - BotIcon, - FeedbackIcon, - PeopleIcon, - ProductionCheckmarkIcon, -} from "@/components/ui/icons"; +import { BotIcon, FeedbackIcon, PeopleIcon, ProductionCheckmarkIcon } from "@/components/ui/icons"; import { IconProps } from "@/types"; type TKPIS = { @@ -77,17 +72,12 @@ export const Kpi = () => { return (
{KPIs.map((kpi, id) => ( -
+
-

+

{kpi.figure?.toLocaleString()}

{kpi.label}

diff --git a/frontend/src/components/landing/tagline/tagline.tsx b/frontend/src/components/landing/tagline/tagline.tsx index a235447fe..c3d8bd45e 100644 --- a/frontend/src/components/landing/tagline/tagline.tsx +++ b/frontend/src/components/landing/tagline/tagline.tsx @@ -7,9 +7,7 @@ export const TaglineBanner = () => {

{SHARED_CONTENT.homepage.tagline.firstSegment} {SHARED_CONTENT.homepage.tagline.secondSegment} - - {SHARED_CONTENT.homepage.tagline.thirdSegment} - + {SHARED_CONTENT.homepage.tagline.thirdSegment} {SHARED_CONTENT.homepage.tagline.fourthSegment} {SHARED_CONTENT.homepage.tagline.fifthSegment}

diff --git a/frontend/src/components/layouts/footer/footer.tsx b/frontend/src/components/layouts/footer/footer.tsx index 02491b11c..a5cb0ee18 100644 --- a/frontend/src/components/layouts/footer/footer.tsx +++ b/frontend/src/components/layouts/footer/footer.tsx @@ -2,13 +2,7 @@ import { CreativeCommonsBadge } from "@/assets/images"; import { Image } from "@/components/ui/image"; import { Link } from "@/components/ui/link"; import { SHARED_CONTENT } from "@/constants"; -import { - FacebookIcon, - GitHubIcon, - InstagramIcon, - XIcon, - YouTubeIcon, -} from "@/assets/svgs"; +import { FacebookIcon, GitHubIcon, InstagramIcon, XIcon, YouTubeIcon } from "@/assets/svgs"; import { MadeWithLove } from "@/components/shared"; import { footerLinks } from "@/constants/general"; @@ -106,11 +100,7 @@ export const Footer = () => { className="w-7 h-7 flex items-center justify-center bg-white rounded-full" > - {`${media.name} + {`${media.name} ))} diff --git a/frontend/src/components/layouts/model-forms-layout.tsx b/frontend/src/components/layouts/model-forms-layout.tsx index b6b20a04a..d883908b6 100644 --- a/frontend/src/components/layouts/model-forms-layout.tsx +++ b/frontend/src/components/layouts/model-forms-layout.tsx @@ -3,10 +3,7 @@ import { Head } from "@/components/seo"; import { MODELS_CONTENT, MODELS_ROUTES } from "@/constants"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; -import { - ProgressBar, - ProgressButtons, -} from "@/features/model-creation/components"; +import { ProgressBar, ProgressButtons } from "@/features/model-creation/components"; import { CloudIcon, DatabaseIcon, @@ -15,10 +12,7 @@ import { StarIcon, TagsIcon, } from "@/components/ui/icons"; -import { - ModelsProvider, - useModelsContext, -} from "@/app/providers/models-provider"; +import { ModelsProvider, useModelsContext } from "@/app/providers/models-provider"; import { APP_TOUR_IDS } from "@/constants/site-tour"; import { AppTourProvider } from "@/app/providers/tour-provider"; import { TrainingAreaTourDialog } from "@/features/model-creation/components/dialogs/training-area-tour-dialog"; @@ -81,10 +75,7 @@ export const ModelFormsLayout = () => { return ( - +
@@ -134,13 +125,8 @@ const ModelFormRouteValidator = ({ currentPageIndex: number; }) => { const navigate = useNavigate(); - const { - formData, - hasLabeledTrainingAreas, - getFullPath, - validateEditMode, - isEditMode, - } = useModelsContext(); + const { formData, hasLabeledTrainingAreas, getFullPath, validateEditMode, isEditMode } = + useModelsContext(); useEffect(() => { if (isEditMode && !validateEditMode) return; @@ -148,26 +134,19 @@ const ModelFormRouteValidator = ({ const prevRoute = getFullPath(pages[currentPageIndex - 1].path); if (pathname.includes(MODELS_ROUTES.TRAINING_DATASET)) { // When a user is in the training dataset page, they must have filled the model details page form. - if (!formData.modelName && !formData.modelDescription) - navigate(prevRoute); + if (!formData.modelName && !formData.modelDescription) navigate(prevRoute); } else if (pathname.includes(MODELS_ROUTES.TRAINING_AREA)) { // When a user is in the training area, they must have completed the training dataset form - if (!formData.selectedTrainingDatasetId || !formData.tmsURL) - navigate(prevRoute); + if (!formData.selectedTrainingDatasetId || !formData.tmsURL) navigate(prevRoute); } else if (pathname.includes(MODELS_ROUTES.TRAINING_SETTINGS)) { // When a user is in the training settings, they must have completed the training area - if (!formData.datasetName || !hasLabeledTrainingAreas) - navigate(prevRoute); + if (!formData.datasetName || !hasLabeledTrainingAreas) navigate(prevRoute); } else if (pathname.includes(MODELS_ROUTES.MODEL_SUMMARY)) { // When a user is in the summary page, they must have zoom levels set from the settings // and datastet name set from the training dataset // and training areas with their labels fetched - if ( - formData.zoomLevels.length === 0 || - !hasLabeledTrainingAreas || - !formData.datasetName - ) + if (formData.zoomLevels.length === 0 || !hasLabeledTrainingAreas || !formData.datasetName) navigate(prevRoute); } }, [pathname, formData, currentPageIndex, validateEditMode]); diff --git a/frontend/src/components/layouts/navbar/nav-logo.tsx b/frontend/src/components/layouts/navbar/nav-logo.tsx index 2912b6a34..f7981f284 100644 --- a/frontend/src/components/layouts/navbar/nav-logo.tsx +++ b/frontend/src/components/layouts/navbar/nav-logo.tsx @@ -15,16 +15,8 @@ export const NavLogo = ({}: { onClick?: () => void }) => { title={SHARED_CONTENT.navbar.logoAlt} className="flex items-center gap-2" > - {SHARED_CONTENT.navbar.logoAlt} -

- fAIr -

+ {SHARED_CONTENT.navbar.logoAlt} +

fAIr

); }; diff --git a/frontend/src/components/layouts/navbar/navbar.tsx b/frontend/src/components/layouts/navbar/navbar.tsx index 9f5db6b05..09ebb38a7 100644 --- a/frontend/src/components/layouts/navbar/navbar.tsx +++ b/frontend/src/components/layouts/navbar/navbar.tsx @@ -58,8 +58,7 @@ export const NavBar = () => { const location = useLocation(); const isTryFairPage = location.pathname.includes(APPLICATION_ROUTES.TRY_FAIR); - const isAuthenticated = - (isTryFairPage && DISABLE_AUTH_ON_TRY_FAIR) || _isAuthenticated; + const isAuthenticated = (isTryFairPage && DISABLE_AUTH_ON_TRY_FAIR) || _isAuthenticated; const isHankoAuth = AUTH_PROVIDER === "hanko"; return ( <> @@ -72,19 +71,13 @@ export const NavBar = () => {
-
{!isTryFairPage && (
- +
)} {_isAuthenticated && } @@ -102,11 +95,7 @@ export const NavBar = () => { /> )} <> - + @@ -123,19 +112,13 @@ export const NavBar = () => {
-
@@ -28,12 +26,7 @@ export const GuideCard = ({ guide }: { guide: TGuide }) => { {LEARN_PAGE_CONTENT.comingSoonText} ) : guide.isLink ? ( - + )}
diff --git a/frontend/src/components/learn/learn-course-card.tsx b/frontend/src/components/learn/learn-course-card.tsx index 8c2f0c254..9dcf509fe 100644 --- a/frontend/src/components/learn/learn-course-card.tsx +++ b/frontend/src/components/learn/learn-course-card.tsx @@ -10,44 +10,30 @@ export const CourseCard = ({ course }: { course: TCourse }) => {
- {course.title} + {course.title}
{!course.available && (
- - {LEARN_PAGE_CONTENT.comingSoonText} - + {LEARN_PAGE_CONTENT.comingSoonText}
)}
-

- {course.title} -

+

{course.title}

-

- {course.courseLength} -

+

{course.courseLength}

-

- {course.language} -

+

{course.language}

-

- {course.duration} -

+

{course.duration}

diff --git a/frontend/src/components/learn/update-card.tsx b/frontend/src/components/learn/update-card.tsx index cc9fc6454..538159df2 100644 --- a/frontend/src/components/learn/update-card.tsx +++ b/frontend/src/components/learn/update-card.tsx @@ -6,13 +6,7 @@ import { APPLICATION_ROUTES } from "@/constants"; import useCopyToClipboard from "@/hooks/use-clipboard"; import { ToolTip } from "@/components/ui/tooltip"; -export const UpdateCard = ({ - update, - onClick, -}: { - update: TFairVideo; - onClick?: () => void; -}) => { +export const UpdateCard = ({ update, onClick }: { update: TFairVideo; onClick?: () => void }) => { const { copyToClipboard, isCopied } = useCopyToClipboard(); const handleShareClick = async (e: React.MouseEvent) => { e.stopPropagation(); @@ -53,11 +47,7 @@ export const UpdateCard = ({

{formatDate(update.date, true)}

-

- {video.name} -

+

{video.name}