BW Activity Plugin — Design & Implementation Plan
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 instrtolower()so the key is consistently lowercase on both ends. One-line change inhandle_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,user_idauthoritative; email cross-checked if both present). Invalid rows shown in red and skipped on confirm. Blank CSV rows silently skipped. Unknown activity types mapped toother. Parsed rows stored in a 30-min WP transient between preview and confirm steps. CSV export updated to includeuser_emailcolumn (batch-fetched viaget_users()— no N+1 queries). Smoke-tested withbw-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 singleget_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: placelang="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
clustercolumn +key-pointsactivity 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 usingborder-bottomon.bw-bar-trackwith 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 ACFtimezonefield, 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-14for a Montreal UTC-4 user). Confirmed: site timezone = UTC (areb_options), user ACFtimezone=-4. Fix: addedget_utc_offset()call inactivity_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; newget_user_types()andsum_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_activityvia[bw_activity_total]shortcodes (single source of truth, fast indexed queries, currently only consumer). GamiPress_gamipress_*_pointsfields abandoned. PrimerPrint page created on staging:pracstage.baseworks.com/primerprint/.course_clusterACF 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 viewSession 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.phpupdated to include two transformations that were missing from the original implementation: (1)clusterderivation from slug at insert time (^[0-9]+-[0-9]+→primer,^foundation→foundation,^elements→elements); (2) key-points re-categorization — any slug containing-key-points-getsactivity_type = 'key-points'regardless of what Form 69 stored astheory. Migration tested for user ID 2 first, then run for all users — both confirmed working. Also: PrimerPrint footer now showsfirst_name(fallback todisplay_name) instead ofdisplay_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:blockon 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;hasPointerguard skips mouse handlers entirely on touch devices;touchendwithe.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 blockalign-self:flex-end. Loading text fixed: “Loading your PrimerPrint”. Hours label: nohsuffix, label reads “hours on Primer”. Print isolation:body{visibility:hidden}+ wrappervisibility:visible;position:absolutehides WordPress chrome; SVGstroke-dasharraystripped beforewindow.print(). PDF export deferred to Phase 3 (deploy to production first). Stable snapshot saved on server asprimerprint.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_Yreduced 20→14 desktop, 8 mobile (set at JS runtime).SVG_H1152px desktop, 684px mobile. Controls bar moved above the cream.bw-pp-documentframe. Mobile footer restructured: stats in left column (number + label per row), user name right-aligned. Widermax-width: 1020px..bw-dotfont-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#F5F2EEbackground, DM Mono + DM Serif Display via Google Fonts, full print CSS) andassets/primerprint.js(self-contained IIFE: 79-lesson data, REST API fetch withcluster=primer&format=primerprint, animated SVG draw viastroke-dashoffset+ CSS@keyframes bw-pp-draw, speed controls ½×/1×/2×, Replay button, two PDF buttons injecting dynamic@pagerules 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-dotfont-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 +!importantto override BuddyBoss theme<a>reset; Poppins@font-faceadded directly tofrontend.css(self-hosted via Elementor font cache, relative path works on both staging and production);smart_revisitempty-state message added (“You need to complete more lessons to unlock the smart revisit.”). Data fix: 33 video blocks were missingvideo_category,lesson_slug, andvideo_durationACF 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.
Database Schema
Section titled “Database Schema”Two tables created on plugin activation.
wp_bw_activity_types
Section titled “wp_bw_activity_types”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:
| Slug | Label | Color | Notes |
|---|---|---|---|
| foundation | Foundation | #35c5f4 | |
| elements | Elements | #44beaa | |
| lab | Lab | #c161c1 | Primer short practice drills |
| form | Form | #6276bf | |
| theory | Theory | #4A3F8F | Primer conceptual lessons; also used in Baseworks Meta |
| key-points | Key Points | #B85C1A | Primer only. Terracotta — matches PrimerPrint legend. Added 2026-03-14. |
| practice | Practice | #2785a4 | Primer full practice sessions |
| in-person | In Person | #6276bf | |
| other | Other | #ffffff | |
| test | Test | #000000 |
wp_bw_activity
Section titled “wp_bw_activity”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 ascreated_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 ACFcourse_clusterfield on the lesson/hub post (passed as Automator token). Used by PrimerPrint to filterWHERE 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 withvalue = Ninstead of N individual rows).duration_dec DECIMAL(6,2)— preserves decimal minutes from existing data (e.g. 4.42, 7.05).source—presto|manual|import|admin. Tracks how the entry was created.notes— free text, for admin-created entries to explain context.
Plugin Architecture
Section titled “Plugin Architecture”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.jsAdmin Interface
Section titled “Admin Interface”Top-level WP admin menu: Activity
Activity Log page (default)
Section titled “Activity Log page (default)”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.
Activity Types page (submenu)
Section titled “Activity Types page (submenu)”Simple CRUD table for wp_bw_activity_types:
- List all types with color swatch, icon preview, sort order
- Add / Edit / Delete
- Drag to reorder
REST API
Section titled “REST API”Endpoint: GET /wp-json/bw/v1/activity
Parameters:
user_id(required) — must be current user or adminfrom/to— optional datetime filterstype— optional activity type filtercluster— optional cluster filter (e.g.cluster=primerfor 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" } ]}Uncanny Automator Integration
Section titled “Uncanny Automator Integration”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_categorypost 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_clusterpost meta (primer,meta,foundation,elements,forms). Added 2026-03-14. - Label — required. Lesson/activity name.
- Slug — optional. Lesson URL slug (
lesson_slugpost meta). - Duration — optional. Decimal minutes (
video_durationpost 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 fromwp_bw_activityvia[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.
Timezone
Section titled “Timezone”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.
Migration from Form 69
Section titled “Migration from Form 69”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 field | wp_bw_activity field |
|---|---|
user_id (field 1187) | user_id |
| Timestamp field 1197 | activity_timestamp |
| Label field 1191 | label |
| Value field 1192 | value |
| Category field 1195 | activity_type |
| Duration field 1196 | duration_dec |
| Slug field 1211 | slug |
frm_items.created_at | created_at |
frm_items.updated_at | updated_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:
-
Populate
cluster: Derive from theslugcolumn of each entry:- Slug matches
^[0-9]+-[0-9]+→cluster = 'primer' - Slug matches
^foundation→cluster = 'foundation' - Slug matches
^elements(if applicable) →cluster = 'elements' - No slug / unrecognised →
cluster = NULL
Alternatively, join against
wp_postmetaonmeta_key = 'lesson_slug'to find the post and read itscourse_clustermeta. - Slug matches
-
Re-categorize Key Points entries: Form 69 historical entries for Key Points lessons have
activity_type = 'theory'. These must be updated toactivity_type = 'key-points'post-import. Identify them by slug: any slug matching*-key-points-*should haveactivity_type = 'key-points'.
Pre-migration lesson/hub post changes already applied on staging (2026-03-14):
video_categorychanged fromtheory→key-pointson all 22 Key Points lessons and all 22 Key Points media hub postscourse_clusterACF 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
Display Shortcodes (Phase 2a)
Section titled “Display Shortcodes (Phase 2a)”The following Formidable views will be replaced with plugin shortcodes in Phase 2 (after admin interface and Automator integration are stable):
| Current | Replacement | Status |
|---|---|---|
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)”| Shortcode | Output | Example use |
|---|---|---|
[bw_activity_total days="N"] | Plain integer: total minutes over last N days | Weekly 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 categories | Total practice time: [bw_activity_total] min |
[bw_activity_streak] | Plain integer: current streak in days | Current 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.
Shortcode parameters
Section titled “Shortcode parameters”[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=YYYYquery params
[bw_activity_list user_id="current" display=""]
user_id— defaults to current logged-in userdisplay="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 userdays— number of days to look back (e.g.days="30"); omit for all-time totalcategory— 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-separatedvideo_categoryvalues to filter by. Default:"lab,practice". Anyvideo_categoryslug works (e.g."theory","key-points").- Queries
learndash_user_activity(completed lessons) filtered byvideo_categorypost meta. Picks randomly in PHP (noORDER 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.cssreplaces this - Formidable views 20872, 18375, 18134, 18665 — once page editors are updated to use new shortcodes
PrimerPrint Online (Phase 2b)
Section titled “PrimerPrint Online (Phase 2b)”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. Readsdata-*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"]inclass-shortcodes.php— outputs wrapper div withdata-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-dashoffsetCSS@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 viasetTimeoutatindex × 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, callwindow.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_Ypx;padding-top = 30 - STEP_Y/2px 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: 1200pxwrapper; sidebar280pxdesktop,224pxtablet (601–920px), hidden mobileinitDimensions()called at runtime: desktopSTEP_Y=14 DOT_R=5, tabletSTEP_Y=11 DOT_R=4, mobileSTEP_Y=8 DOT_R=3SVG_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.5remto align with lesson rows - Desktop resize: debounced
resizelistener (250ms) callsrunAnimation()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 nohsuffix — label “hours on Primer” - Logo:
display: blockon inline SVG removes inline baseline gap
Mobile UX (final — v0.5.4):
hasPointerflag viawindow.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/.csson 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-groupflex column; each stat = row (value + label baseline-aligned); user blockalign-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-dashoffsetstripped beforewindow.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.
PDF Export (Phase 3)
Section titled “PDF Export (Phase 3)”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#F5F2EEbackground in print- Margins, orientation
- Test in Chrome “Save as PDF” and system print dialog
- Consider
html2canvas+jsPDFas alternative if CSS print has persistent limitations
Implementation Order
Section titled “Implementation Order”- ✅ Plugin scaffold + table creation
- Migration tool → run for test user (Asia, user ID 2) on staging
- ✅ Admin list view (read-only first, just to see the data)
- ✅ Add/Edit/Delete in admin
- ✅ Activity Types admin CRUD (partially built — types table seeded, no CRUD UI yet)
- ✅ REST API endpoint (
GET /bw/v1/activitywithformat=primerprint) - ✅ Automator action registration
- ✅ Update Automator recipes on staging, test end-to-end (recipe 21934 confirmed working)
- ✅ Migrate production recipes 18036 + 18647
- ✅ Phase 2a: shortcodes replacing Formidable views
- ✅ Phase 2b: PrimerPrint JS adaptation
- Production deploy
- ✅ CSV export (already built) — updated v0.8.0: now includes
user_emailcolumn - ✅ CSV import (v0.8.0 on staging — test + deploy to production pending)
- Phase 3: Primer PDF Export
Production deployment checklist
Section titled “Production deployment checklist”Order confirmed 2026-03-19.
course_clusteris 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.
- Create
course_clusterACF field on production (export field group from staging via ACF → Tools → Export, import on production) - Back up the live site
- Deploy and activate the plugin on production
- Confirm no critical errors — WP admin loads, no PHP errors, Activity menu appears
- Run
sites/practice.baseworks.com/data-scripts/2026-03-14-populate-cluster-and-key-points.phpon production — populatescourse_clusteron all lesson/hub posts + fixeskey-pointsvideo_category - Set up Automator recipes: new Presto → BW Activity Entry recipe; remove Formidable step from Recipe 18036; deactivate Recipe 18647 (GamiPress points — replaced by shortcodes)
- Run Form 69 migration (
wp-admin → Activity → Migration) - Verify migration data (spot-check a few users before touching front end)
- Replace all shortcodes in Elementor pages (Dashboard, Primer Hero, History page, Member sidebar widget) + create PrimerPrint page
- Deactivate old Formidable recipes entirely (18036, 18647)
- Deactivate Code Snippets 30–34 — blocked: Japanese widget versions still active (Phase 4)
- Deactivate GamiPress — blocked: Japanese widget versions still active (Phase 4)
- CSV import + user_email in export — v0.8.0 deployed to production (2026-03-21)
- Return to staging for Phase 3 (PDF export refinement)
Status
Section titled “Status”- 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/activitywithformat=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)
-
clustercolumn added towp_bw_activity(2026-03-14) -
key-pointsactivity type added (2026-03-14) -
course_clusterACF field populated on all lesson + media hub posts on staging (2026-03-14) - Key Points
video_categorychanged fromtheory→key-pointson 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:categoryattribute (comma-separated slugs), all-time mode (omitdays), 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.csscreated — 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_buttoncovers 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:
langparameter 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
[bw_activity_list] Enhancements (Phase 5)
Section titled “[bw_activity_list] Enhancements (Phase 5)”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_pagein 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
URL param composition
Section titled “URL param composition”| Param | Source | Resets on change |
|---|---|---|
bw_list_page | Pagination links | — |
bw_list_per_page | Filter form select | resets bw_list_page to 1 |
bw_list_types[] | Filter checkboxes | resets bw_list_page to 1 |
Pagination links preserve bw_list_types[] and bw_list_per_page from the current GET state.
DB changes (class-db.php)
Section titled “DB changes (class-db.php)”build_where(): extend to accepttypeskey (array) →WHERE activity_type IN (...)(alongside existing single-valuetypekey)- New
get_user_types($user_id):SELECT DISTINCT activity_typefor this user, intersected againstareb_bw_activity_typesfor 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 ascount_entries()
Shortcode signature
Section titled “Shortcode signature”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.
Japanese Language Support (Phase 4)
Section titled “Japanese Language Support (Phase 4)”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:
- Place
lang="ja"shortcodes on pages 14527 and 14803 on staging - Test with a Japanese-language user session on staging
- Deploy v0.7.0 to production
- Deactivate Code Snippets 30–34
- 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.