Refactor task audits, integrate OpenClaw, and fix timezone handling
This commit is contained in:
@@ -103,7 +103,8 @@ function App() {
|
||||
|
||||
React.useEffect(() => {
|
||||
window.dbUsers = dbUsers;
|
||||
}, [dbUsers]);
|
||||
window.workspace = workspace;
|
||||
}, [dbUsers, workspace]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
@@ -172,15 +173,10 @@ function App() {
|
||||
|
||||
const addTask = async ({ title, description, assignee, priority }) => {
|
||||
try {
|
||||
const t = await api.createTask({
|
||||
await api.createTask({
|
||||
title, description, assignee_id: assignee, priority,
|
||||
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);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -201,6 +201,24 @@ async def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db), c
|
||||
|
||||
db.commit()
|
||||
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"}))
|
||||
return db_task
|
||||
|
||||
|
||||
+19
-4
@@ -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) {
|
||||
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 diff = (now - then) / 1000;
|
||||
if (diff < 60) return 'just now';
|
||||
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
|
||||
if (diff < 86400) return Math.floor(diff/3600) + 'h 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) {
|
||||
if (typeof iso === 'string' && !iso.endsWith('Z') && !iso.includes('+')) iso += 'Z';
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) +
|
||||
' · ' + d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||
const tz = getSafeTimezone();
|
||||
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) {
|
||||
|
||||
+20
-2
@@ -917,7 +917,13 @@ function AuditScreen({ entries, onOpen }) {
|
||||
const actor = r.actor === 'system' ? null : findUser(r.actor);
|
||||
return (
|
||||
<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">
|
||||
{actor ? <Avatar user={actor} size={20} /> : <span className="audit__sys">SYS</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 [wsTz, setWsTz] = React.useState(workspace ? workspace.timezone : '');
|
||||
const [wsSaved, setWsSaved] = React.useState(false);
|
||||
const [wsError, setWsError] = React.useState('');
|
||||
|
||||
// User editing state
|
||||
const [editingUserId, setEditingUserId] = React.useState(null);
|
||||
@@ -1234,6 +1241,16 @@ function WorkspaceTab({ user, isAdmin, dbUsers = [], onSwitchUser, onCreateUser,
|
||||
};
|
||||
|
||||
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 });
|
||||
setWsSaved(true);
|
||||
setTimeout(() => setWsSaved(false), 2000);
|
||||
@@ -1378,12 +1395,13 @@ function WorkspaceTab({ user, isAdmin, dbUsers = [], onSwitchUser, onCreateUser,
|
||||
<label className="field">
|
||||
<span className="field__label">Timezone</span>
|
||||
<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>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="settings__save-row" style={{ marginTop: '1rem' }}>
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user