Refactor task audits, integrate OpenClaw, and fix timezone handling

This commit is contained in:
NPS Agent
2026-05-21 11:33:32 +09:30
parent a9494742bd
commit 98cf813f00
5 changed files with 60 additions and 13 deletions
+3 -7
View File
@@ -103,7 +103,8 @@ function App() {
React.useEffect(() => { React.useEffect(() => {
window.dbUsers = dbUsers; window.dbUsers = dbUsers;
}, [dbUsers]); window.workspace = workspace;
}, [dbUsers, workspace]);
React.useEffect(() => { React.useEffect(() => {
const handleKeyDown = (e) => { const handleKeyDown = (e) => {
@@ -172,15 +173,10 @@ function App() {
const addTask = async ({ title, description, assignee, priority }) => { const addTask = async ({ title, description, assignee, priority }) => {
try { try {
const t = await api.createTask({ await api.createTask({
title, description, assignee_id: assignee, priority, title, description, assignee_id: assignee, priority,
added_by: meId, source: 'manual', status: 'open', tags: [] added_by: meId, source: 'manual', status: 'open', tags: []
}); });
await api.addAudit({
actor: meId, action: 'task_created',
summary: 'Created task "' + title + '" for ' + (merge(assignee)||{}).name,
target: t.id
});
setAdding(null); setAdding(null);
} catch(e) { } catch(e) {
console.error(e); console.error(e);
+18
View File
@@ -201,6 +201,24 @@ async def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db), c
db.commit() db.commit()
db.refresh(db_task) db.refresh(db_task)
# Create audit log entry
assignee_name = "Unassigned"
if db_task.assignee_id:
assignee = db.query(models.User).filter(models.User.id == db_task.assignee_id).first()
if assignee:
assignee_name = assignee.name
db_audit = models.AuditLog(
id=f"al_{uuid.uuid4().hex[:8]}",
actor=current_user.id,
action='task_created',
summary=f'Created task "{db_task.title}" for {assignee_name}',
target=db_task.id
)
db.add(db_audit)
db.commit()
await manager.broadcast(json.dumps({"type": "refresh"})) await manager.broadcast(json.dumps({"type": "refresh"}))
return db_task return db_task
+19 -4
View File
@@ -74,21 +74,36 @@ function SourceTag({ source }) {
); );
} }
function getSafeTimezone() {
const tz = window.workspace ? window.workspace.timezone : undefined;
if (!tz) return undefined;
try {
Intl.DateTimeFormat(undefined, { timeZone: tz });
return tz;
} catch (e) {
return undefined; // Fallback to browser time if invalid
}
}
function relTime(iso) { function relTime(iso) {
const now = new Date('2026-05-08T10:30:00'); if (typeof iso === 'string' && !iso.endsWith('Z') && !iso.includes('+')) iso += 'Z';
const now = new Date();
const then = new Date(iso); const then = new Date(iso);
const diff = (now - then) / 1000; const diff = (now - then) / 1000;
if (diff < 60) return 'just now'; if (diff < 60) return 'just now';
if (diff < 3600) return Math.floor(diff/60) + 'm ago'; if (diff < 3600) return Math.floor(diff/60) + 'm ago';
if (diff < 86400) return Math.floor(diff/3600) + 'h ago'; if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
if (diff < 86400*7) return Math.floor(diff/86400) + 'd ago'; if (diff < 86400*7) return Math.floor(diff/86400) + 'd ago';
return then.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return then.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: getSafeTimezone() });
} }
function fmtDateTime(iso) { function fmtDateTime(iso) {
if (typeof iso === 'string' && !iso.endsWith('Z') && !iso.includes('+')) iso += 'Z';
const d = new Date(iso); const d = new Date(iso);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + const tz = getSafeTimezone();
' · ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: tz }) +
' · ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: tz });
} }
function findUser(id) { function findUser(id) {
BIN
View File
Binary file not shown.
+20 -2
View File
@@ -917,7 +917,13 @@ function AuditScreen({ entries, onOpen }) {
const actor = r.actor === 'system' ? null : findUser(r.actor); const actor = r.actor === 'system' ? null : findUser(r.actor);
return ( return (
<li key={r.id} className="audit__row"> <li key={r.id} className="audit__row">
<span className="audit__time mono">{new Date(r.at).toLocaleTimeString('en-US',{hour:'numeric', minute:'2-digit'})}</span> <span className="audit__time mono">{(() => {
let safeTz = undefined;
if (window.workspace && window.workspace.timezone) {
try { Intl.DateTimeFormat(undefined, { timeZone: window.workspace.timezone }); safeTz = window.workspace.timezone; } catch(e) {}
}
return new Date(typeof r.at === 'string' && !r.at.endsWith('Z') && !r.at.includes('+') ? r.at + 'Z' : r.at).toLocaleTimeString('en-US',{hour:'numeric', minute:'2-digit', timeZone: safeTz});
})()}</span>
<span className="audit__actor"> <span className="audit__actor">
{actor ? <Avatar user={actor} size={20} /> : <span className="audit__sys">SYS</span>} {actor ? <Avatar user={actor} size={20} /> : <span className="audit__sys">SYS</span>}
<span>{actor ? actor.name : 'System'}</span> <span>{actor ? actor.name : 'System'}</span>
@@ -1189,6 +1195,7 @@ function WorkspaceTab({ user, isAdmin, dbUsers = [], onSwitchUser, onCreateUser,
const [wsName, setWsName] = React.useState(workspace ? workspace.name : ''); const [wsName, setWsName] = React.useState(workspace ? workspace.name : '');
const [wsTz, setWsTz] = React.useState(workspace ? workspace.timezone : ''); const [wsTz, setWsTz] = React.useState(workspace ? workspace.timezone : '');
const [wsSaved, setWsSaved] = React.useState(false); const [wsSaved, setWsSaved] = React.useState(false);
const [wsError, setWsError] = React.useState('');
// User editing state // User editing state
const [editingUserId, setEditingUserId] = React.useState(null); const [editingUserId, setEditingUserId] = React.useState(null);
@@ -1234,6 +1241,16 @@ function WorkspaceTab({ user, isAdmin, dbUsers = [], onSwitchUser, onCreateUser,
}; };
const handleUpdateWorkspace = async () => { const handleUpdateWorkspace = async () => {
setWsError(''); // Clear previous errors
if (wsTz) {
try {
Intl.DateTimeFormat(undefined, { timeZone: wsTz });
} catch (e) {
setWsError('Not a proper timezone (e.g., Asia/Tokyo)');
return; // Stop the save process
}
}
await onUpdateWorkspace({ name: wsName, timezone: wsTz }); await onUpdateWorkspace({ name: wsName, timezone: wsTz });
setWsSaved(true); setWsSaved(true);
setTimeout(() => setWsSaved(false), 2000); setTimeout(() => setWsSaved(false), 2000);
@@ -1378,12 +1395,13 @@ function WorkspaceTab({ user, isAdmin, dbUsers = [], onSwitchUser, onCreateUser,
<label className="field"> <label className="field">
<span className="field__label">Timezone</span> <span className="field__label">Timezone</span>
<input className="field__input" value={wsTz} onChange={e => setWsTz(e.target.value)} disabled={!isAdmin} /> <input className="field__input" value={wsTz} onChange={e => setWsTz(e.target.value)} disabled={!isAdmin} />
{wsError && <div className="field__error" style={{ color: 'var(--red)', marginTop: '4px', fontSize: '13px' }}>{wsError}</div>}
</label> </label>
</div> </div>
{isAdmin && ( {isAdmin && (
<div className="settings__save-row" style={{ marginTop: '1rem' }}> <div className="settings__save-row" style={{ marginTop: '1rem' }}>
{wsSaved && <span className="settings__saved mono"><Icon.Check /> Saved</span>} {wsSaved && <span className="settings__saved mono"><Icon.Check /> Saved</span>}
<button className="btn btn--ghost" onClick={() => { setWsName(workspace.name); setWsTz(workspace.timezone); }} disabled={!wsDirty}>Discard</button> <button className="btn btn--ghost" onClick={() => { setWsName(workspace.name); setWsTz(workspace.timezone); setWsError(''); }} disabled={!wsDirty}>Discard</button>
<button className="btn btn--primary" onClick={handleUpdateWorkspace} disabled={!wsDirty}>Update workspace</button> <button className="btn btn--primary" onClick={handleUpdateWorkspace} disabled={!wsDirty}>Update workspace</button>
</div> </div>
)} )}