Sadly since there was the feeling of assurance that this would be resolved quickly, and yet it was not, I’ve implemented a workaround for now that will patch the client on the server:
Before:
After:
Execution example:
# STATUS CHECK BEFORE INSTALL:
$ mm-remotehour.sh status
Client dir: /opt/mattermost/client
Entry HTML: /opt/mattermost/client/root.html
Static dir: /opt/mattermost/client
Prefix: custom
CSS path: /opt/mattermost/client/custom-hide-remote-hour.css
Linked: no
CSS exists: no
Dir owner: mattermost:mattermost
HTML owner: mattermost:mattermost
Last backup: (none)
# INSTALL:
$ mm-remotehour.sh install
Client dir: /opt/mattermost/client
Entry HTML: /opt/mattermost/client/root.html
Static dir: /opt/mattermost/client
CSS file: /opt/mattermost/client/custom-hide-remote-hour.css
Backup: /username/.mm-backups/mattermost-remotehour/20250821T030546Z
Injected link block.
Done.
# STATUS CHECK AFTER INSTALL:
$ mm-remotehour.sh status
Client dir: /opt/mattermost/client
Entry HTML: /opt/mattermost/client/root.html
Static dir: /opt/mattermost/client
Prefix: custom
CSS path: /opt/mattermost/client/custom-hide-remote-hour.css
Linked: yes
CSS exists: yes
Dir owner: mattermost:mattermost
HTML owner: mattermost:mattermost
Last backup: /username/.mm-backups/mattermost-remotehour/latest
Script $HOME/bin/mm-remotehour.sh
:
#!/usr/bin/env sh
# Mattermost: hide the "Remote/Late time" chip via CSS injection.
# Generic, ownership-safe, idempotent, reversible, with backups.
# Usage: ./mm-remotehour.sh [install|uninstall|status|reinstall|repair]
# Requires: POSIX sh, awk, sed, grep, cp, mv, chmod, chown (if root), systemctl (optional)
set -eu
umask 022
# --- configurable (env overrides allowed) ------------------------------------
CLIENT_DIR="${CLIENT_DIR:-/opt/mattermost/client}" # adjust if needed
PREFIX="${PREFIX:-custom}" # only affects filename
CSS_NAME="${CSS_NAME:-${PREFIX}-hide-remote-hour.css}"
SERVICE_NAME="${SERVICE_NAME:-mattermost}" # systemd unit (optional)
BACKUP_BASE="${BACKUP_BASE:-$HOME/.mm-backups/mattermost-remotehour}"
MARK_BEGIN="<!-- REMOTE_HOUR_INJECT_START -->"
MARK_END="<!-- REMOTE_HOUR_INJECT_END -->"
# -----------------------------------------------------------------------------
CSS_PATH=""
ENTRY_HTML=""
STATIC_DIR=""
TARGET_OWN_GRP=""
CACHE_BUSTER=""
die() { echo "$@" >&2; exit 1; }
need_dir() { [ -d "$1" ] || die "Directory not found: $1"; }
get_owner_group() {
if owner=$(stat -c '%U' "$1" 2>/dev/null) && group=$(stat -c '%G' "$1" 2>/dev/null); then
printf '%s:%s\n' "$owner" "$group"; return 0
fi
set -- $(ls -ld "$1")
printf '%s:%s\n' "$3" "$4"
}
can_write() {
[ -w "$1" ] && return 0
[ "$(id -u)" -eq 0 ] && return 0
return 1
}
ensure_backup_dir() { mkdir -p "$BACKUP_BASE"; }
timestamp() { date -u +%Y%m%dT%H%M%SZ; }
backup_now() {
ensure_backup_dir
ts="$(timestamp)"
dest="$BACKUP_BASE/$ts"
mkdir -p "$dest"
cp -p "$ENTRY_HTML" "$dest/$(basename "$ENTRY_HTML")" 2>/dev/null || true
[ -n "$CSS_PATH" ] && [ -f "$CSS_PATH" ] && cp -p "$CSS_PATH" "$dest/$(basename "$CSS_PATH")"
ln -sfn "$dest" "$BACKUP_BASE/latest"
printf 'Backup: %s\n' "$dest"
}
detect_entry_html() {
if [ -f "$CLIENT_DIR/root.html" ]; then ENTRY_HTML="$CLIENT_DIR/root.html"; return; fi
if [ -f "$CLIENT_DIR/index.html" ]; then ENTRY_HTML="$CLIENT_DIR/index.html"; return; fi
ent=$(grep -l '</head>' "$CLIENT_DIR"/*.html 2>/dev/null | head -n1 || true)
[ -n "${ent:-}" ] && ENTRY_HTML="$ent"
}
detect_static_dir() {
ref=$(grep -o '/static/[^"]*' "$ENTRY_HTML" | head -n1 || true)
if [ -n "$ref" ]; then
rel=${ref#/static/}
if [ -f "$CLIENT_DIR/$rel" ]; then STATIC_DIR="$CLIENT_DIR"; return; fi
if [ -f "$CLIENT_DIR/static/$rel" ]; then STATIC_DIR="$CLIENT_DIR/static"; return; fi
fi
if [ -d "$CLIENT_DIR/static" ]; then STATIC_DIR="$CLIENT_DIR/static"; else STATIC_DIR="$CLIENT_DIR"; fi
}
write_css_tmp() {
tmp_css="$(mktemp)"
# Key idea: target the stable wrapper (.postBoxIndicator) and the chip (.RemoteUserHour).
# 1) Hide the whole row when the chip is present (via :has()).
# 2) Fallback: hide the chip + icon; if the row becomes empty, collapse it.
cat >"$tmp_css" <<'EOF'
/* Primary: remove the entire banner row if it contains the RemoteUserHour chip */
.postBoxIndicator:has(.RemoteUserHour) {
display: none !important;
}
/* Fallback for browsers without :has(): hide the chip itself (and its icon) */
.postBoxIndicator .RemoteUserHour,
.postBoxIndicator [class*="RemoteUserHour"],
.postBoxIndicator .moonIcon {
display: none !important;
}
/* If the row becomes empty after hiding the chip, collapse it fully */
.postBoxIndicator:empty {
display: none !important;
margin: 0 !important;
padding: 0 !important;
min-height: 0 !important;
height: 0 !important;
}
EOF
printf '%s\n' "$tmp_css"
}
safe_overwrite() {
src="$1"; dest="$2"
if [ ! -e "$dest" ]; then mv "$src" "$dest"; return 0; fi
can_write "$dest" || { echo "No write permission on $dest (try root)"; rm -f "$src"; exit 1; }
cat "$src" >"$dest"; rm -f "$src"
}
fix_owner_mode() {
file="$1"
if [ "$(id -u)" -eq 0 ]; then chown "$TARGET_OWN_GRP" "$file" 2>/dev/null || true; fi
chmod 0644 "$file" 2>/dev/null || true
}
link_present() { grep -Fq "$MARK_BEGIN" "$ENTRY_HTML"; }
# Single-line-safe insertion INSIDE <head> (before </head> if present; else after <head ...>)
insert_link_block() {
tmp="$(mktemp)"
awk -v begin="$MARK_BEGIN" -v end="$MARK_END" -v css="$CSS_NAME" -v buster="$CACHE_BUSTER" '
BEGIN { inserted=0 }
{
if (!inserted) {
low = tolower($0)
p = index(low, "</head>")
if (p > 0) {
pre = substr($0, 1, p-1)
post = substr($0, p)
print pre
print " " begin
print " <link rel=\"stylesheet\" href=\"/static/" css "?v=" buster "\"/>"
print " " end
print post
inserted = 1
next
}
if (match(low, /<head[^>]*>/)) {
head_close = RSTART + RLENGTH - 1
print substr($0, 1, head_close)
print " " begin
print " <link rel=\"stylesheet\" href=\"/static/" css "?v=" buster "\"/>"
print " " end
print substr($0, head_close+1)
inserted = 1
next
}
}
print
}
END {
if (!inserted) {
print " " begin
print " <link rel=\"stylesheet\" href=\"/static/" css "?v=" buster "\"/>"
print " " end
}
}
' "$ENTRY_HTML" >"$tmp"
safe_overwrite "$tmp" "$ENTRY_HTML"
fix_owner_mode "$ENTRY_HTML"
}
remove_link_block() {
tmp="$(mktemp)"
awk -v begin="$MARK_BEGIN" -v end="$MARK_END" '
BEGIN { skip=0 }
index($0, begin) { skip=1; next }
index($0, end) { skip=0; next }
!skip { print }
' "$ENTRY_HTML" >"$tmp"
safe_overwrite "$tmp" "$ENTRY_HTML"
fix_owner_mode "$ENTRY_HTML"
}
remove_direct_link_line_for_our_css() {
tmp="$(mktemp)"
esc="$(printf '%s\n' "/static/$CSS_NAME" | sed 's/[.[\*^$(){}?+|/]/\\&/g')"
awk -v pat="$esc" 'index($0, pat) { next } { print }' "$ENTRY_HTML" >"$tmp"
safe_overwrite "$tmp" "$ENTRY_HTML"
fix_owner_mode "$ENTRY_HTML"
}
restart_service_if_any() {
if command -v systemctl >/dev/null 2>&1; then
systemctl try-restart "$SERVICE_NAME" 2>/dev/null || true
fi
}
ensure_entry_writable_or_root() {
if ! can_write "$ENTRY_HTML"; then
echo "Cannot write $ENTRY_HTML; run as root or as its owner ($(get_owner_group "$ENTRY_HTML"))."
exit 1
fi
}
ensure_entry_owner_matches_dir_if_root() {
if [ "$(id -u)" -eq 0 ]; then
current="$(get_owner_group "$ENTRY_HTML")"
if [ "$current" != "$TARGET_OWN_GRP" ]; then
chown "$TARGET_OWN_GRP" "$ENTRY_HTML" 2>/dev/null || true
chmod a+r "$ENTRY_HTML" 2>/dev/null || true
fi
fi
}
init_env() {
need_dir "$CLIENT_DIR"
detect_entry_html
[ -n "$ENTRY_HTML" ] || die "Could not find entry HTML in $CLIENT_DIR"
detect_static_dir
CSS_PATH="$STATIC_DIR/$CSS_NAME"
TARGET_OWN_GRP="$(get_owner_group "$CLIENT_DIR")"
}
do_install() {
init_env
echo "Client dir: $CLIENT_DIR"
echo "Entry HTML: $ENTRY_HTML"
echo "Static dir: $STATIC_DIR"
echo "CSS file: $CSS_PATH"
ensure_entry_writable_or_root
backup_now
# 1) Write/refresh CSS
tmp_css="$(write_css_tmp)"
if [ -f "$CSS_PATH" ]; then
safe_overwrite "$tmp_css" "$CSS_PATH"
else
mv "$tmp_css" "$CSS_PATH"
fi
fix_owner_mode "$CSS_PATH"
# Cache buster from mtime (fallback to current UTC seconds)
CACHE_BUSTER="$(stat -c %Y "$CSS_PATH" 2>/dev/null || date -u +%s)"
# 2) Inject link block inside <head>
if link_present; then
echo "CSS link block already present; refreshed CSS only."
else
insert_link_block
echo "Injected link block."
fi
ensure_entry_owner_matches_dir_if_root
restart_service_if_any
echo "Done."
}
do_uninstall() {
init_env
echo "Client dir: $CLIENT_DIR"
echo "Entry HTML: $ENTRY_HTML"
echo "Static dir: $STATIC_DIR"
echo "CSS file: $CSS_PATH"
ensure_entry_writable_or_root
backup_now
if link_present; then
remove_link_block
echo "Removed link block."
else
echo "No markers found; removing any direct link to this CSS if present."
remove_direct_link_line_for_our_css
fi
[ -f "$CSS_PATH" ] && { rm -f "$CSS_PATH"; echo "Removed $(basename "$CSS_PATH")."; }
ensure_entry_owner_matches_dir_if_root
restart_service_if_any
echo "Reverted."
}
do_status() {
init_env
echo "Client dir: $CLIENT_DIR"
echo "Entry HTML: $ENTRY_HTML"
echo "Static dir: $STATIC_DIR"
echo "Prefix: $PREFIX"
echo "CSS path: $CSS_PATH"
printf "Linked: "; if link_present; then echo "yes"; else echo "no"; fi
printf "CSS exists: "; if [ -f "$CSS_PATH" ]; then echo "yes"; else echo "no"; fi
echo "Dir owner: $TARGET_OWN_GRP"
echo "HTML owner: $(get_owner_group "$ENTRY_HTML")"
if [ -e "$BACKUP_BASE/latest" ]; then
echo "Last backup: $BACKUP_BASE/latest"
else
echo "Last backup: (none)"
fi
}
do_repair() {
init_env
echo "Repairing ownership and perms to match $CLIENT_DIR -> $TARGET_OWN_GRP"
if [ "$(id -u)" -ne 0 ]; then echo "Repair needs root to chown files. Abort."; exit 1; fi
backup_now
chown "$TARGET_OWN_GRP" "$ENTRY_HTML" 2>/dev/null || true
chmod a+r "$ENTRY_HTML" 2>/dev/null || true
[ -f "$CSS_PATH" ] && chown "$TARGET_OWN_GRP" "$CSS_PATH" 2>/dev/null || true
[ -f "$CSS_PATH" ] && chmod 0644 "$CSS_PATH" 2>/dev/null || true
echo "Repair complete."
}
cmd="${1:-install}"
case "$cmd" in
install) do_install ;;
uninstall) do_uninstall ;;
status) do_status ;;
reinstall) do_uninstall; do_install ;;
repair) do_repair ;;
*) echo "Usage: $0 [install|uninstall|status|reinstall|repair]"; exit 2 ;;
esac