Skip to content

BW Activity Plugin — Design & Implementation Plan

Created 2026-03-13
Updated 2026-03-21
Status v0.8.0 live on production — CSV import + email-in-export complete; Phase 3 (PDF export) pending
Tags infrastructurepractice-siteactivity-trackingplugin-developmenttechnical

Session note (2026-03-21 — v0.8.0 bugfix: import transient key case mismatch): Import showed “session expired” immediately on upload. Root cause: wp_generate_password(16, false) returns mixed-case characters; sanitize_key() lowercases the key when reading it back from the URL, so the stored and looked-up transient names never matched. Fix: wrap generation in strtolower() so the key is consistently lowercase on both ends. One-line change in handle_csv_upload(). Deployed to staging and production.

Session note (2026-03-21 — v0.8.0 deployed to production): Tested by Asia on staging, confirmed working. Deployed to practice.baseworks.com. Production checklist complete — all Phase 4 items done. Next: Phase 3 (PDF export refinement on staging).

Session note (2026-03-21 — v0.8.0 on staging: CSV import built): CSV import feature built and deployed to staging. New “Import CSV” submenu under Activity admin. Two-step flow: upload CSV → preview table showing per-row validation results → confirm & insert. User matching supports user_id, email, or both (user_id authoritative; email cross-checked if both present). Invalid rows shown in red and skipped on confirm. Blank CSV rows silently skipped. Unknown activity types mapped to other. Parsed rows stored in a 30-min WP transient between preview and confirm steps. CSV export updated to include user_email column (batch-fetched via get_users() — no N+1 queries). Smoke-tested with bw-activity-2026-03-21-import.csv (4 rows: Asia, Patrick, Manon, Mimi — all user lookups confirmed on staging). Ready to test via pracstage.baseworks.com/wp-admin → Activity → Import CSV. After confirming, deploy to production.

Session note (2026-03-20 — Phase 4 complete, production fully live): v0.7.0 confirmed working on production by Asia. GamiPress plugin deactivated. Code Snippets 30–34 (frm-date, dailymax, frm_activity_bars, frm_activity_streak, frm_activity_dots) deactivated. All Formidable activity views fully replaced by plugin shortcodes on both English and Japanese pages. CSV export confirmed already built and working — not a to-do. Next pending items: CSV import (new feature), PDF export refinement (Phase 3).

Session note (2026-03-20 — Phase 4 lang param built, v0.7.0 on staging): Japanese language support added to all shortcodes via lang="ja" attribute (default "en"). A single get_strings($lang) helper centralises all UI text; format_minutes_lang($minutes, $lang) handles time formatting (EN: 1h 30m, JA: 1時間30分). All six shortcodes updated: [bw_activity_bars], [bw_activity_calendar], [bw_activity_list], [bw_activity_total], [course_continue_button], [smart_revisit], [bw_primerprint]. Japanese strings confirmed from existing Formidable views (view 19665 直近7日間, 最後の4週間, N日連続; view 19615 詳細/日付/). Smoke-tested via WP-CLI on pracstage — EN output unchanged, JA output correct. Plugin at v0.7.0 on staging. Next: place lang="ja" shortcodes on Japanese pages (14527 dashboard, 14803 history), confirm with Asia, then deploy to production + deactivate Code Snippets 30–34 and GamiPress.

Session note (2026-03-13): Plugin scaffold, DB, admin UI, REST API, and Automator integration are all complete on staging. Automator action is fully working end-to-end. Next session: data migration (Form 69 → new tables), PrimerPrint API adaptation, then production deploy. See Status below.

Session note (2026-03-14, morning): Added cluster column + key-points activity type. Automator action now has Cluster field (token-supporting). Admin interface upgraded: cluster column + filter, bulk delete with checkboxes (select all/deselect all), per-page selector (25/50/100/200), paginate_links pagination. Three bugs fixed: division-by-zero when per_page absent from URL, bulk delete silently failing (duplicate select in bottom tablenav), per-page reverting to 50 (was POSTing instead of GET-navigating). All tested and working on staging.

Session note (2026-03-14, evening): Phase 2a shortcodes built and deployed to staging (v0.3.0). See Display Shortcodes (Phase 2a) for updated shortcode names. Five shortcodes registered: [bw_activity_bars], [bw_activity_calendar], [bw_activity_list], [bw_activity_total], [bw_activity_streak]. Single CSS file (assets/frontend.css) covers all widgets — GamiPress CSS dependency can be removed once tested. [bw_smart_revisit] deferred. Next: test shortcodes on staging pages, swap out Formidable views, then Form 69 migration + production deploy.

Session note (2026-03-15): Design pass on all three shortcode widgets — font corrected to Poppins (self-hosted via BuddyBoss/Redux); bar width narrowed to 30%; labels bumped to 14px; dots refactored to use Unicode character (eliminates oval/rectangle artefact from CSS circles); dot streak highlight moved to wrapper cell (background on wrap, border for today, dot is color-only); bar baseline made continuous by using border-bottom on .bw-bar-track with no column gap; list layout cleaned up (centered date/min, breathing room padding, no type color on description text, correct link color via CSS); calendar header row added gray background; legend gap tightened. get_utc_offset() simplified to ACF user meta only — Form 71 submissions write directly to the ACF timezone field, so no separate Form 71 query is needed. filemtime() adopted for CSS versioning (auto cache-busting on every deploy). Mobile media query added.

Session note (2026-03-16, morning): Timezone bug fixed in activity list. List was showing UTC dates (2026/03/15) while bars correctly showed the user-local date (2026-03-14 for a Montreal UTC-4 user). Confirmed: site timezone = UTC (areb_options), user ACF timezone = -4. Fix: added get_utc_offset() call in activity_list() and applied offset when formatting dates. Plugin bumped to v0.3.1. Next: place shortcodes on staging pages, swap Formidable views, then Form 69 migration + production deploy.

Session note (2026-03-20 — Phase 5 complete, v0.6.0 live on production): [bw_activity_list] enhancements built, tested on pracstage, and deployed to production. Three new features in full-list mode (display=“N” mode unchanged): (1) category filter — checkboxes, additive OR logic, only shows types the user actually has entries for; (2) per-page selector — 10 / 25 / 50 / All, GET-param driven, defaults to 10; (3) summary bar below the table — entry count + total duration (Xh Ym), light gray background. All operate via GET params (bw_list_types[], bw_list_per_page) composing cleanly with pagination (bw_list_page). Filter/per-page changes reset to page 1. DB: build_where() extended for types[] array; new get_user_types() and sum_entries() methods. Plugin at v0.6.0. Asia confirmed working on both pracstage and production.

Session note (2026-03-20 — production deploy complete): Plugin v0.5.5 deployed to practice.baseworks.com. Migration ran successfully: 2,604 entries, 72 users, data back to December 2020. Breakdown: theory 632, lab 577, key-points 476, foundation 473, elements 283, in-person 116, practice 40, other 5, form 2. Automator Recipe 22021 (Presto → BW Activity Entry) created and tested on production. Recipes 18036 and 18647 deactivated. All shortcodes replaced in Elementor pages (Dashboard, Primer Hero, History page, Member sidebar widget). PrimerPrint page created at /primerprint/ and added to menu. Steps 11–12 (Code Snippets 30–34, GamiPress deactivation) blocked pending Japanese widget support — see Japanese Language Support (Pending) below.

Session note (2026-03-19, evening — staging complete + production deploy plan confirmed): All shortcodes placed and tested on staging. Activity Types admin CRUD confirmed already built and working (was incorrectly marked incomplete). GamiPress decision: Recipe 18647 will be deactivated — no separate user meta fields will be maintained. Totals on the History page are computed directly from wp_bw_activity via [bw_activity_total] shortcodes (single source of truth, fast indexed queries, currently only consumer). GamiPress _gamipress_*_points fields abandoned. PrimerPrint page created on staging: pracstage.baseworks.com/primerprint/. course_cluster ACF field confirmed staging-only — needs to be created on production before deploy. Production deployment checklist confirmed and updated below.

Staging shortcode placement (confirmed 2026-03-19):

  • Dashboard: [course_continue_button id=15932], [bw_activity_bars], [bw_activity_list display=3], [bw_activity_total days="7"] min over last 7 days, [bw_activity_streak] days
  • Primer Course Page Hero: [course_continue_button id=15932]
  • History page: [bw_activity_total category="foundation"], [bw_activity_total category="elements"], [bw_activity_total category="in-person"], [bw_activity_total], [bw_activity_list], [bw_activity_calendar]
  • Member Single sidebar widget: [bw_activity_total days="7"] (weekly learning time), [bw_activity_streak] days (current streak) — replacing previous Formidable view

Session note (2026-03-19 — v0.5.6: Form 69 migration + PrimerPrint first name): Migration from Form 69 completed on staging for all users. class-migration.php updated to include two transformations that were missing from the original implementation: (1) cluster derivation from slug at insert time (^[0-9]+-[0-9]+primer, ^foundationfoundation, ^elementselements); (2) key-points re-categorization — any slug containing -key-points- gets activity_type = 'key-points' regardless of what Form 69 stored as theory. Migration tested for user ID 2 first, then run for all users — both confirmed working. Also: PrimerPrint footer now shows first_name (fallback to display_name) instead of display_name. SSH config aliases set up on Asia’s Mac Mini for all three sites.

Session note (2026-03-18 — Phase 2b v0.5.2–v0.5.4 polish + mobile UX): Layout: max-width: 1200px, left sidebar padding (1.5rem), user block far right in footer, logo vertical alignment fixed (display:block on SVG). Tablet breakpoint added (601–920px): sidebar 224px, STEP_Y=11, DOT_R=4. Mobile: tooltip redesigned to screen-centre fixed position (top:50%; left:50%; transform:translate(-50%,-50%)) — zoom-proof; hasPointer guard skips mouse handlers entirely on touch devices; touchend with e.preventDefault() blocks simulated mouse events. Desktop resize: debounced listener re-centers SVG spine; mobile excluded (pinch-zoom would re-trigger). Mobile controls: 2×2 CSS grid (speed+replay | PDF-Letter+PDF-A4). Mobile footer: full single column with stats as baseline rows, user block align-self:flex-end. Loading text fixed: “Loading your PrimerPrint”. Hours label: no h suffix, label reads “hours on Primer”. Print isolation: body{visibility:hidden} + wrapper visibility:visible;position:absolute hides WordPress chrome; SVG stroke-dasharray stripped before window.print(). PDF export deferred to Phase 3 (deploy to production first). Stable snapshot saved on server as primerprint.v054stable.js/.css. Plugin at v0.5.4.

Session note (2026-03-17 — Phase 2b v0.5.1 polish): Sidebar restored to desktop layout (was accidentally removed in v0.5.0). Two-column grid: 280px sidebar | viz. STEP_Y reduced 20→14 desktop, 8 mobile (set at JS runtime). SVG_H 1152px desktop, 684px mobile. Controls bar moved above the cream .bw-pp-document frame. Mobile footer restructured: stats in left column (number + label per row), user name right-aligned. Wider max-width: 1020px. .bw-dot font-size confirmed 19px.

Session note (2026-03-17 — Phase 2b initial build): [bw_primerprint] shortcode built and deployed to staging (v0.5.0). Two new asset files: assets/primerprint.css (cream #F5F2EE background, DM Mono + DM Serif Display via Google Fonts, full print CSS) and assets/primerprint.js (self-contained IIFE: 79-lesson data, REST API fetch with cluster=primer&format=primerprint, animated SVG draw via stroke-dashoffset + CSS @keyframes bw-pp-draw, speed controls ½×/1×/2×, Replay button, two PDF buttons injecting dynamic @page rules for Letter and A4, tap-to-show tooltip on mobile, all controls hidden on print). Arc amplitudes cap at 82% of half-width for mobile responsiveness. Sidebar removed entirely — viz is full-width. Plugin bumped to v0.5.0. .bw-dot font-size also updated to 19px (confirmed). Next: test on staging — create a page with [bw_primerprint], verify fetch + animation with Asia’s data (user_id=2), iterate on visual details.

Session note (2026-03-16, afternoon — Phase 2a wrap-up): Phase 2a completed and confirmed working. Two new shortcodes built, tested, and approved: [course_continue_button id=X text="..." title=true/false] (v0.4.0) and [smart_revisit id=X number=N category="..."] (v0.4.0). Polishing pass (v0.4.1): button CSS fixed with higher-specificity selectors + !important to override BuddyBoss theme <a> reset; Poppins @font-face added directly to frontend.css (self-hosted via Elementor font cache, relative path works on both staging and production); smart_revisit empty-state message added (“You need to complete more lessons to unlock the smart revisit.”). Data fix: 33 video blocks were missing video_category, lesson_slug, and video_duration ACF meta — synced from corresponding lesson posts. All plugin files synced to changelog repo. Uncanny Toolkit for LearnDash can now be deactivated. Phase 2a complete — next: place shortcodes in staging page editors, swap Formidable views, Form 69 migration, production deploy.

Replaces Formidable Form 69 as the activity log database on practice.baseworks.com. Developed on pracstage.baseworks.com, then deployed to production.

Related: practice-site-platform-infrastructure · primer-journey-visualizer


Formidable Forms stores entries in an EAV model (frm_items + frm_item_metas — one row per field per entry). At scale this becomes slow for the time-series queries the activity system needs: streaks, calendar views, per-user history, aggregate stats. With PrimerPrint moving online (needs full lesson history per user via API), a purpose-built table is necessary.

See scalability audit in practice-site-platform-infrastructure.


Two tables created on plugin activation.

Editable via admin UI. Pre-populated with the 9 current types on install.

CREATE TABLE wp_bw_activity_types (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(50) NOT NULL UNIQUE,
label VARCHAR(100) NOT NULL,
color VARCHAR(20) NOT NULL DEFAULT '#cccccc',
icon_url VARCHAR(500) DEFAULT NULL,
sort_order SMALLINT NOT NULL DEFAULT 0
);

Initial data:

SlugLabelColorNotes
foundationFoundation#35c5f4
elementsElements#44beaa
labLab#c161c1Primer short practice drills
formForm#6276bf
theoryTheory#4A3F8FPrimer conceptual lessons; also used in Baseworks Meta
key-pointsKey Points#B85C1APrimer only. Terracotta — matches PrimerPrint legend. Added 2026-03-14.
practicePractice#2785a4Primer full practice sessions
in-personIn Person#6276bf
otherOther#ffffff
testTest#000000
CREATE TABLE wp_bw_activity (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
activity_type VARCHAR(50) NOT NULL DEFAULT 'other',
cluster VARCHAR(50) DEFAULT NULL,
value SMALLINT NOT NULL DEFAULT 1,
activity_timestamp DATETIME NOT NULL,
label VARCHAR(255) NOT NULL,
slug VARCHAR(255) DEFAULT NULL,
duration_dec DECIMAL(6,2) DEFAULT NULL,
source VARCHAR(50) NOT NULL DEFAULT 'presto',
notes TEXT DEFAULT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, activity_timestamp),
INDEX idx_user_type (user_id, activity_type),
INDEX idx_cluster (cluster),
INDEX idx_slug (slug)
);

Field notes:

  • activity_timestamp — the display/stats timestamp. Set by the event source (Presto, manual entry, import). This is what all calculations and views use. NOT the same as created_at.
  • label — always required. The human-readable name (e.g. “5.1. The Basic Science of Movement Transitions” or “Montreal Study Group Session 7”).
  • slug — optional. Lesson URL key, used to link to the lesson page and for PrimerPrint spine matching. Empty for in-person, other non-lesson entries.
  • cluster — optional. The course domain this entry belongs to: primer, meta, foundation, elements, forms. Populated via ACF course_cluster field on the lesson/hub post (passed as Automator token). Used by PrimerPrint to filter WHERE cluster = 'primer', and by the REST API (?cluster=primer). NULL for entries that predate this field or for in-person entries not tied to a specific course.
  • value — normally 1. Can be higher for aggregated imports (e.g. syncing Tokyo studio attendance as a single entry with value = N instead of N individual rows).
  • duration_dec DECIMAL(6,2) — preserves decimal minutes from existing data (e.g. 4.42, 7.05).
  • sourcepresto | manual | import | admin. Tracks how the entry was created.
  • notes — free text, for admin-created entries to explain context.

Location: wp-content/plugins/bw-activity/ Changelog repo: sites/practice.baseworks.com/plugins/bw-activity/

bw-activity/
bw-activity.php — bootstrap, register hooks, activation/deactivation
includes/
class-db.php — all DB reads/writes; no raw SQL outside this class
class-admin.php — WP admin pages (activity log + type manager)
class-api.php — REST API endpoints
class-automator.php — Uncanny Automator action registration
class-migration.php — one-time import from Form 69
assets/
admin.css
admin.js

Top-level WP admin menu: Activity

WP_List_Table with:

  • Columns: Date | User | Type | Cluster | Lesson Label | Duration | Value | Source | Actions (Edit / Delete)
  • Filters: User dropdown · Date range · Activity type · Cluster
  • Bulk delete: checkboxes on each row, select-all in header and footer, Bulk Actions dropdown + Apply button (with confirmation dialog). Only shown in top tablenav.
  • Per-page selector: 25 / 50 / 100 / 200 (default 50). Navigates via GET URL — persists across filter changes.
  • Pagination: paginate_links() above and below the table.
  • Export CSV button (respects current filters)
  • Add Entry button

Add / Edit Entry form fields:

  • User (dropdown, searchable)
  • Activity Timestamp (datetime — the display timestamp, not created_at)
  • Activity Type (dropdown from wp_bw_activity_types)
  • Label (required)
  • Slug (optional — lesson URL key)
  • Duration (decimal minutes)
  • Value (default 1)
  • Notes (textarea)

This replaces the previous workflow of going into Formidable admin to add entries manually. Recipe 19551 (user-facing manual entry form) is separate and handled via the existing Formidable form until a user-facing replacement is built.

Simple CRUD table for wp_bw_activity_types:

  • List all types with color swatch, icon preview, sort order
  • Add / Edit / Delete
  • Drag to reorder

Endpoint: GET /wp-json/bw/v1/activity

Parameters:

  • user_id (required) — must be current user or admin
  • from / to — optional datetime filters
  • type — optional activity type filter
  • cluster — optional cluster filter (e.g. cluster=primer for PrimerPrint)
  • format=primerprint — returns data shaped for the PrimerPrint visualizer

PrimerPrint response shape:

{
"user_id": 2,
"entries": [
{
"activity_timestamp": "2026-03-13 16:18:00",
"label": "5.1. The Basic Science of Movement Transitions",
"slug": "5-1-the-basic-science-of-movement-transitions",
"duration_dec": 4.42,
"activity_type": "theory"
}
]
}

Status: COMPLETE on staging ✓ (2026-03-13)

Custom Automator action “Log a BW activity entry” is registered and fully working.

Action fields (all support Automator tokens):

  • Activity Type — text field, token-supporting. Pass video_category post meta. Falls back to 'other' if blank or unrecognised slug. Valid slugs: theory, key-points, lab, practice, foundation, elements, form, in-person.
  • Cluster — optional. Pass course_cluster post meta (primer, meta, foundation, elements, forms). Added 2026-03-14.
  • Label — required. Lesson/activity name.
  • Slug — optional. Lesson URL slug (lesson_slug post meta).
  • Duration — optional. Decimal minutes (video_duration post meta).
  • Value — default 1.
  • Timestamp override — optional. Leave blank to use current time.

Recipe 21934 on staging is live and end-to-end tested:

  • Trigger: Presto Player video completion (post 21935)
  • Action: Log BW activity entry (post 21971) — configured with Presto tokens for all 5 dynamic fields
  • Tested: recipe fires, action completes, entry appears in areb_bw_activity

Key technical note: In Uncanny Automator v7, the sentence token code in set_sentence() must match the options_group key (= get_action_meta()) for the React editor to render the form. Using individual field option_code values as sentence tokens causes the form to render as plain text. See dev log: docs/BW-ACTIVITY-AUTOMATOR-DEV-LOG.md

Recipe migration plan (confirmed 2026-03-19):

  • Recipe 18036 (Presto Bunny → Formidable Entry): remove Formidable step; new Recipe 21934 handles BW Activity logging
  • Recipe 18647 (Presto Bunny → Activity Points count): deactivate entirely. This recipe updated GamiPress user meta fields (_gamipress_foundation-completed_points, _gamipress_elements-completed_points, _gamipress_baseworks-completed_points, _gamipress_session-time-m_points). Decision: these fields are abandoned — no separate user meta totals will be maintained. All point totals on the History page are computed on-demand from wp_bw_activity via [bw_activity_total] shortcodes. Single source of truth, fast indexed queries, and currently the only consumer of these totals. GamiPress plugin will be deactivated after deploy.

User timezone is stored as a numeric UTC offset integer in ACF user meta field timezone (e.g. -4 for EDT, 0 for UTC, 9 for JST). Reading get_user_meta( $user_id, 'timezone', true ) always returns the current value.

The plugin’s get_utc_offset( $user_id ) reads this field directly. Used in all three shortcodes for streak day boundary calculations and date display — an activity at 23:30 UTC-4 counts as that day in Montreal, not the next UTC day.

Confirmed (2026-03-16): Site timezone = UTC (areb_options: timezone_string = UTC). All activity_timestamp values in areb_bw_activity are stored in UTC. User ID 2 ACF timezone = -4. The list shortcode had a bug where it displayed UTC dates instead of user-local dates — fixed in v0.3.1 by applying the UTC offset when formatting timestamps.

How users set their timezone (updated 2026-03-20)

Section titled “How users set their timezone (updated 2026-03-20)”

Users set their timezone via the TimeZone selectbox in their BuddyBoss profile (xprofile field 117). The field stores the label text (e.g. "UTC -5:00").

Code Snippet 45 (xprofile_updated_profile hook) syncs this to the ACF timezone field on every profile save: it parses the numeric offset from the label string (e.g. "UTC -5:00"-5) and writes it to timezone user meta.

Previously: Form 71 (Formidable) was the timezone entry point, placed on the practice history page. That was replaced by the BuddyBoss profile field as a more natural location. Formidable Form 71 and the old Uncanny Automator recipe 18554 / trigger 22094 are no longer the mechanism for this — Automator was blocked from updating Administrator accounts, and it passed the raw label string rather than the parsed integer anyway.

BuddyBoss time display (updated 2026-03-20)

Section titled “BuddyBoss time display (updated 2026-03-20)”

BuddyBoss displays timestamps in the site timezone (UTC). Because the site timezone is UTC and messages are stored in UTC, timestamps that should read “8:05 PM” showed as “12:05 AM”.

Code Snippet 46 fixes this for message thread view. It hooks bb_get_the_thread_message_sent_time (a BuddyBoss filter that fires on both page load and AJAX — messages are JS-rendered so the AJAX path is the one that matters). Inside the filter it reads the viewer’s timezone user meta, recalculates the timestamp from UTC using their offset, and returns the corrected time string. Falls back to the original output if no timezone is stored for the user.

Scope of fix: message thread inline timestamps only (the “8:05 PM” per-message display). The message list sidebar (“Yesterday”, “Monday 1:00 PM”) uses a different internal function (bb_get_thread_sent_date) that has no direct filter — lower priority, not fixed.

What’s still site-timezone: activity feed relative times (“2 hours ago”), forum post dates, notifications. These are lower impact since relative times don’t expose the raw clock.

Future: could migrate to named timezone strings (e.g. America/Montreal) for proper DST handling, but numeric offsets are sufficient for now.

Asia’s note: There is an ACF user field “timezone.” If it is empty, the timezone could be UTC. If it is not empty, then it should be used as offset for certain function where it makes sense ( for example currently in BuddyBoss when we send each other private messages, it often looks like we send messages at 4 a.m ). This is not only related to the activity but broader to site functionality. Ideally we should also at some point implement an onboarding timezone setting — when people purchase something they provide their billing address, so we know their city and could preset the timezone based on that.


One-time migration tool accessible via WP Admin → Activity → Migration tab (visible only to admins, only while Form 69 data exists).

Migration maps:

Form 69 fieldwp_bw_activity field
user_id (field 1187)user_id
Timestamp field 1197activity_timestamp
Label field 1191label
Value field 1192value
Category field 1195activity_type
Duration field 1196duration_dec
Slug field 1211slug
frm_items.created_atcreated_at
frm_items.updated_atupdated_at
source = 'import'
cluster — derive from slug (see note below)

Migration note — cluster and key-points re-categorization:

When importing Form 69 historical entries, two additional transformations are required beyond the field mapping above:

  1. Populate cluster: Derive from the slug column of each entry:

    • Slug matches ^[0-9]+-[0-9]+cluster = 'primer'
    • Slug matches ^foundationcluster = 'foundation'
    • Slug matches ^elements (if applicable) → cluster = 'elements'
    • No slug / unrecognised → cluster = NULL

    Alternatively, join against wp_postmeta on meta_key = 'lesson_slug' to find the post and read its course_cluster meta.

  2. Re-categorize Key Points entries: Form 69 historical entries for Key Points lessons have activity_type = 'theory'. These must be updated to activity_type = 'key-points' post-import. Identify them by slug: any slug matching *-key-points-* should have activity_type = 'key-points'.

Pre-migration lesson/hub post changes already applied on staging (2026-03-14):

  • video_category changed from theorykey-points on all 22 Key Points lessons and all 22 Key Points media hub posts
  • course_cluster ACF field populated on all 309 lessons (primer 79, elements 68, foundation 70, meta 24, forms 68) and all 193 media hub posts (primer 76, elements 34, foundation 35, meta 13, forms 35; Ignition 5 skipped)
  • Run the same script on production before going live: sites/practice.baseworks.com/data-scripts/2026-03-14-populate-cluster-and-key-points.php

The following Formidable views will be replaced with plugin shortcodes in Phase 2 (after admin interface and Automator integration are stable):

CurrentReplacementStatus
view=20872 — Recent Progress[bw_activity_bars]✅ Built (v0.3.1)
view=18375 — Recent Activity[bw_activity_bars] (same widget, bars are the display)✅ Built (v0.3.1)
view=18134 — Calendar[bw_activity_calendar]✅ Built (v0.3.1)
view=18665 — Activity List[bw_activity_list]✅ Built (v0.3.1)
view=18473 (form 70) — Smart Revisit[smart_revisit id=X number=N category="..."]✅ Built (v0.4.0)

Shortcode name change (2026-03-14): Original plan had separate [bw_recent_progress] and [bw_recent_activity]. These were consolidated into [bw_activity_bars] since the two views are nearly identical. [bw_activity_list display="3"] replaces the 3-item limited view; [bw_activity_list] (no parameter) shows the full paginated list.

Additional utility shortcodes (also built in v0.3.0)

Section titled “Additional utility shortcodes (also built in v0.3.0)”
ShortcodeOutputExample use
[bw_activity_total days="N"]Plain integer: total minutes over last N daysWeekly learning time: [bw_activity_total days="7"] min
[bw_activity_total days="N" category="slug1,slug2"]Total minutes over last N days, filtered to specified category(ies)Foundation + Elements (7d): [bw_activity_total days="7" category="foundation,elements"] min
[bw_activity_total category="slug1,slug2"]All-time total minutes, filtered to specified category(ies)Total Foundation hours: [bw_activity_total category="foundation"] min
[bw_activity_total]All-time total minutes, all categoriesTotal practice time: [bw_activity_total] min
[bw_activity_streak]Plain integer: current streak in daysCurrent streak: [bw_activity_streak] days

These are text-output-only (no HTML wrapper) — intended for inline use within existing text blocks, replacing [frm-stats] calls.

[bw_activity_bars user_id="current"]

  • user_id — defaults to current logged-in user

[bw_activity_calendar user_id="current"]

  • user_id — defaults to current logged-in user
  • Month/year navigation via ?bw_cal_month=N&bw_cal_year=YYYY query params

[bw_activity_list user_id="current" display=""]

  • user_id — defaults to current logged-in user
  • display="3" — show last N items, no pagination
  • omit display — show all, paginated (10/page via ?bw_list_page=N)

[bw_activity_total user_id="current" days="" category=""]

  • user_id — defaults to current logged-in user
  • days — number of days to look back (e.g. days="30"); omit for all-time total
  • category — comma-separated list of activity type slugs to filter by (e.g. category="foundation,in-person"); omit for all categories
  • Output: plain integer (minutes, rounded). Examples: [bw_activity_total days="7"], [bw_activity_total category="foundation,elements"], [bw_activity_total days="30" category="in-person"]

[bw_activity_streak user_id="current"]

[course_continue_button id="15932" text="Continue" title="true"] (added v0.4.0)

  • id — required. LearnDash course post ID.
  • text — button label. Default: "Continue".
  • title"true" shows “Your Next Lesson: …” below the button; "false" hides it. Default: "true".
  • Finds the first incomplete lesson in order; if all complete, links to the last lesson.
  • Replaces the Uncanny Toolkit “Resume Button” feature — Uncanny Toolkit can be deactivated.

[smart_revisit id="15932" number="2" category="lab,practice"] (added v0.4.0)

  • id — required. Comma-separated LearnDash course IDs. e.g. id="15932,6040".
  • number — how many lessons to show. Default: 2.
  • category — comma-separated video_category values to filter by. Default: "lab,practice". Any video_category slug works (e.g. "theory", "key-points").
  • Queries learndash_user_activity (completed lessons) filtered by video_category post meta. Picks randomly in PHP (no ORDER BY RAND()). Returns empty if no matches.
  • Output is a bare <ul class="bw-smart-revisit"> list — surrounding text (“Revisit one of these…”) goes in the page editor, not the shortcode.

What can be removed once shortcodes are live

Section titled “What can be removed once shortcodes are live”
  • Code Snippets 30–34 (frm-date, dailymax, frm_activity_bars, frm_activity_streak, frm_activity_dots) — all migrated into plugin
  • GamiPress plugin CSS — the old activity list reused GamiPress table styles; assets/frontend.css replaces this
  • Formidable views 20872, 18375, 18134, 18665 — once page editors are updated to use new shortcodes

Shortcode: [bw_primerprint] or [bw_primerprint user_id="X"] (admin only for other users).

Architecture (files added in v0.5.0):

  • assets/primerprint.js — self-contained IIFE. Reads data-* attributes from .bw-primerprint-wrap, fetches REST API, builds entire DOM (controls bar, header, sidebar, SVG viz, footer) in JS. No PHP rendering beyond the wrapper div.
  • assets/primerprint.css — all PrimerPrint styles. DM Mono + DM Serif Display loaded via Google Fonts (wp_register_style), enqueued on demand when shortcode fires.
  • PHP shortcode [bw_primerprint user_id="X"] in class-shortcodes.php — outputs wrapper div with data-user-id, data-nonce, data-api-root, data-user-name. Non-admins always see their own print.

Visualization design decisions:

  • Fetches GET /wp-json/bw/v1/activity?user_id=X&cluster=primer&format=primerprint (cluster=primer hardcoded — PrimerPrint is Primer-only by design)
  • SVG spine: 79 lesson dots colored by type (Theory #4A3F8F, Key Points #B85C1A, Practice #2785a4); unvisited dots faint gray
  • Thread: S-curves + nested loops (same lesson). Amplitude: AMP_SEQUENTIAL=22, AMP_IN_SEGMENT=45, AMP_PER_SEG=90, capped at 82% of half viz-width
  • Animation: stroke-dashoffset CSS @keyframes bw-pp-draw, total duration normalised to 10 s at 1× (so 5-entry and 500-entry users see same animation length). Dots light up via setTimeout at index × segMs
  • Speed: ½× · 1× · 2× buttons
  • Replay: rebuilds path elements with fresh CSS animations; resets dot states
  • Controls bar: lives ABOVE the cream document frame (outside .bw-pp-document), so replay is reachable without scrolling
  • PDF: two buttons (Letter / A4) inject a dynamic @page { size: … } stylesheet, call window.print(), then remove it. Controls bar hidden on print via @media print
  • Background: #F5F2EE (cream)

Layout:

  • Desktop: two-column grid inside .bw-pp-body — sidebar (240px) | viz area (1fr). STEP_Y=14, DOT_R=5, SVG_H=1152px
  • Mobile (≤600px): sidebar hidden, viz full-width. STEP_Y=8, DOT_R=3, SVG_H=684px — fits ~one smartphone screen
  • Sidebar row height = STEP_Y px; padding-top = 30 - STEP_Y/2 px to align first row with first SVG dot (y=30)
  • Sidebar rows light up as animation reaches each lesson (parallel setTimeout chain)
  • Tooltip on dot hover (desktop) and tap (mobile)
  • Font: DM Mono (body), DM Serif Display (title, user name)

Dimensions and layout (final — v0.5.4, 2026-03-18):

  • max-width: 1200px wrapper; sidebar 280px desktop, 224px tablet (601–920px), hidden mobile
  • initDimensions() called at runtime: desktop STEP_Y=14 DOT_R=5, tablet STEP_Y=11 DOT_R=4, mobile STEP_Y=8 DOT_R=3
  • SVG_H = 78 × STEP_Y + 60; sidebar row height = STEP_Y; padding-top = max(2, 30 − STEP_Y/2 − 23) px
  • Sidebar head: extra left padding 1.5rem to align with lesson rows
  • Desktop resize: debounced resize listener (250ms) calls runAnimation() to re-center SVG. Desktop-only (not attached on mobile — prevents pinch-zoom triggering re-animation)
  • Footer: user block margin-left: auto (far right); hours value no h suffix — label “hours on Primer”
  • Logo: display: block on inline SVG removes inline baseline gap

Mobile UX (final — v0.5.4):

  • hasPointer flag via window.matchMedia('(hover: hover) and (pointer: fine)') — mouse handlers registered only on pointer devices
  • Tooltip on touch: touchend + e.preventDefault() blocks browser-simulated mouse events; position fixed at screen centre (top:50%; left:50%; transform:translate(-50%,-50%)) — zoom-proof
  • Auto-dismiss 3.5s; document.addEventListener('touchstart', hideTouchTip, {passive:true}) dismisses on any other tap
  • Stable snapshot: primerprint.v054stable.js / .css on server (revert point)

Mobile layout (final — v0.5.4):

  • Controls: 2×2 CSS grid (grid-template-columns: 1fr auto): speed group + replay in col 1; PDF Letter + PDF A4 in col 2
  • Footer: flex-direction: column; align-items: stretch; .bw-pp-stats-group flex column; each stat = row (value + label baseline-aligned); user block align-self: flex-end

Print isolation (partial, Phase 2b):

  • body { visibility: hidden } + .bw-primerprint-wrap { visibility: visible; position: absolute; top:0; left:0 } — hides WP chrome in print
  • SVG stroke-dasharray/stroke-dashoffset stripped before window.print(), restored after 2s — forces full path render
  • Controls bar + tooltip hidden in @media print
  • Full PDF customization (paper size, background color, margins, orientation) → Phase 3

Page placement: Embed [bw_primerprint] in a dedicated page. Heading, description, and surrounding content go in normal Elementor blocks. The shortcode outputs only the framed artifact + controls bar.


Deferred from Phase 2b per 2026-03-18 decision: deploy to production first, then refine PDF export on staging.

Phase 3 scope (to develop on staging after production deploy):

  • Letter and A4 paper sizes working cleanly — correct scaling, no crop
  • @page { background: ... } or equivalent to preserve cream #F5F2EE background in print
  • Margins, orientation
  • Test in Chrome “Save as PDF” and system print dialog
  • Consider html2canvas + jsPDF as alternative if CSS print has persistent limitations

  1. ✅ Plugin scaffold + table creation
  2. Migration tool → run for test user (Asia, user ID 2) on staging
  3. ✅ Admin list view (read-only first, just to see the data)
  4. ✅ Add/Edit/Delete in admin
  5. ✅ Activity Types admin CRUD (partially built — types table seeded, no CRUD UI yet)
  6. ✅ REST API endpoint (GET /bw/v1/activity with format=primerprint)
  7. ✅ Automator action registration
  8. ✅ Update Automator recipes on staging, test end-to-end (recipe 21934 confirmed working)
  9. ✅ Migrate production recipes 18036 + 18647
  10. ✅ Phase 2a: shortcodes replacing Formidable views
  11. ✅ Phase 2b: PrimerPrint JS adaptation
  12. Production deploy
  13. ✅ CSV export (already built) — updated v0.8.0: now includes user_email column
  14. ✅ CSV import (v0.8.0 on staging — test + deploy to production pending)
  15. Phase 3: Primer PDF Export

Order confirmed 2026-03-19. course_cluster is the only ACF field that needs to be created on production (video_category, lesson_slug, video_duration already existed). No separate GamiPress user meta fields will be maintained — shortcodes handle all totals.

  1. Create course_cluster ACF field on production (export field group from staging via ACF → Tools → Export, import on production)
  2. Back up the live site
  3. Deploy and activate the plugin on production
  4. Confirm no critical errors — WP admin loads, no PHP errors, Activity menu appears
  5. Run sites/practice.baseworks.com/data-scripts/2026-03-14-populate-cluster-and-key-points.php on production — populates course_cluster on all lesson/hub posts + fixes key-points video_category
  6. Set up Automator recipes: new Presto → BW Activity Entry recipe; remove Formidable step from Recipe 18036; deactivate Recipe 18647 (GamiPress points — replaced by shortcodes)
  7. Run Form 69 migration (wp-admin → Activity → Migration)
  8. Verify migration data (spot-check a few users before touching front end)
  9. Replace all shortcodes in Elementor pages (Dashboard, Primer Hero, History page, Member sidebar widget) + create PrimerPrint page
  10. Deactivate old Formidable recipes entirely (18036, 18647)
  11. Deactivate Code Snippets 30–34 — blocked: Japanese widget versions still active (Phase 4)
  12. Deactivate GamiPress — blocked: Japanese widget versions still active (Phase 4)
  13. CSV import + user_email in export — v0.8.0 deployed to production (2026-03-21)
  14. Return to staging for Phase 3 (PDF export refinement)

  • Design approved (2026-03-13)
  • Plugin scaffold + tables created on staging (2026-03-13)
  • Admin interface built — list view, add/edit/delete (2026-03-13)
  • REST API built — GET /bw/v1/activity with format=primerprint (2026-03-13)
  • Automator action registered and working end-to-end (2026-03-13)
  • Test recipe 21934 on staging confirmed: Presto → BW Activity entry ✓ (2026-03-13)
  • cluster column added to wp_bw_activity (2026-03-14)
  • key-points activity type added (2026-03-14)
  • course_cluster ACF field populated on all lesson + media hub posts on staging (2026-03-14)
  • Key Points video_category changed from theorykey-points on all lessons + hub posts on staging (2026-03-14)
  • Automator action updated: added Cluster field, updated Activity Type description (v0.2.0)
  • Admin interface upgraded: cluster column + filter, bulk delete with checkboxes + select-all, per-page selector (25/50/100/200), paginate_links pagination (2026-03-14)
  • Three admin bugs fixed: division-by-zero on page load, bulk delete silently failing, per-page reverting to 50 (2026-03-14)
  • Migration class updated: cluster derivation + key-points re-categorization added (2026-03-19, v0.5.6)
  • Form 69 migration run on staging — user ID 2 (test), then all users (2026-03-19) ✓
  • Activity Types admin CRUD — Edit/Delete per row + Add Type form (was already built; status tracker was stale)
  • CSV export in admin — already built and working (confirmed 2026-03-20)
  • CSV import in admin
  • PrimerPrint JS adapted to use REST API — [bw_primerprint] shortcode built (v0.5.0, 2026-03-17)
  • PrimerPrint v0.5.1: sidebar restored, STEP_Y 14/8 desktop/mobile, controls above frame, mobile footer restructured (2026-03-17)
  • PrimerPrint v0.5.2–v0.5.4: layout polish, tablet breakpoint (601–920px), desktop resize handler, mobile touch tooltip (screen-centre, zoom-proof), mobile controls 2×2 grid, mobile footer single column, logo fix, loading text fix, hours label fix (2026-03-18)
  • Stable snapshot saved on server: primerprint.v054stable.js/.css (2026-03-18)
  • Print isolation: WordPress chrome hidden in print, SVG paths forced to full render (2026-03-18)
  • [bw_activity_total] extended: category attribute (comma-separated slugs), all-time mode (omit days), human-readable time format (Xh Ym) (2026-03-18, v0.5.5)
  • PrimerPrint footer: first name instead of display name, fallback to display name (2026-03-19, v0.5.6)
  • Production recipes updated: Recipe 22021 created (Presto → BW Activity Entry), Recipe 18036 deactivated, Recipe 18647 deactivated (2026-03-20) ✓
  • Phase 2a shortcodes built — [bw_activity_bars], [bw_activity_calendar], [bw_activity_list], [bw_activity_total], [bw_activity_streak] (2026-03-14)
  • Single CSS file assets/frontend.css created — covers all three display widgets (2026-03-14)
  • Design pass — font (Poppins), bar/dot/list/calendar layout, Unicode dot fix, continuous baseline (2026-03-15)
  • filemtime() CSS auto-versioning — no more manual cache busting on deploy (2026-03-15)
  • get_utc_offset() simplified to ACF user meta only — Form 71 fallback removed (2026-03-15)
  • Timezone bug fixed in activity list — list now shows user-local date, matching bars (2026-03-16, v0.3.1)
  • Phase 2a shortcodes tested on staging — swap Formidable views in page editors (2026-03-19) ✓
  • [course_continue_button] shortcode — replaces Uncanny Toolkit resume button (2026-03-16, v0.4.0)
  • [smart_revisit] shortcode — replaces Formidable view=18473 of Form 70 (2026-03-16, v0.4.0)
  • Button CSS + Poppins @font-face + empty-state message polishing pass (2026-03-16, v0.4.1)
  • 33 video blocks missing ACF meta (video_category, lesson_slug, video_duration) fixed on staging (2026-03-16)
  • All plugin files synced to changelog repo (2026-03-16)
  • Phase 2a shortcodes confirmed working by Asia (2026-03-16) ✓
  • Uncanny Toolkit for LearnDash deactivated (safe now — course_continue_button covers its only use)
  • Phase 2a shortcodes placed on staging dashboard + history page + Primer Hero + Member sidebar; Formidable views swapped (2026-03-19) ✓
  • All shortcodes replaced on production — Dashboard, Primer Hero, History page, Member sidebar widget (2026-03-20) ✓
  • PrimerPrint page created on production (/primerprint/) and added to menu (2026-03-20) ✓
  • Form 69 migration run on production — 2,604 entries, 72 users, data from 2020-12-26 (2026-03-20) ✓
  • Plugin files synced to changelog repo (v0.5.5, 2026-03-20)
  • Production deploy complete (2026-03-20) ✓
  • Phase 5: [bw_activity_list] category filter (checkbox, OR logic, user-specific types) built and tested on pracstage (2026-03-20, v0.6.0)
  • Phase 5: per-page selector (10 / 25 / 50 / All) built and tested on pracstage (2026-03-20, v0.6.0)
  • Phase 5: summary bar (entry count + total duration, light gray) built and tested on pracstage (2026-03-20, v0.6.0)
  • Phase 5: v0.6.0 deployed to production — confirmed working by Asia (2026-03-20) ✓
  • Phase 4: lang parameter built for all shortcodes — get_strings() + format_minutes_lang() (2026-03-20, v0.7.0 on staging)
  • Phase 4: lang="ja" shortcodes placed on Japanese pages (14527 dashboard, 14803 history) — tested and confirmed on staging (2026-03-20)
  • Phase 4: v0.7.0 deployed to production — EN + JA confirmed working (2026-03-20)
  • Code Snippets 30–34 deactivated (2026-03-20) ✓
  • GamiPress deactivated (2026-03-20) ✓
  • PDF export refinement → Phase 3

Status: complete — v0.6.0 live on production (2026-03-20)

Use case that prompted this: a user wants to see only their in-person practice record — filtered list, total duration, control over how many items are shown per page.

New features (full-list mode only — display="N" mode unchanged)

Section titled “New features (full-list mode only — display="N" mode unchanged)”

1. Category filter — checkbox-based, additive (OR) logic

  • Checkboxes rendered above the table, one per activity type
  • Only types the user actually has entries for are shown (no empty categories)
  • GET param: bw_list_types[] (array of slugs, e.g. ?bw_list_types[]=in-person&bw_list_types[]=foundation)
  • No checkboxes checked = no filter = show all (same as current behavior)
  • Submitting the filter form resets to page 1 (no bw_list_page in form)

2. Per-page selector

  • Dropdown: 10 / 25 / 50 / All
  • GET param: bw_list_per_page (default 10, “All” passes a large sentinel value)
  • Changing per-page resets to page 1
  • Lives in the same filter form as the checkboxes

3. Summary bar (below the table)

  • Shows: entry count + total duration formatted as Xh Ym
  • Computed from the active filter state (same WHERE clause as the list query)
  • Dark teal background, white text — styled to stand out from the table
ParamSourceResets on change
bw_list_pagePagination links
bw_list_per_pageFilter form selectresets bw_list_page to 1
bw_list_types[]Filter checkboxesresets bw_list_page to 1

Pagination links preserve bw_list_types[] and bw_list_per_page from the current GET state.

  • build_where(): extend to accept types key (array) → WHERE activity_type IN (...) (alongside existing single-value type key)
  • New get_user_types($user_id): SELECT DISTINCT activity_type for this user, intersected against areb_bw_activity_types for label/color/sort_order — returns only types the user has records for
  • New sum_entries($args): returns ['count' => N, 'duration_sum' => X] with same WHERE logic as count_entries()

No changes to the shortcode itself. The new controls operate via GET params, not shortcode attributes. [bw_activity_list] on the History page continues to work as before; the filter/per-page/summary appear automatically in the full-list mode.


Status: lang param built and on staging (v0.7.0). Next step: place shortcodes on the two Japanese pages.

Implementation: Added lang attribute to all shortcodes (default "en"). All UI strings centralised in get_strings($lang) in class-shortcodes.php. Time formatting handled by format_minutes_lang($minutes, $lang) (EN: 1h 30m, JA: 1時間30分).

Supported shortcodes with lang="ja":

[bw_activity_bars lang="ja"]
[bw_activity_list lang="ja"]
[bw_activity_streak lang="ja"]
[bw_activity_total lang="ja"]
[bw_activity_calendar lang="ja"]
[course_continue_button id=15932 lang="ja"]
[smart_revisit id=X lang="ja"]
[bw_primerprint lang="ja"]

Pages that need updating (staging → production):

  • Page 14527 — Japanese dashboard (“Login to Baseworks Platform”). Uses Formidable view 19665 ([Tracking] Activity: 7 day Bars + 28 Dots - JAPANESE). Replace with [bw_activity_bars lang="ja"] + [bw_activity_streak] (streak is a plain integer — no lang strings).
  • Page 14803 — Japanese Practice History. Uses Formidable view 19615 ([Tracking] Activity History List - JAPANESE). Replace with [bw_activity_list lang="ja"] and [bw_activity_calendar lang="ja"].

Japanese string source: Confirmed from existing Formidable views — 直近7日間, 最後の4週間, N日連続, 詳細, 日付, . Additional strings provided by Asia 2026-03-20.

Remaining steps:

  1. Place lang="ja" shortcodes on pages 14527 and 14803 on staging
  2. Test with a Japanese-language user session on staging
  3. Deploy v0.7.0 to production
  4. Deactivate Code Snippets 30–34
  5. Deactivate GamiPress

Note on date formatting: Japanese list and calendar use Y/m/d date format — same as the existing Formidable view (2025/06/09). No change needed.

Note on activity type labels: Labels in the list filter checkboxes and calendar legend come from wp_bw_activity_types.label (DB). These are currently English (Foundation, Elements, etc.). Japanese labels could be added to the DB if needed — not required for Phase 4.

Note on timezone: The get_utc_offset() function is language-agnostic — no changes needed there.