About:Pharmacopedia.ext: Difference between revisions
From Pharmacopedia
More actions
| [checked revision] | [checked revision] |
MDElliottMD (talk | contribs) Doc pass for v0.9.7.0: ADHD screening (ASRS, AMAAS), Formal testing + per-field privacy, plants skin + assessment card family |
MDElliottMD (talk | contribs) Reconcile the spec to v0.9.8.0: Administer to others, the diptych, full-width layout + Appearance rail, the fungi sub-skin; parser-tag / special-page / module / assessment / table catalogues brought current |
||
| Line 1: | Line 1: | ||
= Pharmacopedia extension specification = | = Pharmacopedia extension specification = | ||
'''Version:''' 0.9. | '''Version:''' 0.9.8.0 · '''Requires:''' MediaWiki >= 1.46.0 · PHP >= 8.5 | ||
'''Author:''' MDElliottMD · '''License:''' GPL-2.0-or-later | '''Author:''' MDElliottMD · '''License:''' GPL-2.0-or-later | ||
'''Source:''' <code>/var/www/mediawiki/extensions/Pharmacopedia/</code> | '''Source:''' <code>/var/www/mediawiki/extensions/Pharmacopedia/</code> | ||
The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited medicine reference with rich user-profile, assessment, life-story, and visibility-sharing infrastructure. It adds parser tags, special pages, API modules, a chip-picker / autosave UI framework, a vis-timeline-based visual life timeline, a granular per-record sharing subsystem, and a database schema that together support: | The Pharmacopedia extension turns a MediaWiki install into a structured, community-edited medicine reference with rich user-profile, assessment, life-story, and visibility-sharing infrastructure. It adds parser tags, special pages, API modules, two dark skins, a chip-picker / autosave UI framework, a vis-timeline-based visual life timeline, a granular per-record sharing subsystem, an observer-perspective subsystem, a token-gated subsystem for administering assessments to people outside the wiki, a two-origin diptych front-of-house, and a database schema that together support: | ||
* Structured medicine pages via the <code><nowiki>{{MedTemplate}}</nowiki></code> template | * Structured medicine pages via the <code><nowiki>{{MedTemplate}}</nowiki></code> template | ||
| Line 12: | Line 12: | ||
* Two-perspective data capture (personal vs. provider) wherever clinically meaningful | * Two-perspective data capture (personal vs. provider) wherever clinically meaningful | ||
* User profile with dimensional personality / autism assessments (CATI, CAT-Q, MBTI, Enneagram, PID-5-BF, OCEAN/BFI-10) and rich auto-generated reports | * User profile with dimensional personality / autism assessments (CATI, CAT-Q, MBTI, Enneagram, PID-5-BF, OCEAN/BFI-10) and rich auto-generated reports | ||
* Further self-report instruments (BPNS, NFCS, OCI-PCP, WHOQOL-BREF) on the same auto-scored, report-bearing pattern | |||
* ADHD screening (ASRS and AMAAS-SR) plus a Formal testing log of standardized-test scores, every score field carrying its own visibility | * ADHD screening (ASRS and AMAAS-SR) plus a Formal testing log of standardized-test scores, every score field carrying its own visibility | ||
* Diagnosis autocomplete backed by ~41,500 ICD-10-CM, ICD-11, and DSM-5 codes | * Diagnosis autocomplete backed by ~41,500 ICD-10-CM, ICD-11, and DSM-5 codes | ||
* '''Life-story timeline''': visual swimlanes (vis-timeline 7.7.3, vendored Apache-2.0) plus a synchronized trait-trajectory overlay (vis.Graph2d); card-list view alongside; quick-add free-text observation parser; range-date episode form with severity slider | * '''Life-story timeline''': visual swimlanes (vis-timeline 7.7.3, vendored Apache-2.0) plus a synchronized trait-trajectory overlay (vis.Graph2d); card-list view alongside; quick-add free-text observation parser; range-date episode form with severity slider | ||
* '''Per-record sharing subsystem''': rule types include public, private, users, cohort, link_token, reciprocal; time-bounded; audit log of who-viewed-what; bulk free-text → structured ref upgrader; privacy mode that disables legacy fallback | * '''Per-record sharing subsystem''': rule types include public, private, users, cohort, link_token, reciprocal; time-bounded; audit log of who-viewed-what; bulk free-text → structured ref upgrader; privacy mode that disables legacy fallback | ||
* '''Observer perspectives''': token-gated, no-account second-party input on something a user owns, under a two-gate consent model | |||
* '''Administer to others''': send any registered assessment scale to people outside the wiki by one-time link, collect their results, and follow them over time, under per-owner public-key encryption with an optional zero-knowledge passphrase mode | |||
* '''Two-origin diptych''': the Main Page and Category index render as chromeless full-viewport splashes giving the pharmaceutical and plant origins equal face | |||
* Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces | * Chip-picker / autosave / slider-precise-input UI framework shared across editor surfaces | ||
* Date + time + range kit: PCPDatePicker (point/range/possibility), PCPTimePicker (fuzzy time-only: "4p", "noon", "quarter past 6") | * Date + time + range kit: PCPDatePicker (point/range/possibility), PCPTimePicker (fuzzy time-only: "4p", "noon", "quarter past 6") | ||
| Line 36: | Line 40: | ||
* '''Backend (PHP):''' <code>includes/</code>, one class per parser tag, store, special page, or API module. Auto-loaded under <code>MediaWiki\Extension\Pharmacopedia\</code>. Assessment classes under <code>includes/Assessments/</code>. API modules under <code>includes/Api/</code>. | * '''Backend (PHP):''' <code>includes/</code>, one class per parser tag, store, special page, or API module. Auto-loaded under <code>MediaWiki\Extension\Pharmacopedia\</code>. Assessment classes under <code>includes/Assessments/</code>. API modules under <code>includes/Api/</code>. | ||
* '''Frontend (JS):''' multiple ResourceModules per surface area: | * '''Frontend (JS / CSS):''' multiple ResourceModules per surface area: | ||
** <code>ext.pharmacopedia</code>: main IIFE (chip-picker, dx autocomplete, BFI-10 compute, vote logic for both binary and choice/multi) | ** <code>ext.pharmacopedia</code>: main IIFE (chip-picker, dx autocomplete, BFI-10 compute, vote logic for both binary and choice/multi) | ||
** <code>ext.pharmacopedia.styles</code>: base extension stylesheet (self-hosted Geist / Newsreader / Source Serif fonts, core component styling) | |||
** <code>ext.pharmacopedia.blocksave</code>: debounced autosave per block (race-safe) | ** <code>ext.pharmacopedia.blocksave</code>: debounced autosave per block (race-safe) | ||
** <code>ext.pharmacopedia.datepicker</code>: range / possibility-mix date widget | ** <code>ext.pharmacopedia.bounceback</code>: preserves the reader's scroll position across POST-then-reload actions via sessionStorage | ||
** <code>ext.pharmacopedia.timepicker</code>: time-only widget (fuzzy parsing, extracted from DatePicker) | ** <code>ext.pharmacopedia.confirmdelete</code>: styled red warning prompt replacing <code>window.confirm()</code> on destructive actions | ||
** <code>ext.pharmacopedia.datepicker</code> + <code>.datepicker.styles</code>: range / possibility-mix date widget | |||
** <code>ext.pharmacopedia.timepicker</code> + <code>.timepicker.styles</code>: time-only widget (fuzzy parsing, extracted from DatePicker) | |||
** <code>ext.pharmacopedia.share</code>: per-record share dialog (People / Link / Cohorts tabs) | ** <code>ext.pharmacopedia.share</code>: per-record share dialog (People / Link / Cohorts tabs) | ||
** <code>ext.pharmacopedia.perspective</code>: observer-perspective form enhancement (slider readout, progress, consent/delete confirm) | |||
** <code>ext.pharmacopedia.administer</code>: the administer-to-others surfaces (take-flow slider readout + "Not sure" toggling, owner-hub styling) | |||
** <code>ext.pharmacopedia.observation</code>: quick-add observation textarea + live preview | ** <code>ext.pharmacopedia.observation</code>: quick-add observation textarea + live preview | ||
** <code>ext.pharmacopedia.refupgrade</code>: bulk linker for free-text → structured refs | ** <code>ext.pharmacopedia.refupgrade</code>: bulk linker for free-text → structured refs | ||
| Line 48: | Line 57: | ||
** <code>ext.pharmacopedia.lifegraph</code>: trait-trajectory overlay (vis.Graph2d) synced to timeline | ** <code>ext.pharmacopedia.lifegraph</code>: trait-trajectory overlay (vis.Graph2d) synced to timeline | ||
** <code>ext.pharmacopedia.kitsync</code>: glue that propagates kit-widget changes into legacy hidden inputs + drives privacy-mode toggle | ** <code>ext.pharmacopedia.kitsync</code>: glue that propagates kit-widget changes into legacy hidden inputs + drives privacy-mode toggle | ||
* ''' | ** <code>ext.pharmacopedia.frontpage</code> / <code>ext.pharmacopedia.categoryindex</code>: the two diptych splash modules, each self-contained so it renders with no skin loaded | ||
** <code>ext.pharmacopedia.appearance</code>: the collapsible Appearance rail (reader text-size control) | |||
** <code>ext.pharmacopedia.skin.plants</code> / <code>ext.pharmacopedia.skin.fungi</code>: the earth-toned plants-skin overlay and the fungi sub-skin override layer | |||
* '''Schema:''' <code>sql/</code>, roughly three dozen core tables plus migration patches. Picked up via the <code>LoadExtensionSchemaUpdates</code> hook. | |||
== Skins, layout, and the Appearance rail == | |||
'''Two skins.''' Every page renders in one of two dark skins. The default '''pharmaceutical skin''' is the violet-on-near-black clinical identity. The '''plants skin''' is an earth-toned treatment (<code>ext.pharmacopedia.skin.plants</code>) applied to plant-origin medicine pages. Both skins are dark; there is no light mode. The '''fungi sub-skin''' (<code>ext.pharmacopedia.skin.fungi</code>) is a specialization of the plants skin for fungal medicine pages: a fungus page carries both the <code>pcp-skin-plants</code> and <code>pcp-skin-fungi</code> body classes, loading the plants base plus a fungi override layer (a damp cool-dark palette, a spore-dust grain, the mushroom mark, fungi section-marker hues); everything else inherits from the plants skin. | |||
<code>Hooks::resolvePcpSkin</code> picks the skin. A content (medicine) page is read by its OWN DIRECT origin category: a direct <code>Category:Fungi</code> tag gives the fungi sub-skin (checked first), a direct <code>Category:Plants</code> tag gives the plants skin, anything else pharma. There is no recursive category walk, so a page sitting inside a dual-parented class category (Category:Psychedelics parents under both origins) is skinned by its own origin tag, not by its class. A category page has no single direct origin, so it is resolved by walking its category chain (plants only when the chain is purely plant). | |||
'''Full-width layout.''' The content frame runs edge to edge rather than in a fixed-width column; prose is held to a readable measure by its own constraint, and the layout gutters are set by layout tokens. | |||
'''The Appearance rail.''' A collapsible rail (<code>ext.pharmacopedia.appearance</code>) gives the reader appearance controls, currently a text-size control. The chromeless diptych splashes suppress the rail. | |||
== Front-of-house: the diptych == | |||
The Main Page and the Category index are built as a '''two-origin diptych''': a pharmaceutical column and a plant column side by side, the two origins of the materia medica given equal face. Both render as '''chromeless full-viewport splashes'''. A <code>body.pcp-diptych-page</code> class, added by the <code>BeforePageDisplay</code> hook on those two pages, hides all Vector chrome, and the module supplies its own topbar and footer. A full-height two-origin split gradient runs the page edge to edge, the pharma dark on the left half and the plant dark on the right, a 1px seam down the middle, so the diptych reads top to bottom with no separate bands. | |||
Two parser tags generate the diptych from live wiki data: | |||
* <code><frontpage></code> (<code>FrontPageTag</code>), the Main Page: a featured medicine and featured plant medicine, class / Pharmako-volume browse lists, recently-updated lists, portal links, a status strip. | |||
* <code><categoryindex></code> (<code>CategoryIndexTag</code>), the Category index: the pharmacological classes and the plant lineages as two side-by-side trees. | |||
<code>DiptychChrome</code> supplies the shared topbar and footer for both. The modules are <code>ext.pharmacopedia.frontpage</code> and <code>ext.pharmacopedia.categoryindex</code>; each carries its own palette tokens so the splash renders correctly with no skin stylesheet loaded. | |||
== Parser tags == | == Parser tags == | ||
| Line 70: | Line 101: | ||
| <code><anecdote></code> || Personal or provider story with up/down vote || <code>AnecdoteTag</code> | | <code><anecdote></code> || Personal or provider story with up/down vote || <code>AnecdoteTag</code> | ||
|- | |- | ||
| <code><problem></code> || A problem | | <code><problem></code> || A problem the medicine addresses; 0–100 efficacy likert slider + "don't know" toggle || <code>ProblemTag</code> | ||
|- | |- | ||
| <code><pharmaInteractions/></code> || Self-closing; renders the Interactions section for the current page || <code>InteractionTag</code> | | <code><pharmaInteractions/></code> || Self-closing; renders the Interactions section for the current page || <code>InteractionTag</code> | ||
|- | |- | ||
| <code><pharmaExperience/></code> || Self-closing; renders the Experience report form (efficacy, burden, dose, route, schedule, stop-reasons) || <code>ExperienceTag</code> | | <code><pharmaExperience/></code> || Self-closing; renders the Experience report form (efficacy, burden, dose, route, schedule, stop-reasons) || <code>ExperienceTag</code> | ||
|- | |||
| <code><pharmaLiterature/></code> || Self-closing; per-medicine "Relevant literature" section: approved <code>pcp_literature</code> entries plus a collapsed submission form || <code>LiteratureTag</code> | |||
|- | |||
| <code><classGrid/></code> || A grid of medicine-class categories (those tagged Category:MedCategory); <code>count</code> + <code>exclude</code> attributes; 5-minute cache || <code>ClassGridTag</code> | |||
|- | |||
| <code><classTree/></code> || The MedCategory classes as a hierarchy with member counts; <code>exclude</code> attribute; 5-minute cache || <code>ClassTreeTag</code> | |||
|- | |||
| <code><frontpage/></code> || The two-origin diptych Main Page (see [[#Front-of-house: the diptych|Front-of-house]]) || <code>FrontPageTag</code> | |||
|- | |||
| <code><categoryindex/></code> || The two-origin diptych Category index || <code>CategoryIndexTag</code> | |||
|} | |} | ||
All non-self-closing tags take a <code>slug</code> argument and (where relevant) a <code>title</code>, <code>label</code>, <code>author</code>, <code>ref</code>, or <code>perspective</code>. | All non-self-closing rating tags take a <code>slug</code> argument and (where relevant) a <code>title</code>, <code>label</code>, <code>author</code>, <code>ref</code>, or <code>perspective</code>. | ||
=== Tag wikitext examples === | === Tag wikitext examples === | ||
| Line 174: | Line 215: | ||
* '''Enneagram''' (9 type sliders + 45-item screening test) | * '''Enneagram''' (9 type sliders + 45-item screening test) | ||
* '''MBTI''' (4 dichotomy sliders + 32-item OEJTS test) | * '''MBTI''' (4 dichotomy sliders + 32-item OEJTS test) | ||
* ''' | * '''Self-report assessments''' (PID-5-BF, CATI, CAT-Q, BPNS, NFCS, OCI-PCP, WHOQOL-BREF, each a collapsible inline test) | ||
* '''ADHD screening''' (ASRS adult-ADHD screener and AMAAS-SR attention self-report, each a collapsible inline test) | * '''ADHD screening''' (ASRS adult-ADHD screener and AMAAS-SR attention self-report, each a collapsible inline test) | ||
* '''Formal testing''' (a log of standardized-test scores; raw score, percentile and pass/fail each carry their own privacy setting) | * '''Formal testing''' (a log of standardized-test scores; raw score, percentile and pass/fail each carry their own privacy setting) | ||
| Line 226: | Line 267: | ||
Autocomplete via <code>action=pharmacopediadxsearch</code>, multi-token AND search (e.g. "ADHD inattentive" matches the F90.0 row that contains both substrings); ORDER BY <code>FIELD(da_system, 'ICD-10-CM', 'ICD-11', 'DSM-5', ...)</code> so ICD-10-CM leads, then ICD-11, then everything else. | Autocomplete via <code>action=pharmacopediadxsearch</code>, multi-token AND search (e.g. "ADHD inattentive" matches the F90.0 row that contains both substrings); ORDER BY <code>FIELD(da_system, 'ICD-10-CM', 'ICD-11', 'DSM-5', ...)</code> so ICD-10-CM leads, then ICD-11, then everything else. | ||
=== | === Self-report assessments === | ||
The six dimensional assessments all have continuous-slider items, a "Not sure" toggle per item, auto-computed subscale + total scores, and a rich auto-generated report at <code>Special:MyAssessment/{key}</code>: | |||
{| class="wikitable" | {| class="wikitable" | ||
| Line 244: | Line 285: | ||
|- | |- | ||
| '''OCEAN (Big Five)''' || 5 direct sliders + optional BFI-10 (10 items) || 0–100 per trait || none (personality, not pathology) || trait deep-dives (high / mid / low at your score), BFI-10 item table, cross-system mapping (MBTI ↔ Enneagram ↔ PID-5-BF) pulling live data from the profile | | '''OCEAN (Big Five)''' || 5 direct sliders + optional BFI-10 (10 items) || 0–100 per trait || none (personality, not pathology) || trait deep-dives (high / mid / low at your score), BFI-10 item table, cross-system mapping (MBTI ↔ Enneagram ↔ PID-5-BF) pulling live data from the profile | ||
|} | |||
Four further self-report instruments are available on the same collapsible-inline-test, auto-scored, report-bearing pattern: | |||
{| class="wikitable" | |||
! Instrument !! Items !! Structure !! Measures | |||
|- | |||
| '''BPNS-PCP''' || 21 || 3 subscales (Autonomy, Competence, Relatedness), 7-point Likert || basic psychological need satisfaction, per Self-Determination Theory | |||
|- | |||
| '''NFCS-PCP''' || 15 || 5 facets (Order, Predictability, Decisiveness, Ambiguity intolerance, Closed-mindedness), 6-point Likert || need for cognitive closure (brief 15-item Roets & Van Hiel form) | |||
|- | |||
| '''OCI-PCP''' || 18 || 6 subscales (Washing, Obsessing, Hoarding, Ordering, Checking, Neutralizing), 0–4 per item; screening cutoff total ≥ 21 || obsessive-compulsive symptoms, adapted from the OCI-R | |||
|- | |||
| '''WHOQOL-BREF-PCP''' || 26 || 4 domains (Physical, Psychological, Social, Environment) + 2 overall items, 5-point Likert || quality of life | |||
|} | |} | ||
All assessment items submit raw responses to <code>pcp_profile_fields</code> under namespace <code>{key}_raw</code> (e.g. <code>cati_raw</code>); the computed scores live under namespace <code>{key}</code> with keys like <code>subscale_SOC</code>, <code>total</code>, plus a <code>taken_at</code> timestamp. Re-scoring happens automatically on every save (autosave fires <code>scoreResponses()</code> on the full raw set, skipping "unsure" rows). Each completed assessment also auto-upserts a Life-story keyframe row (see [[#Life-story timeline|Life-story timeline]]) so trajectories plot over time. | All assessment items submit raw responses to <code>pcp_profile_fields</code> under namespace <code>{key}_raw</code> (e.g. <code>cati_raw</code>); the computed scores live under namespace <code>{key}</code> with keys like <code>subscale_SOC</code>, <code>total</code>, plus a <code>taken_at</code> timestamp. Re-scoring happens automatically on every save (autosave fires <code>scoreResponses()</code> on the full raw set, skipping "unsure" rows). Each completed assessment also auto-upserts a Life-story keyframe row (see [[#Life-story timeline|Life-story timeline]]) so trajectories plot over time. | ||
All twelve assessments (the six dimensional, these four, and the two ADHD screeners below) are registered in <code>AssessmentRegistry</code> and so can also be administered to outside respondents (see [[#Administer assessments to others|Administer assessments to others]]). | |||
=== ADHD screening === | === ADHD screening === | ||
| Line 388: | Line 445: | ||
Every permitted view through a rule (not legacy) writes a row to <code>pcp_visibility_view_log</code>. <code>Special:MyShareLog</code> shows the last 200 views with timestamp, viewer (or anonymous IP masked to /24), namespace, key, rule id. | Every permitted view through a rule (not legacy) writes a row to <code>pcp_visibility_view_log</code>. <code>Special:MyShareLog</code> shows the last 200 views with timestamp, viewer (or anonymous IP masked to /24), namespace, key, rule id. | ||
== Observer perspectives == | |||
A structured way to collect second-party (observer) input on something a user owns, without the observer needing an account. The v1 use case is an observer-rated attention report: a registered user invites someone who knows them well to rate them. A two-gate consent model governs the whole flow. | |||
* '''Gate 1, contribution.''' An owner-issued, token-bearing invite link is the only way to submit a perspective. The invitee URL carries only an opaque random token (<code>VisibilityResolver::generateLinkToken()</code>); the owner's identity is never in the URL. The invitee sees only the owner's chosen display name. | |||
* '''Gate 2, publication.''' Every submission is born <code>psp_consent = 0</code>, private to the owner. Only the owner, re-checked for ownership, can flip it to published. A perspective is never publicly visible while unconsented. | |||
Flow: the owner opens <code>Special:MyPerspectives</code>, picks a display name, mints an invite link, and sends it. The invitee opens <code>Special:Perspective/<token></code>, reads a generic intro and a non-anonymity notice, optionally states their relationship to the owner, fills the type-specific slider form (with per-item "not sure" markers), clears a Turnstile challenge, and submits. The perspective lands in the owner's consent inbox, where the owner can publish it, return it to private, or delete it. | |||
<code>Special:Perspective</code> is unlisted and unauthenticated; its POST path is defended by pingLimiter, a per-token APCu velocity cap, and fail-closed Turnstile. <code>pcp_perspective_invite</code> holds the token-bearing invitations (<code>pvi_max_uses</code> NULL means an unlimited reusable link); <code>pcp_perspective</code> holds the submissions, each with a <code>psp_validity</code> response-quality flag and the <code>psp_consent</code> gate. The client module <code>ext.pharmacopedia.perspective</code> adds the slider readout and progress bar on the invitee form and the confirm steps on the owner inbox. | |||
== Administer assessments to others == | |||
A registered user (the '''owner''') can send any of the twelve registered assessment scales to people outside the wiki (the '''respondents'''), collect their results, and follow them over time. Respondents need no account; a one-time invite link is their only credential. The subsystem is built so that, by default, a respondent's results are readable only by the owner, and the owner can additionally choose a zero-knowledge mode in which not even a site administrator can read them. | |||
=== Assessment registry === | |||
<code>AssessmentRegistry</code> (<code>includes/Assessments/AssessmentRegistry.php</code>) is the single source of truth: an assessment registered there once autopopulates the owner's scale-picker, the respondent take-flow, and the dashboards. Each entry names a scorer class and a '''response model''': | |||
* <code>radio</code>, discrete labelled radio buttons (CATI, CAT-Q, PID-5-BF, NFCS, BPNS, OCI-PCP, WHOQOL-BREF, ASRS) | |||
* <code>slider</code>, one continuous slider per item with uniform end anchors (OCEAN, Enneagram, AMAAS) | |||
* <code>bipolar</code>, one slider per item with the item's own two opposing phrases as the anchors (MBTI) | |||
Every take-flow item also carries a "Not sure" control that disables the item. | |||
=== The four surfaces === | |||
* '''Entry point''', a control that opens the feature. | |||
* '''Owner hub''' (<code>Special:AdministerAssessments</code>): the owner manages respondents (a named contact list), composes a send (a respondent plus one or more scales), generates a one-time link, and reviews each respondent's results over time. The owner can delete an invite. | |||
* '''Respondent take-flow''' (<code>Special:RespondToAssessment/<token></code>), reached only by the invite link: a consent screen, then the model-aware item forms; once every scale is done the same URL becomes a revisitable results dashboard. Either party can delete. | |||
* '''Key setup''', where the owner chooses how their results are protected (see below). | |||
=== Cryptography === | |||
Each owner has an '''X25519 keypair'''. The public key is stored in the clear; the secret key is wrapped at rest. A respondent submits while the owner is absent, so the result is sealed to the owner's public key with <code>crypto_box_seal</code>, which needs only the public key; only the owner's secret key can open it. Two protection modes: | |||
* '''Managed''' (default, seamless): the secret key is wrapped with a server master key held in a file outside the web root, never in the database. No passphrase to remember, and a forgotten site password never costs the owner their data. A site administrator could technically read results. | |||
* '''Passphrase''' (opt-in, zero-knowledge): the secret key is wrapped with an Argon2id-derived key from a passphrase only the owner knows. Not even the site can read the results. '''The passphrase cannot be recovered or reset'''; losing it makes every collected result permanently unreadable. | |||
AES-256-GCM is used for key wrapping in both modes. <code>AdminCrypto</code> (<code>includes/Assessments/AdminCrypto.php</code>) is the helper: <code>setupOwnerKey</code>, <code>verifyPassphrase</code>, <code>unlockSecretKey</code>, <code>encryptForOwner</code> / <code>decryptForOwner</code>, <code>encryptForRespondent</code> / <code>decryptForRespondent</code>, <code>mintInviteToken</code> / <code>hashInviteToken</code>. Invite tokens are 256-bit; only their SHA-256 hash is stored, so a stolen database yields no usable tokens. The respondent's own copy of each result is sealed to a key derived from the raw invite token, so the link doubles as the respondent's read credential. A scheme-version column on the key and result rows lets the cipher or KDF change in a future v2. | |||
=== De-identified research pool === | |||
Each submission also writes one row to <code>pcp_administer_research</code>, decoupled from the result write, with no foreign key to the invite, respondent, or owner. <code>res_id</code> is a random 128-bit value (not sequential) and the only time field is <code>res_month</code> ('YYYY-MM', no day), so a research row cannot be time-correlated or rank-correlated back to the owner-side tables. The payload is the plaintext item responses plus the computed score. Deleting an invite removes the owner-sealed and respondent-sealed result copies but intentionally leaves the de-identified research row, as the consent screen states. | |||
=== Tables === | |||
* <code>pcp_administer_respondents</code>, an owner's named contacts (the label is sealed to the owner's public key, so the roster is readable only by the owner) | |||
* <code>pcp_administer_invites</code>, one token-bearing link per send (stores only the SHA-256 of the token) | |||
* <code>pcp_administer_assessments</code>, the scale(s) inside an invite, each with the owner-sealed (<code>aa_payload_enc</code>) and respondent-sealed (<code>aa_respondent_enc</code>) result copies | |||
* <code>pcp_administer_userkey</code>, per-owner key material (public key, wrapped secret key, Mode-A salt and verifier) | |||
* <code>pcp_administer_research</code>, the de-identified research pool | |||
The 12-instrument submission handler reuses the existing assessment scorers in <code>includes/Assessments/</code> rather than reimplementing scoring. | |||
== Choice / multi voting == | == Choice / multi voting == | ||
| Line 436: | Line 547: | ||
Slider numbers are also clickable: a single delegated handler on every <code><output></code> next to a range slider swaps it for a number input on click, accepts a precise typed value (clamps to the slider's <code>min</code>/<code>max</code>), commits with Enter, cancels with Escape. | Slider numbers are also clickable: a single delegated handler on every <code><output></code> next to a range slider swaps it for a number input on click, accepts a precise typed value (clamps to the slider's <code>min</code>/<code>max</code>), commits with Enter, cancels with Escape. | ||
Scroll position is preserved across the rare reloads (delete operations on diagnoses / medicines / experience reports, and the auto-reload after a new diagnosis or medicine is added) via <code>sessionStorage</code>. | Scroll position is preserved across the rare reloads (delete operations on diagnoses / medicines / experience reports, and the auto-reload after a new diagnosis or medicine is added) via <code>sessionStorage</code> (the <code>ext.pharmacopedia.bounceback</code> module). | ||
== Special pages == | == Special pages == | ||
| Line 450: | Line 561: | ||
|- | |- | ||
| <code>Special:MyAssessment/cati</code>, <code>/catq</code>, <code>/pid5bf</code>, <code>/mbti</code>, <code>/enneagram</code>, <code>/ocean</code> || Rich report per assessment; 🔗 Share chip in subtitle for owner | | <code>Special:MyAssessment/cati</code>, <code>/catq</code>, <code>/pid5bf</code>, <code>/mbti</code>, <code>/enneagram</code>, <code>/ocean</code> || Rich report per assessment; 🔗 Share chip in subtitle for owner | ||
|- | |||
| <code>Special:TakeAssessment/<key></code> || Generic paginated self-assessment runner; stores raw items and computes scores | |||
|- | |- | ||
| <code>Special:MyLifeStory</code> || Owner editor + visual timeline + card list. Quick-add observation textarea + Event/Episode buttons. 🔗 Share chip in subtitle. Privacy-mode + free-text-refs banners. | | <code>Special:MyLifeStory</code> || Owner editor + visual timeline + card list. Quick-add observation textarea + Event/Episode buttons. 🔗 Share chip in subtitle. Privacy-mode + free-text-refs banners. | ||
| Line 458: | Line 571: | ||
|- | |- | ||
| <code>Special:LifeStory/<name></code> || Public life-story view (read-only, visibility-filtered) | | <code>Special:LifeStory/<name></code> || Public life-story view (read-only, visibility-filtered) | ||
|- | |||
| <code>Special:LifeImage</code> || Visibility-gated image streamer for life-story event images | |||
|- | |- | ||
| <code>Special:MyCohorts</code> || Owner-managed groups for share-with-cohort flows; create / rename / delete / add+remove members | | <code>Special:MyCohorts</code> || Owner-managed groups for share-with-cohort flows; create / rename / delete / add+remove members | ||
| Line 464: | Line 579: | ||
|- | |- | ||
| <code>Special:MyRefLinks</code> || Bulk linker: find free-text refs in observations that now match a structured entity; one-click upgrade or dismiss | | <code>Special:MyRefLinks</code> || Bulk linker: find free-text refs in observations that now match a structured entity; one-click upgrade or dismiss | ||
|- | |||
| <code>Special:MyPerspectives</code> || Owner: mint observer-perspective invite links and review the consent inbox | |||
|- | |||
| <code>Special:Perspective/<token></code> || Public, token-gated observer-perspective form (no account; unlisted, anti-abuse defended) | |||
|- | |||
| <code>Special:AdministerAssessments</code> || Owner hub: send assessment scales to outside respondents and follow their results | |||
|- | |||
| <code>Special:RespondToAssessment/<token></code> || Token-gated respondent take-flow and revisitable results dashboard | |||
|- | |- | ||
| <code>Special:SaveProfileBlock</code> || AJAX endpoint for autosave (POST-only, JSON response) | | <code>Special:SaveProfileBlock</code> || AJAX endpoint for autosave (POST-only, JSON response) | ||
| Line 471: | Line 594: | ||
| <code>Special:Problem/<slug></code> || Individual problem page | | <code>Special:Problem/<slug></code> || Individual problem page | ||
|- | |- | ||
| <code>Special:SuggestProblem</code> || User-facing form to suggest a new problem | | <code>Special:SuggestProblem</code> || User-facing form to suggest a new problem (page-tied or standalone) | ||
|- | |||
| <code>Special:SuggestAnecdote</code> || Logged-in users propose an anecdote for a medicine page | |||
|- | |||
| <code>Special:SuggestEffect</code> || Logged-in users propose an effect for a medicine page | |||
|- | |||
| <code>Special:SuggestTitration</code> || Logged-in users propose a titration schedule for a medicine page | |||
|- | |- | ||
| <code>Special:ManageProblems</code> || Sysop tool for problem-repository moderation | | <code>Special:ManageProblems</code> || Sysop tool for problem-repository moderation | ||
|- | |- | ||
| <code>Special: | | <code>Special:ManageEffects</code> || Sysop CRUD for the global effects vocabulary | ||
|- | |- | ||
| <code>Special:ManageInteractions</code> || Sysop bulk-edit interaction reports | | <code>Special:ManageInteractions</code> || Sysop bulk-edit interaction reports | ||
|- | |||
| <code>Special:ReviewExperience</code> || Sysop queue for pending experience reports | |||
|- | |- | ||
| <code>Special:DeletePharmaElement</code> || Sysop delete tool for any votable element | | <code>Special:DeletePharmaElement</code> || Sysop delete tool for any votable element | ||
|- | |- | ||
| <code>Special:PCPCtrls</code> || Sysop | | <code>Special:VerifyProvider</code> || User-facing form to apply for provider verification and check status | ||
|- | |||
| <code>Special:ProviderApplications</code> || Sysop queue to approve / reject provider-verification applications | |||
|- | |||
| <code>Special:VerificationDoc</code> || Permission-gated streamer for provider-verification document files | |||
|- | |||
| <code>Special:FeatureRequests</code> || User-facing feature-request board (submit and browse) | |||
|- | |||
| <code>Special:RequestReview</code> || Sysop feature-request review console (status counters, prioritized queue, triage) | |||
|- | |||
| <code>Special:LiteratureDoc</code> || Download proxy for approved literature PDFs (reviewers may preview pending) | |||
|- | |||
| <code>Special:LiteratureQueue</code> || Sysop literature review queue (approve / reject / delete) | |||
|- | |||
| <code>Special:PharmacopediaActivity</code> || Recent-activity feed: last 30 votes, effect reports, comments, literature submissions | |||
|- | |||
| <code>Special:NewUsers</code> || The 20 most recently registered accounts | |||
|- | |||
| <code>Special:ProfileAnalysis</code> || Sysop dashboard of cross-table profile aggregates; per-section CSV export | |||
|- | |||
| <code>Special:ProfileFilter</code> || Sysop cross-filter UI over user profiles (demographics, OCEAN ranges, dx, med); CSV export | |||
|- | |||
| <code>Special:PCPCtrls</code> || Sysop controls hub (gated by <code>$wgSpecialPageLockdown</code>) | |||
|- | |||
| <code>Special:AdminCtrls</code> || Admin-controls landing page (renders sysop-editable <code>MediaWiki:Adminctrls-body</code>) | |||
|- | |||
| <code>Special:DatePickerTest</code> || Developer sandbox exercising the date-input widget (point / range / possibility) | |||
|} | |} | ||
| Line 572: | Line 729: | ||
! Table !! Purpose | ! Table !! Purpose | ||
|- | |- | ||
| <code>pcp_votable_elements</code> || Stable per-(page, slug) handle reused by votes / likert / comments. | | <code>pcp_votable_elements</code> || Stable per-(page, slug) handle reused by votes / likert / comments. Also <code>ve_options</code> + <code>ve_options_h</code> + <code>ve_results_policy</code> for choice votes. | ||
|- | |- | ||
| <code>pcp_votes</code> || Binary +1/−1 votes (v_value) AND choice/multi votes (v_choices CSV + v_options_h drift hash) | | <code>pcp_votes</code> || Binary +1/−1 votes (v_value) AND choice/multi votes (v_choices CSV + v_options_h drift hash) | ||
| Line 584: | Line 741: | ||
| <code>pcp_interactions</code> || Interaction edges (canonical-ordered pair) | | <code>pcp_interactions</code> || Interaction edges (canonical-ordered pair) | ||
|- | |- | ||
| <code>pcp_comments</code> || Threaded discussions | | <code>pcp_comments</code> || Threaded <code><discuss></code>-tag discussions (soft-delete, optional display name) | ||
|- | |- | ||
| <code>pcp_user_profiles</code> || Per-user profile meta (alias, attribution, voter hash, prof_research_id) | | <code>pcp_user_profiles</code> || Per-user profile meta (alias, attribution, voter hash, prof_research_id) | ||
| Line 614: | Line 771: | ||
| <code>pcp_visibility_view_log</code> || Audit trail of rule-permitted views (Special:MyShareLog) | | <code>pcp_visibility_view_log</code> || Audit trail of rule-permitted views (Special:MyShareLog) | ||
|- | |- | ||
| <code>pcp_literature</code> || | | <code>pcp_perspective_invite</code> || Token-bearing observer-perspective invitations (display name, object, type, max-uses) | ||
|- | |||
| <code>pcp_perspective</code> || Submitted observer perspectives (payload JSON, validity flag, consent gate) | |||
|- | |||
| <code>pcp_administer_respondents</code> || An owner's named contacts; the label is sealed to the owner's public key | |||
|- | |||
| <code>pcp_administer_invites</code> || One token-bearing administer link per send; stores only SHA-256 of the token | |||
|- | |||
| <code>pcp_administer_assessments</code> || The scale(s) in an invite; owner-sealed and respondent-sealed result copies | |||
|- | |||
| <code>pcp_administer_userkey</code> || Per-owner key material (X25519 public key, wrapped secret key, Mode-A salt + verifier) | |||
|- | |||
| <code>pcp_administer_research</code> || De-identified research pool: random id, coarsened month, no link back to the owner | |||
|- | |||
| <code>pcp_provider_apps</code> || Provider-verification applications (profession, specialty, jurisdiction, license, status, doc paths) | |||
|- | |||
| <code>pcp_literature</code> || Per-page literature submissions (citation metadata, optional PDF, status, reviewer) | |||
|- | |||
| <code>pcp_feature_request</code>, <code>pcp_feature_request_attachment</code>, <code>pcp_feature_request_comment</code> || Feature-request board: requests, ClamAV-scanned attachments, threaded comments | |||
|- | |- | ||
| <code>pcp_formal_tests</code> || Catalog of standardized tests (abbrev, full name, category, score format) | | <code>pcp_formal_tests</code> || Catalog of standardized tests (abbrev, full name, category, score format) | ||
| Line 625: | Line 800: | ||
* '''Synthetic Event needs bubbles:true to trigger delegated listeners.''' <code>new Event('input')</code> defaults to <code>bubbles:false</code>, so listeners on parent wrappers never see programmatic dispatches. Native input/change events bubble by default; only JS-fired ones don't. Pass <code>{ bubbles: true }</code> explicitly. Bit DatePicker calendar-cell clicks 2026-05-18, typing in the text field autosaved fine, but picking a date from the calendar didn't, because blocksave's wrapper listener never received the event. | * '''Synthetic Event needs bubbles:true to trigger delegated listeners.''' <code>new Event('input')</code> defaults to <code>bubbles:false</code>, so listeners on parent wrappers never see programmatic dispatches. Native input/change events bubble by default; only JS-fired ones don't. Pass <code>{ bubbles: true }</code> explicitly. Bit DatePicker calendar-cell clicks 2026-05-18, typing in the text field autosaved fine, but picking a date from the calendar didn't, because blocksave's wrapper listener never received the event. | ||
* '''MediaWiki form-field names collide with reserved URL params.''' A form <code><input name="title"></code> or <code>name="action"></code> with user-controlled value silently HIJACKS MW's dispatch when POSTed (body param overrides URL param). Symptom: form submits but lands on a wiki article named whatever the user typed, with URL bar still showing the special page. Bit the episode form 2026-05-18 (user typed 'Fake one' as title → got a 404 'create article: Fake one' page). Fix: prefix ALL custom form inputs with <code>pcp_</code> (both the input <code>name="..."</code> AND the matching <code>$request->getVal('...', ...)</code> read). Self-referential hidden inputs (<code>name="title" value="<getPageTitle()>"</code>) are safe. | * '''MediaWiki form-field names collide with reserved URL params.''' A form <code><input name="title"></code> or <code>name="action"></code> with user-controlled value silently HIJACKS MW's dispatch when POSTed (body param overrides URL param). Symptom: form submits but lands on a wiki article named whatever the user typed, with URL bar still showing the special page. Bit the episode form 2026-05-18 (user typed 'Fake one' as title → got a 404 'create article: Fake one' page). Fix: prefix ALL custom form inputs with <code>pcp_</code> (both the input <code>name="..."</code> AND the matching <code>$request->getVal('...', ...)</code> read). Self-referential hidden inputs (<code>name="title" value="<getPageTitle()>"</code>) are safe. | ||
* '''Cargo string fields cap at ~300 chars.''' <code>structure</code> and <code>mechanism</code> on MedTemplate are short VARCHARs; long prose goes in MEDIUMBLOB sections. Overruns silently lose Cargo data (MySQL Error 1406). | * '''Cargo string fields cap at ~300 chars.''' <code>structure</code> and <code>mechanism</code> on MedTemplate are short VARCHARs; long prose goes in MEDIUMBLOB sections. Overruns silently lose Cargo data (MySQL Error 1406). | ||
* '''VARBINARY + LOWER() is a no-op.''' MariaDB's LOWER() returns binary types unchanged. <code>pcp_diagnosis_abbreviations</code> was migrated VARBINARY → VARCHAR/utf8mb4 so case-insensitive LIKE works natively without CONVERT() wrappers. | * '''VARBINARY + LOWER() is a no-op.''' MariaDB's LOWER() returns binary types unchanged. <code>pcp_diagnosis_abbreviations</code> was migrated VARBINARY → VARCHAR/utf8mb4 so case-insensitive LIKE works natively without CONVERT() wrappers. | ||
| Line 638: | Line 812: | ||
* '''<code><span></code> can't contain <code><p></code>.''' MediaWiki auto-wraps tag content in <code><p></code> when there are newlines; browsers auto-close any <code><span></code> before the <code><p></code>, scattering child elements into wrong DOM positions. Use <code><div></code> for parser-tag wrappers whose content can span paragraphs (choice votes, life-story cards). | * '''<code><span></code> can't contain <code><p></code>.''' MediaWiki auto-wraps tag content in <code><p></code> when there are newlines; browsers auto-close any <code><span></code> before the <code><p></code>, scattering child elements into wrong DOM positions. Use <code><div></code> for parser-tag wrappers whose content can span paragraphs (choice votes, life-story cards). | ||
* '''vis.Graph2d group <code>style</code> belongs at TOP LEVEL''' not nested in <code>options</code>. Nesting silently fails (no error, just invisible lines). Bit the trait-trajectory graph on first build. | * '''vis.Graph2d group <code>style</code> belongs at TOP LEVEL''' not nested in <code>options</code>. Nesting silently fails (no error, just invisible lines). Bit the trait-trajectory graph on first build. | ||
* '''A symmetric data-key cannot serve an asynchronous flow.''' The administer-to-others result must be encryptable while the owner is absent. A purely symmetric per-owner key has no holder at submission time; the model must be an asymmetric keypair, with the public key stored openly so a sealed box can always be written and only the owner's wrapped secret key can open it. | |||
* '''A de-identified row must carry no order and no link.''' An auto-increment id rank-correlates with the source table and an insert timestamp time-correlates with it. The research pool uses a random 128-bit id and a month-only date so a row genuinely cannot be traced back. | |||
== Hooks == | == Hooks == | ||
| Line 643: | Line 819: | ||
* <code>ParserFirstCallInit</code>: register all parser tags | * <code>ParserFirstCallInit</code>: register all parser tags | ||
* <code>LoadExtensionSchemaUpdates</code>: install / migrate schema (the sql/ directory + patches) | * <code>LoadExtensionSchemaUpdates</code>: install / migrate schema (the sql/ directory + patches) | ||
* <code>BeforePageDisplay</code>: inject the ext.pharmacopedia.* ResourceLoader modules | * <code>BeforePageDisplay</code>: inject the ext.pharmacopedia.* ResourceLoader modules; resolve and apply the page skin (the <code>pcp-skin-*</code> body class) and, on the Main Page / Category index, the <code>pcp-diptych-page</code> chromeless class | ||
* <code>UserGetRights</code> + <code>UserEffectiveGroups</code>: verified-provider role wiring | * <code>UserGetRights</code> + <code>UserEffectiveGroups</code>: verified-provider role wiring | ||
* Various special-page registrations via <code>SpecialPage_initList</code> | * Various special-page registrations via <code>SpecialPage_initList</code> | ||
| Line 651: | Line 827: | ||
* <code>$wgPharmacopediaInteractionCategoryMarker</code> (default <code>Category:MedCategory</code>): only categories tagged with this marker appear in the add-interaction modal | * <code>$wgPharmacopediaInteractionCategoryMarker</code> (default <code>Category:MedCategory</code>): only categories tagged with this marker appear in the add-interaction modal | ||
* <code>$wgPharmacopediaVoteHashSecret</code> (required): HMAC secret for <code>v_voter_hash</code> so vote rows can't be mapped back to user accounts by anyone reading the DB without the secret | * <code>$wgPharmacopediaVoteHashSecret</code> (required): HMAC secret for <code>v_voter_hash</code> so vote rows can't be mapped back to user accounts by anyone reading the DB without the secret | ||
* <code>$wgPharmacopediaAdminKeyDir</code>: filesystem directory, outside the web root, holding the administer-to-others server master key (managed mode); never in the database, never in the DB backup set | |||
* (Various permission-grant arrays via standard MW <code>$wgGroupPermissions</code>) | * (Various permission-grant arrays via standard MW <code>$wgGroupPermissions</code>) | ||
| Line 659: | Line 836: | ||
|-- includes/ | |-- includes/ | ||
| |-- Hooks.php | | |-- Hooks.php | ||
| |-- *Tag.php (parser tags: VoteTag, EffectTag, ProblemTag, ...) | | |-- *Tag.php (parser tags: VoteTag, EffectTag, ProblemTag, | ||
| | ClassGridTag, ClassTreeTag, LiteratureTag, | |||
| | FrontPageTag, CategoryIndexTag, ...) | |||
| |-- DiptychChrome.php (shared topbar + footer for the diptych splashes) | |||
| |-- *Store.php (data access: EffectStore, ProblemStore, ElementStore, LifeStoryStore, ...) | | |-- *Store.php (data access: EffectStore, ProblemStore, ElementStore, LifeStoryStore, ...) | ||
| |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations) | | |-- UserProfileStore.php (the big one; profile / dx / meds / abbreviations) | ||
| Line 666: | Line 846: | ||
| |-- ObservationParser.php (plain-text -> structured observation) | | |-- ObservationParser.php (plain-text -> structured observation) | ||
| |-- SpecialMyProfile.php (the user profile editor; large) | | |-- SpecialMyProfile.php (the user profile editor; large) | ||
| |-- SpecialMyAssessment.php (rich reports for | | |-- SpecialMyAssessment.php (rich reports for the dimensional assessments; large) | ||
| |-- SpecialMyLifeStory.php (life-story editor + visual timeline; large) | | |-- SpecialMyLifeStory.php (life-story editor + visual timeline; large) | ||
| |-- | | |-- SpecialMyPerspectives.php / SpecialPerspective.php (observer perspectives) | ||
| |-- | | |-- SpecialAdministerAssessments.php / SpecialRespondToAssessment.php (administer to others) | ||
| |-- SpecialPCPCtrls.php (sysop controls hub) | |||
| |-- | |||
| |-- Special*.php (other special pages) | | |-- Special*.php (other special pages) | ||
| |-- ProfileDatasets.php (countries, languages, genders, religions, etc.) | | |-- ProfileDatasets.php (countries, languages, genders, religions, etc.) | ||
| |-- DatePicker.php (range / possibility-mix date widget backend + injectBirthdayContextOnce) | | |-- DatePicker.php (range / possibility-mix date widget backend + injectBirthdayContextOnce) | ||
| |-- Assessments/ | | |-- Assessments/ | ||
| | |-- AssessmentRegistry.php (single source of truth for the 12 instruments) | |||
| | |-- AdminCrypto.php (X25519 / Argon2id / AES-256-GCM helper) | |||
| | |-- Cati.php, CatiNorms.php | | | |-- Cati.php, CatiNorms.php | ||
| | |-- Catq.php | | | |-- Catq.php, CatqNorms.php | ||
| | |-- Pid5bf.php | | | |-- Pid5bf.php, Pid5bfNorms.php | ||
| | |-- Mbti.php | | | |-- Mbti.php | ||
| | |-- Enneagram.php | | | |-- Enneagram.php | ||
| | |-- OceanNorms.php | | | |-- Ocean.php, OceanNorms.php | ||
| | |-- Asrs.php, Amaas.php | |||
| | |-- Bpns.php, BpnsNorms.php | |||
| | |-- Nfcs.php, NfcsNorms.php | |||
| | |-- Ocipcp.php | |||
| | |-- WhoqolBref.php, WhoqolBrefNorms.php | |||
| | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads) | | | `-- Raadsr.php (deprecated 2026-05-17 in favor of CATI, kept for archival reads) | ||
| `-- Api/ | | `-- Api/ | ||
| Line 690: | Line 876: | ||
|-- resources/ | |-- resources/ | ||
| |-- ext.pharmacopedia.js (single IIFE: chip-picker, dx autocomplete, vote logic, ...) | | |-- ext.pharmacopedia.js (single IIFE: chip-picker, dx autocomplete, vote logic, ...) | ||
| |-- ext.pharmacopedia.styles.css (base stylesheet, self-hosted fonts) | |||
| |-- ext.pharmacopedia.blocksave.js (debounced autosave per block) | | |-- ext.pharmacopedia.blocksave.js (debounced autosave per block) | ||
| |-- ext.pharmacopedia. | | |-- ext.pharmacopedia.bounceback.js (scroll-position preservation) | ||
| |-- ext.pharmacopedia.datepicker.js + .css | | |-- ext.pharmacopedia.confirmdelete.* (styled destructive-action prompt) | ||
| |-- ext.pharmacopedia.timepicker.js + .css | | |-- ext.pharmacopedia.datepicker.js + .datepicker.styles.css | ||
| |-- ext.pharmacopedia.timepicker.js + .timepicker.styles.css | |||
| |-- ext.pharmacopedia.share.js + .css | | |-- ext.pharmacopedia.share.js + .css | ||
| |-- ext.pharmacopedia.perspective.* (observer-perspective form enhancement) | |||
| |-- ext.pharmacopedia.administer.* (administer-to-others surfaces) | |||
| |-- ext.pharmacopedia.observation.js + .css | | |-- ext.pharmacopedia.observation.js + .css | ||
| |-- ext.pharmacopedia.refupgrade.js + .css | | |-- ext.pharmacopedia.refupgrade.js + .css | ||
| Line 700: | Line 890: | ||
| |-- ext.pharmacopedia.lifegraph.js + .css | | |-- ext.pharmacopedia.lifegraph.js + .css | ||
| |-- ext.pharmacopedia.kitsync.js | | |-- ext.pharmacopedia.kitsync.js | ||
| |-- ext.pharmacopedia.frontpage.* / ext.pharmacopedia.categoryindex.* (diptych splashes) | |||
| |-- ext.pharmacopedia.appearance.* (the Appearance rail) | |||
| |-- ext.pharmacopedia.skin.plants.css (plants-skin overlay) | |||
| `-- vendor/vis-timeline/ (vis-timeline 7.7.3, Apache-2.0 + MIT) | | `-- vendor/vis-timeline/ (vis-timeline 7.7.3, Apache-2.0 + MIT) | ||
| |-- vis-timeline-graph2d.min.js | | |-- vis-timeline-graph2d.min.js | ||