Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df6264a0 authored by Romain Hunault's avatar Romain Hunault 💻
Browse files

Display an app compatibilty score for each device

parent a59ce373
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
# AGENTS Methods Log
- 2025-12-04: Added `scripts/build_site.rb` to read the JSON-based config, echo the `VERSION`, respect `LANGUAGES` overrides, emit `dynamic/selected-languages.yml`, log failures into `build_site.err.txt`, and run Jekyll through the generated config so Docker and development use the same workflow.
- 2025-12-04: Centralized install-mode metadata in `htdocs/_data/install_modes.yml`, normalized the string keys in `devices.html`, and let the template honor shared URLs/labels plus per-device overrides instead of duplicating the logic.
- 2025-12-04: Implemented the compatibility scoring table inside `htdocs/_i18n/en/pages/devices.html`, scoring each device on bootloader state, build type, certification status, Play Integrity, Rootbeer results, and channel weight, while FP6 and Sunfish now expose the input map.
- 2025-12-03: Noted that the public Docker image `jekyll/jekyll:4` is the supported tag that bundles the Jekyll 4.4.1 runtime required for this project.
- 2025-12-03: Documented the docker commands (`docker run --rm -v "$PWD/htdocs:/srv/jekyll" -p 4000:4000 -it jekyll/jekyll:4 jekyll serve`) to regenerate the Gemfile.lock and to serve the site locally for validation after dependency bumps.
- 2025-12-03: Adopted `hadolint` (via `docker run --rm -i hadolint/hadolint < Dockerfile.jekyll`) as the targeted lint step for Dockerfile updates and combined related RUN commands to satisfy rule DL3059.
+90 −30
Original line number Diff line number Diff line
# General information
The current documentation uses [Jekyll](https://jekyllrb.com/docs) as the static site generator. [Docker](https://www.docker.com/) is required to run the project.
# e.foundation Documentation Site

# How to run
## Overview
The documentation site is a **multi-language Jekyll** project that publishes the /e/ community documentation with an ElasticSearch-backed search layer. A dedicated Docker image (see `Dockerfile`) now orchestrates builds through a helper script that prints the configured version and dynamically limits the languages that are compiled.

## Jekyll container only
## Prerequisites
- Docker (20.10+)
- `docker-compose` 1.29+ for the ElasticSearch + bridge stack
- When working locally without Docker, Ruby 3.x (comes preinstalled inside the `jekyll/jekyll:4` container) is required to run `scripts/build_site.rb` manually.

For faster development process, the following command is recommended:
## Installation & Initialization
1. Clone the repository and enter the directory.
2. Copy the development environment file and customize values:
   ```bash
   cp .env.dev .env
   ```
3. Build the Docker image and start the stack:
   ```bash
   docker build --tag registry.gitlab.e.foundation/e/documentation/user:dev-image .
   docker-compose up -d
   ```
4. Seed the search index after the static files are built:
   ```bash
   docker-compose exec -T bridge_server python main.py /public_html
   ```
5. The site is served through NGINX at the host configured in `.env` (default `edocs.local`).

## Configuration
All non-confidential constants live in `config/param.json`. Each entry has a matching environment variable of the same name and the schema is documented in `config/param.json.example`. Update the `VERSION` field in the example file (format `x.y.z`, bump `z`) whenever you make a user-facing change so debugging reflects the current build.

| Parameter | Default | Description | Format / Notes |
|---|---|---|---|
| `VERSION` | `0.0.1` | Version exposed every time the build script runs; used by the Docker build stage for transparency. | `x.y.z` string; bump `z` per request. |
| `LANGUAGES` | `["en","fr","it","es","de"]` | Languages that are compiled by default. Override with the `LANGUAGES` environment variable to restrict the build (e.g. `LANGUAGES=en`). | JSON array of ISO codes; the script falls back to this value when the env var is absent. |
| `BUILD_OUTPUT_DIR` | `/tmp/e_docs_website` | Directory where Jekyll writes the generated HTML. | String path; can be changed via the `BUILD_OUTPUT_DIR` env var for testing. |
| `JEKYLL_CONFIG` | `_config.yml` | Base Jekyll config file(s). The build script appends the generated language override file (see below). | Can be a comma-separated list; override via `JEKYLL_CONFIG` env var if you need extra config files. |

Secrets belong in `config/secret.json` (copy the `.example` version) and are never checked into production. Keep actual secrets local and, if needed, add their keys to the `secret.json.example` template before sharing the repository.

### Personalization files
Any artifact that is personalized per user or environment is stored under the `dynamic/` directory. The repository tracks only `dynamic/.gitkeep`; everything else under `dynamic/` is ignored. The build script writes `dynamic/selected-languages.yml` on every run to override the `languages` key for Jekyll (see next section).

## Build & Development Workflow
### Language-aware builds with the helper script
`Dockerfile` now relies on `scripts/build_site.rb` instead of calling `jekyll build` directly. This script:
- Parses `config/param.json` and respects the `LANGUAGES` env var when provided (comma-separated values).
- Prints the configured `VERSION` before the build begins.
- Generates `dynamic/selected-languages.yml` so the language filter is passed to Jekyll via `--config`.
- Logs fatal errors into `build_site.err.txt` (emptied at startup) with UTC timestamps.

To build only one language locally and save time during development, run:
```bash
docker run \
--rm \
LANGUAGES=en ruby scripts/build_site.rb
```
You can also set `BUILD_OUTPUT_DIR` and `JEKYLL_CONFIG` if you need custom targets:
```bash
LANGUAGES=en BUILD_OUTPUT_DIR=_site ruby scripts/build_site.rb
```
The Docker build stage executes the same script, so production builds always respect the configured languages.

### Legacy container commands
If you prefer a live-reloading experience, you may continue to run the upstream Jekyll container directly:
```bash
docker run --rm \
  -v="$PWD/htdocs:/srv/jekyll" \
  -v="$PWD/jekyll_cache:/usr/local/bundle" \
  -v="$PWD/gems_cache:/usr/gem" \
@@ -17,28 +69,36 @@ docker run \
  -p 35729:35729 \
  -it \
  jekyll/jekyll:4 \
jekyll serve --profile --livereload --config _config.yml,_config.dev.yml
  jekyll serve --profile --livereload
```

Note: the port 35729 is used for live reload
Note2: it will build only English

Or you could use:

```bash
docker run --rm --volume="$PWD/htdocs:/srv/jekyll" -p 4000:4000 -it jekyll/jekyll:4 jekyll serve
```
## Error Reporting
Every run of `scripts/build_site.rb` clears `build_site.err.txt` and appends only fatal log entries with timestamps. Inspect this file before making changes so you can resolve the most recent failure faster.

## Full compose stack (search)
## Compatibility scoring
The `/devices` page now computes an aggregated compatibility score instead of relying solely on Safetynet/Rootbeer flags. Each device entry in `htdocs/_data/devices` can supply the following `compatibility` map, and those values are rendered directly inside `htdocs/_i18n/en/pages/devices.html`:

Add `edocs.local` to your `/etc/hosts` or customize `NGINX_HOST` in .env as desired.
| Key | Points | Notes |
|---|---|---|
| `bootloader` | `locked_green`: 2, `locked_yellow`: 1, other: 0 | Human-friendly labels appear on the site; extra strings (e.g., `unlockable`) are supported. |
| `build_type` | `user`: 1, `userdebug`: 0 | Defaults to “Not declared” when missing. |
| `google_certified` | `true`: 1, `false`: 0 | Accepts YAML booleans. |
| `play_integrity` | `none`: 0, `basic`: 1, `device`: 2, `strong`: 3 | If omitted, the column displays “None”. |
| `rootbeer` | `pass`: 1, others: 0 | Falls back to the existing `rootbeer_pass` value when the map is absent. |
| `channel` | `official`: 2, `community`: 1, others: 0 | Defaults to `community`. |

```bash
cp .env.dev .env
docker-compose up -d
docker-compose exec -T bridge_server python main.py /public_html
```
Example data for FP6 and Sunfish are already present inside their respective YAML files. Devices without the map still render a score (using the defaults above) so the column never breaks.

# Deployment
## File Layout
- `Dockerfile`: multi-stage container that builds the site and packages it behind the NGINX image.
- `docker-compose*.yml`: orchestrate NGINX, ElasticSearch, and the bridge server for search indexing.
- `scripts/build_site.rb`: orchestrates the Jekyll build, prints the version, filters languages, and logs errors.
- `config/param.json`, `config/param.json.example`: store configurable constants and documentation for defaults.
- `config/secret.json`, `config/secret.json.example`: stash sensitive settings.
- `dynamic/`: personalization files (ignored by Git); `selected-languages.yml` lives here.
- `build_site.err.txt`: log of the last fatal error emitted by the helper script.

Since this project depends on [ElasticSearch](https://www.elastic.co/), the deployment should follow the recommended approach and configuration as detailed in [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html)
## Tips
- Update the version in `config/param.json` and the example file whenever you implement behavior the user should be aware of.
- Use `LANGUAGES=en,fr` to build a smaller subset when focusing on a few locales.
- The helper script ensures you always see the documented version number before the build begins, simplifying troubleshooting in CI.
+7 −0
Original line number Diff line number Diff line
@@ -74,3 +74,10 @@ bootloader_relocking:
- {vendor: 'Any vendor', supported: 'yes'}
safetynet:  1
rootbeer_pass:  1
compatibility:
  bootloader: locked_green
  build_type: user
  google_certified: true
  play_integrity: basic
  rootbeer: pass
  channel: official
+8 −1
Original line number Diff line number Diff line
@@ -62,3 +62,10 @@ recovery_install_command: fastboot flash boot
install_e_file_name: erecovery_install
safetynet:  1
rootbeer_pass:  1
compatibility:
  bootloader: locked_yellow
  build_type: userdebug
  google_certified: true
  play_integrity: basic
  rootbeer: pass
  channel: community
+112 −16
Original line number Diff line number Diff line
@@ -300,21 +300,117 @@
              </ul>
            </td>
            <td class="compatibility">
              {%- assign compatibility = device.compatibility | default: {} -%}
              {%- assign score = 0 -%}

              {%- if device.safetynet == 0 %}
              <span style="color:red;font-size: 60%"> Safetynet :  No</span> <br />
              {%-elsif device.safetynet == 1 %}
              <span style="color:green;font-size: 60%"> Safetynet : Yes </span><br />
              {%-elsif device.safetynet == 2 %}
              <span style="color:orange;font-size: 60%"><a href="{{ device.community_doc_url }}"><font color="orange">Safetynet: Likely yes (to test) </font></a> </span><br />
              {%- endif %}
              {%- assign bootloader_value = compatibility.bootloader | default: '' -%}
              {%- assign bootloader_label = 'Unknown' -%}
              {%- assign bootloader_score = 0 -%}
              {%- case bootloader_value -%}
              {%- when 'locked_green' -%}
              {%- assign bootloader_label = 'Locked (green)' -%}
              {%- assign bootloader_score = 2 -%}
              {%- when 'locked_yellow' -%}
              {%- assign bootloader_label = 'Locked (yellow)' -%}
              {%- assign bootloader_score = 1 -%}
              {%- when 'unlockable' -%}
              {%- assign bootloader_label = 'Unlockable' -%}
              {%- else -%}
              {%- unless bootloader_value == nil or bootloader_value == '' -%}
              {%- assign bootloader_label = bootloader_value | replace: '_', ' ' | capitalize -%}
              {%- endunless -%}
              {%- endcase -%}
              {%- assign score = score | plus: bootloader_score -%}

              {%- if device.rootbeer_pass == 0 %}
              <span style="color:red;font-size: 60%">Rootbeer: No</span>
              {%-elsif device.rootbeer_pass == 1 %}
              <span style="color:green;font-size: 60%">Rootbeer: Yes</span>
              {%- endif %}
              {%- assign build_type_value = compatibility.build_type | default: '' | downcase -%}
              {%- assign build_type_label = 'Unknown' -%}
              {%- assign build_type_score = 0 -%}
              {%- case build_type_value -%}
              {%- when 'user' -%}
              {%- assign build_type_label = 'User' -%}
              {%- assign build_type_score = 1 -%}
              {%- when 'userdebug' -%}
              {%- assign build_type_label = 'Userdebug' -%}
              {%- assign build_type_score = 0 -%}
              {%- else -%}
              {%- if build_type_value == '' -%}
              {%- assign build_type_label = 'Not declared' -%}
              {%- else -%}
              {%- assign build_type_label = build_type_value -%}
              {%- endif -%}
              {%- endcase -%}
              {%- assign score = score | plus: build_type_score -%}

              {%- assign google_value = compatibility.google_certified -%}
              {%- assign google_label = 'No' -%}
              {%- assign google_score = 0 -%}
              {%- if google_value == true or google_value == 'true' -%}
              {%- assign google_label = 'Yes' -%}
              {%- assign google_score = 1 -%}
              {%- endif -%}
              {%- assign score = score | plus: google_score -%}

              {%- assign play_value = compatibility.play_integrity | default: '' | downcase -%}
              {%- assign play_label = 'None' -%}
              {%- assign play_score = 0 -%}
              {%- case play_value -%}
              {%- when 'basic' -%}
              {%- assign play_label = 'Basic' -%}
              {%- assign play_score = 1 -%}
              {%- when 'device' -%}
              {%- assign play_label = 'Device' -%}
              {%- assign play_score = 2 -%}
              {%- when 'strong' -%}
              {%- assign play_label = 'Strong' -%}
              {%- assign play_score = 3 -%}
              {%- else -%}
              {%- if play_value == 'none' or play_value == '' -%}
              {%- assign play_label = 'None' -%}
              {%- else -%}
              {%- assign play_label = play_value -%}
              {%- endif -%}
              {%- endcase -%}
              {%- assign score = score | plus: play_score -%}

              {%- assign rootbeer_value = compatibility.rootbeer | default: '' -%}
              {%- if rootbeer_value == '' -%}
              {%- if device.rootbeer_pass == 1 -%}
              {%- assign rootbeer_value = 'pass' -%}
              {%- else -%}
              {%- assign rootbeer_value = 'fail' -%}
              {%- endif -%}
              {%- endif -%}
              {%- assign rootbeer_label = rootbeer_value | capitalize -%}
              {%- assign rootbeer_score = 0 -%}
              {%- if rootbeer_value == 'pass' -%}
              {%- assign rootbeer_score = 1 -%}
              {%- endif -%}
              {%- assign score = score | plus: rootbeer_score -%}

              {%- assign channel_value = compatibility.channel | default: 'community' | downcase -%}
              {%- assign channel_label = channel_value | capitalize -%}
              {%- assign channel_score = 0 -%}
              {%- case channel_value -%}
              {%- when 'official' -%}
              {%- assign channel_score = 2 -%}
              {%- when 'community' -%}
              {%- assign channel_score = 1 -%}
              {%- else -%}
              {%- assign channel_score = 0 -%}
              {%- endcase -%}
              {%- assign score = score | plus: channel_score -%}

              <div class="compatibility-score">
                <strong>Compatibility score: {{ score }}/10</strong>
                <ul class="list-unstyled compatibility-details">
                  <li>Bootloader: {{ bootloader_label }} ({{ bootloader_score }} pts)</li>
                  <li>Build: {{ build_type_label }} ({{ build_type_score }} pts)</li>
                  <li>Google certified: {{ google_label }} ({{ google_score }} pt{% if google_score != 1 %}s{% endif %})</li>
                  <li>Play Integrity: {{ play_label }} ({{ play_score }} pts)</li>
                  <li>Rootbeer: {{ rootbeer_label }} ({{ rootbeer_score }} pt{% if rootbeer_score != 1 %}s{% endif %})</li>
                  <li>Channel: {{ channel_label }} ({{ channel_score }} pts)</li>
                </ul>
              </div>
            </td>
            <td class="install">