chore: initial commit — Buddy v0.1.0 (Phase A complete)
Buddy is born. First commit of a new standalone WordPress plugin — the spiritual successor to the tamagotchi that once lived inside A-WP-Notes v1.1.5 (gracefully retired). Rebuilt from scratch with all the v3-discipline lessons baked in from day one. PHASE A — pet exists - Dashboard widget at WP Admin → Dashboard showing SVG character + name + mood label + four stats bars. - Dedicated admin page at WP Admin → Buddy → My Buddy (bigger view). - About page with side-by-side intro + plain-prose cards (Logbook About-page pattern carried forward). - Settings page with name-rename form + Updates panel. - Per-user state in user_meta key buddy_state (each WP admin gets their own pet, no shared state). - Inline SVG sprite renderer with three mood tones (happy/neutral/ sad) and three sizes (sm/md/lg). CSS keyframe animations: bobbing + periodic blinking. Zero image files. - Self-hosted update checker wired up from commit 1, ported from Logbook v3.3.5: /releases/latest with /tags?limit=1 fallback, 12h success cache / 1h negative cache. UI on Settings page. - dashicons-pets admin-menu icon — literal paw-print, brand match. ARCHITECTURE LOCKED FROM COMMIT 1 - Single-word brand name "Buddy" — no WP prefix, no future rebrand. - Public GPL v2+ Gitea repo (ranger/a-buddy). - Constants prefix BUDDY_*, function prefix buddy_*, text domain buddy. Clean naming throughout — none of Logbook's wp-notes-* historical-artifact baggage. - Single H1 per admin page, no nested toggle boxes, no duplicate sections — Tier-1 discipline carried forward from Logbook. - All assets local (inline SVG, plain CSS), no third-party CDN, no Gravatar-style external pings. NOT IN THIS RELEASE (planned) - Phase B — Feed/Play/Clean/Sleep interactions + cooldown timers. - Phase C — WP-cron decay + "Buddy is hungry" dismissible notices (port the persistent-dismissal pattern from Logbook). - Phase D — Multiple species (dog, dragon, sprite), per-species personality phrases. - Phase E — Site-health hook: pet stats react to wp_get_site_health() results. The killer feature. - Phase F — Pro tier (€2.99 lifetime) with custom skins + multi-pet. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
|
||||
# Composer (not used today, but reserved if we ever add server-side libs)
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# Build artifacts
|
||||
*.zip
|
||||
@@ -0,0 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to **Buddy** are documented here.
|
||||
Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/) — versioning: [SemVer](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
---
|
||||
|
||||
## [0.1.0] — 2026-05-25
|
||||
|
||||
**Buddy is born.** First release of a new standalone WordPress plugin extracted-and-rebuilt from the tamagotchi feature that once lived inside A-WP-Notes v1.1.5 (now gracefully retired). Buddy stands on its own as a focused, charming companion plugin for the WordPress dashboard.
|
||||
|
||||
### Added — Phase A complete (pet exists)
|
||||
- **Dashboard widget** at WP Admin → Dashboard showing the SVG character, the pet's name, a mood label ("Thriving" / "Content" / "Okay" / "Hungry" / "Distressed"), and four stat bars (Hunger / Happiness / Health / Energy).
|
||||
- **Dedicated admin page** at WP Admin → Buddy → My Buddy showing the same data in a larger format with the pet's age and a placeholder note describing what's coming next.
|
||||
- **About page** at WP Admin → Buddy → About with the side-by-side intro pattern, "What Buddy does" / "Who Buddy is for" / "Version history" cards, and a link to this CHANGELOG.md.
|
||||
- **Settings page** at WP Admin → Buddy → Settings with a name-rename form and the Updates panel.
|
||||
- **Per-user state storage** via `user_meta` (key: `buddy_state`). Each WP admin gets their own Buddy with its own name, species, and stats.
|
||||
- **SVG sprite renderer** with three mood tones (happy / neutral / sad) and three sizes (sm / md / lg). Pure inline SVG with CSS keyframe animations (bobbing motion + periodic blinking). No GIFs, no sprite sheets, no image files for the character.
|
||||
- **Self-hosted update checker** wired up to the Gitea repo from commit 1. Polls `/api/v1/repos/ranger/a-buddy/releases/latest` with a `/tags?limit=1` fallback. 12h success cache, 1h negative cache. UI surface lives on the Settings page.
|
||||
- **Custom admin-menu icon** (`dashicons-pets`, a paw print) reinforcing the pet identity.
|
||||
|
||||
### Architecture (locked from day one)
|
||||
- **Single-word brand name `Buddy`** — no "WP" prefix, no marketplace trademark hurdle.
|
||||
- **Public GPL v2+ Gitea repo** at `ranger/a-buddy` on `git.davidtkeane.com`.
|
||||
- **All persistent notice dismissals** to use `user_meta` + AJAX pattern (port from Logbook when first notice appears).
|
||||
- **CSS-only animations, all assets local** — bundle stays small.
|
||||
- **Single H1 per admin page**, no nested toggle boxes, no duplicate sections — Tier-1 discipline carried forward from Logbook.
|
||||
|
||||
### Not in this release (planned)
|
||||
- Phase B — Feed / Play / Clean / Sleep interactions with cooldown timers.
|
||||
- Phase C — WP-cron stat decay, "Buddy is hungry" dismissible admin notices.
|
||||
- Phase D — Multiple species (dog, dragon, sprite), per-species personality phrases.
|
||||
- Phase E — Site-health integration: pet stats react to `wp_get_site_health()` results, outdated plugins drain health, published posts feed hunger, cleared spam boosts happiness.
|
||||
- Phase F — Pro tier with custom skins, multi-pet farm, social visits.
|
||||
@@ -0,0 +1,47 @@
|
||||
# 🐾 Buddy
|
||||
|
||||
> A friendly little companion that lives in your WordPress dashboard.
|
||||
|
||||
Adopt a small virtual pet that lives in your WordPress admin. Right now Buddy just exists — they bob, they blink, they show their mood. As the plugin grows, their stats will reflect how well you take care of your WordPress site itself: published posts feed them, outdated plugins make them sick, clearing spam makes them happy.
|
||||
|
||||
**Gamifies WordPress maintenance with a bit of charm.**
|
||||
|
||||
---
|
||||
|
||||
## What this is
|
||||
|
||||
- A standalone WordPress plugin, GPL v2+ licensed.
|
||||
- The spiritual successor to the tamagotchi feature that once lived inside A-WP-Notes v1.1.5 (now gracefully retired). Rebuilt from scratch, with all the v3-discipline lessons baked in from day one.
|
||||
- Sister plugin to [Logbook](https://git.davidtkeane.com/ranger/a-logbook) in the RangerHQ plugin family. Logbook is *work*. Buddy is *play*.
|
||||
|
||||
## What it does
|
||||
|
||||
- Dashboard widget shows your Buddy with name, mood, and four stats bars.
|
||||
- Dedicated Buddy admin page for the bigger view.
|
||||
- Settings page where you can rename your Buddy and check for updates.
|
||||
- Each WP admin gets their own Buddy stored per-user in `user_meta` — nothing leaves your site's database.
|
||||
|
||||
## What it doesn't do (yet)
|
||||
|
||||
- No interactions — feed / play / clean / sleep coming in Phase B.
|
||||
- No time-based stat decay yet — Phase C.
|
||||
- Only one species available right now — Phase D will add dog, dragon, sprite, etc.
|
||||
- The killer feature — having Buddy's stats reflect your actual WP site health — is Phase E.
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md) for the full version history and roadmap.
|
||||
|
||||
## Install
|
||||
|
||||
1. Download the latest [release zip](https://git.davidtkeane.com/ranger/a-buddy/releases) from Gitea.
|
||||
2. WordPress admin → Plugins → Add New → Upload Plugin → choose the zip → Install Now → Activate.
|
||||
3. Find Buddy in the admin sidebar (paw-print icon).
|
||||
|
||||
The plugin self-checks for updates via the Gitea repo — see Settings → Buddy → Settings → Updates.
|
||||
|
||||
## Project pace
|
||||
|
||||
This is a **side project** built at a side-project pace. No commercial pressure, no release deadlines. Issues and feedback welcome via Gitea.
|
||||
|
||||
## Licence
|
||||
|
||||
GPL v2 or later — see [LICENSE](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html).
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Buddy plugin styles.
|
||||
* Scoped via .buddy-* class names so we don't bleed into the WP admin.
|
||||
* No external fonts, no @import, no images — just inline-SVG + plain CSS.
|
||||
*/
|
||||
|
||||
/* ── Sprite (the character) ──────────────────────────────────────── */
|
||||
|
||||
.buddy-sprite {
|
||||
display: block;
|
||||
user-select: none;
|
||||
animation: buddyBob 4s ease-in-out infinite;
|
||||
}
|
||||
.buddy-sprite--sm { width: 64px; height: 64px; }
|
||||
.buddy-sprite--md { width: 96px; height: 96px; }
|
||||
.buddy-sprite--lg { width: 160px; height: 160px; }
|
||||
|
||||
/* Eyes blink every ~5s. The fast-step keyframes simulate a quick blink. */
|
||||
.buddy-sprite__eye {
|
||||
transform-origin: center;
|
||||
animation: buddyBlink 5s infinite;
|
||||
}
|
||||
.buddy-sprite--sad .buddy-sprite__eye {
|
||||
animation-duration: 7s; /* sadder Buddy blinks slower */
|
||||
}
|
||||
|
||||
@keyframes buddyBob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-4px); }
|
||||
}
|
||||
@keyframes buddyBlink {
|
||||
0%, 92%, 100% { transform: scaleY(1); }
|
||||
94%, 98% { transform: scaleY(0.1); }
|
||||
}
|
||||
|
||||
/* ── Dashboard widget ───────────────────────────────────────────── */
|
||||
|
||||
.buddy-widget {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
.buddy-widget__pet { flex: 0 0 auto; }
|
||||
.buddy-widget__info { flex: 1 1 auto; min-width: 0; }
|
||||
.buddy-widget__name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.buddy-widget__mood {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 9px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.buddy-widget__mood--happy { background:#e8f5ea; color:#1b6f2d; }
|
||||
.buddy-widget__mood--neutral { background:#f0f0f1; color:#3c434a; }
|
||||
.buddy-widget__mood--sad { background:#fcf0f1; color:#8a2424; }
|
||||
|
||||
.buddy-widget__cta { margin: 10px 0 0; }
|
||||
|
||||
/* ── Stats bars ─────────────────────────────────────────────────── */
|
||||
|
||||
.buddy-stats {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.buddy-stat {
|
||||
display: grid;
|
||||
grid-template-columns: 22px 80px 1fr 36px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.buddy-stat__icon { text-align: center; font-size: 14px; }
|
||||
.buddy-stat__label { font-size: 12px; color: #3c434a; }
|
||||
.buddy-stat__bar {
|
||||
height: 8px;
|
||||
background: #f0f0f1;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.buddy-stat__fill {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4ade80, #22c55e);
|
||||
border-radius: 4px;
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
.buddy-stat__fill--low {
|
||||
background: linear-gradient(90deg, #f97316, #ef4444);
|
||||
}
|
||||
.buddy-stat__num {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #646970;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Main admin page layout ─────────────────────────────────────── */
|
||||
|
||||
.buddy-main {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 22px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
@media (max-width: 782px) {
|
||||
.buddy-main { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.buddy-main__card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 22px;
|
||||
}
|
||||
.buddy-main__card--pet {
|
||||
text-align: center;
|
||||
}
|
||||
.buddy-main__name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 14px 0 4px;
|
||||
}
|
||||
.buddy-main__mood {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 2px 9px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.buddy-main__mood--happy { background:#e8f5ea; color:#1b6f2d; }
|
||||
.buddy-main__mood--neutral { background:#f0f0f1; color:#3c434a; }
|
||||
.buddy-main__mood--sad { background:#fcf0f1; color:#8a2424; }
|
||||
|
||||
.buddy-main__age { color: #646970; font-size: 13px; margin: 0; }
|
||||
.buddy-main__note {
|
||||
margin-top: 14px;
|
||||
padding: 10px 12px;
|
||||
background: #f0f6fc;
|
||||
border-left: 3px solid #2271b1;
|
||||
color: #135e96;
|
||||
font-size: 13px;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy — a friendly pet for your WordPress dashboard
|
||||
*
|
||||
* Plugin Name: Buddy
|
||||
* Plugin URI: https://icanhelp.ie/buddy
|
||||
* Description: Adopt a small companion that lives in your WordPress dashboard. Its mood reflects your site's health — published posts feed it, outdated plugins make it sick, clearing spam makes it happy. Gamifies WordPress maintenance with a bit of charm.
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 5.0
|
||||
* Requires PHP: 7.4
|
||||
* Author: David Keane
|
||||
* Author URI: https://rangersmyth.xyz/
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: buddy
|
||||
*
|
||||
* @package Buddy
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
// Plugin coordinates.
|
||||
if ( ! defined( 'BUDDY_VERSION' ) ) { define( 'BUDDY_VERSION', '0.1.0' ); }
|
||||
if ( ! defined( 'BUDDY_FILE' ) ) { define( 'BUDDY_FILE', __FILE__ ); }
|
||||
if ( ! defined( 'BUDDY_PATH' ) ) { define( 'BUDDY_PATH', plugin_dir_path( __FILE__ ) ); }
|
||||
if ( ! defined( 'BUDDY_URL' ) ) { define( 'BUDDY_URL', plugin_dir_url( __FILE__ ) ); }
|
||||
if ( ! defined( 'BUDDY_BASENAME' ) ) { define( 'BUDDY_BASENAME', plugin_basename( __FILE__ ) ); }
|
||||
|
||||
// Includes — each file owns one concern.
|
||||
require_once BUDDY_PATH . 'inc/state.php'; // user_meta storage + stat helpers
|
||||
require_once BUDDY_PATH . 'inc/sprite.php'; // inline-SVG character renderer
|
||||
require_once BUDDY_PATH . 'inc/dashboard-widget.php'; // the pet on WP Dashboard
|
||||
require_once BUDDY_PATH . 'inc/admin-page.php'; // dedicated Buddy admin page
|
||||
require_once BUDDY_PATH . 'inc/about.php'; // About page
|
||||
require_once BUDDY_PATH . 'inc/settings.php'; // Settings page
|
||||
require_once BUDDY_PATH . 'inc/updater.php'; // self-hosted update checker against Gitea
|
||||
|
||||
/**
|
||||
* Admin menu registration. Buddy gets its own top-level menu — the pet
|
||||
* is a discrete enough thing to deserve a sidebar entry rather than
|
||||
* being buried under Tools or Settings. Icon is dashicons-pets for the
|
||||
* literal-match-to-purpose vibe (paw print).
|
||||
*/
|
||||
add_action( 'admin_menu', 'buddy_register_admin_menu' );
|
||||
function buddy_register_admin_menu() {
|
||||
add_menu_page(
|
||||
__( 'Buddy', 'buddy' ),
|
||||
__( 'Buddy', 'buddy' ),
|
||||
'read', // any logged-in user with read access can see their own pet
|
||||
'buddy',
|
||||
'buddy_render_main_page',
|
||||
'dashicons-pets',
|
||||
72 // sits below Comments, above plugin entries
|
||||
);
|
||||
|
||||
// "My Buddy" — main landing submenu. Same slug as parent so clicking
|
||||
// either entry lands on the same page. Empty callback so only the
|
||||
// parent's renderer fires (lesson learned from Logbook's duplicate-
|
||||
// form bug).
|
||||
add_submenu_page(
|
||||
'buddy',
|
||||
__( 'My Buddy', 'buddy' ),
|
||||
__( 'My Buddy', 'buddy' ),
|
||||
'read',
|
||||
'buddy',
|
||||
''
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'buddy',
|
||||
__( 'Settings', 'buddy' ),
|
||||
__( 'Settings', 'buddy' ),
|
||||
'manage_options',
|
||||
'buddy-settings',
|
||||
'buddy_render_settings_page'
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'buddy',
|
||||
__( 'About', 'buddy' ),
|
||||
__( 'About', 'buddy' ),
|
||||
'read',
|
||||
'buddy-about',
|
||||
'buddy_render_about_page'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue Buddy's CSS + JS on its own admin pages and on the main
|
||||
* dashboard (where the widget lives).
|
||||
*/
|
||||
add_action( 'admin_enqueue_scripts', 'buddy_enqueue_admin_assets' );
|
||||
function buddy_enqueue_admin_assets( $hook ) {
|
||||
$buddy_hooks = array(
|
||||
'index.php', // WP Dashboard (the widget lives here)
|
||||
'toplevel_page_buddy', // Buddy main page
|
||||
'buddy_page_buddy-settings', // Settings
|
||||
'buddy_page_buddy-about', // About
|
||||
);
|
||||
if ( ! in_array( $hook, $buddy_hooks, true ) ) { return; }
|
||||
|
||||
wp_enqueue_style(
|
||||
'buddy-admin',
|
||||
BUDDY_URL . 'assets/css/buddy.css',
|
||||
array(),
|
||||
BUDDY_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activation hook: ensure the version option is set so the updater can
|
||||
* track it.
|
||||
*/
|
||||
register_activation_hook( __FILE__, 'buddy_on_activate' );
|
||||
function buddy_on_activate() {
|
||||
if ( false === get_option( 'buddy_version' ) ) {
|
||||
add_option( 'buddy_version', BUDDY_VERSION );
|
||||
} else {
|
||||
update_option( 'buddy_version', BUDDY_VERSION );
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy About page — explains what the plugin is, who it's for, and
|
||||
* keeps a compact version history with a link out to the canonical
|
||||
* CHANGELOG.md on Gitea.
|
||||
*
|
||||
* Layout: a clean side-by-side intro row at the top (sprite + intro
|
||||
* + CTA), then plain prose cards. Discipline carried from Logbook —
|
||||
* no nested toggle boxes, no duplicate sections, single H1.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
function buddy_render_about_page() {
|
||||
if ( ! current_user_can( 'read' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to view this page.', 'buddy' ) );
|
||||
}
|
||||
?>
|
||||
<style>
|
||||
.buddy-about-intro {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 16px 0 28px;
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.buddy-about-intro__sprite {
|
||||
flex: 0 0 160px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.buddy-about-intro__body {
|
||||
flex: 1 1 320px;
|
||||
min-width: 0;
|
||||
}
|
||||
.buddy-about-intro__body h2 { margin-top: 0; }
|
||||
|
||||
.buddy-about-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ccd0d4;
|
||||
border-radius: 4px;
|
||||
padding: 20px 24px;
|
||||
margin: 0 0 22px;
|
||||
max-width: 720px;
|
||||
}
|
||||
.buddy-about-card h2 { margin: 0 0 10px; font-size: 16px; }
|
||||
.buddy-about-card p:last-child { margin-bottom: 0; }
|
||||
.buddy-about-card ul { margin: 8px 0 0 18px; list-style: disc; }
|
||||
.buddy-about-card ul li { margin-bottom: 4px; }
|
||||
.buddy-about-card--versions ul { list-style: none; margin-left: 0; }
|
||||
.buddy-about-card--versions li { margin-bottom: 12px; }
|
||||
.buddy-about-card--versions .ver { font-weight: 600; color: #2271b1; }
|
||||
.buddy-about-card--versions .latest { display:inline-block; margin-left:6px; padding:1px 7px; background:#00a32a; color:#fff; border-radius:9px; font-size:11px; font-weight:600; }
|
||||
.buddy-about-changelog-link { display: inline-block; margin-top: 6px; font-size: 13px; color: #646970; }
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
<h1 class="wp-heading-inline"><?php esc_html_e( 'About', 'buddy' ); ?></h1>
|
||||
<span class="page-title-action">v<?php echo esc_html( BUDDY_VERSION ); ?></span>
|
||||
<hr class="wp-header-end">
|
||||
</div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="buddy-about-intro">
|
||||
<div class="buddy-about-intro__sprite">
|
||||
<?php buddy_render_sprite( 'default', 'happy', 'lg' ); ?>
|
||||
</div>
|
||||
<div class="buddy-about-intro__body">
|
||||
<h2>Buddy <span style="color:#646970; font-weight:400;">v<?php echo esc_html( BUDDY_VERSION ); ?></span></h2>
|
||||
<p>A friendly little companion that lives in your WordPress dashboard. The idea: keep Buddy's stats up, eventually Buddy's mood will reflect how well you're taking care of your site itself. Gamifies WordPress maintenance with a bit of charm.</p>
|
||||
<p style="margin-bottom:0;">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=buddy' ) ); ?>" class="button button-primary"><?php esc_html_e( 'Go to My Buddy →', 'buddy' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buddy-about-card">
|
||||
<h2><?php esc_html_e( 'What Buddy does', 'buddy' ); ?></h2>
|
||||
<p><?php esc_html_e( 'Buddy is a small virtual pet stored per-user in your WordPress site. Each WP admin gets their own Buddy with its own name, species, and stats. Everything lives in your site\'s own database — nothing leaves the server.', 'buddy' ); ?></p>
|
||||
<p><?php esc_html_e( 'Right now (v0.1.0): Buddy exists. You can see them on the WordPress Dashboard widget and on the dedicated Buddy admin page. Coming next: feed / play / clean / sleep interactions, time-based stat decay, multiple species, and the killer feature — stats that react to your actual WordPress site health.', 'buddy' ); ?></p>
|
||||
</div>
|
||||
|
||||
<div class="buddy-about-card">
|
||||
<h2><?php esc_html_e( 'Who Buddy is for', 'buddy' ); ?></h2>
|
||||
<ul>
|
||||
<li><strong><?php esc_html_e( 'WordPress admins', 'buddy' ); ?></strong> <?php esc_html_e( 'who want a small daily reason to keep their site tidy.', 'buddy' ); ?></li>
|
||||
<li><strong><?php esc_html_e( 'Freelancers', 'buddy' ); ?></strong> <?php esc_html_e( 'managing multiple client sites and wanting a visual gauge of each one\'s health.', 'buddy' ); ?></li>
|
||||
<li><strong><?php esc_html_e( '90s kids', 'buddy' ); ?></strong> <?php esc_html_e( 'who remember when computers had pets in them.', 'buddy' ); ?></li>
|
||||
<li><strong><?php esc_html_e( 'Anyone', 'buddy' ); ?></strong> <?php esc_html_e( 'who wants their WordPress admin to feel slightly less corporate.', 'buddy' ); ?></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="buddy-about-card buddy-about-card--versions">
|
||||
<h2><?php esc_html_e( 'Version history', 'buddy' ); ?></h2>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="ver">v0.1.0</span> — 25 May 2026 <span class="latest">latest</span><br>
|
||||
<?php esc_html_e( 'First release. Phase A complete: Buddy exists. Dashboard widget + dedicated admin page show the SVG character, name, mood label, four stats bars. Self-hosted update checker wired up to Gitea from commit 1. No interactions yet — that\'s next.', 'buddy' ); ?>
|
||||
</li>
|
||||
</ul>
|
||||
<a class="buddy-about-changelog-link"
|
||||
href="https://git.davidtkeane.com/ranger/a-buddy/src/branch/main/CHANGELOG.md"
|
||||
target="_blank" rel="noopener">
|
||||
<?php esc_html_e( 'View the full CHANGELOG.md →', 'buddy' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy main admin page — renders the bigger view at WP Admin → Buddy
|
||||
* → My Buddy. Same data as the dashboard widget but with more room
|
||||
* for Phase B (interactions), Phase D (species picker), etc. to slot
|
||||
* in cleanly later.
|
||||
*
|
||||
* Discipline anchored from Logbook v3 lessons: one H1 per page, no
|
||||
* nested toggles, no duplicate sections. Each card owns one concern.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
/**
|
||||
* Top-level "Buddy → My Buddy" page renderer. Hooked via
|
||||
* add_menu_page() in buddy.php.
|
||||
*/
|
||||
function buddy_render_main_page() {
|
||||
if ( ! current_user_can( 'read' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to view Buddy.', 'buddy' ) );
|
||||
}
|
||||
|
||||
$state = buddy_get_state();
|
||||
$mood = buddy_overall_mood( $state );
|
||||
$tone = buddy_mood_label( $mood );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1 class="wp-heading-inline">🐾 <?php esc_html_e( 'Buddy', 'buddy' ); ?></h1>
|
||||
<span class="page-title-action">v<?php echo esc_html( BUDDY_VERSION ); ?></span>
|
||||
<hr class="wp-header-end">
|
||||
|
||||
<p class="description" style="max-width: 720px;">
|
||||
<?php esc_html_e( 'Your dashboard pet. Keep its stats up — eventually they will reflect how well you take care of your WordPress site.', 'buddy' ); ?>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=buddy-about' ) ); ?>"><?php esc_html_e( 'Read more on the About page →', 'buddy' ); ?></a>
|
||||
</p>
|
||||
|
||||
<div class="buddy-main">
|
||||
<div class="buddy-main__card buddy-main__card--pet">
|
||||
<?php buddy_render_sprite( $state['species'], $tone['tone'], 'lg' ); ?>
|
||||
<h2 class="buddy-main__name">
|
||||
<?php echo esc_html( $state['name'] ); ?>
|
||||
<span class="buddy-main__mood buddy-main__mood--<?php echo esc_attr( $tone['tone'] ); ?>">
|
||||
<?php echo esc_html( $tone['label'] ); ?>
|
||||
</span>
|
||||
</h2>
|
||||
<p class="buddy-main__age">
|
||||
<?php
|
||||
$age_seconds = max( 0, time() - (int) $state['born_at'] );
|
||||
$age_days = (int) floor( $age_seconds / DAY_IN_SECONDS );
|
||||
echo esc_html(
|
||||
$age_days < 1
|
||||
? __( 'Adopted today', 'buddy' )
|
||||
: sprintf( _n( '%d day old', '%d days old', $age_days, 'buddy' ), $age_days )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="buddy-main__card buddy-main__card--stats">
|
||||
<h2><?php esc_html_e( 'How is Buddy doing?', 'buddy' ); ?></h2>
|
||||
<?php buddy_render_stats_bars( $state ); ?>
|
||||
<p class="buddy-main__note">
|
||||
<?php esc_html_e( 'Coming soon: feed, play, clean, and sleep actions to keep Buddy thriving. Also: stats will react to your WordPress site\'s health.', 'buddy' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy dashboard widget — the pet appears on the main WordPress
|
||||
* Dashboard (`/wp-admin/index.php`) so the user sees it without
|
||||
* navigating anywhere. This is the "Buddy exists" baseline of Phase A.
|
||||
*
|
||||
* The widget shows: the SVG character + the pet's name + current
|
||||
* mood label + four stats bars. No interaction yet (Phase B will add
|
||||
* Feed / Play / Clean / Sleep). The render also includes a "Visit
|
||||
* Buddy" link that jumps to the dedicated admin page.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
add_action( 'wp_dashboard_setup', 'buddy_register_dashboard_widget' );
|
||||
function buddy_register_dashboard_widget() {
|
||||
if ( ! current_user_can( 'read' ) ) { return; }
|
||||
|
||||
wp_add_dashboard_widget(
|
||||
'buddy_dashboard_widget',
|
||||
'🐾 ' . __( 'Buddy', 'buddy' ),
|
||||
'buddy_render_dashboard_widget'
|
||||
);
|
||||
}
|
||||
|
||||
function buddy_render_dashboard_widget() {
|
||||
$state = buddy_get_state();
|
||||
$mood = buddy_overall_mood( $state );
|
||||
$tone = buddy_mood_label( $mood );
|
||||
?>
|
||||
<div class="buddy-widget">
|
||||
<div class="buddy-widget__pet">
|
||||
<?php buddy_render_sprite( $state['species'], $tone['tone'], 'md' ); ?>
|
||||
</div>
|
||||
<div class="buddy-widget__info">
|
||||
<div class="buddy-widget__name">
|
||||
<?php echo esc_html( $state['name'] ); ?>
|
||||
<span class="buddy-widget__mood buddy-widget__mood--<?php echo esc_attr( $tone['tone'] ); ?>">
|
||||
<?php echo esc_html( $tone['label'] ); ?>
|
||||
</span>
|
||||
</div>
|
||||
<?php buddy_render_stats_bars( $state ); ?>
|
||||
<p class="buddy-widget__cta">
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=buddy' ) ); ?>" class="button button-small">
|
||||
<?php esc_html_e( 'Visit Buddy →', 'buddy' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the four stats as labelled progress bars. Reused by the
|
||||
* dedicated admin page so they look identical wherever Buddy appears.
|
||||
*/
|
||||
function buddy_render_stats_bars( array $state ) {
|
||||
$stats = array(
|
||||
'hunger' => array( 'label' => __( 'Hunger', 'buddy' ), 'icon' => '🍎' ),
|
||||
'happiness' => array( 'label' => __( 'Happiness', 'buddy' ), 'icon' => '😊' ),
|
||||
'health' => array( 'label' => __( 'Health', 'buddy' ), 'icon' => '💚' ),
|
||||
'energy' => array( 'label' => __( 'Energy', 'buddy' ), 'icon' => '⚡' ),
|
||||
);
|
||||
echo '<ul class="buddy-stats">';
|
||||
foreach ( $stats as $key => $meta ) {
|
||||
$val = max( 0, min( 100, (int) ( $state[ $key ] ?? 0 ) ) );
|
||||
$low = $val < 30;
|
||||
printf(
|
||||
'<li class="buddy-stat"><span class="buddy-stat__icon" aria-hidden="true">%s</span>'
|
||||
. '<span class="buddy-stat__label">%s</span>'
|
||||
. '<span class="buddy-stat__bar"><span class="buddy-stat__fill%s" style="width:%d%%"></span></span>'
|
||||
. '<span class="buddy-stat__num">%d</span></li>',
|
||||
esc_html( $meta['icon'] ),
|
||||
esc_html( $meta['label'] ),
|
||||
$low ? ' buddy-stat__fill--low' : '',
|
||||
$val,
|
||||
$val
|
||||
);
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy settings page — placeholder shell for Phase A. Settings will
|
||||
* grow in later phases:
|
||||
* Phase B: cooldown timers
|
||||
* Phase C: decay tick interval
|
||||
* Phase D: species picker (currently only one available)
|
||||
* Phase E: site-health integration toggle
|
||||
*
|
||||
* For v0.1.0 this page exists to (a) host the Updates panel from the
|
||||
* Logbook-style update checker, and (b) let the user rename their
|
||||
* Buddy.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
/**
|
||||
* Handle the rename form POST. Capability-gated to anyone who can
|
||||
* read (every WP user) since each user owns their own Buddy.
|
||||
*/
|
||||
add_action( 'admin_init', 'buddy_handle_rename_post' );
|
||||
function buddy_handle_rename_post() {
|
||||
if ( empty( $_POST['buddy_rename_submit'] ) ) { return; }
|
||||
if ( ! current_user_can( 'read' ) ) { return; }
|
||||
if ( ! isset( $_POST['buddy_rename_nonce'] )
|
||||
|| ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['buddy_rename_nonce'] ) ), 'buddy_rename' ) ) {
|
||||
return;
|
||||
}
|
||||
$new_name = isset( $_POST['buddy_name'] ) ? sanitize_text_field( wp_unslash( $_POST['buddy_name'] ) ) : '';
|
||||
if ( $new_name === '' || mb_strlen( $new_name ) > 32 ) { return; }
|
||||
buddy_update_state( array( 'name' => $new_name ) );
|
||||
|
||||
add_action( 'admin_notices', function () {
|
||||
printf(
|
||||
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
|
||||
esc_html__( 'Buddy renamed.', 'buddy' )
|
||||
);
|
||||
} );
|
||||
}
|
||||
|
||||
function buddy_render_settings_page() {
|
||||
if ( ! current_user_can( 'read' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to view this page.', 'buddy' ) );
|
||||
}
|
||||
$state = buddy_get_state();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'Buddy Settings', 'buddy' ); ?></h1>
|
||||
|
||||
<form method="post" style="max-width: 720px; background:#fff; padding:18px 22px; border:1px solid #ccd0d4; border-radius:4px; margin-top: 16px;">
|
||||
<?php wp_nonce_field( 'buddy_rename', 'buddy_rename_nonce' ); ?>
|
||||
<h2 style="margin-top:0;"><?php esc_html_e( 'Name', 'buddy' ); ?></h2>
|
||||
<p>
|
||||
<label for="buddy_name"><?php esc_html_e( 'What is your Buddy called?', 'buddy' ); ?></label><br>
|
||||
<input type="text"
|
||||
name="buddy_name"
|
||||
id="buddy_name"
|
||||
value="<?php echo esc_attr( $state['name'] ); ?>"
|
||||
maxlength="32"
|
||||
class="regular-text">
|
||||
</p>
|
||||
<p>
|
||||
<button type="submit" name="buddy_rename_submit" class="button button-primary">
|
||||
<?php esc_html_e( 'Save name', 'buddy' ); ?>
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
// Updates panel — defined in inc/updater.php.
|
||||
if ( function_exists( 'buddy_render_updates_panel' ) ) {
|
||||
buddy_render_updates_panel();
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy sprite rendering — pure inline SVG, no image files, no GIFs,
|
||||
* no sprite sheets. CSS animations live alongside in buddy.css.
|
||||
*
|
||||
* Phase A: one species ("default") — a small round yellow chibi
|
||||
* character with eyes and a smile. Phase D will branch this into
|
||||
* dog / dragon / sprite / etc.
|
||||
*
|
||||
* Mood tone changes the expression: 'happy' = open smile, 'neutral'
|
||||
* = flat mouth, 'sad' = downturned. Eyes blink via CSS keyframes
|
||||
* regardless of tone (it's always alive).
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
/**
|
||||
* Echo the inline SVG for a buddy character.
|
||||
*
|
||||
* @param string $species Currently only 'default'.
|
||||
* @param string $tone 'happy' | 'neutral' | 'sad'
|
||||
* @param string $size 'sm' (64px), 'md' (96px), 'lg' (160px). Just sets a class.
|
||||
*/
|
||||
function buddy_render_sprite( $species = 'default', $tone = 'happy', $size = 'md' ) {
|
||||
$species = sanitize_key( $species );
|
||||
$tone = in_array( $tone, array( 'happy', 'neutral', 'sad' ), true ) ? $tone : 'happy';
|
||||
$size = in_array( $size, array( 'sm', 'md', 'lg' ), true ) ? $size : 'md';
|
||||
|
||||
// Mouth path varies by tone.
|
||||
$mouth = array(
|
||||
'happy' => 'M 42 60 Q 50 70 58 60', // smile
|
||||
'neutral' => 'M 42 64 L 58 64', // flat
|
||||
'sad' => 'M 42 66 Q 50 58 58 66', // frown
|
||||
);
|
||||
$mouth_d = $mouth[ $tone ];
|
||||
|
||||
// Body fill: subtle shift by tone.
|
||||
$body_fill = ( $tone === 'sad' ) ? '#d8b04a' : '#f4c64e';
|
||||
?>
|
||||
<svg class="buddy-sprite buddy-sprite--<?php echo esc_attr( $size ); ?> buddy-sprite--<?php echo esc_attr( $tone ); ?>"
|
||||
viewBox="0 0 100 100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-label="<?php echo esc_attr( sprintf( __( 'Buddy is %s', 'buddy' ), $tone ) ); ?>">
|
||||
<!-- body -->
|
||||
<circle cx="50" cy="55" r="32" fill="<?php echo esc_attr( $body_fill ); ?>" stroke="#c9941d" stroke-width="2" />
|
||||
<!-- left eye -->
|
||||
<g class="buddy-sprite__eye buddy-sprite__eye--left">
|
||||
<circle cx="40" cy="46" r="5" fill="#2c3338" />
|
||||
<circle cx="41.2" cy="45" r="1.5" fill="#fff" />
|
||||
</g>
|
||||
<!-- right eye -->
|
||||
<g class="buddy-sprite__eye buddy-sprite__eye--right">
|
||||
<circle cx="60" cy="46" r="5" fill="#2c3338" />
|
||||
<circle cx="61.2" cy="45" r="1.5" fill="#fff" />
|
||||
</g>
|
||||
<!-- mouth -->
|
||||
<path d="<?php echo esc_attr( $mouth_d ); ?>"
|
||||
stroke="#2c3338" stroke-width="2" fill="none" stroke-linecap="round" />
|
||||
<!-- cheeks (only when happy or neutral) -->
|
||||
<?php if ( $tone !== 'sad' ) : ?>
|
||||
<circle cx="33" cy="58" r="3" fill="#f4866a" opacity="0.55" />
|
||||
<circle cx="67" cy="58" r="3" fill="#f4866a" opacity="0.55" />
|
||||
<?php endif; ?>
|
||||
<!-- tiny feet -->
|
||||
<ellipse cx="42" cy="88" rx="6" ry="3" fill="#c9941d" />
|
||||
<ellipse cx="58" cy="88" rx="6" ry="3" fill="#c9941d" />
|
||||
</svg>
|
||||
<?php
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy state — persistence for the pet's stats and identity.
|
||||
*
|
||||
* Storage: per-user, in `wp_usermeta` under key `buddy_state`. A single
|
||||
* JSON-shaped associative array. Picked user_meta over site-wide
|
||||
* options so multiple admins on the same site each get their own pet,
|
||||
* matching the per-user mental model.
|
||||
*
|
||||
* Stats range 0–100. Higher = better. Decay happens via WP-cron later
|
||||
* (Phase C); for Phase A the stats just persist as last set.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
const BUDDY_META_KEY = 'buddy_state';
|
||||
|
||||
/**
|
||||
* Default state for a freshly-adopted pet. Used the first time a user
|
||||
* visits a Buddy-rendering page.
|
||||
*/
|
||||
function buddy_default_state() {
|
||||
return array(
|
||||
'name' => __( 'Buddy', 'buddy' ),
|
||||
'species' => 'default',
|
||||
'hunger' => 80,
|
||||
'happiness' => 80,
|
||||
'health' => 90,
|
||||
'energy' => 70,
|
||||
'born_at' => time(),
|
||||
'last_tick' => time(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current user's Buddy state, falling back to defaults and
|
||||
* persisting them on first read so the pet has a "birthday" timestamp.
|
||||
*
|
||||
* @param int $user_id Optional. Defaults to current user.
|
||||
* @return array { name, species, hunger, happiness, health, energy, born_at, last_tick }
|
||||
*/
|
||||
function buddy_get_state( $user_id = 0 ) {
|
||||
$user_id = $user_id ? (int) $user_id : get_current_user_id();
|
||||
if ( ! $user_id ) { return buddy_default_state(); }
|
||||
|
||||
$state = get_user_meta( $user_id, BUDDY_META_KEY, true );
|
||||
if ( ! is_array( $state ) || empty( $state ) ) {
|
||||
$state = buddy_default_state();
|
||||
update_user_meta( $user_id, BUDDY_META_KEY, $state );
|
||||
}
|
||||
|
||||
// Merge against defaults so newly-added keys appear with sensible
|
||||
// values for users adopted under earlier versions.
|
||||
return array_merge( buddy_default_state(), $state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a partial state update for the current user. Unknown keys
|
||||
* are silently dropped; values are clamped to valid ranges.
|
||||
*
|
||||
* @param array $patch Keys: name (str), species (str), hunger/happiness/health/energy (int 0–100).
|
||||
* @return array The full updated state.
|
||||
*/
|
||||
function buddy_update_state( array $patch, $user_id = 0 ) {
|
||||
$user_id = $user_id ? (int) $user_id : get_current_user_id();
|
||||
if ( ! $user_id ) { return buddy_default_state(); }
|
||||
|
||||
$state = buddy_get_state( $user_id );
|
||||
|
||||
$stat_keys = array( 'hunger', 'happiness', 'health', 'energy' );
|
||||
foreach ( $stat_keys as $k ) {
|
||||
if ( array_key_exists( $k, $patch ) ) {
|
||||
$state[ $k ] = max( 0, min( 100, (int) $patch[ $k ] ) );
|
||||
}
|
||||
}
|
||||
if ( array_key_exists( 'name', $patch ) ) {
|
||||
$name = sanitize_text_field( (string) $patch['name'] );
|
||||
if ( $name !== '' && mb_strlen( $name ) <= 32 ) {
|
||||
$state['name'] = $name;
|
||||
}
|
||||
}
|
||||
if ( array_key_exists( 'species', $patch ) ) {
|
||||
$allowed_species = array( 'default' ); // Phase D will widen this.
|
||||
$sp = sanitize_key( (string) $patch['species'] );
|
||||
if ( in_array( $sp, $allowed_species, true ) ) {
|
||||
$state['species'] = $sp;
|
||||
}
|
||||
}
|
||||
|
||||
update_user_meta( $user_id, BUDDY_META_KEY, $state );
|
||||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Average of the four stats — used as a single "mood" indicator for
|
||||
* the dashboard widget. Returns 0–100.
|
||||
*/
|
||||
function buddy_overall_mood( array $state ) {
|
||||
$sum = (int) $state['hunger'] + (int) $state['happiness'] + (int) $state['health'] + (int) $state['energy'];
|
||||
return (int) round( $sum / 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a one-word emoji / status label based on overall mood. Pure
|
||||
* cosmetic, used in the dashboard widget header.
|
||||
*/
|
||||
function buddy_mood_label( $mood_score ) {
|
||||
if ( $mood_score >= 80 ) { return array( 'label' => __( 'Thriving', 'buddy' ), 'tone' => 'happy' ); }
|
||||
if ( $mood_score >= 60 ) { return array( 'label' => __( 'Content', 'buddy' ), 'tone' => 'happy' ); }
|
||||
if ( $mood_score >= 40 ) { return array( 'label' => __( 'Okay', 'buddy' ), 'tone' => 'neutral' ); }
|
||||
if ( $mood_score >= 20 ) { return array( 'label' => __( 'Hungry', 'buddy' ), 'tone' => 'sad' ); }
|
||||
return array( 'label' => __( 'Distressed', 'buddy' ), 'tone' => 'sad' );
|
||||
}
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/**
|
||||
* Buddy — self-hosted update checker against the Gitea repo.
|
||||
*
|
||||
* Direct port of the Logbook v3.3.5 updater (proven in production).
|
||||
* Polls Gitea's /releases/latest, falls back to /tags?limit=1 when no
|
||||
* formal Release object exists, compares against BUDDY_VERSION,
|
||||
* renders an Updates panel on the Settings page. Cached 12h on
|
||||
* success, 1h on negative responses.
|
||||
*
|
||||
* Repo coordinates are constants you can override via define() in
|
||||
* wp-config.php if the repo ever moves.
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) { exit; }
|
||||
|
||||
if ( ! defined( 'BUDDY_GITEA_HOST' ) ) { define( 'BUDDY_GITEA_HOST', 'https://git.davidtkeane.com' ); }
|
||||
if ( ! defined( 'BUDDY_GITEA_OWNER' ) ) { define( 'BUDDY_GITEA_OWNER', 'ranger' ); }
|
||||
if ( ! defined( 'BUDDY_GITEA_REPO' ) ) { define( 'BUDDY_GITEA_REPO', 'a-buddy' ); }
|
||||
|
||||
function buddy_gitea_repo_url() {
|
||||
return BUDDY_GITEA_HOST . '/' . BUDDY_GITEA_OWNER . '/' . BUDDY_GITEA_REPO;
|
||||
}
|
||||
function buddy_gitea_releases_url() {
|
||||
return buddy_gitea_repo_url() . '/releases';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest release/tag, normalised. Returns null on hard
|
||||
* error, or an array including `version`. See Logbook updater for the
|
||||
* full shape.
|
||||
*/
|
||||
function buddy_fetch_latest_release( $force_refresh = false ) {
|
||||
$cache_key = 'buddy_gitea_latest';
|
||||
|
||||
if ( ! $force_refresh ) {
|
||||
$cached = get_site_transient( $cache_key );
|
||||
if ( is_array( $cached ) ) { return $cached; }
|
||||
}
|
||||
|
||||
$base_api = BUDDY_GITEA_HOST . '/api/v1/repos/' . BUDDY_GITEA_OWNER . '/' . BUDDY_GITEA_REPO;
|
||||
|
||||
// Try formal Release first.
|
||||
$response = wp_remote_get( $base_api . '/releases/latest', array( 'timeout' => 8 ) );
|
||||
if ( is_wp_error( $response ) ) { return null; }
|
||||
|
||||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||
$body = ( $code === 200 ) ? json_decode( wp_remote_retrieve_body( $response ), true ) : null;
|
||||
|
||||
// Fallback to /tags if no Release object exists yet.
|
||||
if ( $code !== 200 || ! is_array( $body ) || empty( $body['tag_name'] ) ) {
|
||||
$tags_response = wp_remote_get( $base_api . '/tags?limit=1', array( 'timeout' => 8 ) );
|
||||
if ( ! is_wp_error( $tags_response )
|
||||
&& (int) wp_remote_retrieve_response_code( $tags_response ) === 200 ) {
|
||||
$tags = json_decode( wp_remote_retrieve_body( $tags_response ), true );
|
||||
if ( is_array( $tags ) && ! empty( $tags[0]['name'] ) ) {
|
||||
$body = array(
|
||||
'tag_name' => $tags[0]['name'],
|
||||
'html_url' => buddy_gitea_repo_url() . '/src/tag/' . rawurlencode( $tags[0]['name'] ),
|
||||
'body' => isset( $tags[0]['message'] ) ? $tags[0]['message'] : '',
|
||||
'published_at' => isset( $tags[0]['commit']['created'] ) ? $tags[0]['commit']['created'] : null,
|
||||
'assets' => array(),
|
||||
);
|
||||
$code = 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $code !== 200 || ! is_array( $body ) || empty( $body['tag_name'] ) ) {
|
||||
$info = array(
|
||||
'version' => null,
|
||||
'html_url' => buddy_gitea_releases_url(),
|
||||
'download_url' => null,
|
||||
'body' => '',
|
||||
'published_at' => null,
|
||||
'error_code' => $code,
|
||||
);
|
||||
set_site_transient( $cache_key, $info, HOUR_IN_SECONDS );
|
||||
return $info;
|
||||
}
|
||||
|
||||
$version = ltrim( (string) $body['tag_name'], 'vV' );
|
||||
|
||||
// Prefer a .zip asset; fall back to Gitea source-archive URL.
|
||||
$download_url = null;
|
||||
if ( ! empty( $body['assets'] ) && is_array( $body['assets'] ) ) {
|
||||
foreach ( $body['assets'] as $asset ) {
|
||||
if ( isset( $asset['name'], $asset['browser_download_url'] )
|
||||
&& substr( strtolower( $asset['name'] ), -4 ) === '.zip' ) {
|
||||
$download_url = $asset['browser_download_url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( ! $download_url ) {
|
||||
$download_url = buddy_gitea_repo_url() . '/archive/' . rawurlencode( $body['tag_name'] ) . '.zip';
|
||||
}
|
||||
|
||||
$info = array(
|
||||
'version' => $version,
|
||||
'html_url' => isset( $body['html_url'] ) ? esc_url_raw( $body['html_url'] ) : '',
|
||||
'download_url' => esc_url_raw( $download_url ),
|
||||
'body' => isset( $body['body'] ) ? wp_strip_all_tags( $body['body'] ) : '',
|
||||
'published_at' => isset( $body['published_at'] ) ? $body['published_at'] : null,
|
||||
);
|
||||
|
||||
set_site_transient( $cache_key, $info, 12 * HOUR_IN_SECONDS );
|
||||
return $info;
|
||||
}
|
||||
|
||||
function buddy_update_status( $force_refresh = false ) {
|
||||
$current = defined( 'BUDDY_VERSION' ) ? BUDDY_VERSION : '0.0.0';
|
||||
$latest = buddy_fetch_latest_release( $force_refresh );
|
||||
|
||||
if ( ! $latest || empty( $latest['version'] ) ) {
|
||||
$msg = __( 'No releases tagged on the Gitea repo yet.', 'buddy' );
|
||||
if ( $latest && ! empty( $latest['error_code'] ) && (int) $latest['error_code'] !== 404 ) {
|
||||
$msg = sprintf( __( 'Could not reach Gitea (HTTP %d). Try again in a few minutes.', 'buddy' ), (int) $latest['error_code'] );
|
||||
}
|
||||
return array(
|
||||
'status' => 'unknown',
|
||||
'current' => $current,
|
||||
'message' => $msg,
|
||||
'repo_url' => buddy_gitea_repo_url(),
|
||||
);
|
||||
}
|
||||
|
||||
if ( version_compare( $latest['version'], $current, '>' ) ) {
|
||||
return array(
|
||||
'status' => 'available',
|
||||
'current' => $current,
|
||||
'latest' => $latest['version'],
|
||||
'html_url' => $latest['html_url'],
|
||||
'download_url' => $latest['download_url'],
|
||||
'published_at' => $latest['published_at'],
|
||||
'body' => $latest['body'],
|
||||
'message' => sprintf( __( 'A new version (v%1$s) is available — you are on v%2$s.', 'buddy' ), $latest['version'], $current ),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'status' => 'up-to-date',
|
||||
'current' => $current,
|
||||
'latest' => $latest['version'],
|
||||
'message' => sprintf( __( 'You are up to date (v%s).', 'buddy' ), $current ),
|
||||
'repo_url' => buddy_gitea_repo_url(),
|
||||
);
|
||||
}
|
||||
|
||||
add_action( 'wp_ajax_buddy_check_updates', 'buddy_ajax_check_updates' );
|
||||
function buddy_ajax_check_updates() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Insufficient permissions.', 403 );
|
||||
}
|
||||
check_ajax_referer( 'buddy_check_updates', 'nonce' );
|
||||
delete_site_transient( 'buddy_gitea_latest' );
|
||||
wp_send_json_success( buddy_update_status( true ) );
|
||||
}
|
||||
|
||||
function buddy_render_updates_panel() {
|
||||
$status = buddy_update_status( false );
|
||||
$nonce = wp_create_nonce( 'buddy_check_updates' );
|
||||
$repo_url = buddy_gitea_repo_url();
|
||||
$rel_url = buddy_gitea_releases_url();
|
||||
?>
|
||||
<div class="buddy-updates" style="max-width:720px; margin-top:24px; padding:18px 20px; background:#fff; border:1px solid #ccd0d4; border-radius:4px;">
|
||||
<h2 style="margin-top:0;"><?php esc_html_e( 'Updates', 'buddy' ); ?></h2>
|
||||
<p style="margin:0 0 12px;">
|
||||
<?php esc_html_e( 'Buddy is self-hosted on Gitea. Click Check now to ask the repo whether there is a newer release than the one you are running.', 'buddy' ); ?>
|
||||
</p>
|
||||
|
||||
<p id="buddy-update-status" style="margin:0 0 12px;">
|
||||
<strong><?php esc_html_e( 'Status:', 'buddy' ); ?></strong>
|
||||
<span id="buddy-update-status-text"><?php echo esc_html( $status['message'] ); ?></span>
|
||||
<?php if ( $status['status'] === 'available' && ! empty( $status['download_url'] ) ) : ?>
|
||||
<br>
|
||||
<a href="<?php echo esc_url( $status['download_url'] ); ?>" class="button button-primary" style="margin-top:8px;">
|
||||
<?php
|
||||
/* translators: %s is the latest version number, e.g. "0.2.0" */
|
||||
echo esc_html( sprintf( __( 'Download v%s (.zip)', 'buddy' ), $status['latest'] ) );
|
||||
?>
|
||||
</a>
|
||||
<?php if ( ! empty( $status['html_url'] ) ) : ?>
|
||||
<a href="<?php echo esc_url( $status['html_url'] ); ?>" target="_blank" rel="noopener" style="margin-left:8px;"><?php esc_html_e( 'View release notes →', 'buddy' ); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
|
||||
<p style="margin:0 0 4px;">
|
||||
<button type="button" id="buddy-check-updates-btn" class="button" data-nonce="<?php echo esc_attr( $nonce ); ?>">
|
||||
↻ <?php esc_html_e( 'Check now', 'buddy' ); ?>
|
||||
</button>
|
||||
<a href="<?php echo esc_url( $repo_url ); ?>" target="_blank" rel="noopener" class="button" style="margin-left:6px;"><?php esc_html_e( 'View on Gitea', 'buddy' ); ?></a>
|
||||
<a href="<?php echo esc_url( $rel_url ); ?>" target="_blank" rel="noopener" class="button" style="margin-left:6px;"><?php esc_html_e( 'View all releases', 'buddy' ); ?></a>
|
||||
</p>
|
||||
<p style="margin:10px 0 0; color:#646970; font-size:12px;">
|
||||
<?php esc_html_e( 'Manual update path: download the .zip, deactivate the plugin in WordPress, upload via Plugins → Add New → Upload, reactivate. Your Buddy survives the upgrade (state is stored in user_meta).', 'buddy' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var btn = document.getElementById('buddy-check-updates-btn');
|
||||
var statusText = document.getElementById('buddy-update-status-text');
|
||||
if (!btn || !statusText) { return; }
|
||||
|
||||
btn.addEventListener('click', function () {
|
||||
var nonce = btn.getAttribute('data-nonce');
|
||||
btn.disabled = true;
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = '↻ Checking…';
|
||||
statusText.textContent = 'Asking Gitea…';
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'buddy_check_updates');
|
||||
fd.append('nonce', nonce);
|
||||
|
||||
fetch(ajaxurl, { method: 'POST', credentials: 'same-origin', body: fd })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (!res || !res.success) {
|
||||
statusText.textContent = (res && res.data) ? String(res.data) : 'Check failed.';
|
||||
} else {
|
||||
statusText.textContent = res.data.message || 'Check complete.';
|
||||
if (res.data.status === 'available' && res.data.download_url) {
|
||||
setTimeout(function () { window.location.reload(); }, 400);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function () { statusText.textContent = 'Network error — try again in a moment.'; })
|
||||
.finally(function () {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
Reference in New Issue
Block a user