Refactor task audits, integrate OpenClaw, and fix timezone handling
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
+20
-2
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user