Story: Implement the profile journeys in ores.web
Table of Contents
This page documents a story in Sprint 26. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
The three Profile journeys run in ores.web against the real server. A member sets their own photo, name, job title and contact details, and a tenant administrator corrects a colleague's. The screen is one screen with panels, and each panel says who may write it. A reporting-line change is a proposal that a hierarchical superior or an administrator approves, not a plain edit.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 26 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-05 |
3. Acceptance
- The three Profile journeys run in
ores.webagainst the real server: a member sets their own photo, name, job title and contact details, and a tenant administrator corrects a colleague's. - The self-service subjects exist, and each is restricted to the caller's own account and to the fields a member owns. Every other field is refused with a code the screen branches on.
- The photo can be replaced: an upload path into
ores.assetsreturns an image id, and the accepted formats and the maximum size are stated once and enforced by the picker and by the server. - One Save writes both records and names the record that failed. Nothing on the screen claims the pair is atomic.
- A reporting-line change is a proposal. The screen states who must approve it, the value does not change while the proposal waits, and no approval is claimed that nothing performs.
- The screen says which panel the caller may write, and every panel the caller may not write says why.
- The two email fields are labelled so that neither reads as the other.
- A signed-in person reaches the journeys from the shell's navigation, and the trigger that opens it shows the person's own name and photo once the account has been read.
- Every gap the three journey documents record is closed or stated on the screen, and the statement is recorded.
4. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| Prototype the profile screen | DONE | 2026-09-30 | 2026-09-30 | Build the throwaway static prototype that argues the profile screen's layout: which panel is writable for whom, how a photo is replaced and approved, and how a reporting-line proposal waits for approval, in three structural variants. |
| Prototype the application navigation | DONE | 2026-09-30 | 2026-10-01 | Build the throwaway static prototype that argues how a signed-in person reaches any journey in the catalogue: five structural models, over the catalogue's real 22 journeys in 8 groups, for a member and a tenant administrator. |
| Record the privileged-access need | DONE | 2026-10-01 | 2026-10-01 | Write the capture that records why the shell's modes need a recorded break-glass process behind them, what the systems that do this well have in common, and the decisions it leaves open. |
| Define the session mode and capability contract | DONE | 2026-10-01 | 2026-10-01 | Write the target state that fixes how the browser learns its mode and what it may see: the mode is computed by the server and delivered with the session, and the menu is built from the permissions the server already answers. The protocol change that implies, and what does not change. |
| Implement the super administrator sign-in | DONE | 2026-10-01 | 2026-10-01 | Deliver the session mode and the system administration landing, so the super administrator signs in and reaches the tenant management journeys. |
| Show the tenants a deployment holds | DONE | 2026-10-01 | 2026-10-01 | Give the system administration area a read-only tenant roster, as its own journey, and the read the screen needs. |
| Bound the signed-in layout and unblock the tenant details step | DONE | 2026-10-01 | 2026-10-01 | The splash is unbounded on a signed-in journey, and a seed profile that inherits the administrator password leaves the second step of New tenant with a disabled Continue. |
| Make the tenant roster a workspace | DONE | 2026-10-01 | 2026-10-01 | Take the table out of its card, put the title, the count and the primary action in a page header, and give the shell a width for list-shaped screens. |
| Paint the tenant status with the badge catalogue | DONE | 2026-10-01 | 2026-10-01 | The roster hardcodes the colours and the labels of the tenant statuses. Use the platform's badge catalogue, which already maps tenant_status to badges with their own colours, labels and tooltips. |
| Put the badge on the tenant status row | DONE | 2026-10-01 | 2026-10-01 | The badge belongs to the value, not to a mapping table. Give the tenant status its own badge reference, take the label from the status's own name, and read the statuses instead of the junction. |
| Return to a setup journey in progress | DONE | 2026-10-01 | 2026-10-01 | A setup journey's position is only in the browser, so leaving the tenant creation window loses it and nothing offers the way back. The way back is the tenant list, which needs the provisioning run to name the tenant it acts on. |
| Add the profile self-service subjects | DONE | 2026-10-04 | 2026-10-04 | Add iam.v1.accounts.update-self and iam.v1.account_contact_informations.update-self, each restricted to the caller's own account and to the fields a member owns, so a member can write their own profile and contact record. |
| Upload a profile image | DONE | 2026-10-04 | 2026-10-05 | Add the upload path into ores.assets that stores an image and returns its id, and state the accepted formats and size once, so the picker and the server enforce one rule. |
| Build the profile screen | DONE | 2026-10-05 | 2026-10-05 | Build the three Profile journeys as one screen in ores.web on the journey runtime: the two writable panels with a save and a reason each, the read-only sign-in summary, the photo picker on the upload path, and the reporting-line proposal that states who approves. |
5. Decisions
- The shell is a menu, the cards, and the avatar. The header carries a menu of areas, an area opens its journeys as cards, and the person's own journeys are under their name and photo. Nothing else: there is no sidebar and no second list of what the cards already say. (Prototype verdict, 2026-10-01.)
- The mode is read from the sign-in, never chosen. A super administrator works in the system tenant's context, a tenant administrator in a tenant's, and a party user in a party's — the contexts the server already scopes everything by, so the mode cannot disagree with the data and there is no switch to get wrong.
- People is the privileged party user's area. Managing the people of a party is a party job done by somebody with authority in it, not a tenant administrator's job, and a regular party user is not shown the door to it at all.
- The bootstrap journey is a door, not an area. First run belongs to a plain installation, where there is no account to sign in with and therefore no shell to draw a menu in. It is the way out of the state the shell cannot be entered from, so no menu offers it. The system administration menu is the Tenants area, which holds the tenant management journeys: creating a tenant, and retiring or resetting one. Tune the tenant is the tenant administrator's and stays in their mode. (Product owner, 2026-10-01.)
- The mode is stated by the server and read by the browser. The session view
and the login answer carry it, and the shell draws its menu from the value it
is given. The BFF states it today, from the tenant the login answered with,
because that is the fact the session already holds and the rule stays out of
the browser; when
ores.iamstates it on the login answer the rule goes and nothing on the client moves. (Implementation, 2026-10-01.) - A mode with no implemented journeys shows the session, not an empty menu. The application mode declares no areas because a tenant-scoped session and the permission codes it needs do not exist yet. It shows the tenant and the party the person works in, which are facts, rather than drawing areas it cannot fill. (Implementation, 2026-10-01.)
- The roster of tenants is its own journey. The list existed only as step 1 of Retire or reset a tenant, so finding out what tenants a deployment holds meant opening a journey that destroys one. Reading is its own goal, and the catalogue already has the shape: See who has access is the same read-only roster for the people of a party. See the tenants joins the Tenancy group, and Retire or reset a tenant picks its tenant from it. (Product owner, 2026-10-01.)
- A read the browser may not make is refused at the route, not only hidden. The mode says which context a session acts in, so a route can refuse a session that is acting inside a tenant even while the subject behind it checks no permission. A menu that omits an area is structure; a route that refuses a caller is a boundary, and only the second one is authority. (Implementation, 2026-10-01.)
- The signed-in shell bounds its screen at the public shell's width. A journey that stands something up draws the same banner before and after a sign-in, and a banner fills the column it is given. One shell capped its column and the other did not, so the same screen was a comfortable width at the door and a wall once somebody was through it. One number, in both shells. (Implementation, 2026-10-01.)
- An offer to inherit a password stands only while there is one to inherit. A starting point may propose that the tenant's administrator takes the creating administrator's password, and a journey with no creating administrator has nothing to hand over. The proposal is dropped where the details are built rather than drawn and then refused, because a choice that cannot be kept leaves a step nobody can leave. (Implementation, 2026-10-01.)
- Two bounds, named for the shape they suit. A rail, a form or a dialog takes the column; a list or a grid takes the workspace. One bound for every screen was wrong in both directions and produced both defects: a banner unbounded on a wide display, and a table held to the width a form wants with a dead margin on either side. The shell owns the bound so a journey cannot forget it, and the default is the column, because a form across a wide display is the harder defect to read. (Implementation, 2026-10-01.)
- A table is the page, not a panel on it. A card that already carries the title and the count, drawn around a table, makes the table narrower than the screen and reads as a box in the middle of nothing. The title, the count and the primary action belong in a page header, and the table belongs under it. (Review, 2026-10-01.)
- A control that opens nothing is not drawn. The roster has no search, no filters and no row actions, because the list subject has no filter and the journey a row action would open is not built. They wait for the things behind them rather than appearing as buttons that do nothing. (Implementation, 2026-10-01.)
- A status is painted by the badge catalogue, not by the screen.
ores.dqholds the badge catalogue, so a screen holds no colours of its own and two screens showing the same status agree with each other. (Product owner, 2026-10-01.) - The badge belongs to the value, not to a mapping. A status row names the badge that paints it; nothing maps a value to a badge, because a value that names its own presentation cannot drift from it. (Product owner, 2026-10-01.)
- The badge supplies the colours; the value supplies the words. A pill's label is the value row's own name, never the badge's. A shared badge vocabulary is what lets one visual language cover every state in the platform, and it stops being that the moment it renames somebody else's state: a suspended tenant reads Suspended in the frozen colour, not Frozen. (Product owner, 2026-10-01.)
- A junction that can be replaced by a reference is not kept.
badge_mappingstill serves the domains that have not moved and is read by nothing on the path that has; it goes when the last domain carries its own reference, not before, because deleting it early takes those domains' colours with it. (Implementation, 2026-10-01.) - A run belongs to the tenant that asked for it, and acts on the one it names. The two roles are different and the engine already keeps the first: a provisioning run's rows are scoped to the super administrator's own tenant, so the person who asked for the run can read it, while the tenant the run builds is the one every step acts on. A run owned by the tenant it creates could not be followed by the session that started it, because that session is in another tenant. (Implementation, 2026-10-01.)
- The run names what it acts on, as a field and not only as payload. A
workflow instance states a target: a caller-supplied kind and id, opaque to
the engine, naming the entity the run acts on. It does not stay in
request_jsonalone, where nothing can index it, filter by it or report it, and where only the run's own handler can find it. The engine gains one generic field rather thanores.iamgaining a table that would leave the run itself unable to say what it is for, and leave the next component to build the same table again. (Product owner, 2026-10-01.) - A journey abandoned before it provisions is restarted, not resumed. Nothing is written before the run starts, so there is no draft tenant and no draft status to invent: a person who leaves the form before submitting starts again, and what a screen has to offer back is the work that has a run behind it. (Product owner, 2026-10-01.)
- A tenant can have any number of runs in flight. No index limits runs per target, because a second attempt does not have to wait for the first one to stop. The roster shows the run that changed last. (Product owner, 2026-10-01.)
- The target is a kind and an id, not an oresmd URI. oresmd is the URI grammar for market data. It is not a general way to name entities, so the workflow target does not use it. (Product owner, 2026-10-01.)
- A run is resumed from its own page, and the form is not restored. The roster
links an unfinished run to
/tenants/runs/:id. That page shows the provisioning step alone: the same rail and the same retry. The hand-off step is not offered there, because the browser never stored the administrator password it needs. A tenant detail screen is a later piece of work. (Implementation, 2026-10-01.) - The screen is one page, and each panel saves itself. Variant B of the prototype is accepted: the profile and the contact record carry their own versions and their own change reasons, so each panel writes its own record with its own reason and a half-finished edit never rides along with the one that is ready. The rail variant is rejected because this screen is a page a person returns to, not a run with a beginning and an end, which is what makes a rail right for the registration door. (Prototype verdict, 2026-09-30.)
- The member writes their own record.
iam.v1.accounts.update-selfandiam.v1.account_contact_informations.update-selfare added, restricted to the caller's own account and to the fields a member owns:full_name,job_title,image_idand the contact record. Without them two of the three journeys are read-only, which is the largest gap the group document records. (Product owner, 2026-09-30.) - The photo is replaced, not only chosen. An upload path into
ores.assetsreturns an image id, and the accepted formats and the maximum size are stated. Step 2 of the flagship journey cannot happen at all otherwise. (Product owner, 2026-09-30.) - A reporting-line change is a proposal. The person whose line it is proposes the change; their hierarchical superior or a tenant administrator approves it, and the line changes only then. An administrator acting on a colleague's record is the approver, so their own change takes effect. (Product owner, 2026-09-30.) It is not a plain edit on the profile screen, and it is not a plain edit on Membership's tree either: both documents change to record the proposal.
- One Save, two writes, and the failure is named. The profile and the contact record are separate records and no composite operation is added now, so the screen sends both and names the record that failed. (Product owner, 2026-09-30.)
- Two emails stay two, and both are labelled.
account.emailis the address a person signs in with; the contact record'semailis the address colleagues use. The screen shows both, labelled apart, and neither reads as the other. - The sign-in and access panel is read-only here. It belongs to Protect my account and Know what I may do, so the profile screen carries it as a summary with the reason stated on it.
- The two platform defects the end-to-end run found are captured, not fixed here. The member's own saves are refused by the generated insert trigger's ordering, and an upload over the message fabric's payload cap cannot reach the validator; neither is in this branch's diff, so both go to the product backlog for triage: A self write is refused on a tenant with accounts and A profile image over the payload cap cannot reach the validator. (End-to-end run, 2026-10-05.)
- An upload larger than 1 MB travels over HTTP storage, not the message fabric. The 1 MB payload cap silently narrows the 2 MB the picker and the validator state, so the bytes of a large upload leave the request and reply fabric and travel over the storage surface instead. (Product owner, 2026-10-05.)
6. Result
Closed 2026-10-05. The screen, its routes and its wire contract are built and reviewed, and the end-to-end run over the real stack proves everything the branch owns: the administrator's write to a colleague's record passes — the picker's read, the account read, the profile write with its unset fields echoed — and so do the picture byte round trip, the member's reads, the read gap's refusal, the grants read, the upload policy, and the refusal of the administrator's route. 67 checks ran; 29 failed, every one of them traced to the two platform defects the Decisions record: the member's own saves are refused, so the member's picture and contact record never land, and the administrator's checks that read them cannot pass. The one direct administrator failure is the same self-write defect: the administrator writing their own contact record.
The two defects are not fixed here because neither is in this branch's diff.
The member's own saves — PUT /api/me/profile and PUT
/api/me/contact-information — are refused with "The write failed." on any
tenant that holds accounts: the generated insert trigger validates
modified_by after the version replace has retired the actor's own row. An
image over roughly three quarters of a megabyte dies in the transport (http
- before the validator can judge it. Both are captured in the inbox for
triage, and acceptance bullet 1 is met for the administrator and blocked for
the member until they land. The raw output is
evidence/profile_journeys_run.txt beside the task.
7. Out of scope
- The approval itself. Nothing in the platform holds one today: the workflow engine can hold the instance, but the queue, the notification and the decision belong to the Draw the reporting line journey and to the Membership group. The profile screen draws the proposal and states who decides; it does not build the decision.
- The roles a person holds, which is the Access group's work, and the password, the sign-in state and the sign-in audit, which are the Credentials group's.
- Which of the two email fields other screens treat as authoritative. The screen labels them apart; the modelling decision is its own work.