1Project Overview
1.1README.md
Landaro
Web application for non-EU citizens coming to Romania. We help them get oriented: useful news, legal guidance, a free account, and Romanian language courses.
This folder holds the project documentation. In this phase we write documentation, not code. Each student documents one module, in their own folder, without stepping on anyone else's. Code comes later, after the documentation is complete and reviewed.
Who we build for (short)
- Non-EU individual, newly arrived in or about to come to Romania.
- Doesn't speak the language, doesn't know the legal steps, doesn't know where to start.
- Wants clear information, in their language, and concrete steps.
Modules (phase 1) and who owns them
| # | Module | Folder | Owner | Short |
| 1 | News | modules/news/ | _(to assign)_ | list of useful news, categories, article page |
| 2 | Legal guidance | modules/legal/ | _(to assign)_ | advice + "ask a legal consultant" |
| 3 | Accounts & Admin | modules/accounts-admin/ | _(to assign)_ | free individual registration + admin panel (admin & user) |
| 4 | Romanian courses | modules/courses/ | _(to assign)_ | 10 days free, then move to a talk with the company |
Write your name in the Owner column when you take your module. One module = one owner.
Shared foundation (students do NOT modify it)
The shared/ folder holds what everyone depends on: glossary, documentation conventions, the common data model, the API contract, and roles.
- It is read-only for students. If you need a change there (e.g. a new field in the
users table), do not edit it — write a request in shared/change-requests.md and we discuss it together.
- This prevents two people from changing the same foundation at the same time.
Golden anti-conflict rule
- You work only in your module's folder and in
shared/change-requests.md.
- Anything that touches someone else (a shared field, a shared endpoint, a role) →
a request in shared/change-requests.md, not a direct change.
- If two modules need to "talk" to each other, write it at the boundary: what one
provides and what the other expects, in each module's "Dependencies" section.
How you start (each student)
- Read this README + the whole
shared/ folder.
- Open your module's folder and read its
README.md (the start guide).
- Fill in your module's
documentation.md, using
shared/doc-template.md as the structure.
- When you have a draft, ask for feedback before going further.
Logical reading order
README.md (this one) → shared/ → modules/<your module>/README.md → write documentation.md.
1.2_decisions.md
Landaro — Consolidated decisions
← Landaro
Single source of truth for the decisions settled in _questions.md (rounds 1–3). The module documentation reflects these. Written in English (per project convention); confirmed with Florin on 2026-10-12. If something here changes, update the affected module docs in the same change.
Global
- Stack: Angular frontend, MySQL database, REST API in plain PHP (OOP) — no framework
(no Laravel/CodeIgniter), no Composer. Clean classes, models, services.
- No external package manager: JWT is hand-written (our own sign/verify functions). PHPMailer
is added manually into lib/PHPMailer/ and required by our own PSR-4-style autoloader. Project folder structure is at the implementer's discretion.
- Auth: JWT bearer token in the
Authorization header, issued on login. sessions table
tracks issued tokens for logout/expiry. Session 7 days (1 day if "Remember me" unchecked); activation link 24h; password-reset link 1h.
- Email: real SMTP via PHPMailer (never PHP
mail()).
- UI languages: English + French. Translations live in frontend JSON files (
en.json,
fr.json), versioned with the code and shipped by the Angular build. A language selector is in the header; the preference is saved on the account (users.locale, default en) and in localStorage. Admin-editable translations without redeploy = later phase.
- Uploads: local disk, in an
uploads/ folder. Images only for v1 (png/jpg/webp), ~5 MB max.
- First admin: created by seed —
test@test.com / test. Changeable later via the profile
page.
- Pagination: default pageSize 10; selectable 10 / 25 / 50 / 100; max 100.
- Dates: stored in UTC, displayed in Europe/Bucharest.
- Settings: a global key-value
settings table + a "Settings" screen in admin. Each module
reads the keys it needs (e.g. legal.max_questions_per_day, legal.disclaimer_text, courses.free_period_days, test pass threshold).
- Audit logging: admin actions (role changes, free-period extensions, lead validation, etc.)
are logged — who, when, what.
Shared users — new fields (via change request)
locale — en / fr, default en.
has_full_access — boolean, default false. Set true when an admin marks a lead valid; grants
unlimited access to everything and suppresses the lead modal.
Access model (cross-module)
- A logged-in user has access to a gated feature when: inside a valid free period OR
has_full_access = true.
- When the free period expires (and
has_full_access is false): Course lessons lock AND
Legal "ask a consultant" (real-consultant chat) is cut off. News stays public; the Legal FAQ stays readable. The lead modal appears on every navigation until a lead is submitted.
- A lead marked
valid by an admin sets has_full_access = true → unlimited access to courses,
consultancy, news; the modal never shows again.
- An admin can also extend a single user's free period by pushing
free_period_ends_at into the
future (logged).
Accounts & Admin
- Password rule: min 8 chars, at least one letter and one digit; UI shows a strength progress bar.
- Self-deactivation allowed; legal questions kept (anonymized) for consultant history; course
progress dropped.
- Consultant/admin accounts are created only by an admin changing an existing user's role.
- Changing email requires re-verifying the new address (old email kept until confirmed).
News
- Two article types: curated (external extract) and original (in-house). Curated requires
a dedicated paste field + source link input; auto-trimmed to 50 words with an ellipsis "…" at the last whole word; source credited with name + link back.
- One optional feature image; a placeholder is shown when absent.
- Admin manages categories and tags freely.
- Related articles = same category, most recent, excluding the current one.
published_at in the future ⇒ the article auto-appears when that moment passes.
Legal
Two separate parts:
- General FAQ (ready-made) — written by admin and consultants; subject + own Legal category +
body + optional image (placeholder when absent); publicly readable.
- Ask a consultant — a logged-in user posts a question; a consultant self-claims it (first
click owns it; others can't take it). An admin may also assign/reassign/free a question. The conversation is a chat between the two people (mIRC-style: either side posts any number of messages, shown chronologically, no reply threading).
- Question statuses:
open (unclaimed) → claimed (a consultant took it) → resolved. Both the
user and the consultant can mark resolved; the user can reopen it; an admin can reopen too.
- Daily limit of questions per user is a setting (default 3), reset at local midnight
(Europe/Bucharest).
- The ask-a-consultant feature is gated by the access model above (lost on free-period expiry
unless has_full_access). The FAQ is not gated.
- Disclaimer: default text shipped by us, editable from an admin Settings section.
- Statistics screen "Legal — Statistics" in admin, plus key figures on the general dashboard:
- Global: question counts by status (open/claimed/resolved); counts today/this week/this month.
- Per consultant: claimed, resolved, average time to first reply, average time to resolution.
- No composite "performance score" — raw metrics only.
Courses
- Hierarchy: course → modules → lessons. Users enroll at the module level (explicit enroll).
- Free period is global per account (10 days from the first lesson started anywhere), set by
Courses triggering Accounts to write users.free_period_ends_at = now + 10 days.
- Lessons: text/HTML + images, and video via YouTube embed played through a custom player
(so the raw YouTube video isn't trivially reachable; videos are private).
- A module has lessons and tests. Module completed = all its lessons completed AND all its
tests passed.
- Tests: multiple-choice (single correct answer), questions set in admin, pass mark ≥ 75%.
Every attempt is recorded (history), unlimited attempts. The certificate states on which attempt the pass was achieved.
- On module completion: a visual "Completed" check next to the user, a certificate on the profile
page with a PDF download button, and an email with the link (exportable as PDF). Certificate contains: user name, module name, completion date, a unique verification ID, the attempt number, and Landaro logo/name.
- After free-period expiry (no full access): lessons lock, progress stays read-only, and a lead
modal appears on every navigation until a lead is submitted — forceful but friendly.
- Company lead: user submits via the modal (a textarea). One lead record per user (upsert). Admin
sees it in the panel AND is notified by email of a new lead. Admin marks it new / valid / invalid; valid grants has_full_access (see access model).
Still soft / may change
- Free period global-per-account (not per-module) — confirmed for now, may revisit.
1.3_questions-build.md
Landaro — Build Questions (before writing code)
I read the entire documentation (shared foundation + all four modules). Before creating any folders or code, I need your decisions on the points below. The docs say we must not improvise. How to answer: write your reply directly under each question, on the **Answer:** line. My recommendation is noted at the end of each item — if a recommendation is fine, just write "ok" or "use recommendation".
1. Scope and sequencing
This is a large build (4 modules, ~20 tables, auth, email, certificates/PDF, i18n). How do you want to approach it?
- A) Build everything end-to-end in one pass (backend + frontend for all four modules).
- B) Go module by module, starting with Accounts & Admin (the foundation everyone depends
on), with your sign-off at each step before moving on.
Recommendation: B — matches the docs ("Accounts & Admin is the foundation module") and gives checkpoints.
Answer: B option
2. Where the code lives
The current landaro/ folder holds only documentation (modules/, shared/). Proposed flat layout inside landaro/:
landaro/backend/ — the PHP API
landaro/frontend/ — the Angular app
landaro/database/ — SQL schema + seed
Is this layout okay, or do you want a different location/name?
Recommendation: use the layout above.
Answer: is perfect, ageree.
3. Environment / tooling versions
To match your machine and avoid guessing:
- PHP version (e.g. 8.1 / 8.2 / 8.3)?
Answer: i dont know really, check my mamp server
- How is PHP served — built-in
php -S, or Apache/Nginx?
Answer: apache
- MySQL or MariaDB, and version?
Answer: i dont know, check my mampserver, but I preffer mysql
- Database name / user to assume in config (e.g.
landaro)?
Answer: landaro
- Angular version (latest is 18/19)? Standalone components or NgModules?
Answer: i preffer last version
- Node version available for the Angular build?
Answer:sure
Recommendation: PHP 8.3 served via php -S for dev, MySQL 8, db landaro, latest Angular with standalone components, current LTS Node.
4. PHP without Composer — autoloader + libraries
The docs say "no Composer," PSR-4-style autoloader, PHPMailer dropped into lib/PHPMailer/. Since I cannot composer require, how should I bring in third-party libs (PHPMailer, and a PDF lib later)?
- A) I download the library source into
backend/lib/... for you.
- B) I scaffold the structure and you drop the library files in manually.
Recommendation: A — I fetch PHPMailer (and FPDF) sources into backend/lib/ so it runs out of the box.
Answer: choose A option
5. SMTP / email
Email uses real SMTP via PHPMailer (never PHP mail()). For local development:
- A) You have SMTP credentials I should wire into a config/env file (host/port/user — I will
NOT hardcode secrets; you fill them in).
- B) I set up config for a local catcher (MailHog / Mailpit) and you provide real credentials
later.
Recommendation: B for dev (Mailpit), with a clearly marked config file for real credentials.
Answer: A version, I already have the email server
6. Certificates (PDF)
Courses issues a certificate as a downloadable PDF plus an email link. With no Composer, a PDF library also has to be added manually.
- A) Add FPDF (lightweight, single-file, no deps) into
backend/lib/ and generate the PDF
on the backend.
- B) Generate the certificate as HTML and let the browser produce the PDF (print-to-PDF),
avoiding a backend PDF lib.
Recommendation: A — FPDF, so the email link and download both serve a real PDF.
Answer: version B
7. Open TODOs in the docs (must not guess)
- 7a. Courses — one test per module, or several? The
tests schema allows several per
module; this changes the "all tests passed" completion rule. Recommendation: one test per module for v1 (completion = lessons done + that test passed). Answer:on test per module
- 7b. News
is_featured — change request #1 is still open (not approved). Include it in v1
or leave it out? Recommendation: leave it out for v1 (it is not approved yet). Answer:include it also
- 7c. Legal daily-limit scope — does the daily limit count all questions created that day
regardless of status (including resolved)? Recommendation: yes — count every question created today, any status. Answer: yes
- 7d. Custom YouTube player — minimum behavior for v1? (e.g. hide title, hide YouTube logo,
disable related videos, block right-click) Recommendation: embed with modestbranding, rel=0 (no related videos), hidden title, and our own play/pause controls overlay — enough to keep the raw video from being trivially grabbed. Answer: agree
8. Anything else you want changed before I start?
Free text — constraints, naming, things to avoid, priorities.
Answer: only if you have other questions.
2Shared Foundation
2.1shared/README.md
Shared — Landaro common foundation
← Landaro
This holds what all modules depend on. Goal: every student starts from the same rules, the same names, the same data model, so modules fit together at the end without clashes.
⚠️ Read-only for students
Do not edit files in shared/. If you need a change (a new field, a new role, a shared endpoint), write a request in change-requests.md. We discuss it and I apply it (or we do it together). This way two people never fight over the same foundation.
What you find here
How you use this folder
- Read it first, before your module.
- When you document, reuse the names from the glossary and the tables from
data-model.md instead of inventing new ones. If the users table already has email, don't create mail.
- If something is missing →
change-requests.md, don't improvise in your module.
2.2shared/roles.md
Shared Roles
← Shared
Who can do what in Landaro. Read-only for students. Every module must use these exact roles. Need a new role or a permission change? Write a change request.
The roles
| Role | Who | Logged in? |
| visitor | anyone browsing the public site | no |
| user | registered non-EU individual | yes |
| admin | platform administrator | yes |
| consultant | legal consultant answering questions | yes |
What each role can do (high level)
visitor
- Read public news.
- Read public info / legal advice pages.
- Register for a free account.
- Cannot: send legal questions, access courses, enter the admin panel.
user
- Everything a visitor can do, plus:
- Manage their own profile.
- Send legal questions to a consultant and read the answers.
- Access Romanian courses during the free period (and see when it ends).
- See their own area in the admin panel (their data only).
admin
- Manage news (create, edit, publish, delete articles).
- Manage users (view, activate, block).
- See and route legal questions.
- Manage courses and lessons.
- Full access to the admin panel.
consultant
- See legal questions assigned to them.
- Reply to legal questions.
- Cannot manage users, news, or courses.
Rules for module authors
- For every screen and every endpoint, state the required role.
- "Own data only" matters: a
user sees only their own profile/questions, never others'.
- If a feature needs a role not listed here, that's a change request, not a new role invented in your module.
2.3shared/data-model.md
Shared Data Model
← Shared
The common tables every module relies on. Read-only for students. If you need a new table or field, request it in change-requests.md. Each module documents its own module-specific tables inside its own folder, but the tables below are shared and must not be duplicated or renamed.
Conventions
- Table and field names: English, lowercase,
snake_case.
- Every table has
id (primary key) and created_at, updated_at timestamps.
- Foreign keys are named
<entity>_id (e.g. user_id).
- Dates/times are stored in UTC.
users (owned by: Accounts & Admin module)
The single source of truth for people with an account. Other modules reference it by user_id; they do not create their own user tables.
| Field | Type | Notes |
| id | integer | primary key |
| email | string | unique, used for login |
| password_hash | string | never store plain passwords |
| full_name | string | user's name |
| country | string | country of origin (non-EU) |
| role | string | one of the roles in roles.md |
| status | string | active / pending / blocked |
| free_period_ends_at | datetime | when the 10-day free course period ends (nullable) |
| created_at | datetime | |
| updated_at | datetime | |
roles (reference)
Roles are a fixed, small list described in roles.md. We store the role as a string on users.role. If we later need more complex permissions, that is a change request.
How modules connect to shared data
- News, Legal, Courses all point to a user via
user_id → users.id.
- A module must not add columns to
users on its own. Need a field? Change request.
- Example: the Courses module needs to know when the free period ends → it reads
users.free_period_ends_at; it does not create its own copy.
Where module-specific tables go
Tables that belong to one module only (e.g. articles, legal_questions, courses, lessons) are documented inside that module's folder, not here. List them in your module's "Data used" section and note that they reference users.id where needed.
2.4shared/api-contract.md
Shared API Contract
← Shared
How all modules talk to the backend, so every endpoint looks and behaves the same. Read-only for students. This is about shape and rules, not the full endpoint list — each module documents its own endpoints following these rules.
Base
- All endpoints live under
/api.
- Routes are English, lowercase, plural nouns:
/api/articles, /api/legal-questions.
- Methods:
GET (read), POST (create), PUT/PATCH (update), DELETE (remove).
Request / response format
- Everything is JSON.
- Request bodies and response bodies use the field names from
data-model.md.
- Successful response:
{ "data": { }, "error": null }
{ "data": [ ], "meta": { "page": 1, "pageSize": 20, "total": 134 }, "error": null }
Errors (same shape everywhere)
{ "data": null, "error": { "code": "validation_error", "message": "Email already used" } }
Common code values: validation_error, not_found, unauthorized, forbidden, rate_limited, server_error. The message is human-readable (English).
HTTP status codes
| Code | When |
| 200 | OK (read/update) |
| 201 | Created |
| 400 | Validation error |
| 401 | Not logged in |
| 403 | Logged in but not allowed (wrong role) |
| 404 | Not found |
| 429 | Too many requests (rate limit) |
| 500 | Server error |
Authentication
- A logged-in request carries a token (how exactly is defined by the Accounts module).
- The backend knows the current user and their
role from that token.
- Modules do not invent their own login — they rely on the Accounts module.
Rules for module authors
- Reuse this envelope (
data / error / meta) for every endpoint.
- Reuse the error
code list above; if you need a new code → change request.
- State the required role per endpoint (see
roles.md), e.g. "admin only".
2.5shared/doc-conventions.md
Documentation Conventions
← Shared
Simple rules so everyone's documentation looks the same and is easy to read.
Language
- All documentation is written in English. Even if we talk in another language,
everything written in these files is English. No exceptions.
- Technical names (tables, fields, API routes): English, lowercase with
_.
Format
- We write in Markdown (
.md).
- Your module documentation lives in
modules/<module>/documentation.md.
- If a file grows beyond ~100 lines, split it into several files with clear names
(e.g. documentation.md + screens.md + data.md) and add a table of contents with links.
- One idea per paragraph. Short sentences. No filler.
Required structure
Start from doc-template.md. Don't skip sections — if one doesn't apply, write "not applicable" and why.
Content rules
- Use the glossary. Same words everywhere (
User (individual), not "client").
- Don't reinvent the data. If you need a field on the user, check
data-model.md
first. If it's missing → change request.
- Write rules, not just screens. E.g. "a user can send max. 3 legal questions per
day" is a rule; it must be written explicitly.
- Concrete examples. Put real examples (a real news title, a step-by-step flow).
- Mark the unknowns. If you don't know something, write
TODO: to clarify — don't guess.
Dependencies between modules
Every documentation has a Dependencies section. There you write clearly:
- "I receive from the Accounts module: the logged-in user (id, role)."
- "I provide to module X: the list of published articles."
This way boundaries are written, not assumed.
Before you say "done"
- You filled in every section of the template.
- You used the glossary terms.
- You listed the dependencies.
- You did not modify anything in
shared/ directly.
2.6shared/doc-template.md
Module documentation: <MODULE NAME>
Template. Copy this content into modules/<module>/documentation.md and fill it in. Don't leave sections empty — if one doesn't apply, write "not applicable" + why.
Owner: <your name> Module: <news / legal / accounts-admin / courses> Last updated: <YYYY-MM-DD>
1. Purpose
What the module solves, for whom, why it's useful. 3–5 sentences.
2. Users & roles
Who uses the module (visitor / user / admin / consultant) and what each does. Use the roles from ../../shared/roles.md.
3. Features (list)
The list of things the module can do. Short, bullet points.
4. Flows (step by step)
For each important feature, the concrete steps. Example: "User → clicks Register → fills the form → receives an email → account active".
5. Screens / pages
What pages the module has and what each contains (no fine design, just content and actions).
6. Data used
Which tables/fields you use. First check ../../shared/data-model.md. If you need something new → request it in ../../shared/change-requests.md (do NOT add it here directly).
7. Business rules
Clear rules, not just screens. E.g. limits, validations, who may do what, what happens on expiry.
8. Dependencies
- I receive from: <which module / which data>
- I provide to: <which module / which data>
- I depend on shared: <what from roles/data-model/api-contract>
9. Messages & errors
What messages the user sees (success, error). E.g. "Email already used", "Free period expired".
10. Open questions (TODO)
What's not clear yet. Each line starts with TODO:.
2.7shared/glossary.md
Landaro Glossary
← Shared
Project terms, with the same meaning for everyone. Use exactly these words in the documentation (and later in the code), so we all speak the same language.
| Term | Meaning |
| Landaro | the name of the application |
| Visitor | someone not logged in; sees public content (news, info pages) |
| User (individual) | non-EU individual, with a free account |
| Admin | platform administrator; manages content and users |
| Legal consultant | the person who answers users' legal questions |
| Module | a major part of the app (news, legal, accounts, courses) |
| Article | an individual news item |
| Legal question | the message a user sends to the consultant |
| Course | the Romanian language course |
| Lesson | a unit inside a course |
| Free period | 10 days of free access to courses for a new user |
| Company lead | the moment a user is invited to bring their company to a talk |
| Admin panel | the administration area (for admin and, partly, for the user) |
| Role | what rights someone has: visitor / user / admin (see roles.md) |
Naming rules
- Write business terms in English (as above).
- Write technical names (tables, fields, endpoints) in English, lowercase, as in
data-model.md and api-contract.md (e.g. users, articles, legal_questions).
- If a new term appears in your module, add it via a change request, so it stays
the same everywhere.
2.8shared/change-requests.md
Change Requests to shared/
← Shared
This is the only place where you ask for a change to the shared foundation (data model, roles, API contract, glossary). You do not edit those files yourself. Add a new row at the top of the table, fill the fields, and tell the mentor. We discuss it and apply it together.
Why this exists
If two students both edit users or roles at the same time, we get conflicts and confusion. Funnelling every shared change through this one list keeps the foundation consistent for everyone.
How to request
- Add a row at the top of the table below (newest first).
- Give it the next free
#.
- Status starts as
open. The mentor changes it to approved / applied / rejected.
- Do not change
shared/ files yourself even after approval unless told to.
Requests
| # | Date | Requested by | Module | What / why | Status |
| 3 | 2026-10-12 | Florin | Courses / all | Add has_full_access (boolean, default false) to users. When an admin marks a company lead valid, set it true → unlimited access to courses, consultancy and news, and the lead modal stops showing. All modules check "valid free period OR has_full_access". | approved |
| 2 | 2026-10-12 | Florin | all (i18n) | Add locale (string, en/fr, default en) to users, to store the user's preferred UI language. | approved |
| 1 | YYYY-MM-DD | <name> | <module> | example: add phone to users because registration needs it | open |
Example of a good request
Add field locale (string) to users. Reason: the Courses module needs to know the user's preferred interface language to show lessons in the right language. Default en.
Short, says what and why, names the module. That's all we need to decide.
3Module — Accounts & Admin
3.1modules/accounts-admin/README.md
Module: Accounts & Admin — Start Guide
← Landaro · Shared
Owner: _(GB)_
Before you write anything, read the main README and the whole shared/ folder. Then write your documentation in your documentation as a set of files (see overview.md as the entry point), using the structure from doc-template.md.
What this module is about
The foundation everyone else leans on: free registration for individuals, login, the user's own profile area, and the admin panel (admin manages users and content; the user sees their own area). This module owns the users table.
In scope
- Free registration for a non-EU individual (User).
- Login / logout, password handling (hashed, never plain).
- The user's own profile + their area in the admin panel.
- Admin panel: list users, activate / block, assign roles.
- Being the single source of "who is logged in and what role they have".
Out of scope (don't document these here)
- News content → News module.
- Legal questions → Legal module.
- Courses & the free period logic → Courses module (but the field
users.free_period_ends_at lives on the shared users table).
Special responsibility
Because you own users, other modules read it through user_id. They cannot add fields to it. When another module needs a new user field, it goes through change-requests.md and we add it to data-model.md together. Document how auth works clearly — everyone else depends on it.
Questions to answer while documenting
- Exact registration flow (form fields, email confirmation? activation?).
- How login works and how other modules know the current user + role.
- What the admin can do to a user (activate, block, change role) and the rules.
- What the user sees vs. what the admin sees in the admin panel.
- Password rules and basic security (hashing, no plain passwords).
Anti-conflict reminder
You own users, but shared changes still go through change-requests.md so the whole team stays in sync.
3.2modules/accounts-admin/overview.md
Accounts & Admin — Overview
← Landaro · Shared
Owner: GB Module: accounts-admin Last updated: 2026-10-12
Accounts & Admin is the foundation module of Landaro. It owns identity for the whole platform: who a person is, whether they are logged in, and what role they hold. Every other module (News, Legal, Courses) leans on it to know the current user and gate what they can do. This module owns the shared users table.
Documentation in this module
Each section of the template lives in its own file so it is easy to read and maintain:
Reading order
Start with purpose.md → users-and-roles.md → flows.md → data-model.md, then the rest as needed.
3.3modules/accounts-admin/purpose.md
Accounts & Admin — Purpose
← overview.md
What this module solves
Accounts & Admin is the identity backbone of Landaro. Before a non-EU newcomer can ask a legal consultant a question, start a Romanian course, or save anything to come back to, the platform has to know who they are. This module provides that: a free account, a secure login, and a single, trustworthy answer to "who is this person and what are they allowed to do".
Who it is for
- The newcomer (User). Someone who just arrived in Romania, or is about to, from outside
the EU. They need a free, low-friction way to register and a profile that remembers them, so the help Landaro offers (legal answers, courses) is tied to one identity.
- The platform itself. Every other module needs a reliable way to check "is this person
logged in, and what is their role?" before showing content or accepting an action. Accounts & Admin is the one place that answers that question.
- The administrator. A staff member who manages the community: approving new accounts,
blocking abusive ones, and assigning roles such as consultant.
Why it matters
If identity is weak, everything built on top of it is weak. A newcomer could lose access to their own legal questions, an unverified account could flood a consultant with spam, or a regular user could stumble into another person's private data. By centralizing registration, login, roles, and the admin panel in one module — and by owning the users table as the single source of truth — Landaro keeps access control consistent and auditable across the whole app. Every other module delegates identity here instead of reinventing it, which is exactly what keeps the system coherent as it grows.
In one line
Accounts & Admin lets the right person in, keeps the wrong person out, and tells every other module who it is talking to.
3.4modules/accounts-admin/users-and-roles.md
Accounts & Admin — Users & roles
← overview.md
Landaro has four roles. Every account holds exactly one role at a time, stored as a string on the users table (see data-model.md). The role decides what the person can see and do across the whole platform, not just inside this module. Roles are defined centrally in ../../shared/roles.md; this module is the one that assigns and enforces them.
The four roles
Visitor
Anyone who is not logged in. They can read public content (published news, published legal guidance) and register for a free account. They cannot ask a consultant a question, open a course, or reach any admin screen.
User (individual)
A registered, activated newcomer. On top of everything a visitor can do, a user can manage their own profile, ask legal questions, access the Romanian course during the free period, and view their personal area. A user can only ever see and edit their own data.
Consultant
A legal specialist. A consultant sees only the legal questions an admin has routed to them and can post answers. They have no control over news, accounts, or routing — they answer, nothing more.
Admin
Full control of the platform. An admin manages accounts (activate, block, assign roles), manages and routes legal questions, and manages news and courses. Admin is the only role that can change another account's role or status.
How access control works
- One role per account. The
role field holds a single value: visitor, user,
admin, or consultant. There is no multi-role account.
- Checked on every request. Every screen and every API endpoint checks the active
session's role before doing anything. An unauthorized request is redirected to login or rejected, never silently allowed.
- "Own data only" for users. A regular user's queries are always scoped to their own
user_id. They cannot read or edit another member's profile or questions under any circumstances.
- Admin area is guarded. Only admin (and, for their own inbox, consultant) can load
administrative interfaces. A user hitting an admin URL directly is denied.
- Roles are fixed, not ad-hoc. New roles are not invented inside a feature. Changing the
role hierarchy is a platform-wide decision and goes through ../../shared/change-requests.md.
Role capability summary
| Capability | Visitor | User | Consultant | Admin |
| Read public content | ✓ | ✓ | ✓ | ✓ |
| Register / log in | ✓ | ✓ | ✓ | ✓ |
| Manage own profile | — | ✓ | ✓ | ✓ |
| Ask a legal question | — | ✓ | — | — |
| Answer routed questions | — | — | ✓ | — |
| Manage users / roles | — | — | — | ✓ |
| Route legal questions | — | — | — | ✓ |
| Manage news & courses | — | — | — | ✓ |
3.5modules/accounts-admin/features.md
Accounts & Admin — Features
← overview.md
The module splits into two groups: what a newcomer uses to manage their own account, and what an admin uses to manage everyone.
Account features (for the user)
Register
A visitor creates a free account with full name, email, password, and country of origin. The account starts in pending status and must be activated by email before it can be used. See the registration flow.
Login / Logout
A registered, activated member signs in with email and password. A successful login opens a session and issues a token carrying the user's id and role, which the rest of the platform reads. Logout ends the session and invalidates the token.
Profile
A private page where a logged-in user views and updates their own details — full name, country, email, and password. It also surfaces read-only account facts such as current role and free-period status (the latter owned by the Courses module).
Admin features (for the admin)
Admin user list
A searchable, filterable table of every account, with name, email, country, role, status, and creation date. The entry point for all member management.
Activate / Block
An admin can activate an account that is stuck in pending, or block an account to immediately cut off its access. Blocking is reversible (unblock), and a blocked account cannot log in.
Assign role
An admin can change a member's role — for example promoting a trusted user to consultant, or granting admin. This is the only way a role changes, and only an admin can do it.
Extend free period
From a user's detail screen an admin can push free_period_ends_at into the future, granting a few extra free days. The action is recorded in the audit log.
Grant full access
When an admin marks a company lead valid (in the Courses lead list), the user's has_full_access becomes true — unlimited access to courses, consultancy, and news, with no more lead modal. This is the account-side effect of a Courses action.
Settings
An admin edits platform-wide settings (key-value) from a Settings screen — e.g. the Legal daily question limit, the Legal disclaimer text, the Courses free-period length, and the test pass threshold. Each module reads the keys it needs.
Language
Every user can pick a UI language (English or French) from the header selector; the choice is saved on their account (locale) and in localStorage.
Cross-cutting responsibility
Beyond these screens, the module has one job every other part of Landaro depends on: being the single source of "who is logged in, what role they hold, and whether they have access". News, Legal, and Courses never implement their own login; they read identity and the access state (free_period_ends_at, has_full_access) from here. See dependencies.md.
3.6modules/accounts-admin/flows.md
Accounts & Admin — Flows
← overview.md
The concrete, step-by-step paths through the module. Field names and statuses match data-model.md.
1. Registration
- Visitor opens the Register page and fills in full name, email, password, password
confirmation, and country of origin, then accepts the terms.
- The system validates the input: email must be well-formed and unique, the two password
fields must match, and the password must meet the strength rule.
- On success, a new row is created in
users with status = 'pending' and the password
stored only as password_hash (never plain text).
- The system creates an
account_tokens row of type email_activation and emails the user
an activation link containing that token.
- The user sees: "Account created successfully. Please check your email to activate it."
2. Email confirmation & activation
- The user opens the email and clicks the activation link.
- The system looks up the token in
account_tokens, checks it is of type
email_activation, not expired, and not already used.
- If valid, the system sets the user's
status to active and stamps used_at on the
token so it cannot be reused.
- The user can now log in. If the token is expired or already used, the system offers to
resend a fresh activation email.
3. Login & token generation
- The user submits email and password on the Login page.
- The system finds the account by email and verifies the password against
password_hash.
- It checks the account is allowed to log in:
status must be active (not pending, not
blocked).
- On success, the system creates a
sessions row and issues an authentication token that
carries the user's id and role, then redirects to the profile/dashboard.
- On failure it returns a generic "Wrong email or password." (it does not reveal whether the
email exists), or the specific blocked/pending message when that is the cause.
4. How other modules read the current user
- A request arrives at News, Legal, or Courses carrying the session token.
- That module asks Accounts & Admin to resolve the token into
user_id + role.
- The module uses the role to decide what to allow — e.g. News lets only an admin publish,
Legal lets only a user ask a question. No other module stores passwords or runs its own login; identity always comes from here.
5. Admin manages an account
- Admin opens the Users List, searches/filters, and clicks into a member.
- From the detail screen the admin can activate a pending account, block/unblock an account,
or change its role.
- Each action updates the matching field on
users (status or role), takes effect
immediately, and shows "User status updated."
3.7modules/accounts-admin/screens.md
Accounts & Admin — Screens / pages
← overview.md
What each page contains and what the user can do on it. Content and actions only — no visual design.
1. Register page
- Shows: Fields for full name, email, password, password confirmation, and country of
origin, plus a checkbox to accept the terms and conditions.
- Does: Validates the input, creates a new account with
pending status, and triggers
the activation email. On error it shows the specific validation message inline.
2. Login page
- Shows: Email and password inputs, a "Remember me" checkbox, and a "Forgot password?"
link.
- Does: Verifies credentials, opens a session with the user's role, and redirects to the
profile or the page the user came from. Surfaces blocked/pending/invalid messages when login cannot proceed.
3. User profile page
- Shows: A read-only overview (registration date, current role, free-period status) and
editable fields for full name, country, email, and password.
- Does: Lets the logged-in user update their own details, change their password, and log
out. A user only ever sees and edits their own profile.
4. Admin panel — users list
- Shows: A table of all accounts with name, email, country, role badge, status (active /
pending / blocked), and creation date.
- Does: Lets the admin search by keyword, filter by role or status, sort, and open a
single member's detail page. Admin-only.
5. Admin panel — user detail & actions
- Shows: A single member's full picture — account history, submitted legal questions,
course progress, free-period status, has_full_access, assigned role, and activity timestamps.
- Does: Lets the admin change the member's role, activate a pending account, block/unblock
access, and extend the free period (set a new free_period_ends_at). Changes apply immediately and are written to the audit log. Admin-only.
6. Admin panel — settings
- Shows: The editable platform settings as key-value pairs, grouped by module (Legal,
Courses, …) — e.g. daily question limit, disclaimer text, free-period days, test pass threshold.
- Does: Lets an admin update a setting; the change takes effect wherever that key is read.
Admin-only.
Note on the password strength bar
The Register and Profile pages show a live password-strength indicator (progress bar) as the user types, reflecting the rule in business-rules.md.
Note on the language selector
A language selector (English / French) sits in the header on every page; changing it updates the user's locale and the UI language immediately.
3.8modules/accounts-admin/business-rules.md
Accounts & Admin — Business rules
← overview.md
Explicit rules, not just screens. These hold regardless of which UI triggers them.
Identity & registration
- Unique email. Every account must have a unique email. A second registration with an
existing email is rejected with "Email already used."
- Activation before use. A new account is created as
pending and cannot log in until the
email activation link is used. Only then does status become active.
- Password strength. A password must meet the minimum strength rule before it is accepted
at registration or reset. (TODO: pin the exact rule — length and character classes.)
Security
- Never store plain passwords. Passwords are stored only as
password_hash. The raw
password is never written to the database or logs.
- Tokens are hashed and single-use. Session and account tokens are stored as hashes.
Activation and reset tokens expire and are marked used_at once redeemed, so a link works at most once.
- Generic login failure. A failed login returns "Wrong email or password." without
revealing whether the email exists, to avoid leaking which addresses are registered.
Access & permissions
- Blocked accounts are locked out. An account with
status = 'blocked' cannot log in or
use any authenticated feature, effective immediately.
- Only admins manage accounts. Changing a role, activating a pending account, or
blocking/unblocking a member can only be done by an admin.
- Own data only. A regular user can read and edit only records tied to their own
user_id. Any attempt to reach another user's data is denied with "Not allowed."
- Role is single and fixed. An account holds exactly one role, and roles are not invented
per feature. Changing the role set is a platform decision via ../../shared/change-requests.md.
- Password strength is shown live. The exact rule is min 8 characters with at least one
letter and one digit. Register and Profile display a strength progress bar as the user types.
- Email change requires re-verification. When a user changes their email in the profile, the
new address must be confirmed via an activation link before it takes effect; the old email stays active until then.
- Admin actions are audited. Role changes, activation/blocking, free-period extensions, and
lead validation are written to audit_log (who, when, what).
Access state
- Access = valid free period OR
has_full_access. Every gated feature across the platform
(Course lessons, Legal real-consultant chat) checks this combined condition.
- Full access is granted by a valid lead. When an admin marks a company lead
valid,
has_full_access becomes true for that user — unlimited access everywhere, and the lead modal stops appearing.
- Admins may extend the free period by moving
free_period_ends_at forward; this is logged.
Sessions
- Sessions expire. When a session token passes
expires_at it is invalid; the user must
log in again and sees "Session expired. Please log in again."
- Remember me. Unchecked, a session lasts ~1 day; checked, it lasts the full 7 days.
3.9modules/accounts-admin/data-model.md
Accounts & Admin — Data model
← overview.md
This module owns three tables. users is the platform's single source of truth for identity; sessions and account_tokens support login and the email/password-reset lifecycle.
users is a shared, owned table: other modules read it through user_id but never add fields to it. A new user field is a platform decision and goes through ../../shared/change-requests.md, then into ../../shared/data-model.md.
users (owned table)
The single source of truth for all platform accounts.
| Field | Type | Notes |
id | integer | primary key |
email | string | unique, used for login |
password_hash | string | hashed password, never plain text |
full_name | string | user's name |
country | string | country of origin (non-EU) |
role | string | visitor, user, admin, or consultant |
status | string | active, pending, or blocked |
locale | string | preferred UI language, en or fr, default en |
free_period_ends_at | datetime | when the 10-day free course ends (nullable; written by Courses logic, read by anyone) |
has_full_access | boolean | default false; set true when an admin marks a company lead valid → unlimited access everywhere, no lead modal |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
locale and has_full_access were added via ../../shared/change-requests.md (requests #2 and #3).
sessions
Tracks active logins. One row per active session; deleting/expiring it logs the user out.
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id |
token_hash | string | hashed session token (the raw token is never stored) |
expires_at | datetime | expiration timestamp in UTC |
created_at | datetime | timestamp in UTC |
account_tokens
Single-use tokens for email activation and password reset. The type field distinguishes the two so one table covers both lifecycles.
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id |
token_hash | string | hashed single-use token |
type | string | email_activation or password_reset |
expires_at | datetime | expiration timestamp in UTC |
used_at | datetime | set when the token is redeemed, so it cannot be reused (nullable) |
created_at | datetime | timestamp in UTC |
settings (owned table)
Global key-value settings edited by an admin from the Settings screen. Each module reads the keys it needs (e.g. legal.max_questions_per_day, legal.disclaimer_text, courses.free_period_days, courses.test_pass_threshold).
| Field | Type | Notes |
id | integer | primary key |
key | string | unique setting key, e.g. legal.max_questions_per_day |
value | text | the value, stored as text and parsed by the reading module |
updated_by | integer | references users.id (last admin who changed it, nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
audit_log (owned table)
Records sensitive admin actions for accountability (role changes, free-period extensions, lead validation, etc.).
| Field | Type | Notes |
id | integer | primary key |
actor_id | integer | references users.id (who did it) |
action | string | e.g. role_changed, free_period_extended, lead_validated |
target_user_id | integer | references users.id (who it affected, nullable) |
details | text | human-readable detail / JSON (e.g. "+5 days", old→new role) |
created_at | datetime | timestamp in UTC |
Relationships
sessions.user_id → users.id (many sessions per user over time).
account_tokens.user_id → users.id (many tokens per user over time).
settings.updated_by → users.id; audit_log.actor_id / target_user_id → users.id.
- Other modules reference
users.id as a foreign key but never alter the users schema.
3.10modules/accounts-admin/messages.md
Accounts & Admin — Messages & errors
← overview.md
The exact text the user sees, grouped by situation, with when each one fires.
Authentication & login
- "Wrong email or password." — The credentials do not match any active record. Kept
deliberately generic so it does not reveal whether the email exists.
- "Account blocked." — A user whose
status is blocked tries to sign in.
- "Please confirm your email address before logging in." — Sign-in attempted on a
pending (unactivated) account.
- "Session expired. Please log in again." — The session token passed its
expires_at or
was invalidated.
Registration & validation
- "Email already used." — Registration with an email that already exists.
- "Password is too weak." — The chosen password does not meet the strength rule.
- "Passwords do not match." — Password and confirmation differ.
Access & permissions
- "Not allowed." — A user tries to view another user's data or perform an admin-only
action without permission.
Success
- "Account created successfully. Please check your email to activate it." — Registration
succeeded and the activation email was sent.
- "Profile updated successfully." — Account details or password change saved.
- "User status updated." — An admin activated, blocked/unblocked, or re-roled a member.
3.11modules/accounts-admin/dependencies.md
Accounts & Admin — Dependencies
← overview.md
This module sits at the bottom of the stack: it provides identity to everyone and depends on almost nothing above it. Boundaries are declared here, from this module's side.
- I receive from: nothing upstream. Accounts & Admin is the foundation module and does
not depend on any other module for data. It is the base everyone else builds on.
- I provide to: every other module — the logged-in user's
id and role, the login/token
mechanism, and the access state (free_period_ends_at, has_full_access).
- News reads id + role to allow only an admin to create/edit/publish articles.
- Legal reads id + role for the FAQ and the consultant chat, and the access state to gate the
real-consultant chat (lost on free-period expiry unless has_full_access).
- Courses reads id + role, triggers me to set
free_period_ends_at on first lesson, and reads
both access fields to lock lessons / show the lead modal.
- All modules read
locale for the UI language, and read settings for their configurable keys.
- I also provide: the
settings key-value store and the audit_log, both owned here.
- I depend on shared:
Mirror note
Every "I receive from Accounts: user id + role" line in News, Legal, and Courses must mirror the "I provide to" line above. If a name changes here, those modules change with it — through ../../shared/change-requests.md, never by editing shared files directly.
3.12modules/accounts-admin/open-questions.md
Accounts & Admin — Open questions (TODO)
← overview.md
Most earlier questions are now decided (see ../../_decisions.md): password rule, token expiries, remember-me, self-deactivation, who creates consultant/admin accounts, email re-verification. What remains:
- TODO: Audit log retention. How long do we keep
audit_log rows — forever, or prune after a
period? Decide before the table grows large.
- TODO: Settings value typing.
settings.value is stored as text. Confirm the per-key parsing
(integer for legal.max_questions_per_day, text for legal.disclaimer_text, etc.) is documented wherever each key is read.
4Module — Romanian Courses
4.1modules/courses/README.md
Module: Romanian Courses — Start Guide
← Landaro · Shared
Owner: _(write your name here when you take this module)_
Before you write anything, read the main README and the whole shared/ folder. Then write your documentation in your documentation as a set of files (see overview.md as the entry point), using the structure from doc-template.md.
What this module is about
Free Romanian language courses for logged-in users. Access is free for 10 days. When the free period ends, the user is invited to bring their company for a talk with the stakeholders (a "company lead") instead of continuing for free.
In scope
- A list of courses and their lessons.
- A lesson page (text/video/exercise — your call, document it).
- Tracking the user's free period (uses
users.free_period_ends_at).
- The "free period ended" state and the company-lead invitation screen.
- Simple progress tracking (which lessons a user finished).
Out of scope (don't document these here)
- Registration / login → Accounts & Admin module.
- Payments / billing → not in phase 1 (after the free period we invite to a talk, we
don't charge online). If you think we need it, raise it as an open question.
- News / legal → their own modules.
What you depend on (shared)
users.id, role, and especially users.free_period_ends_at from
data-model.md.
- Only a logged-in
user can access courses — see roles.md.
- API envelope + errors from
api-contract.md.
- Your own tables (e.g.
courses, lessons, lesson_progress) are module-specific.
Questions to answer while documenting
- When does the 10-day clock start? (at registration? at first course open?)
- What exactly happens on day 11? (locked content + company-lead screen)
- What does the company-lead invitation ask for / collect?
- How is progress stored and shown?
- Can an admin extend or reset a user's free period? (if yes, that's an admin feature)
Anti-conflict reminder
free_period_ends_at lives on the shared users table (owned by Accounts). You read it. If you need a new user field, request it in change-requests.md — don't edit shared files.
4.2modules/courses/overview.md
Romanian Courses — Overview
← Landaro · Shared
Owner: Hansa Module: courses Last updated: 2026-10-12
Romanian Courses gives a logged-in newcomer free Romanian lessons to start breaking the language barrier. A course is organized as modules → lessons, the user enrolls per module, and finishes a module by completing its lessons and passing its tests (earning a certificate). The first 10 days are free (global per account); after that lessons lock and a lead modal invites the user to bring their company in — see free-period.md and company-lead.md.
Documentation in this module
| File | What it covers |
| purpose.md | What the module solves and why it matters |
| users-and-roles.md | Who can access courses and what each does |
| features.md | The list of things the module can do |
| modules-and-tests.md | Course→modules→lessons, enroll, tests, completion |
| free-period.md | The global 10-day free window and what happens at expiry |
| company-lead.md | The post-expiry lead modal and full-access grant |
| certificates.md | What a completed module issues |
| flows.md | Enroll, learn, test, hit the lock, submit a lead |
| screens.md | The pages and what each shows / does |
| data-model.md | All the module's tables |
| business-rules.md | Structure, tests, lock, and access rules |
| messages.md | Success and error messages |
| dependencies.md | What this module gives to and takes from others |
| open-questions.md | Decisions still open, marked as TODO |
Reading order
purpose.md → free-period.md → flows.md → data-model.md, then the rest as needed.
4.3modules/courses/purpose.md
Romanian Courses — Purpose
← overview.md
What this module solves
The single biggest barrier for a non-EU newcomer in Romania is the language. Not speaking Romanian makes everything harder — paperwork, work, daily life. Courses gives the newcomer a free, structured way to start learning from day one, right inside Landaro.
How it works
- A visitor who registers and becomes a user gets access to the Romanian course.
- The first 10 days are free — full access to the free lessons.
- After the free period the lessons lock, and the user is invited to bring their company into
a conversation with the Landaro team to continue (the "company lead").
Who it is for
- The newcomer (user) who wants to start learning Romanian immediately, at no cost, during
their first days in the country.
- The admin who manages the courses, lessons, and their content.
Why it matters
A free trial lowers the barrier to even start learning, which is exactly when a newcomer is most overwhelmed. The 10-day window is long enough to prove value and build a habit, and the company-lead step turns engaged learners into a real conversation with the business — without abruptly cutting off someone who's just getting going.
In one line
Courses gets the newcomer speaking a bit of Romanian for free in their first 10 days, then opens a path to continue through their company.
4.4modules/courses/users-and-roles.md
Romanian Courses — Users & roles
← overview.md
Courses uses the platform roles from ../../shared/roles.md and reads the current user's id, role, and users.free_period_ends_at from Accounts & Admin (see dependencies.md).
Who does what
- Visitor — Cannot open lessons, but can see that a Romanian course exists (a teaser that
encourages registration).
- User — Enrolls in modules, works through lessons, takes tests, and earns certificates
during the free period or with has_full_access. After expiry (no full access) lessons lock, progress goes read-only, and the lead modal appears on every navigation.
- Admin — Manages courses, modules, lessons (incl. video), and tests; sees all users; extends
a user's free period (logged); and reviews company leads, marking them valid (grants full access) or invalid.
Boundaries
- Course content is for logged-in users only; a visitor sees the shell, not the lessons.
- A user's access is governed by their own
free_period_ends_at / has_full_access, never
another user's.
- Admin manages content and can extend a user's free period, but the clock itself starts when the
user starts their first lesson (see free-period.md). users is owned by Accounts; Courses triggers the writes, Accounts performs them.
4.5modules/courses/features.md
Romanian Courses — Features
← overview.md
Learning
- Course → modules → lessons — Browse a course, see its modules, and enroll in a module.
- Enroll per module — An explicit "enroll" action at the module level.
- Lesson pages — Ordered lessons with text/HTML + images and an optional **YouTube video via
a custom player**. Each lesson has a "start" action.
- Progress tracking — Per-lesson progress, so the user resumes where they left off.
Tests & completion
- Module tests — Multiple-choice (single correct answer), set in admin, pass mark ≥ 75%,
unlimited attempts with full history. See modules-and-tests.md.
- Module completion — All lessons completed + all tests passed ⇒ module completed, with a
visual "Completed" check.
- Certificates — On completion: a certificate on the profile with PDF download, plus an email
link. See certificates.md.
Free period & access
- Free-period tracking — A global 10-day window from the first lesson started; shows the
countdown. See free-period.md.
- Lock after expiry — Lessons lock; progress stays read-only.
- Lead modal — After expiry, a modal appears on every navigation until the user submits a
company lead. See company-lead.md.
Admin
- Manage content — Create/edit courses, modules, lessons (incl. video URL), and tests.
- See all users & extend free period — View users and grant extra free days (logged).
- Leads — See submitted leads, be emailed on a new one, and mark each
valid / invalid;
valid grants the user full access.
4.6modules/courses/flows.md
Romanian Courses — Flows
← overview.md
Field names and rules match modules-and-tests.md, free-period.md, and data-model.md.
1. Enroll and start learning
- A logged-in user browses a course and its modules and clicks enroll on a module.
- The user opens a lesson and clicks "start". If it's the first lesson started on the account,
Courses triggers Accounts to set free_period_ends_at = now + free_period_days (10).
- The user works through the lessons; progress is saved per lesson
(not_started → in_progress → completed).
2. Take the test and complete the module
- After all lessons in the module are
completed, the module's test(s) unlock.
- The user takes a test (multiple-choice, single answer). The attempt is saved with its score
and attempt_number.
- If the score is ≥ 75% the attempt passes; otherwise the user can retake (unlimited attempts).
- When all lessons are completed and all tests passed, the enrollment becomes
completed, a
"Completed" check shows, and a certificate is issued (profile PDF + email link).
3. Hit the lock (free period expired)
- The user returns after
free_period_ends_at has passed and has_full_access is false.
- Lesson content locks; progress stays read-only.
- The lead modal appears on every navigation until the user submits a company lead.
4. Submit a company lead
- In the modal the user writes their company details (a textarea) and submits.
- The system stores/updates the single
company_leads row for that user (status = new), and
the modal stops appearing.
- The admin is emailed and sees the lead in the panel; marking it
valid grants the user
has_full_access (unlimited access, no more modal). invalid changes nothing.
5. Admin extends the free period
- From a user's detail screen, the admin sets a new, later
free_period_ends_at.
- The change is logged (who, when, how many days).
4.7modules/courses/screens.md
Romanian Courses — Screens / pages
← overview.md
1. Course & module list
- Shows: The course with its modules; a visitor sees a teaser that the course exists.
- Does: Lets a logged-in user enroll in a module and open it. A visitor is prompted to log in.
2. Lesson page
- Shows: Lesson content (text/HTML + images), an optional video in the custom player, the
lesson's position, and a "start" action on an unstarted lesson.
- Does: Lets the user start and work through a lesson. Starting the first lesson begins the
global free-period clock.
3. Test page
- Shows: The module's multiple-choice questions (unlocked after lessons are completed).
- Does: Lets the user submit answers; shows the score and pass/fail; allows retakes
(unlimited). Each attempt is recorded.
4. Progress view
- Shows: Lesson and module progress, test attempt history, and the free-period countdown.
- Does: Lets the user resume and see completion status. Read-only once the period expires.
5. Lead modal (after expiry)
- Shows: A friendly-but-persistent textarea asking for company details, on every navigation.
- Does: Submits/updates the user's single
company_leads record; disappears once submitted.
6. Admin — content management
- Shows: Courses, modules, lessons (incl. video URL), and tests with their questions/options.
- Does: Lets admin create/edit all course content and set test questions. Admin-only.
7. Admin — users & leads
- Shows: All users with their free-period status, plus the submitted company leads.
- Does: Lets admin extend a user's free period (logged) and mark a lead
valid / invalid
(valid grants full access). Admin-only.
Note on the certificate
A completed module shows a "Completed" check and produces a certificate available on the user's profile (PDF download) and by email — see certificates.md.
4.8modules/courses/business-rules.md
Romanian Courses — Business rules
← overview.md
Structure & enrollment
- Hierarchy is course → modules → lessons. A user enrolls at the module level (explicit
enroll).
- A module is
completed only when all its lessons are completed AND **all its tests are
passed**.
Tests
- Multiple-choice, single correct answer per question; questions set in admin.
- Pass mark is ≥ 75% (
courses.test_pass_threshold, a setting).
- Unlimited attempts, each recorded in
test_attempts with its score and attempt_number.
- A module's test(s) unlock only after the module's lessons are completed.
Certificates
- Issued once per user per completed module.
- Contains name, module, completion date, unique verification ID, the passing attempt number, and
Landaro branding. Available as profile PDF and by email. See certificates.md.
Free period & access
- The free period is global per account: 10 days (
courses.free_period_days) from the first
lesson started anywhere. The clock starts on first lesson start, not at registration or login.
- When Courses detects the first lesson start, it triggers Accounts to set
free_period_ends_at.
- After expiry (and
has_full_access false): lessons lock, progress is read-only, and the lead
modal appears on every navigation until a lead is submitted.
- Access = valid free period OR
has_full_access. A lead marked valid by an admin sets
has_full_access = true (unlimited access everywhere, no modal). An admin may also extend free_period_ends_at (logged).
Content
- Lessons are text/HTML + images, plus an optional YouTube video shown via a custom player;
videos are private and not meant to be reached directly on YouTube.
Company lead
- One
company_leads row per user (upsert); the modal is a single free-text textarea.
- Admin is emailed on a new lead and marks it
new / valid / invalid; marking is logged.
4.9modules/courses/data-model.md
Romanian Courses — Data model
← overview.md
Hierarchy: course → modules → lessons. Users enroll at the module level. The module owns the tables below and reads the access state from the shared users table (read-only).
courses
A top-level course (e.g. "Romanian"). An umbrella for its modules.
| Field | Type | Notes |
id | integer | primary key |
title | string | course title |
slug | string | unique URL-friendly identifier |
description | text | what the course covers (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
modules
A module inside a course. The unit a user enrolls in and completes.
| Field | Type | Notes |
id | integer | primary key |
course_id | integer | references courses.id |
title | string | module title |
position | integer | order within the course |
description | text | what the module covers (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
lessons
A lesson inside a module. Content is text/HTML + images, and optionally a YouTube video shown via a custom player.
| Field | Type | Notes |
id | integer | primary key |
module_id | integer | references modules.id |
title | string | lesson title |
position | integer | order within the module |
content | text | lesson body (text/HTML + images) |
video_url | string | optional YouTube URL, played via the custom player (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
enrollments
A user's explicit enrollment in a module.
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id |
module_id | integer | references modules.id |
status | string | in_progress or completed |
completed_at | datetime | when the module was completed (nullable) |
created_at | datetime | timestamp in UTC |
lesson_progress
Per-lesson progress. The first row's started_at across the account marks when the global free-period clock began (see free-period.md).
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id |
lesson_id | integer | references lessons.id |
status | string | not_started, in_progress, completed |
started_at | datetime | when the user first opened the lesson (nullable) |
completed_at | datetime | when the user finished it (nullable) |
tests
A module's test. Multiple-choice, single correct answer per question.
| Field | Type | Notes |
id | integer | primary key |
module_id | integer | references modules.id |
title | string | test title |
pass_threshold | integer | percent needed to pass; default from settings (75) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
test_questions
A question in a test.
| Field | Type | Notes |
id | integer | primary key |
test_id | integer | references tests.id |
body | text | the question text |
position | integer | order within the test |
test_options
An answer option; exactly one per question is correct.
| Field | Type | Notes |
id | integer | primary key |
question_id | integer | references test_questions.id |
body | string | the option text |
is_correct | boolean | true for the single correct option |
test_attempts
Every attempt a user makes (full history, unlimited attempts).
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id |
test_id | integer | references tests.id |
score | integer | percent scored on this attempt |
passed | boolean | score ≥ pass_threshold |
attempt_number | integer | 1-based attempt counter for this user+test |
created_at | datetime | timestamp in UTC |
certificates
Issued when a user completes a module — see certificates.md.
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id |
module_id | integer | references modules.id |
verification_id | string | unique public verification code |
passing_attempt_number | integer | which test attempt achieved the pass |
issued_at | datetime | timestamp in UTC |
company_leads
Captured when a user submits the lead modal after the free period ends. One lead per user (upsert).
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id (unique — one lead per user) |
body | text | free-text message (the modal is a textarea) |
status | string | new, valid, invalid (set by admin) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
Shared tables (referenced, read-only)
users — read role, free_period_ends_at, and has_full_access to decide locked vs.
unlocked and whether to show the lead modal. Courses triggers Accounts to set free_period_ends_at on the first lesson start, but never writes users directly.
Settings used
courses.free_period_days (default 10) and courses.test_pass_threshold (default 75) live in
the shared settings table (owned by Accounts & Admin).
4.10modules/courses/messages.md
Romanian Courses — Messages & errors
← overview.md
Errors
- "Please log in to access courses." — A visitor tries to open a lesson.
- "Your free period has ended." — The 10-day window passed; lessons are locked. Leads to the
lead modal.
- "Complete the lessons before taking the test." — The user opens a test before finishing the
module's lessons.
- "You didn't reach the pass mark — try again." — A test attempt scored below 75%.
- "Lesson not found." — A search or link points to a lesson that doesn't exist.
Success
- "Enrolled in the module." — The user enrolled in a module.
- "Lesson started." — The user began a lesson (first lesson also starts the free-period clock).
- "Lesson completed." — Progress saved.
- "Test passed — module completed!" — All lessons done and the test passed; certificate issued.
- "Your certificate is ready." — The certificate is available on the profile and sent by email.
- "Company details submitted." — The lead was saved; the modal will stop appearing.
Admin
- "Free period extended." — The admin granted extra days (logged).
- "Lead marked valid — full access granted." / "Lead marked invalid." — The admin
reviewed a lead.
4.11modules/courses/dependencies.md
Romanian Courses — Dependencies
← overview.md
- I receive from: Accounts & Admin — the logged-in user's id and role, and the access state
(free_period_ends_at, has_full_access), to decide locked vs. unlocked and whether to show the lead modal.
- I provide to: Accounts & Admin — a trigger to set
free_period_ends_at when the user starts
their first lesson, and a request to set has_full_access = true when an admin marks a lead valid. No other module reads Courses' data.
Mirror note
My "I receive from Accounts: user id + role + access state" must mirror the "I provide to Courses" line in Accounts & Admin, including the trigger that Accounts writes free_period_ends_at/has_full_access on my behalf. Anything new on a shared table goes through ../../shared/change-requests.md, never a direct edit.
4.12modules/courses/modules-and-tests.md
Romanian Courses — Modules & tests
← overview.md
How the course is structured and how a user completes a module.
Hierarchy
- Course (e.g. "Romanian") is an umbrella. It contains modules.
- A module contains ordered lessons and one or more tests.
- A user enrolls in a module explicitly (an "enroll" action), separate from simply opening a
lesson.
Lessons
- Content is text/HTML + images, and optionally a YouTube video played through a **custom
player** so the raw YouTube page isn't trivially reachable (videos are private).
- A lesson is tracked per user in
lesson_progress: not_started → in_progress →
completed.
- Starting the very first lesson on the account starts the global free-period clock (see
free-period.md).
Tests
- A test is multiple-choice with a single correct answer per question. Admins set the
questions and options in the admin panel.
- The pass mark is ≥ 75% (
courses.test_pass_threshold, a setting).
- Unlimited attempts. Every attempt is stored in
test_attempts with its score and
attempt_number; the full history is kept for statistics and for the certificate.
- A module's test(s) unlock after the module's lessons are completed.
What "module completed" means
A module is completed when all its lessons are completed AND all its tests are passed (an attempt scored ≥ the pass threshold). On completion:
- the user gets a visual "Completed" check,
- a certificate is issued (see certificates.md),
- the enrollment's
status becomes completed with completed_at set.
4.13modules/courses/certificates.md
Romanian Courses — Certificates
← overview.md
When a user completes a module (all lessons completed and all tests passed), Landaro issues a certificate for that module.
What the user gets
- A visual "Completed" check next to the module (and in their profile).
- A certificate on their profile page, with a download PDF button.
- An email with a link to the certificate (which can be exported as PDF).
What the certificate contains
- The user's name.
- The module name.
- The completion date.
- A unique verification ID (
certificates.verification_id).
- The attempt number on which the test was passed (
passing_attempt_number).
- The Landaro logo / name.
Rules
- One certificate per user per completed module (
certificates row).
- The verification ID is unique and can be used to confirm a certificate is genuine.
- The certificate is generated at the moment the module is completed.
4.14modules/courses/free-period.md
Romanian Courses — The free period
← overview.md
The 10-day free window is the defining mechanic of this module. Getting the start moment right matters, because it decides when the clock runs out.
When the clock starts
The countdown starts when the user starts their first lesson — not before. Concretely:
- Not when the user registers.
- Not when the user logs in.
- Not when the user merely reaches or opens the course list.
- Only when the user clicks the "start" action on their first lesson. Lessons have an
explicit start button so this moment is unambiguous.
Once started, the window runs for exactly 10 days from that moment and does not pause.
The window is global per account — 10 days from the first lesson started anywhere, not per module. (courses.free_period_days is a setting, default 10.)
What happens when it expires
- The lessons lock: the user can no longer open lesson content. Their **progress stays
visible, read-only**.
- A lead modal appears on every navigation until the user submits a company lead — forceful
but friendly. (See company-lead.md.)
- Everything that is gated across the platform is affected the same way: besides Course lessons,
the Legal "ask a consultant" chat is also cut off. News and the Legal FAQ stay available.
How a user regains access
- An admin can extend the free period (push
free_period_ends_at forward); logged for audit.
- A company lead marked
valid by an admin sets has_full_access = true → unlimited access
to everything, and the modal never shows again.
How it's stored
- The start moment is captured on the user's first
lesson_progress row (started_at).
- When that first lesson starts, Courses triggers Accounts to set
users.free_period_ends_at = now + free_period_days.
users.free_period_ends_at and users.has_full_access (both owned by Accounts & Admin) decide
locked vs. unlocked. Courses reads them and never writes to users directly.
See the exact rules in business-rules.md and the fields in data-model.md.
4.15modules/courses/company-lead.md
Romanian Courses — Company lead
← overview.md
After the free period expires (and the user does not have has_full_access), the user is invited — persistently — to submit a company lead so the Landaro team can get in touch with their company and open up full access.
The modal
- Appears on every navigation once the free period has expired, until a lead is submitted.
- Intentionally hard to ignore, but friendly in tone. The user can keep browsing (News, FAQ,
read-only progress), but the modal keeps returning.
- It is a simple textarea where the user describes their company / leaves their details.
- Once a lead is submitted, the modal stops appearing.
One lead per user
- There is one
company_leads row per user (upsert): submitting again updates the same
record rather than creating duplicates. The content is free text, so a user can write as much as they like in that single textarea.
What the admin does
- The admin sees the lead in the panel and is emailed when a new lead arrives.
- The admin marks the lead
valid or invalid:
valid → the user's has_full_access becomes true: unlimited access to courses,
consultancy, and news, and the modal never shows again.
invalid → no access change (the user stays gated and the modal keeps appearing).
- Marking a lead is recorded in the audit log.
has_full_access lives on the shared users table (owned by Accounts & Admin); Courses triggers the change but Accounts writes the field.
4.16modules/courses/open-questions.md
Romanian Courses — Open questions (TODO)
← overview.md
Most earlier questions are decided (see ../../_decisions.md): the course→modules→lessons hierarchy, enroll per module, free period global per account, tests (multiple-choice, ≥75%, unlimited attempts with history), certificates, the lead modal, admin extend days, full access on valid lead, and video via custom YouTube player. What remains:
- TODO: Free period scope — confirm. It's global per account for now; you flagged it may
change to per-module later. Flag before build if it changes.
- TODO: One test per module or several?
tests allows multiple per module. Confirm whether a
module ships with exactly one test or can have several (affects the "all tests passed" rule).
- TODO: Custom video player scope. What exactly should the custom player hide/disable (title,
YouTube logo, right-click, related videos)? Define the minimum for v1.
- TODO: Certificate verification page. The certificate has a verification ID — do we need a
public page to look it up, or is the ID just printed for now?
- TODO: Re-enrollment. Can a user re-enroll in / retake a module already completed, and does
that affect the existing certificate?
5Module — Legal Guidance
5.1modules/legal/README.md
Module: Legal Guidance — Start Guide
← Landaro · Shared
Owner: _(write your name here when you take this module)_
Before you write anything, read the main README and the whole shared/ folder. Then write your documentation in your documentation as a set of files (see overview.md as the entry point), using the structure from doc-template.md.
What this module is about
Two things: (1) ready-made legal advice pages (guides, FAQs) that anyone can read, and (2) "ask a legal consultant" — a logged-in user sends a question and a consultant answers.
In scope
- Public advice/guide pages, grouped by topic.
- A logged-in user submits a legal question.
- A consultant sees assigned questions and replies.
- The user reads the answer in their area.
Out of scope (don't document these here)
- Registration / login → Accounts & Admin module.
- News articles → News module.
- General admin of users → Accounts & Admin module.
What you depend on (shared)
users.id + role (only a user can ask; only a consultant can answer). See
roles.md.
- API envelope + error shape from
api-contract.md.
- Your own tables (e.g.
faq_entries, legal_questions, legal_messages) are
module-specific — document them in data-model.md.
Questions to answer while documenting
- What does a legal question contain? (subject, message, topic, status)
- What states does a question go through? (new → assigned → answered → closed)
- Who assigns a question to a consultant? (admin? automatic?)
- Is there a limit per user per day? (write it as a business rule)
- Clear disclaimer: this is guidance, not a substitute for a lawyer. Where is it shown?
Anti-conflict reminder
Work only in this folder. Need a shared change (e.g. a consultant role detail)? Request it in change-requests.md — don't edit shared files.
5.2modules/legal/overview.md
Legal Guidance — Overview
← Landaro · Shared
Owner: Hansa Module: legal Last updated: 2026-10-12
Legal Guidance helps a non-EU newcomer deal with the legal side of arriving in Romania. It has two parts: a public FAQ of ready-made answers (written by admins and consultants), and an ask a consultant flow where a logged-in user posts a question, a consultant self-claims it, and the two chat until it's resolved.
Documentation in this module
| File | What it covers |
| purpose.md | What the module solves and why it matters |
| users-and-roles.md | Visitor, user, admin, consultant and what each does |
| features.md | The two parts (FAQ + ask-a-consultant chat) |
| flows.md | Read FAQ, ask, claim, chat, resolve — step by step |
| screens.md | The pages and what each shows / does |
| data-model.md | FAQ tables + the question/chat tables |
| business-rules.md | Limits, disclaimer, claim and permission rules |
| statistics.md | The admin statistics (questions + consultants) |
| messages.md | Success and error messages |
| dependencies.md | What this module gives to and takes from others |
| open-questions.md | Decisions still open, marked as TODO |
Reading order
purpose.md → users-and-roles.md → flows.md → data-model.md, then the rest as needed.
5.3modules/legal/purpose.md
Legal Guidance — Purpose
← overview.md
What this module solves
A non-EU newcomer arrives in Romania with little idea of the legal steps ahead: residence documents, deadlines, rules, and the paperwork that keeps their stay valid. Legal Guidance is the first place they turn for answers — both for the common questions everyone has and for the specific ones only a real consultant can resolve.
How it helps, in two ways
- Ready-made guidance. Published advices, guides, and FAQs that anyone can read without an
account. These cover the questions that come up again and again, so most newcomers get unblocked immediately.
- Ask a legal consultant. When a guide isn't enough, a logged-in user submits their own
question. A consultant self-claims it and the two chat until it's resolved. This handles the specific, personal cases that generic guidance can't.
Who it is for
- The newcomer who doesn't know where to start and needs trustworthy legal orientation.
- The consultant who answers the questions routed to them.
- The admin who publishes guidance and routes incoming questions to the right person.
Why it matters
Legal mistakes are expensive and stressful for someone new to a country. Clear, correct guidance up front — plus a path to a real human when it's needed — removes a huge source of fear and keeps the newcomer on the right side of the rules.
In one line
Legal Guidance answers the newcomer's legal questions — first with ready-made guidance, then with a real consultant when the guides fall short.
5.4modules/legal/users-and-roles.md
Legal Guidance — Users & roles
← overview.md
Legal uses the platform roles from ../../shared/roles.md and reads the current user's id and role from Accounts & Admin (see dependencies.md).
Who does what
- Visitor — Reads the public FAQ. Cannot ask a consultant; prompted to register first.
- User — Reads the FAQ and, with a valid free period or
has_full_access, can post questions
to a consultant and chat in their own threads.
- Consultant — Writes/edits FAQ entries, sees the pool of unclaimed questions, self-claims
one, and chats to resolve it. Cannot see another consultant's claimed threads.
- Admin — Writes/edits FAQ entries and manages Legal categories, sees every question, can
assign/reassign as an exception, and views the statistics.
Boundaries
- A user sees only their own questions and chats, never another user's.
- Consultants normally self-claim; an admin assigns/reassigns only as an exception.
- A claimed question is private to its asker and the one consultant who claimed it.
5.5modules/legal/features.md
Legal Guidance — Features
← overview.md
Legal has two distinct parts: a public FAQ, and a private chat with a real consultant.
Part 1 — FAQ (ready-made guidance, public)
- FAQ library — Published entries grouped by Legal's own categories, each with a subject, a
ready-made answer, and an optional image (placeholder when absent).
- Written by admin and consultants — Both roles can create and edit FAQ entries.
- Always public — Readable by anyone, including visitors; not gated by the free period.
Part 2 — Ask a consultant (private chat, logged-in users)
- Submit a question — A logged-in user posts a question (subject + first message). Subject to
the daily limit (a setting, default 3).
- Consultant self-claim — Consultants see the pool of unclaimed (
open) questions and claim
one with a click; once claimed, no other consultant can take it.
- Chat thread — The asker and the claiming consultant exchange messages freely
(mIRC-style: either posts any number of messages, shown in chronological order).
- Resolve & reopen — Either side can mark the question
resolved; the user (or an admin) can
reopen it.
- Gated access — This chat is available while the user has a valid free period or
has_full_access. It is cut off when the free period expires without full access.
Management (admin)
- Assign / reassign — Normally consultants self-claim, but an admin can assign or reassign a
question when needed.
- Statistics — A "Legal — Statistics" screen with question and consultant metrics (see
statistics.md).
- Disclaimer — The always-visible disclaimer text is editable from the admin Settings screen.
5.6modules/legal/flows.md
Legal Guidance — Flows
← overview.md
Field names and statuses match data-model.md.
1. Read the FAQ (anyone)
- The person opens the FAQ library on the public site.
- They browse entries grouped by Legal category and read an answer. No account required.
2. Ask a consultant (user)
- A visitor who tries to ask is prompted to register first.
- A logged-in user with access (valid free period or
has_full_access) opens the ask form,
writes a subject and a first message, and submits.
- The system checks the daily limit (
legal.max_questions_per_day, default 3) and creates a
legal_questions row with status = 'open' plus the first legal_messages row.
- The question enters the unclaimed pool.
3. Consultant claims a question
- A consultant opens the pool of
open questions.
- They click one to claim it:
consultant_id is set, claimed_at stamped, status = 'claimed'.
- From that moment the question is theirs; other consultants no longer see it in the pool.
(An admin may still assign/reassign it as an exception.)
4. The chat
- The asker and the claiming consultant exchange
legal_messages, shown chronologically.
- Either side can post any number of messages, in any order.
- Either side can mark the question
resolved (status = 'resolved', resolved_at stamped).
- The user (or an admin) can reopen a resolved question, moving it back to
claimed.
5. Access cut-off
- If the user's free period expires and they do not have
has_full_access, the ask form and the
chat are no longer available to them (News and the FAQ remain).
- A
valid company lead (set by an admin) restores access by granting has_full_access.
5.7modules/legal/screens.md
Legal Guidance — Screens / pages
← overview.md
1. FAQ library (public)
- Shows: Published FAQ entries grouped by Legal category, each with subject, answer, and
optional image (placeholder when absent), plus the always-present disclaimer.
- Does: Lets anyone browse and read; no account required.
2. "Ask a consultant" form (user)
- Shows: A subject field and a first-message field, plus the disclaimer. Visitors see a prompt
to register; users without access see the lead modal instead.
- Does: Creates a
legal_questions row (status = 'open') and the first message, subject to
the daily limit.
3. My questions / chat (user)
- Shows: The user's own questions with status (open / claimed / resolved) and, once claimed,
the chat thread with the consultant.
- Does: Lets the user post messages, mark resolved, and reopen a resolved question. Own
questions only.
4. Consultant — question pool
- Shows: All unclaimed (
open) questions.
- Does: Lets a consultant claim one with a click (it then disappears from everyone else's
pool). Consultant-only.
5. Consultant — my claimed questions / chat
- Shows: The questions this consultant has claimed, with their chat threads.
- Does: Lets the consultant post messages and mark a question resolved. Consultant-only.
6. Admin — FAQ management
- Shows: All FAQ entries and Legal categories.
- Does: Lets admin (and consultants) create/edit/publish FAQ entries; admin manages Legal
categories.
7. Admin — questions overview & statistics
- Shows: Every question with status and ownership, plus the statistics (see
statistics.md).
- Does: Lets an admin assign/reassign a question and read the metrics. Admin-only.
5.8modules/legal/business-rules.md
Legal Guidance — Business rules
← overview.md
FAQ (public)
- Only
published FAQ entries are visible to the public; drafts are hidden.
- Admins and consultants can create and edit FAQ entries. Admin manages Legal categories.
- An FAQ entry has exactly one Legal category and an optional image (placeholder when absent).
Asking a consultant
- Login + access required. Only a logged-in user with a valid free period or
has_full_access can open a question. A visitor is prompted to register; an expired user without full access sees the lead modal instead of the form.
- Daily limit. A user may submit at most
legal.max_questions_per_day questions per day
(default 3), reset at local midnight (Europe/Bucharest). The limit is a setting, editable in admin.
Claiming & chatting
- Self-claim is exclusive. A question starts
open. The first consultant to click it claims
it (status = claimed, consultant_id set); after that no other consultant can take it.
- Admin override. An admin may assign or reassign a question as an exception.
- Free chat. Once claimed, the asker and that one consultant exchange messages freely, shown
chronologically, with no reply threading.
- Resolve. Either the user or the consultant can mark a question
resolved.
- Reopen. The user (or an admin) can reopen a resolved question; it returns to
claimed with
the same consultant.
Statuses
open (unclaimed) → claimed (a consultant owns it) → resolved (done). Reopen moves resolved → claimed.
Always on
- Disclaimer always visible wherever guidance or a chat appears. The text is a setting
(legal.disclaimer_text), editable from admin; we ship a default placeholder.
5.9modules/legal/data-model.md
Legal Guidance — Data model
← overview.md
Legal has two separate parts in the data: a public FAQ, and a private user↔consultant chat. It references the shared users table for identity (read-only).
Part 1 — FAQ (ready-made guidance)
legal_categories
Legal's own categories (separate from News categories), e.g. visas, work, housing.
| Field | Type | Notes |
id | integer | primary key |
name | string | unique category name |
slug | string | URL-friendly identifier |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
faq_entries
Ready-made questions-and-answers written by admins and consultants, publicly readable.
| Field | Type | Notes |
id | integer | primary key |
category_id | integer | references legal_categories.id |
author_id | integer | references users.id (admin or consultant) |
subject | string | the question / title |
body | text | the ready-made answer |
image_path | string | optional feature image; placeholder shown when empty (nullable) |
status | string | draft or published |
published_at | datetime | publication date in UTC (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
Part 2 — Ask a consultant (private chat)
legal_questions
A question a logged-in user submits; a consultant self-claims it, then they chat.
| Field | Type | Notes |
id | integer | primary key |
user_id | integer | references users.id (the asker) |
consultant_id | integer | references users.id (who claimed it; nullable while open) |
subject | string | short title of the question |
status | string | open, claimed, resolved |
claimed_at | datetime | when a consultant claimed it (nullable) |
resolved_at | datetime | when it was marked resolved (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
legal_messages
The chat messages inside a question thread (mIRC-style: either side posts any number, shown chronologically, no reply threading).
| Field | Type | Notes |
id | integer | primary key |
question_id | integer | references legal_questions.id |
sender_id | integer | references users.id (the user or the consultant) |
body | text | the message text |
created_at | datetime | timestamp in UTC |
Shared tables (referenced, read-only)
users — read users.id and users.role to know who is asking, who may answer, and who is an
admin; read the access state (free_period_ends_at, has_full_access) to gate the chat. Legal never writes to users.
Settings used
legal.max_questions_per_day (default 3) and legal.disclaimer_text live in the shared
settings table (owned by Accounts & Admin) and are edited from the admin Settings screen.
5.10modules/legal/messages.md
Legal Guidance — Messages & errors
← overview.md
Errors
- "Register to ask a question from a consultant." — A visitor tries to use the ask form.
- "Your free access has ended — provide a lead to continue talking to a consultant." — A
logged-in user without a valid free period or has_full_access tries to ask or chat.
- "Daily limit for questions reached." — The user hit
legal.max_questions_per_day today.
- "This question has already been claimed." — A consultant tries to claim a question another
consultant already took.
- "Question not found." — The question does not exist or does not belong to the current user.
- "Only the assigned consultant or an admin can act here." — Someone acts outside their role.
Success
- "Your question was submitted." — A question was created and is waiting to be claimed.
- "You claimed this question." — A consultant successfully claimed a question.
- "Message sent." — A chat message was posted.
- "Question marked as resolved." — The user or consultant closed the thread.
- "Question reopened." — A resolved question was reopened.
- "FAQ entry published." — An admin/consultant published a ready-made entry.
5.11modules/legal/dependencies.md
Legal Guidance — Dependencies
← overview.md
- I receive from: Accounts & Admin — the logged-in user's id and role, and the access state
(free_period_ends_at, has_full_access). I use the access state to gate the "ask a consultant" chat; the FAQ is public and not gated.
- I provide to: not applicable — no other module reads Legal's data. FAQ entries, questions,
and chat messages stay inside this module.
Mirror note
My "I receive from Accounts: user id + role + access state" must mirror the "I provide to Legal" line in Accounts & Admin. Anything new on a shared table goes through ../../shared/change-requests.md, never a direct edit.
5.12modules/legal/statistics.md
Legal Guidance — Statistics
← overview.md
An admin sees Legal statistics on a dedicated "Legal — Statistics" screen, with a few key figures also surfaced on the general admin dashboard. These are raw metrics — there is no composite "performance score".
Global metrics
- Question counts by status:
open, claimed, resolved.
- Questions submitted today, this week, and this month.
Per-consultant metrics
- Number of questions claimed.
- Number of questions resolved.
- Average time to first reply (from claim to the consultant's first message).
- Average time to resolution (from claim to
resolved).
Notes
- Metrics are computed from
legal_questions (statuses and timestamps claimed_at,
resolved_at) and legal_messages (first consultant reply time).
- No ranking or scoring is applied; the admin reads the figures and draws conclusions.
5.13modules/legal/open-questions.md
Legal Guidance — Open questions (TODO)
← overview.md
Most earlier questions are decided (see ../../_decisions.md): two separate parts (FAQ + chat), self-claim with admin override, chat model, statuses, who resolves and reopens, own Legal categories, statistics without a performance score. What remains:
- TODO: Notification channel. When a question is claimed, a message arrives, or a question is
resolved — does the other person get an email, an in-app notification, or both? Decide the channel for each event.
- TODO: Daily-limit scope. Does the daily limit count all questions created that day
regardless of status, including ones already resolved? Confirm the exact counting rule.
- TODO: Resolved reopen window. Can a resolved question be reopened forever, or only within a
time window? (Currently assumed: no time limit.)
6Module — News
6.1modules/news/README.md
Module: News — Start Guide
← Landaro · Shared
Owner: _(GB)_
Before you write anything, read the main README and the whole shared/ folder. Then write your documentation in your documentation as a set of files (see overview.md as the entry point), using the structure from doc-template.md.
What this module is about
Useful news for non-EU people coming to Romania: changes in rules, deadlines, practical announcements. A visitor can read; an admin writes and publishes.
In scope
- A list of articles (newest first), with categories.
- A single article page.
- Admin: create / edit / publish / unpublish / delete an article.
- Simple search or filter by category.
Two kinds of article (important)
Every article is one of two types, and the module has to handle both:
- Curated / aggregated — we pick up a piece of news from an external source and
republish only a short extract. There is a hard limit on how much we show (a maximum number of characters or words — decide and document the exact number), and the source must always be credited: the original publisher's name and a link back to the full article. We never reproduce the whole thing.
- Original — we write the full article ourselves. No external source needed; it's
our own content from start to finish. Document how the two differ in the data (e.g. a type field, plus source_name and source_url that are required for curated and empty for original), how they look on the article page (a curated item shows a "Source: …" credit and a "read the full story" link), and the rule that enforces the length limit on curated items.
Out of scope (don't document these here)
- Login and accounts → that's the Accounts & Admin module.
- Legal advice content → that's the Legal module.
- Courses → the Courses module.
What you depend on (shared)
users.id + role to know who may publish (admin only). See
roles.md.
- The API envelope and error shape from
api-contract.md.
- Your own tables (e.g.
articles, categories) are module-specific — document them in
data-model.md, and reference users.id for the author.
Questions to answer while documenting
- What fields does an article have? (title, body, category, status, published date, author)
- Is the article curated or original? (the
type field)
- For a curated article: what is the exact limit we show (characters or words, and how
many), and what source fields are required? (source_name, source_url)
- What validation enforces that limit, and what happens if an admin pastes more than
allowed? (write it as a business rule + an error message)
- What states can an article be in? (draft / published)
- Who sees drafts? (admin only)
- Can an article belong to more than one category?
- How is the list paginated? (reuse
meta from the API contract)
Anti-conflict reminder
Work only in this folder. If you need a shared change (e.g. a new field on users), request it in change-requests.md — don't edit shared files.
6.2modules/news/overview.md
News — Overview
← Landaro · Shared
Owner: GB Module: news Last updated: 2026-10-12
News keeps non-EU newcomers up to date with rule changes, deadlines, and practical announcements. A visitor reads; an admin writes and publishes. Every article is one of two types — curated (a short, credited extract from an external source) or original (written in-house) — and both share the same categories and tags.
Documentation in this module
Reading order
purpose.md → article-types.md → data-model.md → flows.md, then the rest as needed.
6.3modules/news/purpose.md
News — Purpose
← overview.md
What this module solves
Immigration and legal rules in Romania change often, and the official sources are scattered, dense, and rarely written for someone who just arrived. News turns that noise into a clear, dated feed a newcomer can actually follow: what changed, when it takes effect, and what to do about it.
Who it is for
- The newcomer (visitor or user). Someone trying to keep their stay legal who needs to
know about a new deadline, a changed visa rule, or a practical announcement — without digging through government portals.
- The admin. Staff who publish updates, either by writing an original piece or by
curating a short, credited extract from an official or reputable source.
Why it matters
A missed deadline can cost a newcomer their legal status. By surfacing the right update at the right time, in plain language and sorted newest-first, News directly protects the user's stay and reduces the anxiety of not knowing. The two-type model (curated vs. original) lets the team move fast — republish a short extract with full credit when speed matters, or write a thorough original guide when depth matters — without ever reproducing someone else's full article.
In one line
News is the newcomer's early-warning feed for anything that affects their life and legal status in Romania.
6.4modules/news/users-and-roles.md
News — Users & roles
← overview.md
News uses the platform roles defined in ../../shared/roles.md. It does not define its own; it reads the current user's id and role from Accounts & Admin (see dependencies.md).
Who does what
- Visitor — Reads the public feed and any published article. Cannot see drafts and cannot
reach the admin editor.
- User — Same reading rights as a visitor. Being logged in does not add News powers; News
is about reading for everyone except admins.
- Consultant — No special News role. A consultant reads News like any other logged-in
person. News management is not their job.
- Admin — The only role that can create, edit, publish, unpublish, and delete articles,
and the only one who can see drafts.
The split that matters
Everything in News reduces to one line: everyone reads published content, only admins write and manage it. Draft and unpublished content is visible to admins only. There is no in-between "editor" role in this module — publishing power belongs to admin.
6.5modules/news/features.md
News — Features
← overview.md
Reading (public)
- News feed — All published stories in a clean list, newest first, with title, short
summary, category, tags, date, and feature image.
- Single article page — The full story (or the trimmed extract, for curated items), with
category, tags, date, and related articles. Curated items also show a source credit and a link back to the original.
- Filter & search — Narrow the feed by category or by tag (visas, legal changes, housing,
…), or run a plain text search to jump straight to a topic.
Writing & managing (admin)
- Admin article editor — Create and edit articles of either type. Picking
curated
reveals the required source fields; picking original hides them.
- Draft / publish lifecycle — Save as draft, publish, unpublish, or delete. Only published
articles reach the public.
- Categories & tags — Organize both article types with one category per article and any
number of tags.
Type handling (the News-specific part)
- Curated import with auto-trim — Paste an extract from an external source; the system
trims it to the allowed length and requires a source name and link. See article-types.md.
- Original authoring — Write the full piece in-house, like a blog post, with no external
source.
6.6modules/news/flows.md
News — Flows
← overview.md
Field names and the two types match article-types.md and data-model.md.
1. Create an original article
- Admin opens the editor and picks
type = original.
- Admin writes the full title and body, assigns one category, and adds any tags.
- The source fields stay hidden and empty.
- Admin saves as a draft.
2. Create a curated article
- Admin opens the editor and picks
type = curated.
- The
source_name and source_url fields appear and are required.
- Admin pastes the extract and fills in the source name and link.
- On save, the system trims the extract to the allowed length and refuses to save if either
source field is missing or the link is invalid.
- Admin assigns one category and any tags, and saves as a draft.
3. Publish
- Admin reviews the draft.
- Admin clicks Publish;
status becomes published and published_at is set.
- The article now appears in the public feed (once
published_at is at or before now).
4. Read (visitor)
- Visitor opens the news page and sees published articles, newest first.
- Visitor filters by category or tag, or searches.
- Opening a curated article shows the extract plus the source credit and the link back;
opening an original article shows the full in-house body.
6.7modules/news/screens.md
News — Screens / pages
← overview.md
1. News list page
- Shows: A feed of article cards — title, short summary, category, tags, publication date,
and feature image — newest first.
- Does: Lets visitors scroll, open an article, filter by category or tag, and search by
keyword.
2. Single article page
- Shows: Headline, body, date, category, tags, and related articles. An original
article shows the full in-house body. A curated article shows the trimmed extract, followed by a "Source: {source_name}" credit and a "read the full story" link to source_url.
- Does: Lets the reader read the update, follow the source link on curated items, share
the story, and return to the feed.
3. Admin article editor
- Shows: A
type selector (curated / original), title field, rich-text content editor,
category dropdown, tags input, feature image upload, status (Draft / Published), and publishing date. When type = curated, the required source_name and source_url fields appear.
- Does: Lets an admin write an original article or paste a curated extract (auto-trimmed),
edit, save drafts, publish, schedule, unpublish, or delete.
6.8modules/news/business-rules.md
News — Business rules
← overview.md
Publishing & visibility
- Only articles with
status = 'published' appear on the public feed and article pages.
Drafts and archived posts are hidden from everyone except admins.
- An article is only visible if
status = 'published' and published_at is at or before
the current UTC time (so future-dated posts stay hidden until their time).
- Only admins can view drafts, create, edit, change status, or delete articles.
Content integrity
- Every article needs a non-empty title, body, and exactly one category before it can be saved
or published.
- Every article and category must have a unique slug, so URLs never collide.
- An article has exactly one category and zero or more tags.
- Every article has a
type of either curated or original; there is no third option.
- Curated must credit the source.
source_name and source_url are both required when
type = 'curated'; the article cannot be saved or published without them.
- Curated is trimmed. On save, a curated
content is automatically trimmed to a maximum
of 50 words, cut at the last whole word with an ellipsis "…" appended. The full external text is never stored or shown — only the extract plus the link back.
- Original carries no source. When
type = 'original', source_name and source_url
are empty and the body is not trimmed.
Images
- A feature image is optional for both types; when absent, a placeholder image is shown.
6.9modules/news/data-model.md
News — Data model
← overview.md
The module owns four tables and references users.id as the author. It never alters the shared users schema.
categories
News categories (e.g., Visas, Work Permits, Housing, Legal Updates). An article belongs to exactly one category.
| Field | Type | Notes |
id | integer | primary key |
name | string | unique category name |
slug | string | URL-friendly identifier |
description | text | brief summary of the category (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
articles
All published and draft content. type decides curated vs. original; source_name and source_url are required when type = 'curated' and empty when type = 'original'.
| Field | Type | Notes |
id | integer | primary key |
category_id | integer | references categories.id |
author_id | integer | references users.id (admin/author) |
type | string | curated or original |
title | string | article title |
slug | string | unique URL-friendly title |
content | text | body; for curated items this is the trimmed extract |
source_name | string | publisher name — required for curated, empty for original (nullable) |
source_url | string | link to the original — required for curated, empty for original (nullable) |
image_path | string | optional feature image; a placeholder is shown when empty (nullable) |
status | string | draft or published |
published_at | datetime | publication date in UTC (nullable) |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
tags
Free-form tags that apply to both article types (e.g., visa, deadline, housing).
| Field | Type | Notes |
id | integer | primary key |
name | string | unique tag name |
slug | string | URL-friendly identifier |
created_at | datetime | timestamp in UTC |
updated_at | datetime | timestamp in UTC |
article_tags
Join table linking articles to tags (many-to-many).
| Field | Type | Notes |
article_id | integer | references articles.id |
tag_id | integer | references tags.id |
The trim limit (50 words) is a business rule, not a column — see business-rules.md and article-types.md.
Shared schema change requests
| # | Date | Requested by | Module | What / why | Status |
| 1 | 2026-10-08 | Gyanendra Bohora | News | Add is_featured (boolean) to articles to pin urgent legal news to the top of the feed. | open |
6.10modules/news/messages.md
News — Messages & errors
← overview.md
Validation errors
- "Title is required." — Saving/publishing without a title.
- "Content cannot be empty." — Saving an article with no body.
- "Please select a category." — No category chosen before publishing.
- "A category with this name already exists." — Duplicate category name.
- "A curated article must credit its source." —
type = 'curated' saved without
source_name or source_url.
- "Please enter a valid source link." —
source_url is not a valid URL on a curated item.
- "The extract was shortened to 50 words." — Informational notice when a pasted
curated extract exceeds the limit and is auto-trimmed.
System & navigation errors
- "Article not found." — Invalid link, or an attempt to open a draft/deleted article.
- "You are not allowed to access this section." — A non-admin opens the editor or admin panel.
- "You are not allowed to publish articles." — Missing permission to set status to published.
Success messages
- "Article saved as draft." — A draft was stored.
- "Article published successfully." — The post is live.
- "Category created successfully." — A new category was added.
- "Article deleted." — An admin removed an article.
6.11modules/news/dependencies.md
News — Dependencies
← overview.md
- I receive from: Accounts & Admin — the logged-in user's id and role, so I know whether
the person is a visitor (read only) or an admin (create/edit/publish). News does not do its own login.
- I provide to: the public site / homepage — a list of the latest published articles. Not
applicable for other modules: no other Landaro module reads News data.
Mirror note
My "I receive from Accounts: user id + role" must mirror the "I provide to News" line in Accounts & Admin. Anything new on a shared table goes through ../../shared/change-requests.md, not a direct edit.
6.12modules/news/article-types.md
News — Article types
← overview.md
Every article is exactly one type: curated or original. This is the defining distinction of the module and drives the data, the editor, the public page, and the rules.
Curated (aggregated)
We pick up a piece of news from an external source and republish only a short extract, never the whole thing. The admin pastes the article into a dedicated field and fills in the source link.
- The imported text is automatically trimmed to a maximum of 50 words on save, cut at the
last whole word with an ellipsis "…" appended.
- The source must always be credited:
source_name and source_url are both required.
- On the public page the extract is followed by a "Source: {source_name}" credit and a
"read the full story" link pointing to source_url.
Original
We write the full article ourselves, start to finish, like a blog post.
- No external source:
source_name and source_url are empty.
- The body is the complete in-house content and is not trimmed.
Side by side
| Aspect | Curated | Original |
| Where content comes from | external source, short extract | written in-house, full text |
source_name / source_url | required | empty |
| Length limit | trimmed to 50 words + "…" | none |
| Public page shows | extract + "Source: …" + link back | full body |
| Categories & tags | yes | yes |
| Feature image | optional (placeholder if absent) | optional (placeholder if absent) |
Both types are stored in the same articles table, distinguished by the type field — see data-model.md. The rules that enforce all of the above live in business-rules.md.
6.13modules/news/open-questions.md
News — Open questions (TODO)
← overview.md
- TODO: Multilingual article content. The UI is EN/FR, but article bodies — single language
per article, or translated versions? (UI labels are handled by the i18n JSON files; this is about the article content itself.)
- TODO: Expiration & archiving. Do legal news items expire automatically, or does an admin
archive them manually? Recommendation: manual archiving, so long-term guides don't vanish.
- TODO: Featured / pinned articles. Add an
is_featured flag to keep urgent alerts at the
top? Recommendation: yes — already raised as a change request in data-model.md.
- TODO: Notifications. Should publishing an urgent alert email/notify registered users?
Recommendation: an optional "Notify users" checkbox in the editor for breaking updates.
- TODO: File attachments. Can admins attach downloadable PDFs (official forms, decrees) to a
post? Recommendation: allow uploads in the rich-text editor.
7Build & Infrastructure
7.1_build/README.md
Landaro — Build (code)
How to run the implemented application. This folder documents the code; the feature docs live in ../modules/ and ../shared/.
Status
- Milestone 1 — Accounts & Admin: DONE (backend + frontend, verified end to end).
- Milestone 2 — News: DONE (backend + frontend, verified end to end).
- Milestone 3 — Legal: DONE (backend + frontend, verified end to end).
- Milestone 4 — Courses: DONE (backend + frontend, verified end to end).
All four modules complete.
Layout
../backend/ — PHP 8.3 REST API (plain OOP, no framework, no Composer).
../frontend/ — Angular 20 app (standalone components).
../database/schema.sql — full MySQL 8 schema (all modules).
Prerequisites (your MAMP)
- PHP 8.3 (
/Applications/MAMP/bin/php/php8.3.30/bin/php).
- Apache on :8888, MySQL 8 on :8889 (user/pass
root/root).
- Node 22.20 + npm (already present).
One-time setup
See setup.md.
Run it
See run.md.
What is implemented
See implemented.md.
Config & secrets
- Real config:
../backend/config/config.php (git-ignored).
- Fill the
mail block with your SMTP credentials. Until then, emails are written
to ../backend/storage/mail/*.html so activation links are still reachable.
7.2_build/courses.md
Landaro — Courses (Milestone 4)
← build README
Course → modules → lessons; enroll per module; free period; tests; certificates; lead.
Endpoints
| Method | Path | Role | Purpose |
| GET | /courses/catalog | public | courses + module summaries (teaser) |
| GET | /courses/modules/{id} | user | module detail: lessons + progress + test + cert |
| POST | /courses/modules/{id}/enroll | user | enroll in a module |
| GET | /courses/lessons/{id} | user | open lesson (first start begins free period) |
| POST | /courses/lessons/{id}/complete | user | mark lesson completed |
| GET | /courses/modules/{id}/test | user | test (unlocked after lessons; no answers leaked) |
| POST | /courses/modules/{id}/test | user | grade attempt; pass → complete + certificate |
| GET | /courses/certificates/mine | user | my certificates |
| GET | /certificates/{vid} | public | certificate lookup by verification ID |
| POST/GET | /courses/lead | user | submit (upsert) / read my lead |
| GET | /admin/courses/leads | admin | all leads |
| POST | /admin/courses/leads/{id}/review | admin | mark new/valid/invalid (valid → full access) |
| CRUD | /admin/courses[...] | admin | courses, modules, lessons |
| GET/POST | /admin/courses/modules/{id}/test | admin | read / save (replace) the module test |
Test cases
| Case | Expected |
| New user opens first lesson | allowed; free_period_ends_at set to now+10d |
| Expired user (no full access) opens lesson | 403 "Your free period has ended." |
| Test before all lessons done | 400 "Complete the lessons before taking the test." |
| Test questions to client | options have NO is_correct |
| Wrong answers | passed=false, attempt recorded |
| Correct answers ≥75% | passed=true, module completed, certificate issued |
| Certificate lookup by ID | name + module + attempt number |
| Expired user submits lead | lead stored (status new); modal stops |
| Admin marks lead valid | user has_full_access=true, lesson accessible again |
Business rules enforced
- Enroll per module; module completed = all lessons completed AND test passed.
- Free period global per account: clock starts on the FIRST lesson started anywhere
(Courses triggers Accounts to set free_period_ends_at = now + courses.free_period_days).
- Access = valid free period OR
has_full_access; on expiry lessons lock, lead modal shows.
- One test per module (v1); multiple-choice single-answer; pass ≥
courses.test_pass_threshold
(75); unlimited attempts recorded with attempt_number; answers never sent to the client.
- Certificate issued once per user+module (verification ID + passing attempt + branding);
emailed with a link; PDF via browser print (decision 6B).
- One company lead per user (upsert); admin notified by email;
valid grants full access (audited).
Frontend
- Public
/courses — catalog with module summaries.
/courses/modules/:id — enroll, lesson list with progress, test link, certificate link.
/courses/lessons/:id — lesson content + custom YouTube player (nocookie, rel=0,
modestbranding, title mask) + mark complete.
/courses/modules/:id/test — radio-per-question quiz + result (score/pass/retake).
/certificate/:vid — printable certificate (browser print-to-PDF).
- Global lead modal (shown after expiry, every navigation, until submitted).
- Admin
/admin/courses — content manager (courses/modules/lessons/test editor) + leads tab.
- EN/FR strings; nav: public Courses, admin Courses ⚙.
7.3_build/implemented.md
Landaro — Implemented (Milestone 1)
← build README
Backend foundation (shared, used by all modules)
- PSR-4 autoloader (no Composer), front controller + regex router.
- Shared API envelope
{ data, error, meta }, error codes + HTTP statuses from
shared/api-contract.md.
- PDO database layer (parameterized queries), UTC storage / Europe-Bucharest display.
- Hand-written JWT (HS256),
sessions table for logout/expiry.
- CORS for the Angular dev server; Validator; Pagination (10/25/50/100, max 100).
- Mailer via PHPMailer 6.9.3 (in
backend/lib/PHPMailer); disk fallback when SMTP blank.
Accounts & Admin — endpoints
| Method | Path | Role | Purpose |
| POST | /auth/register | public | create pending user + activation email |
| POST | /auth/activate | public | activate via single-use token |
| POST | /auth/resend-activation | public | resend activation (no email enumeration) |
| POST | /auth/login | public | issue JWT + session (remember me) |
| POST | /auth/logout | user | invalidate session |
| GET | /auth/me | user | current user |
| PUT/PATCH | /profile | user | update own details / password / locale / email |
| POST | /profile/confirm-email | public | confirm email change (re-verification) |
| GET | /admin/users | admin | search + filter + paginate |
| GET | /admin/users/{id} | admin | user detail |
| PATCH | /admin/users/{id}/status | admin | activate / block / unblock |
| PATCH | /admin/users/{id}/role | admin | change role |
| POST | /admin/users/{id}/extend-free-period | admin | add free days (audited) |
| GET | /admin/settings | admin | list settings (with defaults) |
| PUT | /admin/settings | admin | upsert a setting (audited) |
Business rules enforced
- Password: min 8 chars, ≥1 letter + ≥1 digit (live strength bar in UI).
- Unique email; generic login failure; blocked/pending login messages.
- Activation before use; single-use, expiring tokens (hashed, never stored raw).
- Remember-me (7d) vs short session (1d); session 7d / 1d, activation 24h, reset 1h.
- Email change requires re-verifying the new address; old email kept until confirmed.
- Admin actions written to
audit_log (status, role, free-period, settings).
- Access model helper:
access = valid free period OR has_full_access.
Frontend (Angular 20)
- Header with nav + EN/FR language selector (JSON dictionaries, saved on account).
- Pages: Home, Login, Register (strength bar), Activate / Confirm-email, Profile,
Admin Users (table + actions + pagination), Admin Settings.
- Auth via signals + JWT interceptor; route guards (
authGuard, adminGuard).
Verified
Health, admin login, /auth/me, admin user list + settings, register (201), weak password (400), duplicate email (400), pending login (403), unauthorized (401), activation + single-use reuse (400), CORS preflight (204). Frontend builds + serves; i18n assets load.
Decisions applied from _questions-build.md
B (module-by-module) · symlink serving · PHPMailer fetched into lib · real SMTP (you fill creds) · certificate PDF = browser print (later module) · one test per module · is_featured included · legal daily limit counts all questions that day · custom player per recommendation (later module).
7.4_build/legal.md
Landaro — Legal (Milestone 3)
← build README
Two parts: a public FAQ, and a gated user↔consultant chat.
Endpoints
| Method | Path | Role | Purpose |
| GET | /legal/faq | public | published FAQ grouped by category |
| GET | /legal/categories | public | Legal categories |
| GET | /legal/disclaimer | public | disclaimer text (setting) |
| POST | /legal/questions | user | ask (gated + daily limit) |
| GET | /legal/questions/mine | user | own questions |
| GET | /legal/questions/pool | consultant/admin | unclaimed pool |
| GET | /legal/questions/claimed | consultant | my claimed |
| POST | /legal/questions/{id}/claim | consultant/admin | exclusive self-claim |
| GET | /legal/questions/{id} | participant | thread + messages |
| POST | /legal/questions/{id}/messages | participant | post message |
| POST | /legal/questions/{id}/resolve | participant | mark resolved |
| POST | /legal/questions/{id}/reopen | user/admin | reopen |
| GET/POST/PUT/DELETE | /admin/legal/faq[...] | admin/consultant | FAQ authoring |
| POST/DELETE | /admin/legal/categories[...] | admin | categories |
| GET | /admin/legal/questions | admin | all questions |
| POST | /admin/legal/questions/{id}/assign | admin | assign/reassign |
| GET | /admin/legal/statistics | admin | global + per-consultant metrics |
Test cases
| Case | Expected |
| Public FAQ / disclaimer | 200 |
| No-access user asks | 403 lead message |
| Daily limit reached | 429 "Daily limit for questions reached." |
| First consultant claims | 200 "You claimed this question." |
| Second consultant claims same | 400 "This question has already been claimed." |
| Non-participant posts | 403 |
| User resolves, then reopens | status resolved → claimed |
| Pool without consultant/admin | 401 |
| Statistics | counts by status + per-consultant averages |
Business rules enforced
- FAQ public = published only; admin + consultants author; one category + optional image.
- Ask gated by access model (valid free period OR
has_full_access).
- Daily limit =
legal.max_questions_per_day (default 3), counts all questions created
since local midnight (Europe/Bucharest), any status.
- Statuses open → claimed → resolved; reopen → claimed. Posting on a resolved thread reopens it.
- Self-claim is atomic/exclusive (conditional UPDATE); admin can assign/reassign.
- Participant authorization: asking user, assigned consultant, or admin.
- Disclaimer from
legal.disclaimer_text, shown on FAQ + chat screens.
- Statistics: raw metrics only (no composite score).
Frontend
- Public
/legal — disclaimer + grouped FAQ accordion + ask entry point.
- User
/legal/my-questions — ask form + question list + chat (reusable chat component).
- Consultant
/consultant — unclaimed pool (claim) + my claimed + chat.
- Admin
/admin/legal — tabs: FAQ management (+ categories), questions overview (assign),
statistics (global + per-consultant).
- Reusable
app-legal-chat (chronological bubbles, send/resolve/reopen).
- EN/FR strings; nav links: public Legal, user My questions, Consultant, admin Legal ⚙.
7.5_build/news.md
Landaro — News (Milestone 2)
← build README
Endpoints
| Method | Path | Role | Purpose |
| GET | /news/articles | public | feed: published + published_at<=now, filters, search, paginated |
| GET | /news/articles/{slug} | public | single article + related (same category, recent) |
| GET | /news/categories | public | list categories |
| GET | /news/tags | public | list tags |
| GET | /admin/news/articles | admin | all articles (drafts included) |
| GET | /admin/news/articles/{id} | admin | one article for editing |
| POST | /admin/news/articles | admin | create (draft or published) |
| PUT | /admin/news/articles/{id} | admin | update |
| DELETE | /admin/news/articles/{id} | admin | delete |
| POST | /admin/news/categories | admin | create category |
| PUT | /admin/news/categories/{id} | admin | update category |
| DELETE | /admin/news/categories/{id} | admin | delete (blocked if in use) |
| POST | /admin/news/upload | admin | upload feature image (png/jpg/webp, 5MB) |
Test cases
| Case | Expected |
| Feed (public) | 200, newest + featured first, only published & due |
| Article by slug | 200 with related list |
| Create curated without source | 400 "A curated article must credit its source." |
| Create curated with 60-word body | content trimmed to 50 words + "…" |
| Create curated with bad source_url | 400 "Please enter a valid source link." |
| Create original | 200/201, no source fields, body untouched |
| Publish | appears in public feed |
| Admin endpoints without admin token | 401/403 |
| Duplicate category name | 400 "A category with this name already exists." |
Business rules enforced
- Two types: curated (source required, 50-word trim + "…") / original (no source).
- Title + content + exactly one category required; unique slugs per table.
- Only published +
published_at<=now are public; drafts admin-only.
- Any number of tags (auto-created by name); related = same category, recent, excl. self.
is_featured included (change request #1) and pins to the top of the feed.
- Feature image optional; placeholder shown when absent; local-disk upload.
Frontend
- Public:
/news (feed with search + category filter, cards), /news/:slug
(article; curated shows "Source: … / read the full story", original shows full body; related articles).
- Admin:
/admin/news — category manager + article list + editor (type toggle reveals
source fields, tags, image upload, schedule date, featured, draft/publish/delete).
- EN/FR strings added; nav links added for public News and admin News.
7.6_build/run.md
Landaro — Running
← build README
Start the frontend
From ../frontend/:
npx ng serve --port 4200
Open http://localhost:4200.
Backend
No start step — Apache (MAMP) serves it through the htdocs symlink at http://localhost:8888/landaro-api. Just make sure MAMP's Apache + MySQL are running.
Quick smoke test (curl)
BASE=http://localhost:8888/landaro-api
# Health
curl -s $BASE/health
# Admin login
curl -s -X POST $BASE/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"test@test.com","password":"test","remember":true}'
Try the UI flows
- Register a user → check
../backend/storage/mail/ for the activation email →
open the activate?token=... link path in the app → account becomes active.
- Log in as the user; edit the Profile, change language (EN/FR) in the header.
- Log in as admin (
test@test.com / test) → Admin → Users: search, filter,
activate/block, change role, extend free period. Admin → Settings: edit keys.
Re-lint / rebuild
# Backend syntax check
find ../backend/src ../backend/public -name '*.php' -print0 | xargs -0 -n1 \
/Applications/MAMP/bin/php/php8.3.30/bin/php -l
# Frontend production build
cd ../frontend && npx ng build
7.7_build/setup.md
Landaro — One-time setup
← build README
1. Database (schema + seed)
From ../backend/:
PHP=/Applications/MAMP/bin/php/php8.3.30/bin/php
$PHP scripts/migrate.php # creates the `landaro` database + all tables
$PHP scripts/seed.php # first admin + default settings
Seeded admin (per _decisions.md, changeable later): test@test.com / test.
2. Serve the API through Apache (symlink into htdocs)
Already created during the build:
ln -sfn "/Users/pixeltime/kiro/_devschool/projects/landaro/backend/public" \
"/Applications/MAMP/htdocs/landaro-api"
API base: http://localhost:8888/landaro-api.
3. SMTP (optional, for real emails)
Edit ../backend/config/config.php → mail block (host, port, username, password, from). Leave blank to keep writing emails to ../backend/storage/mail/.
4. Frontend dependencies
Already installed. If needed again, from ../frontend/:
npm install
Ports in use
- Apache
:8888 → serves the PHP API via the htdocs symlink.
- MySQL
:8889 → MAMP database.
- Angular dev server
:4200 → the frontend (CORS allowed by the API).
7.8_build/test-users.md
Landaro — Test users
Test accounts, all with the password test. They are pre-activated (status = active), so no email activation is needed. They are created by backend/scripts/seed.php (idempotent — re-running it does not duplicate them).
| Email | Password | Role | Status | Notes |
test@test.com | test | admin | active | the original seeded admin |
test@test.ro | test | admin | active | admin added on request |
user@test.ro | test | user | active | simple user; free period starts on the first lesson |
consultant@test.ro | test | consultant | active | legal consultant |
What each role can do (short)
- admin — everything: users, settings, News, Legal (FAQ + statistics), Courses + leads.
- user — own profile, ask a consultant (chat), courses + tests + certificates.
- consultant — claim and answer legal questions, write FAQ entries.
Where to log in
Frontend: http://localhost:4200 → Log in.
Notes
user@test.ro has not started the free period yet and does not have has_full_access.
When the period expires (or to test the lock), the company-lead modal appears. If you want this user to have permanent full access, tell me and I will set has_full_access = 1.
- To recreate the accounts after a fresh
migrate:
php scripts/seed.php (from the backend/ folder).
7.9database/README.md
Landaro — Database
This folder holds everything needed to set up the Landaro database.
| File | What it is |
schema.sql | empty schema only (all tables, no data) |
landaro-backup.sql | full backup: schema + all demo data (news, FAQ, questions, courses, test users) |
Fastest start — import the full backup
landaro-backup.sql is self-contained: it creates the landaro database if needed, drops and recreates every table, and loads all the data. Importing it is all you need — no migrate/seed required.
Just ask Kiro: "import database/landaro-backup.sql into my MySQL database".
Or run it yourself (MAMP MySQL on port 8889, user/pass root/root):
/Applications/MAMP/Library/bin/mysql80/bin/mysql \
--host=127.0.0.1 --port=8889 --user=root --password=root \
< database/landaro-backup.sql
Note: importing replaces the Landaro tables (DROP + CREATE). If you already have data you want to keep, back it up first.
Alternative — build from scratch
If you want an empty database and then generate fresh data:
cd backend
php scripts/migrate.php # create schema
php scripts/seed.php # first admin + test users + default settings
php scripts/demo-seed.php # 20 news, 50 FAQ, 10 questions, 10 courses
Test users (all password test)
See ../_build/test-users.md. Quick reference: test@test.com (admin), test@test.ro (admin), user@test.ro (user), consultant@test.ro (consultant).
Refreshing the backup
After changing data locally, regenerate the backup with:
/Applications/MAMP/Library/bin/mysql80/bin/mysqldump \
--host=127.0.0.1 --port=8889 --user=root --password=root \
--no-tablespaces --single-transaction --skip-comments \
--add-drop-table --databases landaro > database/landaro-backup.sql
7.10frontend/README.md
Frontend
This project was generated using Angular CLI version 20.3.5.
Development server
To start a local development server, run:
ng serve
Once the server is running, open your browser and navigate to http://localhost:4200/. The application will automatically reload whenever you modify any of the source files.
Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
ng generate component component-name
For a complete list of available schematics (such as components, directives, or pipes), run:
ng generate --help
Building
To build the project run:
ng build
This will compile your project and store the build artifacts in the dist/ directory. By default, the production build optimizes your application for performance and speed.
Running unit tests
To execute unit tests with the Karma test runner, use the following command:
ng test
Running end-to-end tests
For end-to-end (e2e) testing, run:
ng e2e
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the Angular CLI Overview and Command Reference page.